././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7388117 liquidctl-1.15.0/0000755000175000017500000000000014776655074012663 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/.gitignore0000644000175000017500000000016214613511524014627 0ustar00jonasjonas*.egg-info/ .krakenduty-poc /.tox/ /build/ /dist/ __pycache__/ liquidctl/_version.py .DS_Store .coverage .vscode/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525812.0 liquidctl-1.15.0/CHANGELOG.md0000644000175000017500000011653614776654764014515 0ustar00jonasjonas# Changelog ## [1.15.0] – 2025-04-01 ### Changes since 1.14.0 Added: - Support for NZXT Kraken Elite 2024 RGB (liquidctl#746) - Support for Corsair HX1200i ATX 3.1 (liquidctl#763) Changed: - Increase the minimum required Python version to 3.9 Fixed: - Permission errors when accessing driver data on Windows with Python 3.13 - Read errors when setting Aquacomputer fan speeds in quick succession (liquidctl#766, liquidctl#767) ## [1.14.0] – 2025-01-01 ### Changes since 1.13.0 Added: - Support for Corsair Hydro H110i GT (liquidctl#637) - Support for Corsair iCue Elite H100i RGB, white version (liquidctl#735) - Support for Corsair iCue Elite H115i RGB (liquidctl#678) - Support for Corsair iCue Elite H150i RGB, white version (liquidctl#725) - Support for NZXT Kraken 2023 standard and Elite models (liquidctl#605) - Support for MSI MPG Coreliquid K360 and two similar variants (liquidctl#564) - Support for NZXT RGB & Fan Controller with PID `201f` (liquidctl#733) - Support for NZXT RGB & Fan Controller with PID `2020` (liquidctl@a64e73e63eb3) - Support the ASUS Ryujin II 360, minus the screen (liquidctl#653) - Corsair Commander Core: partial/experimental support for speed profiles (liquidctl#687) Changed: - Corsair RMi/HXi PSUs: enforce a 30% minimum value for user-set fan duties (liquidctl#730) - NZXT Kraken Z?3/2023: error out when trying to set the screen is unsupported due to firmware v2.x - extra scripts: add `.py` extension to all scripts written in Python - extra/yoda: make `psutil` dependency optional Deprecated: - `experimental` fields in the `list --json` output Removed: - "Experimental" tags, notes and suffixes Fixed: - Corsair HX1000i/HX1500i: fix input power curves (liquidctl#675) - NZXT Kraken Z?3/2023: partially support setting the screen with firmware v2.x (liquidctl#692) - Linux: skip unreadable EEPROMs (liquidctl#731) - Windows: replace port numbers with bus address when setting a storage key (liquidctl#703) - extra/yoda: accept `--unsafe` flags - extra/yoda: output CPU frequency with the correct unit of MHz - Convert `PyUsbDevice` addresses to strings (liquidctl#743) - Various issues with Unix file permission handling in `liquidctl.keyval` (liquidctl#659) ### BLAKE3 checksums The checksum algorithm has been changed from SHA256 to BLAKE3. ``` $ b3sum dist/liquidctl-1.14.0{.tar.gz,-py3-none-any.whl} 699650664c92d78478478e1d992b23456a6ffc5efe83b508fa666dfad341598b dist/liquidctl-1.14.0.tar.gz 86eaff9c3c285a721c013f2e989d7cab9fb2d2eec9fb6949e9998de11d4f6a70 dist/liquidctl-1.14.0-py3-none-any.whl ``` ## [1.13.0] – 2023-07-26 ### Changes since 1.12.1 Added: - Corsair Hydro Elite RGB: add support for H100i and H150i Elite RGB (liquidctl#556, PR liquidctl#557, PR liquidctl#559) Changed: - NZXT H1 V2: no longer experimental - Aura LED motherboards: no longer experimental Fixed: - Corsair HXi (2022): read exactly 4 bytes for timedeltas (liquidctl#575) - Corsair Commander Pro: fix `fan_mode` (liquidctl#615, PR liquidctl#616) - Python 3.11: fix deprecation on `locale.getDefaultLocale` - Python 3.11.4: fix exception handling when parsing null bytes in persisted data ### SHA256 checksums ``` ee17241689c0bf3de43cf4d97822e344f5b57513d16dd160e37fa0e389a158c7 dist/liquidctl-1.13.0.tar.gz 405a55d531022082087c97ed8f4cc315b25493ad22be88ca8bd6852d49621a47 dist/liquidctl-1.13.0-py3-none-any.whl ``` *Five years ago I started liquidctl. One month after that, v1.0.0rc1 was tagged with support for the first devices.* ## [1.12.1] – 2023-01-14 ### Changes since 1.12.0 Fixed: - Corsair HXi and RMi: check that the response matches the command that was sent (liquidctl#463) ### SHA256 checksums ``` 3f98b8400c9cd3e47925cafbe34b9d7a51705bf85ce1ec8d95d107a360f6f29e dist/liquidctl-1.12.1.tar.gz 9e13d36bd9fa439ec244ea89f52ad080776406129d35db940a8343352e42dea7 dist/liquidctl-1.12.1-py3-none-any.whl ``` ## [1.12.0] – 2023-01-08 ### Changes since 1.11.1 Added: - Aquacomputer D5 Next: add support for reading virtual temp sensors (PR liquidctl#510) - Aquacomputer Octo: add support for reading virtual temp sensors (PR liquidctl#525) - Aquacomputer Farbwerk 360: add support for reading virtual temp sensors (PR liquidctl#527) - Aquacomputer Quadro: add support for reading virtual temp sensors (PR liquidctl#528) - Commander Pro: add support for changing fan modes during initialization (liquidctl#472, PR liquidctl#474) - Corsair HXi and RMi: add support for HX1500i and 2022's re-issue HX1000i PSUs - Corsair Commander ST: extend experimental Commander Core monitoring and fan control support to the Commander ST (liquidctl#511) - NZXT Kraken X3/Z3: add support for expanded HWMON driver capabilities (PR liquidctl#529) - NZXT RGB & Fan Controller: add experimental support for 2022's 3+6-channel `1e71:2011` variant (liquidctl#541) - NZXT HUE2: add accessory IDs for F120/F140 RGB fans Changed: - Corsair Hydro Pro: move firmware version to initialize output - NZXT Kraken Z3: use updated winusbcdc (PR liquidctl#535) - NZXT RGB & Fan Controller: downgrade 3+6-channel `1e71:2019` variant to experimental (liquidctl#541) - NZXT RGB & Fan Controller: disable broken lighting support on 3+6-channel controllers (liquidctl#541) Fixed: - CLI: remove occasional newline when logging `sys.version` - Corsair Hydro Pro: reduce expectations on `_CMD_READ_FAN_SPEED` responses (liquidctl#536) Removed: - CLI: remove long deprecated --hid option ### SHA256 checksums ``` 639e62d8845cd8d3718941e7894865f9c06abfc2826546606335e30f607d6fc3 dist/liquidctl-1.12.0.tar.gz 748e7c9d49f06f885cc191278c0aa3b5da5f7623edd7f4c12a99f52b72b1cad6 dist/liquidctl-1.12.0-py3-none-any.whl ``` ## [1.11.1] – 2022-10-19 ### Changes since 1.11.0 Fixed: - USB and HID: increase default timeout to 5s (liquidctl#526) ### Notes for downstream packagers See notes for 1.11.0 release. ### SHA256 checksums ``` 278c1aca8d891bfe8e0c164dfe6651261a0423b29f9c24cef060c3613f2a4fd7 dist/liquidctl-1.11.1.tar.gz 629d6e7db0eab3d6e7d0a58c23be6765d169eb2c1d29ddaef2fde60c603429e9 dist/liquidctl-1.11.1-py3-none-any.whl ``` ## [1.11.0] – 2022-10-16 ### Changes since 1.10.0 Added: - Corsair Commander Core: extend experimental monitoring and fan control support to the Commander Core XT (PR liquidctl#478) - Aquacomputer D5 Next: add experimental monitoring and pump/fan control support (PR liquidctl#482, PR liquidctl#489, PR liquidctl#499) - Aquacomputer Farbwerk 360: add experimental monitoring support (PR liquidctl#491) - Aquacomputer Octo: add experimental monitoring and fan control support (PR liquidctl#492, PR liquidctl#508) - Aquacomputer Quadro: add experimental monitoring and fan control support (PR liquidctl#493, PR liquidctl#509) - NZXT Kraken Z3: add experimental LCD screen support (PR liquidctl#479) - NZXT Kraken X3: support new USB PID (liquidctl#503) - NZXT RGB & Fan Controller: support new USB PID (liquidctl#485) Changed: - ASUS Aura LED: refer to as ASUS instead of AsusTek - Corsair RMi/HXi: rename temperature sensors according to their location - NZXT Kraken X40/X60: document that alerts are not supported (liquidctl#477) Fixed: - HWMON: fix Python<3.9 compatibility (PR liquidctl#483) - Corsair Hydro Pro: fix duplicate use of second alert temperature (PR liquidctl#484) - HWMON: support builtin drivers and log driver instead of module name (liquidctl#502) - Corsair Commander Core: support 2.10.219 firmware (PR liquidctl#501, PR liquidctl#513) - USB devices: add default timeouts to all IO methods (liquidctl#488) - USB HIDs: add default timeouts to compatible IO methods (liquidctl#488) Removed: - API: make `UsbDriver.SUPPORTED_DEVICES` private ### Notes for downstream packagers New Python dependencies: [crcmod], [pillow] and (Windows-only:) [winusbcdc]. [crcmod]: https://pypi.org/project/crcmod/ [pillow]: https://pypi.org/project/Pillow/ [winusbcdc]: https://pypi.org/project/WinUsbCDC/ ### SHA256 checksums ``` a3b53e317ba9211e05be88d9158efdc02c51ae067ee974d3d9f0b79716cf7ba3 dist/liquidctl-1.11.0.tar.gz 0c59dac7bdc09d7a16da410060154dca86258d989308034a919242a4739ca8f3 dist/liquidctl-1.11.0-py3-none-any.whl ``` *In memory of Lucinda Alves Silva Malaco (1924–2021) and Peter Eckersley (1979–2022).* ## [1.10.0] – 2022-07-03 ### Changes since 1.9.1 Added: - Add experimental support for NZXT H1 V2 case Smart Device (PR liquidctl#451) - Add experimental driver for Asus Aura LED USB controllers (PR liquidctl#456) Changed: - Hydro Platinum/Pro XT: only compute packets that will be sent - Kraken X2: report modern firmware versions in simplified form - Smart Device (V1)/Grid+ V3: report firmware version in simplified form - Debug: make it clear when a device is identified - Nvidia: promote all supported cards to stable status Fixed: - Skip `keyval` unit test on Windows when lacking sufficient permissions to create symlinks (liquidctl#460) Removed: - API: remove deprecated firmware version from the output of `KrakenX2.get_status()` ### SHA256 checksums ``` f9dc1dacaf1d3a44b80000baac490b44c5fa7443159bd8d2ef4dbb1af49cc7ba dist/liquidctl-1.10.0.tar.gz acc65602e598dabca94f91b067ac7ad7f4d2920653b91d694ad421be6eaef172 dist/liquidctl-1.10.0-py3-none-any.whl ``` ## [1.9.1] – 2022-04-05 ### Changes since 1.9.0 Fixed: - Remove excess `_input` suffix when reading `pwmN` attributes from hwmon (liquidctl#445, PR liquidctl#446) ### Notes for downstream packagers Starting with 1.9.0, liquidctl now uses a PEP 517 build. See the notes for the 1.9.0 release for more information. ### SHA256 checksums ``` b4467e842d9a6adc804317a991354db041417f4f7dcf7d76799f2b1593ed1276 dist/liquidctl-1.9.1.tar.gz a23312c07b1ceec850e7739a2428e9fc47c95cd0650269653a9e726d53c12057 dist/liquidctl-1.9.1-py3-none-any.whl ``` ## [1.9.0] – 2022-04-05 ### Changes since 1.8.1 Added: - Add support for persisting settings on modern Asetek 690LC coolers (liquidctl#355) - Add support for setting fixed fan/pump speeds on the Corsair Commander Core (PR liquidctl#405) - Identify some devices with a matching Linux hwmon device (liquidctl#403, PR liquidctl#429) - Add `--direct-access` to force it in spite of the presence of kernel drivers (liquidctl#403, PR liquidctl#429) - Add security policy: `SECURITY.md` - Enable experimental support for EVGA GTX 1070 and 1070 Ti cards using the existing `EvgaPascal` driver: - EVGA GTX 1070 FTW \[DT Gaming|Hybrid\] - EVGA GTX 1070 Ti FTW2 - Enable experimental support for various ASUS GTX and RTX cards using the existing `RogTuring` driver: - ASUS Strix GTX 1050 OC - ASUS Strix GTX 1050 Ti OC - ASUS Strix GTX 1060 \[OC\] 6GB - ASUS Strix GTX 1070 - ASUS Strix GTX 1070 Ti \[Advanced\] - ASUS Strix GTX 1080 \[Advanced|OC\] - ASUS Strix GTX 1080 Ti \[OC\] - ASUS Strix GTX 1650 Super OC - ASUS Strix GTX 1660 Super OC - ASUS Strix GTX 1660 Ti OC - ASUS Strix RTX 2060 \ - ASUS Strix RTX 2060 Super \[Advanced|Evo Advanced|OC\] - ASUS Strix RTX 2070 \[Advanced|OC\] - ASUS Strix RTX 2070 Super \ - ASUS Strix RTX 2080 OC - ASUS Strix RTX 2080 Super \ - ASUS Strix RTX 2080 Ti - ASUS TUF RTX 3060 Ti OC - API: add `liquidctl.__version__` - extra/contrib: add script for n-color RGB Fusion 2.0 color cycling (PR liquidctl#424, PR liquidctl#426) Changed: - Log the Python interpreter version - If possible, log the version of all Python requirements - Move reporting of Kraken X2 firmware version to initialization - Move reporting of Smart Device V1/Grid+ V3 firmware version and accessories to initialization (PR liquidctl#429) - Don't re-initialize devices with a Linux hwmon driver (liquidctl#403, PR liquidctl#429) - If possible, read status from Linux hwmon (liquidctl#403, PR liquidctl#429) - Switch to a PEP 517 build (liquidctl#430, PR liquidctl#431) - Replace ah-hoc version management with `setuptools_scm` (liquidctl#430, PR liquidctl#431) - Allow directly invoking the CLI with `python -m liquidctl` - Windows: provide libsub-1.0.dll automatically with `libusb-package` - API: improve and clarify the documentation of `BaseDriver` methods - API: rename `CorsairAsetekProDriver` to `HydroPro` Deprecated: - Deprecate directly invoking the CLI with `python -m liquidctl.cli` (use `python -m liquidctl`) - API: deprecate including the firmware version in the output from `KrakenX2.get_status()` (read it from `.initialize()`) - API: deprecate `CorsairAsetekProDriver` alias (use `HydroPro`) Removed: - API: remove long deprecated support for connecting to Kraken X2 devices with `KrakenX2.initialize()` (use standardized `.connect()`) - API: remove long deprecated support for disconnecting from Kraken X2 devices with `KrakenX2.finalize()` (use standardized `.disconnect()`) - API: remove long deprecated `.find_all_supported_devices()` (use `liquidctl.find_liquidctl_devices()` or `.find_supported_devices()`) Fixed: - Let all unexpected SMBus exceptions bubble up (liquidctl#416) - Reset Kraken X2 fan and pump profiles during initialization (possibly related to liquidctl#395) - Remove redundant prefix from CLI error messages ### Notes for downstream packagers liquidctl now uses a PEP 517 build: [PyPA/build] and [PyPA/installer] are suggested for a typical downstream package build process: ```bash # build python -m build --wheel [--no-isolation] # install python -m installer --destdir= dist/*.whl ``` Additionally, liquidctl has switched from an ad-hoc solution to version management to [setuptools_scm]. If the git tags aren't available, setuptools_scm supports environment variables to externally inject the version number. ```bash export SETUPTOOLS_SCM_PRETEND_VERSION_FOR_LIQUIDCTL=1.9.0 python -m build [args] python -m installer [args] ``` [PyPA/build]: https://github.com/pypa/build [PyPA/installer]: https://github.com/pypa/installer [setuptools_scm]: https://github.com/pypa/setuptools_scm ### SHA256 checksums ``` 9e1ae595be2c3ea5899e12741c11307da27e86bc88f7f93c5ae40bb2aa03dc70 dist/liquidctl-1.9.0.tar.gz 3820c29c0fc86bd6bd601d55a593f1cd476cd563875b45488bef26fc272abf6d dist/liquidctl-1.9.0-py3-none-any.whl ``` ## [1.8.1] – 2022-01-21 ### Changes since 1.8.0 Fixed: - Strip non-determinism from sdist/egg SOURCES.txt metadata ### SHA256 checksums ``` 0859dfe673babe9af10e4f431e0baa974961f0b2c973a37e64eb6c6c2fddbe73 dist/liquidctl-1.8.1.tar.gz ``` ## [1.8.0] – 2022-01-06 ### Changes since 1.7.2 Added: - Add support for the Corsair Hydro H60i Pro XT Changed: - Support for Corsair Hydro Pro coolers is no longer considered experimental - Support for Corsair Hydro Platinum and Pro XT coolers is no longer considered experimental - Support for Corsair Hydro Pro XT coolers is no longer considered experimental - Support for NZXT Kraken Z coolers remains incomplete (no support for the LCD screen), but is no longer considered experimental - Support for Corsair Lighting Node Core and Lighting Node Pro controllers is no longer considered experimental - Support for the Corsair Obsidian 1000D case is no longer considered experimental Fixed: - Read DDR4 temperature sensor by word instead of with SMBus Block Read (liquidctl#400) - Fix tolerant handling of single channel name in Corsair Lighting Node Core ### SHA256 checksums ``` 99b8ec4da617a01830951a8f1a37d616f50eed6d260220fe5c26d1bf90e1e91e dist/liquidctl-1.8.0.tar.gz ``` ## [1.7.2] – 2021-10-05 Changelog since 1.7.1: ### Added - Enable support for new variant of the NZXT Smart Device V2 (PR liquidctl#364) ### Changed - Default `--maximum-leds` to the maximum possible number of LEDs (liquidctl#367, PR liquidctl#368) ### Fixed - Fix moving flag in SD2/HUE2 `alternating` modes (liquidctl#385) ### SHA256 checksums ``` b2337e0ca3bd36de1cbf581510aacfe23183d7bb176ad0dd43904be213583de3 dist/liquidctl-1.7.2.tar.gz ``` ## [1.7.1] – 2021-07-16 _Summary for the 1.7.1 release: fix a bug when colorizing the log output._ Changelog since 1.7.0: ### Fixed - Fix `KeyError` when logging due to colorlog<6 - Swap DEBUG and INFO level colors ### SHA256 checksums ``` 10f650b9486ddac184330940550433685ae0abc70b66fe92d994042491aab356 dist/liquidctl-1.7.1.tar.gz 5f35d4ac8ad6da374877d17c7a36bbb202b0a74bd773ebe45444f0089daba27b dist/liquidctl-1.7.1-bin-windows-x86_64.zip ``` ## [1.7.0] – 2021-07-06 _Summary for the 1.7.0 release: support for Commander Core/Capellix, Obsidian 1000D, new Smart Device V2 variant; `--json` output; improvements in initialize/status output; colorize the log output._ Changelog since 1.6.1: ### Added - Add initial experimental support for the Corsair Commander Core/iCUE Elite Capellix AIOs (PR liquidctl#340) - Enable experimental support for Corsair Obsidian 1000D (liquidctl#346) - Enable support for new variant of the NZXT Smart Device V2 (liquidctl#338) - List experimental partial support for the NZXT Kraken Z53 - Add machine readable output with `--json` (PR liquidctl#314) - Add CONTRIBUTING.md and document our development process ### Changed - Change Grid+ V3/Smart Device (V1) status output (PR liquidctl#326) - Change Commander Pro status/initialize output (PR liquidctl#326) - Colorize the log output (new dependency: `colorlog`; PRs liquidctl#318, liquidctl#329) - Mark Kraken X31, X41, X61 as no longer experimental - Mark Vengeance RGB and DDR4 temperature sensors as no longer experimental - Mark Commander pro as no longer experimental - Mark NZXT E500, E650, E850 as no longer experimental - Change main branch name to "main" - Improve the documentation ### Fixed - Make `find_supported_devices()` account for `legacy_690lc` on Asetek 690LC drivers - Remove accidentally inherited `downgrade_to_legacy()` (unstable) from `Hydro690Lc` ### SHA256 checksums ``` 053675aca9ba9a3c14d8ef24d1a2e75c592c55a1b8ba494447bc13d3ae523d6f dist/liquidctl-1.7.0.tar.gz d0f8f24961a22c7664c330d286e1c63d4df753d5fbe21ac77eb6488b27508751 dist/liquidctl-1.7.0-bin-windows-x86_64.zip ``` ## [1.6.1] – 2021-05-01 _Summary for the 1.6.1 release: one bug fix for HUE 2 controllers._ Changelog since 1.6.0: ### Fixed - Smart Device V2/HUE 2: check if fan controller before initializing fan reporting (liquidctl#331) ### SHA256 checksums ``` e3b6aa5ae55204f8d9a8813105269df7dc8f80087670e3eac88b722949b3843f dist/liquidctl-1.6.1.tar.gz d14a32b7c0de5a2d25bc8280c32255da25e9bc32f103d099b678810a9a1b6c9c dist/liquidctl-1.6.1-bin-windows-x86_64.zip ``` ## [1.5.2] – 2021-05-01 _Summary for the 1.5.2 release: one bug fix for HUE 2 controllers._ Changelog since 1.5.1: ### Fixed - Smart Device V2/HUE 2: check if fan controller before initializing fan reporting (liquidctl#331) ### SHA256 checksums ``` 5738fda03f1d7bfb4416461a70351a5e040f1b57229674dd0f1f6f81d3750812 dist/liquidctl-1.5.2.tar.gz ``` ## [1.6.0] – 2021-04-06 _Summary for the 1.6.0 release: support for Corsair Lighting Node Core, Hydro H150i Pro XT, and all Hydro Pro coolers; estimate input power and efficiency for Corsair HXi and RMi PSUS; enable support for ASUS Strix GTX 1070 and new NZXT RGB & Fan Controller variant; formally deprecate `-d`/`--device`._ _Note for Linux package maintainers: the i2c-dev kernel module may now be loaded automatically because of `extra/linux/71-liquidctl.rules`; this substitutes the use of `extra/linux/modules-load.conf`, which has been removed._ Changelog since 1.5.1: ### Added - Add experimental support for the Corsair Lighting Node Core - Add experimental support for the Corsair Hydro H150i Pro XT - Add experimental support for the Corsair Hydro H100i Pro, H115i Pro and H150i Pro coolers - Enable support for the ASUS Strix GTX 1070 - Enable support for new variant of the NZXT RGB & Fan Controller - Add `sync` pseudo lighting channel to Commander/Lighting Node Pro devices - Add duty cycles to Hydro Platinum and Pro XT status output - Add input power and efficiency estimates to the status output of Corsair HXi and RMi PSUs - Add the Contributor Covenant, version 1.4 as our code of conduct ### Changed - Remove `pro_xt_lighting` unsafe feature guard - Enforce correct casing of constants in driver APIs - Use udev rules for automatic loading of kernel modules (replaces previous `modules-load.d` configuration) - Remove warnings when reporting or setting the OCP mode of Corsair HXi and RMi PSUs - Rename Corsair HXi and RMi "Total power" status item to "Total power output" - Handle both US and UK spellings of `--direction` values - Improve the documentation ### Fixed - Replace "ID" with "#" when listing all devices - Add `keyval.load_store` method, atomic at the filesystem level - Add "Hydro" to Platinum and Pro XT device descriptions ### Removed - Remove modules-load configuration file for Linux (use the supplied udev rules instead) - [extra] remove `krakencurve-poc`, use `yoda` instead ### Deprecated - Deprecate `-d`/`--device`; prefer `--match` or other selection options ### SHA256 checksums ``` 486dc366f10810a4efb301f3ceda10657a09937e9bc936cecec792ac26c2f186 dist/liquidctl-1.6.0.tar.gz 9b2e144c1fa63aaf41dc3d6a264b2e78e14a5f424b86e3a5f4b80396677000e6 dist/liquidctl-1.6.0-bin-windows-x86_64.zip ``` ## [1.5.1] – 2021-02-19 _Summary for the 1.5.1 release: fixes to error reporting, handling of runtime data, and other bugs._ Changelog since 1.5.0: ### Fixed - Handle corrupted runtime data (liquidctl#278) - Fix item prefixes in list output when `--match` is passed - Remove caching of temporarily stored data - Append formated exception to "unknown error" messages - Only attempt to disconnect from a device if already connected - Only attempt to set the USB configuration if no other errors have been detected - Return the context manager when overriding `connect()` - Fix construction of fallback search paths for runtime data ### SHA256 checksums ``` e2d97be0319501bcad9af80c837abdbfd820620edcf9381068a443ad971327eb liquidctl-1.5.1-bin-windows-x86_64.zip 9480e2dfbb0406fa8d57601a43a0f7c7573de1f5f24920b0e4000786ed236a8b liquidctl-1.5.1.tar.gz ``` ## [1.5.0] – 2021-01-27 _Summary for the 1.5.0 release: Corsair Commander Pro and Lighting Node Pro support; EVGA GTX 1080 FTW and ASUS Strix RTX 2080 Ti OC support on Linux; Corsair Vengeance RGB and TSE2004-compatible DDR4 modules support on Intel on Linux; `--direction` flag, replacing previous "backwards-" modes; improved error handling and reporting; new project home; other improvements and fixes._ _Note for Linux package maintainers: this release introduces a new dependency, Python 'smbus' (from the i2c-tools project); additionally, since trying to access I²C/SMBus devices without having the i2c-dev kernel module loaded will result in errors, `extra/linux/modules-load.conf` is provided as a suggestion; finally, `extra/linux/71-liquidctl.rules` will now (as provided) give unprivileged access to i801_smbus adapters._ Changelog since 1.4.2: ### Added - Add SMBus and I²C support on Linux - Add support for EVGA GTX 1080 FTW on Linux - Add support for ASUS Strix RTX 2080 Ti OC on Linux - Add experimental support for DIMMs with TSE2004-compatible temperature sensors on Intel/Linux - Add experimental support for Corsair Vengeance RGB on Intel/Linux - Add experimental support for the Corsair Commander Pro - Add experimental support for the Corsair Lighting Node Pro - Add `--direction` modifier to animations - Add `--non-volatile` to control persistence of settings (NVIDIA GPUs) - Add `--start-led`, `--maximum-leds` and `--temperature-sensor` options (Corsair Commander/Lighting Node devices) - Add support for CSS-style hexadecimal triples - Implement the context manager protocol in the driver API - Export `find_liquidctl_devices` from the top-level `liquidctl` package - Add modules-load configuration file for Linux - Add completion script for bash - [extra] Add `LQiNFO.py` exporter (liquidctl -> HWiNFO) - [extra] Add `prometheus-liquidctl-exporter` exporter (liquidctl -> Prometheus) ### Changed - Move GitHub project into liquidctl organization - Improve error handling and reporting - Make vendor and product IDs optional in drivers - Mark Kraken X53, X63, X73 as no longer experimental - Mark NZXT RGB & Fan Controller as no longer experimental - Mark RGB Fusion 2.0 controllers as no longer experimental - Change casing of "PRO" device names to "Pro" - Improve the documentation ### Fixed - Fix potential exception when a release number is not available - Enforce USB port filters on HID devices - Fix backward `rainbow-pulse` mode on Kraken X3 devices - Fix compatibility with hidapi 0.10 and multi-usage devices (RGB Fusion 2.0 controllers) - Fix lighting settings in Platinum SE and Pro XT coolers - Generate and verify the checksums of zip and exe built on AppVeyor ### Deprecated - Deprecate `backwards-` pseudo modes; use `--direction=backward` instead ### SHA256 checksums ``` 370eb9c662111b51465ac5e2649f7eaf423bd22799ef983c4957468e9d957c15 liquidctl-1.5.0-bin-windows-x86_64.zip 762561a8b491aa98f0ccbbab4f9770813a82cc7fd776fa4c21873b994d63e892 liquidctl-1.5.0.tar.gz ``` ## [1.4.2] – 2020-11-01 _Summary for the 1.4.2 release: standardized hexadecimal parsing in the CLI; fixes for Windows and mac OS; improvements to Hydro Platinum/Pro XT and Kraken X3 drivers._ Changelog since 1.4.1: ### Added - Add `Modern690Lc.downgrade_to_legacy` (unstable API) ### Changed - Accept hexadecimal inputs regardless of a `0x` prefix - Warn on faulty temperature readings from Kraken X3 coolers - Warn on Hydro Platinum/Pro XT firmware versions that are may be too old - Update PyInstaller used for the Windows executable - Update PyUSB version bundled with the Windows executable - Improve the documentation ### Fixed - Fix data path on mac OS - Only set the sticky bit for data directories on Linux - Fix check of maximum number of colors in Hydro Platinum super-fixed mode - Fix HID writes to Corsair HXi/RMi power supplies on Windows - Ensure Hydro Platinum/Pro XT is in static LEDs hardware mode ### SHA256 checksums ``` 83517ccb06cfdda556bc585a6a45edfcb5a21e38dbe270454ac97639d463e96d dist/liquidctl-1.4.2-bin-windows-x86_64.zip 39da5f5bcae1cbd91e42e78fdb19f4f03b6c1a585addc0b268e0c468e76f1a3c dist/liquidctl-1.4.2.tar.gz ``` ## [1.4.1] – 2020-08-07 _Summary for the 1.4.1 release: fix a regression with NZXT E-series PSUs, an unreliable test case, and some ignored Hidapi errors; also make a few other small improvements to the documentation and test suite._ Changelog since 1.4.0: ### Changed - Improve the documentation - Improve the test suite ### Fixed - Don't use report IDs when writing to NZXT E-series PSUs (liquidctl#166) - Recognize and raise Hidapi write errors - Use a mocked device to test backward compatibility with liquidctl 1.1.0 ### SHA256 checksums ``` 895e55fd70e1fdfe3b2941d9139b91ffc4e902a469b077e810c35979dbe1cfdf liquidctl-1.4.1-bin-windows-x86_64.zip 59a3bc65b3f3e71a5714224401fe6e95dfdee591a1d6f4392bc4e6d6ad72ff8d liquidctl-1.4.1.tar.gz ``` ## [1.4.0] – 2020-07-31 _Summary for the 1.4.0 release: fourth-generation NZXT Kraken coolers, Corsair Platinum and Pro XT coolers, select Gigabyte RGB Fusion 2.0 motherboards, additional color formats, improved fan and pump profiles in third-generation Krakens, and other improvements._ Changelog since 1.3.3: ### Added - Add experimental support for NZXT Kraken X53, X63 and X73 coolers - Add experimental partial support for NZXT Kraken Z63 and Z73 coolers - Add experimental support for Corsair H100i, H100i SE and H115i Platinum coolers - Add experimental partial support for Corsair H100i and H115i Pro XT coolers - Add experimental support for Gigabyte motherboards with RGB Fusion 2.0 5702 and 8297 controllers - Enable experimental support for the NZXT RGB & Fan Controller - Add support for HSV, HSL and explicit RGB color representations - Add `sync` lighting channel to HUE 2 devices - Add tentative names for the different +12 V rails of NZXT E-series PSUs - Add +uaccess udev rules for Linux distributions and users - Add `--pump-mode` option to `initialize` (Corsair Platinum/Pro XT coolers) - Add `--unsafe` option to enable additional bleeding-edge features - Add a test suite - [extra] Add more general `yoda` script for software-based fan/pump control (supersedes `krakencurve-poc`) ### Changed - Increase resolution of fan and pump profiles in Kraken X42/X52/X62/X72 coolers - Use hidapi to communicate with HIDs on Windows - Use specific errors when features are not supported by the device or the driver - Store runtime data on non-Linux systems in `~/Library/Caches` (macOS), `%TEMP%` (Windows) or `/tmp` (Unix) - Mark Corsair HXi/RMi PSUs as no longer experimental - Mark Smart Device V2 and HUE 2 controllers as no longer experimental - Switch to a consistent module, driver and guide naming scheme (aliases are kept for backward compatibility) - Improve the documentation - [extra] Refresh `krakencurve-poc` syntax and sensor names, and get CPU temperature on macOS with iStats ### Fixed - Add missing identifiers for some HUE2 accessories (liquidctl#95; liquidctl#109) - Fix CAM-like decoding of firmware version in NZXT E-series PSUs (liquidctl#46, comment) - Use a bitmask to select the lighting channel in HUE 2 devices (liquidctl#109) - Close the underlying cython-hidapi `device` - Don't allow `HidapiDevice.clear_enqueued_reports` to block - Don't allow `HidapiDevice.address` to fail with non-Unicode paths - Store each runtime data value atomically ### Deprecated - Deprecate and ignore `--hid` override for API selection ### Removed - Remove the PyUsbHid device backend for HIDs ### SHA256 checksums ``` 250b7665b19b0c5d9ae172cb162bc920734eba720f3e337eb84409077c582966 liquidctl-1.4.0-bin-windows-x86_64.zip b35e6f297e67f9e145794bb57b88c626ef2bfd97e7fbb5b098f3dbf9ae11213e liquidctl-1.4.0.tar.gz ``` ## [1.3.3] – 2020-02-18 _Summary for the 1.3.3 release: fix possibly stale data with HIDs and other minor issues._ Changelog since 1.3.2: ### Fixed - Add missing identifiers for HUE+ accessories on HUE 2 channels - Forward hid argument from `UsbHidDriver.find_supported_devices` - Prevent reporting stale data during long lived connections to HIDs (liquidctl#87) ### SHA256 checksums ``` 1422a892f9c2c69f5949cd831083c6fef8f6a1f6e3215e90b696bfcd557924b4 liquidctl-1.3.3-bin-windows-x86_64.zip d13180867e07420c5890fe1110e8f45fe343794549a9ed7d5e8e76663bc10c24 liquidctl-1.3.3.tar.gz ``` ## [1.3.2] – 2019-12-11 _Summary for the 1.3.2 release: fix fan status reporting from Smart Device V2._ Changelog since 1.3.1: ### Fixed - Parse Smart Device V2 fan info from correct status message ### SHA256 checksums ``` acf44a491567703c109c03f446c3c0761e5f9b97098613f8ecb4366a1d2afd50 liquidctl-1.3.2-bin-windows-x86_64.zip bb742947c15f4a3987685641c0dd73184c4a40add5ad818ced68e5ace3631b6b liquidctl-1.3.2.tar.gz ``` ## [1.3.1] – 2019-11-23 _Summary for the 1.3.1 release: fix parsing of `--verbose` and documentation improvements._ Changelog since 1.3.0: ### Changed - List included dependencies and versions in Windows' bundle - Improve the documentation ### Fixed - Fix parsing of `--verbose` in commands other than `list` ### SHA256 checksums ``` de272dad305dc6651265640a280bedb21bc680a62117e625004c6aad2104da63 liquidctl-1.3.1-bin-windows-x86_64.zip 6092a6fae477908c80adc825b290e39f0b26e604593884da23d40e892e553309 liquidctl-1.3.1.tar.gz ``` ## [1.3.0] – 2019-11-17 _Summary for the 1.3.0 release: man page, Corsair RXi/HXi and NZXT E power supplies, Smart Device V2 and HUE 2 family, improved device discovery and selection._ Changelog since 1.3.0rc1: ### Added - Enable experimental support for the NZXT HUE 2 - Enable experimental support for the NZXT HUE 2 Ambient - Add `-m, --match ` to allow filtering devices by description - Add `-n` short alias for `--pick` ### Changed - Allow `initialize` methods to optionally return status tuples - Conform to XDG basedir spec and prefer `XDG_RUNTIME_DIR` - Improve directory names for internal data - Ship patched PyUSB and libusb 1.0.22 on Windows - Improve the documentation ### Fixed - Release the USB interface of NZXT E-series PSUs as soon as possible - Fix assertion in retry loops with NZXT E-series PSUs - Fix LED blinking when executing `status` on a Smart Device V2 - Add missing identifier for 250 mm HUE 2 LED strips - Restore experimental tag for the NZXT Kraken X31/X41/X61 family ### Removed - Remove dependency on appdirs ### SHA256 checksums ``` ff935fd3d57dead4d5218e02f834a825893bc6716f96fc9566a8e3989a7c19fe liquidctl-1.3.0-bin-windows-x86_64.zip ce0483b0a7f9cf2618cb30bdf3ff4195e20d9df6c615f69afe127f54956e42ce liquidctl-1.3.0.tar.gz ``` ## [1.3.0rc1] – 2019-11-03 Changelog since 1.2.0: ### Added - Add experimental support for Corsair HX750i, HX850i, HX1000i and HX1200i power supplies - Add experimental support for Corsair RM650i, RM750i, RM850i and RM1000i power supplies - Add experimental support for NZXT E500, E650 and E850 power supplies - Add experimental support for the NZXT Smart Device V2 - Add liquidctl(8) man page - Add `initialize all` variant/helper - Add `--pick ` device selection option - Add `--single-12v-ocp` option to `initialize` (Corsair HXi/RMi PSUs) ### Changed - Reduce the number of libusb and hidapi calls during device discovery - Improve the visual hierarchy of the output `list` and `status` - Allow `list --verbose` to run without root privileges (Linux) or special drivers (Windows) - Change the default API for HIDs on Linux to hidraw - Consider stable: Corsair H80i v2, H100i v2, H115i; NZXT Kraken X31, X41, X61; NZXT Grid+ V3 ### Fixed - Don't try to reattach the kernel driver more than once - Fixed Corsair H80i GT device name throughout the program - Fixed Corsair H100i GT device name in listing ### Deprecated - Use `liquidctl.driver.find_liquidctl_devices` instead of `liquidctl.cli.find_all_supported_devices` ### SHA256 checksums ``` $ sha256sum liquidctl-1.3.0rc1* 7a16a511baf5090c34cd3dfc5c21068a298515f31315be63e9b991ea17654671 liquidctl-1.3.0rc1-bin-windows-x86_64.zip 1ef517ba33e366167f9a225c6a6afcc4899d01cbd7853bd5852ac15ae81d5005 liquidctl-1.3.0rc1-py3-none-any.whl 15583d6ebecad722e1562164cef7097a358d6a57aa33a1a5e25741690548dbfa liquidctl-1.3.0rc1.tar.gz ``` ## [1.2.0] – 2019-09-27 _Summary for the 1.2.0 release: support for Asetek "5-th gen." 690LC coolers and improvements for HIDs and Mac OS._ Changelog since 1.2.0rc4: ### Changed - Include extended version information in pre-built executables for Windows ### Fixed - Improve handling of USB devices with no active configuration ## [1.2.0rc4] – 2019-09-18 Changelog since 1.2.0rc3: ### Added - Add support for adding git commit and tree cleanliness information to `--version` - Add support for adding distribution name and package information to `--version` ### Changed - Enable modern features for all Asetek 690LC coolers from Corsair - Include version information in `--debug` - Make docs and code consistent on which devices are only experimentally supported - Revert "Mark Kraken X31, X41, X51 and X61 as no longer experimental" - Improve the documentation ## [1.2.0rc3] – 2019-09-15 Changelog since 1.2.0rc2: ### Added - [extra] Add experimental `liquiddump` script ### Changed - Copy documentation for EVGA and Corsair 690LC coolers into the tree - Use modern driver with fan profiles for Corsair H115i (liquidctl#41) - Claim the interface proactively when starting a transaction on any Asetek 690LC (liquidctl#42) ### Fixed - Rework USBXPRESS flow control in Asetek 690LC devices to allow simultaneous reads from multiple processes (liquidctl#42) - Fix missing argument forwarding to legacy Asetek 690LC coolers - Fix broken link to Mac OS example configuration ## [1.2.0rc2] – 2019-09-12 Changelog since 1.2.0rc1: ### Added - Support the EVGA CLC 360 - Add `--alert-threshold` and `--alert-color` ### Changed - Mark Kraken X31, X41, X51 and X61 as no longer experimental - Improve supported devices list and links to documentation - Don't enable PyUSB tracing automatically with `--debug` - Cache values read from or stored on the filesystem - Prefer to save driver data in /run when OS is Linux ### Fixes - Force bundling of `hid` module in Windows executable - Change default Asetek 690LC `--time-per-color` for fading mode (liquidctl#29) ## [1.2.0rc1] – 2019-04-14 Changelog since 1.1.0: ### Added - Add support for EVGA CLC 120 CL12, 240 and 280 coolers - Add experimental support for NZXT Kraken X31, X41 and X61 coolers - Add experimental support for Corsair H80i v2, H100i v2 and H115i - Add experimental support for Corsair H80i GT, H100i GTX and H110i GTX - Add support for macOS - Make automatic bundled builds for Windows with AppVeyor - Add support for hidapi for HIDs (default/required on macOS) - Add release number, bus and address listing - Add `sync` pseudo channel for setting all Smart Device/Grid+ V3 fans at once - Add `--hid ` override for HID API selection - Add `--release`, `--bus`, `--address` device filters - Add `--time-per-color` and `--time-off` animation options - Add `--legacy-690lc` option for Asetek 690LC devices - Document possible support of NZXT Kraken X40 and X60 coolers ### Changed - Revamp driver and device model in `liquidctl.driver.{base,usb}` modules ### Removed - Remove `--dry-run` ## [1.1.0] – 2018-12-15 _Summary for the 1.1.0 release: support for NZXT Smart Device, Grid+ V3 and Kraken M22._ Changelog since 1.1.0rc1: ### Added - [extra] Add proof of concept `krakencurve-poc` script for software-based speed control ### Changed - Change Kraken M22 from experimental to implemented - Only show exception tracebacks if -g has been set - Improve the documentation ### Fixes - Use standard NotImplementedError exception ## [1.1.0rc1] - 2018-11-14 Changelog since 1.0.0: ### Added - Add support for the NZXT Smart Device - Add experimental support for the NZXT Grid+ V3 - Add experimental support for the NZXT Kraken M22 - Add `initialize` command for the NZXT Smart Device, NZXT Grid+ V3 and similar products - Add device filtering options: `--vendor`, `--product`, `--usb-port` and `--serial` - Add `super-breathing`, `super-wave` and `backwards-super-wave` modes for Krakens - Add `--debug` to complement `--verbose` - Add special Kraken `set_instantaneous_speed(channel, speed)` API - Expose Kraken `supports_lighting`, `supports_cooling` and `supports_cooling_profiles` properties - [extra] Add proof of concept `krakenduty-poc` script for status-duty translation ### Changed - Lower the minimum pump duty to 50% - No longer imply `--verbose` from `--dry-run` - Improve the API for external code that uses our drivers - Switch to the standard Python `logging` module - Improve the documentation ### Fixes - Fix standalone module entry point for the CLI - [Kraken] Fix fan and pump speed configuration on firmware v2.1.8 or older ### Deprecated - [Kraken] Deprecate `super`; use `super-fixed` instead - [Kraken] Deprecate undocumented API behavior of `initialize()` and `finalize()`; use `connect()` and `disconnect()` instead ### Removed - Remove unused symbols in `liquidctl.util` ## [1.0.0] - 2018-08-31 _Summary for the 1.0.0 release: support for NZXT Kraken X42/X52/X62/X72 coolers._ Changelog since 1.0.0rc1: ### Added - Add helper color mode: `off` - Add backward variant of `moving-alternating` color mode ### Changed - Improve the documentation - Allow covering marquees with only one color ### Fixes - Fix mentions to incorrect Kraken generation - Correct the modifier byte for the `moving-alternating` mode ## [1.0.0rc1] - 2018-08-26 ### Added - Add driver for NZXT Kraken X42, X52, X62 and X72 coolers ## About the changelog All notable changes to this project are documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html) and [PEP 404](https://www.python.org/dev/peps/pep-0440/#semantic-versioning). ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/CODE_OF_CONDUCT.md0000644000175000017500000000643514562144457015461 0ustar00jonasjonas# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/CONTRIBUTING.md0000644000175000017500000000352514562144457015110 0ustar00jonasjonas# Contributing to liquidctl Thank you for your interest in contributing to liquidctl! There are many ways to contribute and we appreciate all of them. We ask you, however, to adhere to our [code of conduct]. [code of conduct]: CODE_OF_CONDUCT.md ## Reporting bugs and other issues Reporting problems with the software is an easy and very valuable way to contribute to the project. Please check the existing [issues] and, if none matches your problem, use the appropriate template to create a [new issue]. [issues]: https://github.com/liquidctl/liquidctl/issues [new issue]: https://github.com/liquidctl/liquidctl/issues/new/choose ## Contributing changes to code and/or documentation Awesome! We generally use [pull requests] to review and (assuming everything goes well) merge contributions. [pull requests]: https://github.com/liquidctl/liquidctl/pulls It is not necessary to discuss a change beforehand in an issue, but that can sometimes be helpful (e.g. to decide the best approach). You can also reach out to us and other fellow contributors on [Discord]. [Discord]: https://discord.gg/GyCBjQhqCd Please also read the following documents, they will give you a general idea of our development process and coding styling: - [Development process](docs/developer/process.md) - [Style guide](docs/developer/style-guide.md) And, depending on what you are doing, some of these may also be useful: - [Capturing USB Traffic](docs/developer/capturing-usb-traffic.md) - [Techniques for analyzing USB protocols](docs/developer/techniques-for-analyzing-usb-protocols.md) - [Porting drivers from OpenCorsairLink](docs/developer/porting-drivers-from-opencorsairlink.md) (introduces the liquidctl driver model) - other documents in [`docs/developer`](docs/developer) --- _The first sentences in this file were taken from rust-lang/rust's CONTRIBUTING.md document._ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705043660.0 liquidctl-1.15.0/LICENSE.txt0000644000175000017500000010451514550163314014471 0ustar00jonasjonas GNU 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. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/MANIFEST.in0000644000175000017500000000003414562144457014405 0ustar00jonasjonasrecursive-exclude .github * ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7387798 liquidctl-1.15.0/PKG-INFO0000644000175000017500000000641014776655074013761 0ustar00jonasjonasMetadata-Version: 2.4 Name: liquidctl Version: 1.15.0 Summary: Cross-platform tool and drivers for liquid coolers and other devices Home-page: https://github.com/liquidctl/liquidctl Author: Jonas Malaco Author-email: jonas@protocubo.io Project-URL: Documentation, https://github.com/liquidctl/liquidctl/blob/main/README.md Project-URL: Changelog, https://github.com/liquidctl/liquidctl/blob/main/CHANGELOG.md Project-URL: Source, https://github.com/liquidctl/liquidctl Keywords: aio,aquacomputer,asus,clc,cli,corsair,cross-platform,dram,driver,evga,fan-controller,gigabyte,hue2,kraken,led-controller,liquid-cooler,nzxt,power-supply,smart-device Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: System :: Hardware :: Hardware Drivers Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE.txt Requires-Dist: colorlog Requires-Dist: crcmod==1.7 Requires-Dist: docopt Requires-Dist: hidapi Requires-Dist: pyusb Requires-Dist: pillow Requires-Dist: libusb-package; sys_platform == "win32" or sys_platform == "cygwin" Requires-Dist: winusbcdc>=1.5; sys_platform == "win32" Requires-Dist: smbus; sys_platform == "linux" Dynamic: license-file # liquidctl – liquid cooler control Cross-platform tool and drivers for liquid coolers and other devices. Go to the [project homepage] for more information. ``` $ liquidctl list Device #0: Corsair Vengeance RGB DIMM2 Device #1: Corsair Vengeance RGB DIMM4 Device #2: NZXT Smart Device (V1) Device #3: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl initialize all NZXT Smart Device (V1) ├── Firmware version 1.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip └── LED count (total) 20 NZXT Kraken X (X42, X52, X62 or X72) └── Firmware version 6.2 # liquidctl status NZXT Smart Device (V1) ├── Fan 1 speed 1499 rpm ├── Fan 1 voltage 11.91 V ├── Fan 1 current 0.05 A ├── Fan 1 control mode PWM ├── Fan 2 [...] ├── Fan 3 [...] └── Noise level 61 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 34.7 °C ├── Fan speed 798 rpm └── Pump speed 2268 rpm # liquidctl status --match vengeance --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 └── Temperature 37.5 °C Corsair Vengeance RGB DIMM4 └── Temperature 37.8 °C # liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 # liquidctl --match kraken set pump speed 70 # liquidctl --match kraken set sync color fixed 0080ff # liquidctl --match "smart device" set led color moving-alternating "hsv(30,98,100)" "hsv(30,98,10)" --speed slower ``` [project homepage]: https://github.com/liquidctl/liquidctl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525812.0 liquidctl-1.15.0/README.md0000644000175000017500000010712714776654764014157 0ustar00jonasjonas# liquidctl – liquid cooler control _Cross-platform tool and drivers for liquid coolers and other devices_ [![Status of the tests](https://github.com/liquidctl/liquidctl/workflows/tests/badge.svg)](https://github.com/liquidctl/liquidctl/commits/main) [![Developer's Discord server](https://img.shields.io/discord/780568774964805672)](https://discord.gg/GyCBjQhqCd) [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/4949/badge)](https://bestpractices.coreinfrastructure.org/projects/4949) --- Notice: please check out our [open invitation for new team members](https://github.com/liquidctl/liquidctl/issues/569). --- ``` $ liquidctl list Device #0: Corsair Vengeance RGB DIMM2 Device #1: Corsair Vengeance RGB DIMM4 Device #2: NZXT Smart Device (V1) Device #3: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl initialize all NZXT Smart Device (V1) ├── Firmware version 1.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip └── LED count (total) 20 NZXT Kraken X (X42, X52, X62 or X72) └── Firmware version 6.2 # liquidctl status NZXT Smart Device (V1) ├── Fan 1 speed 1499 rpm ├── Fan 1 voltage 11.91 V ├── Fan 1 current 0.05 A ├── Fan 1 control mode PWM ├── Fan 2 [...] ├── Fan 3 [...] └── Noise level 61 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 34.7 °C ├── Fan speed 798 rpm └── Pump speed 2268 rpm # liquidctl status --match vengeance --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 └── Temperature 37.5 °C Corsair Vengeance RGB DIMM4 └── Temperature 37.8 °C # liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 # liquidctl --match kraken set pump speed 70 # liquidctl --match kraken set sync color fixed 0080ff # liquidctl --match "smart device" set led color moving-alternating "hsv(30,98,100)" "hsv(30,98,10)" --speed slower ``` ## Contents [Contents]: #contents 1. [Supported devices] 1. [Installation] 1. [Linux distributions] 1. [macOS Homebrew] 2. [FreeBSD and DragonFly BSD Ports] 1. [Manual installation] 1. [Linux dependencies] 1. [macOS system dependencies] 1. [Windows system dependencies] 1. [Creating a virtual environment] 1. [Installing from PyPI or GitHub] 1. [Allowing access to the devices] 1. [Additional files] 1. [Working locally] 1. [The command-line interface] 1. [Listing and selecting devices] 1. [Initializing and interacting with devices] 1. [Supported color specification formats] 1. [Using liquidctl in other programs and scripts] 1. [Automation and running at boot] 1. [Set up Linux using systemd] 1. [Set up Windows using Task Scheduler] 1. [Set up macOS using various methods] 1. [Troubleshooting] 1. [Additional documentation] 1. [License] 1. [Related projects] ## Supported devices [Supported devices]: #supported-devices The following devices are supported by liquidctl. In the table, MRLV stands for the _minimum recommended liquidctl version._ The linked documents contain specific usage instructions and other useful information. | Type | Device family and specific documentation | Notes | MRLV | | :-- | :-- | --: | :-: | | AIO liquid cooler | [ASUS Ryujin II 360](docs/asus-ryujin-guide.md) | _p_ | 1.14.0 | | AIO liquid cooler | [Corsair Hydro H110i GT](docs/coolit-guide.md) | _p_ | 1.14.0 | | AIO liquid cooler | [Corsair Hydro H80i GT, H100i GTX, H110i GTX](docs/asetek-690lc-guide.md) | _Z_ | 1.9.1 | | AIO liquid cooler | [Corsair Hydro H80i v2, H100i v2, H115i](docs/asetek-690lc-guide.md) | _Z_ | 1.9.1 | | AIO liquid cooler | [Corsair Hydro Pro H100i, H115i, H150i](docs/asetek-pro-guide.md) | _Z_ | 1.9.1 | | AIO liquid cooler | [Corsair Hydro Platinum H100i, H100i SE, H115i](docs/corsair-platinum-pro-xt-guide.md) | | 1.8.1 | | AIO liquid cooler | [Corsair Hydro Pro XT H60i, H100i, H115i, H150i](docs/corsair-platinum-pro-xt-guide.md) | | 1.8.1 | | AIO liquid cooler | [Corsair iCUE Elite Capellix H100i, H115i, H150i](docs/corsair-commander-core-guide.md) | _Bp_ | 1.14.0 | | AIO liquid cooler | [Corsair iCUE Elite RGB H100i, H115i, H150i](docs/corsair-platinum-pro-xt-guide.md) | | 1.14.0 | | AIO liquid cooler | [EVGA CLC 120 (CL12), 240, 280, 360](docs/asetek-690lc-guide.md) | _Z_ | 1.9.1 | | AIO liquid cooler | [MSI MPG Coreliquid K360](docs/msi-mpg-coreliquid-guide.md) | _p_ | 1.14.0 | | AIO liquid cooler | [NZXT Kraken M22](docs/kraken-x2-m2-guide.md) | | 1.10.0 | | AIO liquid cooler | [NZXT Kraken X40, X60](docs/asetek-690lc-guide.md) | _LZ_ | 1.9.1 | | AIO liquid cooler | [NZXT Kraken X31, X41, X61](docs/asetek-690lc-guide.md) | _LZ_ | 1.9.1 | | AIO liquid cooler | [NZXT Kraken X42, X52, X62, X72](docs/kraken-x2-m2-guide.md) | _h_ | 1.11.1 | | AIO liquid cooler | [NZXT Kraken X53, X63, X73](docs/kraken-x3-z3-guide.md) | _h_ | 1.11.1 | | AIO liquid cooler | [NZXT Kraken Z53, Z63, Z73](docs/kraken-x3-z3-guide.md) | _h_ | 1.14.0 | | AIO liquid cooler | [NZXT Kraken 2023 Standard, Elite](docs/kraken-x3-z3-guide.md) | _p_ | 1.14.0 | | AIO liquid cooler | [NZXT Kraken 2024 Elite RGB](docs/kraken-x3-z3-guide.md) | __ | 1.15.0 | | Pump controller | [Aquacomputer D5 Next](docs/aquacomputer-d5next-guide.md) | _hp_ | 1.15.0 | | Fan/LED controller | [Aquacomputer Octo](docs/aquacomputer-octo-guide.md) | _hp_ | 1.15.0 | | Fan/LED controller | [Aquacomputer Quadro](docs/aquacomputer-quadro-guide.md) | _hp_ | 1.15.0 | | Fan/LED controller | [Corsair Commander Pro](docs/corsair-commander-guide.md) | _h_ | 1.11.1 | | Fan/LED controller | [Corsair Commander Core, Core XT, ST](docs/corsair-commander-core-guide.md) | _Bp_ | 1.14.0 | | Fan/LED controller | [Corsair Lighting Node Core, Pro](docs/corsair-commander-guide.md) | | 1.8.1 | | Fan/LED controller | [Corsair Obsidian 1000D](docs/corsair-commander-guide.md) | | 1.9.1 | | Fan/LED controller | [NZXT Grid+ V3](docs/nzxt-smart-device-v1-guide.md) | _h_ | 1.11.1 | | Fan/LED controller | [NZXT HUE 2, HUE 2 Ambient](docs/nzxt-hue2-guide.md) | | 1.7.2 | | Fan/LED controller | [NZXT RGB & Fan Controller](docs/nzxt-hue2-guide.md) | _h_ | 1.11.1 | | Fan/LED controller | [NZXT RGB & Fan Controller (3+6 channels)](docs/nzxt-hue2-guide.md) | _hp_ | 1.12.1 | | Fan/LED controller | [NZXT Smart Device](docs/nzxt-smart-device-v1-guide.md) | _h_ | 1.11.1 | | Fan/LED controller | [NZXT Smart Device V2](docs/nzxt-hue2-guide.md) | _h_ | 1.11.1 | | Fan/LED controller | [NZXT H1 V2](docs/nzxt-hue2-guide.md) | | 1.10.0 | | DDR4 memory | [Corsair Vengeance RGB](docs/ddr4-guide.md) | _Uax_ | 1.7.2 | | DDR4 memory | [Generic DDR4 temperature sensor](docs/ddr4-guide.md) | _Uax_ | 1.8.1 | | Power supply | [Corsair HX750i, HX850i, HX1000i, HX1200i, HX1500i](docs/corsair-hxi-rmi-psu-guide.md) | _h_ | 1.14.0 | | Power supply | [Corsair ATX 3.1 HX1200i](docs/corsair-hxi-rmi-psu-guide.md) | _h_ | 1.15.0 | | Power supply | [Corsair RM650i, RM750i, RM850i, RM1000i](docs/corsair-hxi-rmi-psu-guide.md) | _h_ | 1.14.0 | | Power supply | [NZXT E500, E650, E850](docs/nzxt-e-series-psu-guide.md) | _p_ | 1.7.2 | | LED controller | [Aquacomputer Farbwerk 360](docs/aquacomputer-farbwerk360-guide.md) | _hp_ | 1.15.0 | | Graphics card RGB | [Select ASUS GTX and RTX cards](docs/nvidia-guide.md) | _Ux_ | 1.9.1 | | Graphics card RGB | [Select EVGA GTX 1070, 1070 Ti and 1080 cards](docs/nvidia-guide.md) | _Ux_ | 1.9.1 | | Motherboard RGB | [ASUS Aura LED motherboards](docs/asus-aura-led-guide.md) | | 1.10.0 | | Motherboard RGB | [Gigabyte RGB Fusion 2.0 motherboards](docs/gigabyte-rgb-fusion2-guide.md) | | 1.5.2 | _B_ _Broken in at least one significant way._
_L_ _Requires the `--legacy-690lc` flag._
_U_ _Requires `--unsafe` features._
_Z_ _Requires replacing the device driver [on Windows][Windows system dependencies]._
_a_ _Architecture-specific limitations._
_h_ _Can leverage hwmon driver._
_p_ _Only partially supported._
_x_ _Only supported on Linux._
## Installation [Installation]: #installation The following sections cover the various methods to set up liquidctl. ### Linux distributions [Linux distributions]: #linux-distributions A considerable number of Linux distributions already package liquidctl, generally at fairly recent versions. ```bash # Alpine sudo apk add liquidctl # Arch/Artix/[Manjaro]/Parabola sudo pacman -S liquidctl # Fedora sudo dnf install liquidctl # Manjaro sudo pamac install liquidctl # Nix nix-env -iA nixos.liquidctl ``` liquidctl is also available in some non-official/community-based repositories, as well as, at older versions, for more distributions. [Repology] shows more information about the packaging status in various distributions. [Repology]: https://repology.org/project/liquidctl/versions ### macOS Homebrew [macOS Homebrew]: #macos-homebrew For macOS, liquidctl is available on Homebrew, generally at the most recent version. It is also easy to install the latest development snapshot from the official source code repository. ```bash # latest stable version brew install liquidctl # or latest development snapshot from the official source code repository brew install liquidctl --HEAD ``` ### FreeBSD and DragonFly BSD Ports [FreeBSD and DragonFly BSD Ports]: #freebsd-and-dragonfly-bsd-ports On FreeBSD and DragonFly BSD, liquidctl is maintained in the Ports Collections, and is available as a pre-built binary package. ``` pkg install py39-liquidctl ``` ### Manual installation [Manual installation]: #manual-installation _Warning: on systems that still default to Python 2, replace `python` with `python3`._ _Changed in 1.9.0: liquidctl now uses a PEP 517 build system._
liquidctl can be manually installed from the Python Package Index (PyPI), or directly from the source code repository. In order to manually install it, certain system-level dependencies must be satisfied first. In some cases it may also be preferable to use the Python libraries already provided by the operating system. #### Linux dependencies [Linux dependencies]: #linux-dependencies On Linux, the following dependencies are required at runtime (common package names are listed in parenthesis): - Python 3.9 or later _(python3, python)_ - pkg\_resources Python package _(python3-setuptools, python3-pkg-resources, python-setuptools)_ - PyUSB _(python3-pyusb, python3-usb, python-pyusb)_ - colorlog _(python3-colorlog, python-colorlog)_ - crcmod 1.7 _(python3-crcmod, python-crcmod)_ - cython-hidapi _(python3-hidapi, python3-hid, python-hidapi)_ - docopt _(python3-docopt, python-docopt)_ - pillow _(python-pillow, python3-pil)_ - smbus Python package _(python3-i2c-tools, python3-smbus, i2c-tools)_ - LibUSB 1.0 _(libusb-1.0, libusb-1.0-0, libusbx)_ Additionally, to build, install and test liquidctl, the following are also needed: - setuptools\_scm Python package _(python3-setuptools-scm, python3-setuptools_scm, python-setuptools-scm)_ - pip (optional) _(python3-pip, python-pip)_ - pytest (optional) _(python3-pytest, pytest, python-pytest)_ #### macOS system-level dependencies [macOS system dependencies]: #macos-system-level-dependencies On macOS, Python (3.9 or later) and LibUSB 1.0 must be installed beforehand. ``` brew install python libusb ``` #### Windows system-level dependencies [Windows system dependencies]: #windows-system-level-dependencies On Windows, Python (3.9 or later) must be installed beforehand, which can be done from the [official website][python.org]. It is recommended to select the option to add `python` and other tools to the `PATH`. A LibUSB 1.0 DLL is also necessary, but it will generally be provided automatically by liquidctl. In case that's not possible, and a USB "No backend available" error is shown, the suitable DLL from an official [LibUSB release] should be copied into `C:\Windows\System32\`. The DLL must match your Python installation: in most cases it will be latest VS build for x64 in the archive from LibUSB (e.g. `VS2015-x64/dll/libusb-1.0.dll`). Additionally, products that are not Human Interface Devices (HIDs), or that do not use the Microsoft HID Driver, require a libusb-compatible driver (these are listed in [Supported devices] with a `Z` note). In most cases of these cases the Microsoft WinUSB driver is recommended, and it can easily be set up for a device using [Zadig]: open Zadig, select your device from the dropdown list and, finally, click "Replace Driver". _Warning: replacing the driver for a device where that is not necessary will likely cause it to become inaccessible from liquidctl._
_Changed in 1.9.0: a LibUSB 1.0 DLL is now provided by libusb-package, provided there are suitable wheels available at the time of installation._
[python.org]: https://www.python.org/ [LibUSB release]: https://github.com/libusb/libusb/releases [Zadig]: https://zadig.akeo.ie/ #### Creating a virtual environment [Creating a virtual environment]: #creating-a-virtual-environment Setting up a virtual environment is an optional step. Even so, installing Python packages directly in the global environment is not generally advised. Instead, it is usual to first set up a [virtual environment]: ```bash # create virtual enviroment at python -m venv ``` Once set up, the virtual environment can be activated on the current shell (more information in the [official documentation][virtual environment]). Alternatively, the virtual environment can also be used directly, without activation, by prefixing all `python` invocations with the environment's bin directory. ```bash # Linux/macOS/BSDs (POSIX) /bin/python [arguments] # Windows \Scripts\python [arguments] ``` [virtual environment]: https://docs.python.org/3/library/venv.html #### Installing from PyPI or GitHub [Installing from PyPI or GitHub]: #installing-from-pypi-or-github [pip] can be used to install liquidctl from the Python Package Index (PyPI). This will also install the necessary Python libraries. ```bash # the latest stable version python -m pip install liquidctl # a specific version (e.g. 1.15.0) python -m pip install liquidctl==1.15.0 ``` If [git] is installed, pip can also install the latest snapshot of the official liquidctl source code repository on GitHub. ```bash # the latest snapshot of the official source code repository (requires git) python -m pip install git+https://github.com/liquidctl/liquidctl#egg=liquidctl ``` [git]: https://git-scm.com/ [pip]: https://pip.pypa.io/en/stable/ To set up rootless access to devices on Linux and BSDs, install documentation and completions, or to learn more about auxiliary scripts, continue reading for the next few sections. #### Allowing access to the devices [Allowing access to the devices]: #allowing-access-to-the-devices Access permissions are not a concern on platforms like macOS or Windows, where unprivileged access is already allowed by default. However, devices are not generally accessible by unprivileged users on Linux, FreeBSD or DragonFly BSD. For Linux, we provide a set of udev rules in [`71-liquidctl.rules`] that can be used to allow unprivileged read and write access to all devices supported by liquidctl. These rules are generally already included in downstream Linux packages of liquidctl. Alternatively, `sudo`, `doas` and similar mechanisms can be used to invoke `liquidctl` as the super user, on both Linux and BSDs. [`71-liquidctl.rules`]: extra/linux/71-liquidctl.rules #### Additional files [Additional files]: #additional-files Other files and tools are included in the source tree, which may be of use in certain scenarios: - [liquidctl(8) man page][liquidctl.8]; - [completions for the liquidctl CLI in Bash][liquidctl.bash]; - [host-based automatic fan/pump speed control][yoda.py]; - [send liquidctl data to HWiNFO][LQiNFO.py]; - [and more...][extra/]. [LQiNFO.py]: extra/windows/LQiNFO.py [extra/]: extra/ [liquidctl.8]: liquidctl.8 [liquidctl.bash]: extra/completions/liquidctl.bash [yoda.py]: extra/yoda.py ### Working locally [Working locally]: #working-locally _Changed in 1.9.0: liquidctl now uses a PEP 517 build system._
When working on the project itself, it is sometimes useful to set up a local development environment, making it possible to directly run the CLI and the test suite, without first building and installing a local package. For this, start by installing [git] and any system-level dependencies mentioned in [Manual installation]. Then, clone the repository and change into the created directory: ``` git clone https://github.com/liquidctl/liquidctl cd liquidctl ``` Optionally, set up a [virtual environment][Creating a virtual environment]. Finally, if the necessary Python build, test and runtime libraries are not already installed on the environment (virtual or global), manually install them: ``` python -m pip install --upgrade pip setuptools setuptools_scm wheel python -m pip install --upgrade colorlog crcmod==1.7 docopt hidapi pillow pytest pyusb python -m pip install --upgrade "libusb-package; sys_platform == 'win32' or sys_platform == 'cygwin'" python -m pip install --upgrade "smbus; sys_platform == 'linux'" python -m pip install --upgrade "winusbcdc>=1.5; sys_platform == 'win32'" ``` At this point, the environment is set up. To run the test suite, execute: ``` python -m pytest ``` To run the CLI directly, without building and installing a local package, execute: ``` python -m liquidctl [arguments] ``` And to install `liquidctl` into the environment: ``` python -m pip install . ``` ## Introducing the command-line interface [The command-line interface]: #introducing-the-command-line-interface The complete list of commands and options can be found in `liquidctl --help` and in the man page, but the following topics cover the most common operations. Brackets `[ ]`, parenthesis `( )`, less than/greater than `< >` and ellipsis `...` are used to describe, respectively, optional, required, positional and repeating elements. Example commands are prefixed with a number sign `#`, which also serves to indicate that on Linux root permissions (or suitable udev rules) may be required. The `--verbose` option will print some extra information, like automatically made adjustments to user-provided settings. And if there is a problem, the `--debug` flag will make liquidctl output more information to help identify its cause; be sure to include this when opening a new issue. _Note: in addition to `--debug`, setting the `PYUSB_DEBUG=debug` and `LIBUSB_DEBUG=4` environment variables can be helpful with problems suspected to relate to PyUSB or LibUSB._ ### Listing and selecting devices [Listing and selecting devices]: #listing-and-selecting-devices A good place to start is to ask liquidctl to list all recognized devices. ``` $ liquidctl list Device #0: NZXT Smart Device (V1) Device #1: NZXT Kraken X (X42, X52, X62 or X72) ``` In case more than one supported device is found, one them can be selected with `--match `, where `` matches part of the desired device's description using a case insensitive comparison. ``` $ liquidctl --match kraken list Result #0: NZXT Kraken X (X42, X52, X62 or X72) ``` More device properties can be show by passing `--verbose` to `liquidctl list`. Any of those can also be used to select a particular product. ``` $ liquidctl --bus hid --address /dev/hidraw4 list Result #0: NZXT Smart Device (V1) $ liquidctl --serial 1234567890 list Result #0: NZXT Kraken X (X42, X52, X62 or X72) ``` Ambiguities for any given filter can be solved with `--pick `. ### Initializing and interacting with devices [Initializing and interacting with devices]: #initializing-and-interacting-with-devices Devices will usually need to be initialized before they can be used, though each device has its own requirements and limitations. This and other information specific to a particular device will appear on the documentation linked from the [Supported devices] section. Devices can be initialized individually or all at once. ``` # liquidctl [options] initialize [all] ``` Most devices provide some status information, like fan speeds and liquid temperatures. This can be queried for all devices or using the filtering methods mentioned before. ``` # liquidctl [options] status ``` Fan and pump speeds can be set to fixed values or, if the device supports them, custom profiles. The specific documentation for each device will list the available modes, as well as which sensor is used for custom profiles. In general, liquid coolers only support custom profiles that are based on the internal liquid temperature probe. ``` # liquidctl [options] set speed ( ) ... # liquidctl [options] set speed ``` Lighting is controlled in a similar fashion. The specific documentation for each device will list the available channels, modes and additional options. ``` # liquidctl [options] set color [] ... ``` ### Supported color specification formats [Supported color specification formats]: #supported-color-specification-formats When configuring lighting effects, colors can be specified in different representations and formats: - as an implicit hexadecimal RGB triple, either with or without the `0x` prefix: e.g. `ff7f3f` - as an explicit RGB triple: e.g. `rgb(255, 127, 63)` - as a HSV (hue‑saturation‑value) triple: e.g. `hsv(20, 75, 100)` * hue ∊ [0, 360] (degrees); saturation, value ∊ [0, 100] (percent) * note: this is sometimes called HSB (hue‑saturation‑brightness) - as a HSL (hue‑saturation‑lightness) triple: e.g. `hsl(20, 100, 62)` * hue ∊ [0, 360] (degrees); saturation, lightness ∊ [0, 100] (percent) Color arguments containing spaces, parenthesis or commas need to be quoted, as these characters can have special meaning on the command-line; the easiest way to do this on all supported platforms is with double quotes. ``` # liquidctl --match kraken set ring color fading "hsv(0,80,100)" "hsv(180,80,100)" ``` On Linux it is also possible to use single-quotes and `\(`, `\)`, `\ ` escape sequences. ## Using liquidctl in other programs and scripts [Using liquidctl in other programs and scripts]: #using-liquidctl-in-other-programs-and-scripts The liquidctl driver APIs can be used to build Python programs that monitor or control the devices, and offer features beyond the ones provided by the CLI. The APIs are documented, and this documentation can be accessed through `pydoc`, or directly read from the source files. ```python from liquidctl import find_liquidctl_devices # Find all connected and supported devices. devices = find_liquidctl_devices() for dev in devices: # Connect to the device. In this example we use a context manager, but # the connection can also be manually managed. The context manager # automatically calls `disconnect`; when managing the connection # manually, `disconnect` must eventually be called, even if an # exception is raised. with dev.connect(): print(f'{dev.description} at {dev.bus}:{dev.address}:') # Devices should be initialized after every boot. In this example # we assume that this has not been done before. print('- initialize') init_status = dev.initialize() # Print all data returned by `initialize`. if init_status: for key, value, unit in init_status: print(f'- {key}: {value} {unit}') # Get regular status information from the device. status = dev.get_status() # Print all data returned by `get_status`. print('- get status') for key, value, unit in status: print(f'- {key}: {value} {unit}') # For a particular device, set the pump LEDs to red. if 'Kraken' in dev.description: print('- set pump to radical red') radical_red = [0xff, 0x35, 0x5e] dev.set_color(channel='pump', mode='fixed', colors=[radical_red]) ``` More examples can be found in the scripts in [`extra/`](extra/). In addition to the APIs, the `liquidctl` CLI is friendly to scripting: errors cause it to exit with non-zero codes and only functional output goes to `stdout`, everything else (error messages, warnings and other auxiliary information) going to `stderr`. The `list`, `initialize` and `status` commands also support a `--json` flag to switch the output to JSON, a more convenient format for machines and scripts. In `--json` mode, setting `LANG=C` on the environment causes non-ASCII characters to be escaped. ``` # liquidctl --match kraken list --json | jq [ { "description": "NZXT Kraken X (X42, X52, X62 or X72)", "vendor_id": 7793, "product_id": 5902, "release_number": 512, "serial_number": "49874481333", "bus": "hid", "address": "/dev/hidraw3", "port": null, "driver": "Kraken2", }, ... ] # liquidctl --match kraken status --json | jq [ { "bus": "hid", "address": "/dev/hidraw3", "description": "NZXT Kraken X (X42, X52, X62 or X72)", "status": [ { "key": "Liquid temperature", "value": 30.1, "unit": "°C" }, { "key": "Fan speed", "value": 1014, "unit": "rpm" }, ... ] }, ... ] ``` Note that the examples above pipe the output to [jq], as the original output has no line breaks or indentation. An alternative to jq is to use [`python -m json.tool`][json.tool], which is already included in standard Python distributions. Finally, the stability of both the APIs and the CLI commands is documented in our [stability guarantee]. In particular, the specific keys, values and units returned by the commands above, as well as their API equivalents, _are subject to changes._ Consumers should verify that the returned data matches their expectations, and react accordingly. [jq]: https://stedolan.github.io/jq/ [json.tool]: https://docs.python.org/3/library/json.html#module-json.tool [stability guarantee]: docs/developer/process.md#stability-and-backward-compatibility ## Automation and running at boot [Automation and running at boot]: #automation-and-running-at-boot In most cases you will want to automatically apply your settings when the system boots. Generally a simple script or a basic service is enough, and some specifics about this are given in the following sections. For even more flexibility, you can also write a Python program that calls the driver APIs directly. ### Set up Linux using systemd [Set up Linux using systemd]: #set-up-linux-using-systemd On systems running Linux and systemd a service unit can be used to configure liquidctl devices. A simple example is provided bellow, which you can edit to match your preferences. Save it to `/etc/systemd/system/liquidcfg.service`. ``` [Unit] Description=AIO startup service [Service] Type=oneshot ExecStart=liquidctl initialize all ExecStart=liquidctl --match kraken set pump speed 90 ExecStart=liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 ExecStart=liquidctl --match "smart device" set sync speed 55 ExecStart=liquidctl --match kraken set sync color fading 350017 ff2608 [Install] WantedBy=default.target ``` After reloading the configuration, the new unit can be started manually or set to automatically run during boot using standard systemd tools. ``` # systemctl daemon-reload # systemctl start liquidcfg # systemctl enable liquidcfg ``` A slightly more complex example can be seen at [jonasmalacofilho/dotfiles](https://github.com/jonasmalacofilho/dotfiles/tree/master/liquidctl), which includes dynamic adjustments of the lighting depending on the time of day. If necessary, it is also possible to have the service unit explicitly wait for the device to be available: see [making systemd units wait for devices](docs/linux/making-systemd-units-wait-for-devices.md). ### Set up Windows using Task Scheduler [Set up Windows using Task Scheduler]: #set-up-windows-using-task-scheduler The configuration of devices can be automated by writing a batch file and setting up a new task for (every) login using Windows Task Scheduler. The batch file can be really simple and only needs to contain the invocations of liquidctl that would otherwise be done manually. ```batchfile liquidctl initialize all liquidctl --match kraken set pump speed 90 liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 liquidctl --match "smart device" set sync speed 55 liquidctl --match kraken set sync color fading 350017 ff2608 ``` Make sure that liquidctl is available in the context where the batch file will run: in short, `liquidctl --version` should work within a _normal_ Command Prompt window. You may need to install Python with the option to set the PATH variable enabled, or manually add the necessary folders to the PATH. A slightly more complex example can be seen in [issue #14](https://github.com/liquidctl/liquidctl/issues/14#issuecomment-456519098) ("Can I autostart liquidctl on Windows?"), that uses the LEDs to convey progress or eventual errors. Chris' guide on [Replacing NZXT’s CAM software on Windows for Kraken](https://codecalamity.com/replacing-nzxts-cam-software-on-windows-for-kraken/) is also a good read. As an alternative to using Task Scheduler, the batch file can simply be placed in the startup folder; you can run `shell:startup` to [find out where that is](https://support.microsoft.com/en-us/help/4026268/windows-10-change-startup-apps). ### Set up macOS using various methods [Set up macOS using various methods]: #set-up-macos-using-various-methods You can follow either or both of the guides below to automatically configure your devices during login or after waking from sleep. The guides are hosted on tonymacx86: - [This guide](https://www.tonymacx86.com/threads/gigabyte-z490-vision-d-thunderbolt-3-i5-10400-amd-rx-580.298642/post-2138475) is for controllers that lose their state during sleep (e.g. Gigabyte RGB Fusion 2.0) and need to be reinitialized after wake-from-sleep. This guide uses _Automator_ to initialize supported devices at login, and _sleepwatcher_ to initialize supported devices after wake-from-sleep. - [This guide](https://www.tonymacx86.com/threads/asus-z690-proart-creator-wifi-thunderbolt-4-i7-12700k-amd-rx-6800-xt.318311/post-2306524) is for controllers that do not lose their state during sleep (e.g. ASUS Aura LED). This driver uses the _launchctl_ method to initialize supported devices at login. ## Troubleshooting [Troubleshooting]: #troubleshooting ### Device not listed (Windows) This is likely caused by having replaced the standard driver of a USB HID. If the device in question is not marked in [Supported devices] as requiring a special driver, try uninstalling the custom driver. ### Device not listed (Linux) First, check that the liquidctl version (`liquidctl --version`) you're running supports the device in question. An overview of supported devices and minimum recommended versions for them are available in [Supported devices]. Support for new devices can happen to be available only in Git for some time. If the device is supported, this is usually caused by having an unexpected kernel driver bound to a USB HID. In most cases this is the result of having used a program that accessed the device (directly or indirectly) via libusb-1.0, but failed to reattach the original driver before terminating. This can be temporarily solved by manually rebinding the device to the kernel `usbhid` driver. Replace `` and `` with the correct values from `lsusb -vt` (also assumes there is only HID interface, adjust if necessary): ``` echo '-:1.0' | sudo tee /sys/bus/usb/drivers/usbhid/bind ``` A more permanent solution is to politely ask the authors of the program that is responsible for leaving the kernel driver detached to use `libusb_attach_kernel_driver` or `libusb_set_auto_detach_kernel_driver`. ### Access denied or open failed (Linux) These errors are usually caused by a lack of permission to access the device. On Linux distros that normally requires root privileges. Alternatively to running liquidctl as root (or with `sudo`), you can install the udev rules provided in [`extra/linux/71-liquidctl.rules`](extra/linux/71-liquidctl.rules) to allow unprivileged access to the devices supported by liquidctl. ### Other problems If your problem is not listed here, try searching the [issues](https://github.com/liquidctl/liquidctl/issues). If no issue matches your problem, you still need help, or you have found a bug, please open one. When commenting on an issue, please describe the problem in as much detail as possible. List your operating system and the specific devices you own. Also include the arguments and output of all relevant/failing liquidctl commands, using the `--debug` option to enable additional debug information. ## Additional documentation [Additional documentation]: #additional-documentation Be sure to browse [`docs/`](docs/) for additional documentation, and [`extra/`](extra/) for some example scripts and other possibly useful things. You are also encouraged to contribute to the documentation and to these examples, including adding new files that cover your specific use cases or solutions. ## License [License]: #license Copyright 2018–2023 Jonas Malaco, Marshall Asch, CaseySJ, Tom Frey, Andrew Robertson, ParkerMc, Aleksa Savic, Shady Nawara and contributors Some modules also incorporate or use as reference work by leaty, Ksenija Stanojevic, Alexander Tong, Jens Neumaier, Kristóf Jakab, Sean Nelson, Chris Griffith, notaz, realies and Thomas Pircher. This is mentioned in the module docstring, along with appropriate additional copyright notices. This program is free software: you can redistribute it and/or modify it under the terms of the GNU 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . ## Related projects [Related projects]: #related-projects ### [liquidctl/liquidtux](https://github.com/liquidctl/liquidtux) Sibling project of Linux kernel _hwmon_ drivers for devices supported by liquidctl. ### [coolercontrol/coolercontrol](https://gitlab.com/coolercontrol/coolercontrol) Graphical interface to monitor and control cooling devices supported by liquidctl. ### [CalcProgrammer1/OpenRGB](https://gitlab.com/CalcProgrammer1/OpenRGB) Graphical interface to control many different types of RGB devices. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/SECURITY.md0000644000175000017500000000146214562144457014446 0ustar00jonasjonas# Security policy ## Reporting a vulnerability If you have found a vulnerability or suspect a security problem in liquidctl, please write to . You can use PGP-encrypted email: the public key is [`5841AF7406AF7AD0`](http://jonasmalaco.com/06af7ad0.asc). Any security issues reported this way will be addressed urgently and, until responsibly disclosed, confidentially. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/conftest.py0000644000175000017500000000056014613511524015040 0ustar00jonasjonasimport sys collect_ignore = [ "setup.py", "extra/contrib/fusion_rgb_cycle.py", # depends on coloraide "extra/prometheus-liquidctl-exporter.py", # depends on prometheus_client ] if sys.platform != "linux": collect_ignore.append("tests/test_smbus.py") if sys.platform not in ["win32", "cygwin"]: collect_ignore.append("extra/windows/LQiNFO.py") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7084546 liquidctl-1.15.0/docs/0000755000175000017500000000000014776655074013613 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739674900.0 liquidctl-1.15.0/docs/README.md0000644000175000017500000000512014754252424015054 0ustar00jonasjonasContents ======== General topics -------------- - [**The project README: basic setup and use of liquidctl**](../README.md) Device guides ---------------------- - [ASUS Aura LED (USB-based) controllers](asus-aura-led-guide.md) - [ASUS Ryujin II liquid coolers](asus-ryujin-guide.md) - [Aquacomputer D5 Next watercooling pump](aquacomputer-d5next-guide.md) - [Aquacomputer Farbwerk 360 RGB controller](aquacomputer-farbwerk360-guide.md) - [Aquacomputer Octo fan controller](aquacomputer-octo-guide.md) - [Aquacomputer Quadro fan controller](aquacomputer-quadro-guide.md) - [Asetek 690LC liquid coolers](asetek-690lc-guide.md) - [Asetek Pro liquid coolers](asetek-pro-guide.md) - [Corsair Commander Core and Core XT](corsair-commander-core-guide.md) - [Corsair Commander Pro, Obsidian 1000D and Lighting Node Pro/Core](corsair-commander-guide.md) - [Corsair HXi and RMi series PSUs](corsair-hxi-rmi-psu-guide.md) - [Corsair Hydro Platinum, Pro XT and Elite RGB all-in-one liquidctl coolers](corsair-platinum-pro-xt-guide.md) - [DDR4 DIMMs](ddr4-guide.md) - [Fourth-generation (X3/Z3/2023/2024) NZXT liquid coolers](kraken-x3-z3-2023-guide.md) - [Gigabyte RGB Fusion 2.0 lighting controllers](gigabyte-rgb-fusion2-guide.md) - [NVIDIA graphics cards](nvidia-guide.md) - [NZXT E-series PSUs](nzxt-e-series-psu-guide.md) - [NZXT HUE 2 and Smart Device V2 controllers](nzxt-hue2-guide.md) - [NZXT Smart Device (V1) and Grid+ V3](nzxt-smart-device-v1-guide.md) - [Third-generation (X2,M2) NZXT liquid coolers](kraken-x2-m2-guide.md) Linux topics --------------------- - [Making systemd units wait for devices](linux/making-systemd-units-wait-for-devices.md) Windows topics ----------------------- - [Running your first command-line-program](windows/running-your-first-command-line-program.md) Documentation for developers ---------------------------- - [**Development process (including stability guarantees)**](developer/process.md) - [Capturing USB traffic](developer/capturing-usb-traffic.md) - [Interacting with devices with Linux hwmon drivers](developer/hwmon) - [Porting drivers from OpenCorsairLink](developer/porting-drivers-from-opencorsairlink.md) - [Protocol notes](developer/protocol/) - [Release checklist](developer/release-checklist.md) - [Style guide](developer/style-guide.md) - [Techniques for analyzing USB protocols](developer/techniques-for-analyzing-usb-protocols.md) - [Virtual Machine Setup for Traffic Capture](developer/creating-vm-for-capture.md) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/aquacomputer-d5next-guide.md0000644000175000017500000000462014613511524021122 0ustar00jonasjonas# Aquacomputer D5 Next watercooling pump _Driver API and source code available in [`liquidctl.driver.aquacomputer`](../liquidctl/driver/aquacomputer.py)._ _New in 1.11.0._
## Initialization Initialization is _currently_ not required, but is recommended. It outputs the firmware version: ``` # liquidctl initialize Aquacomputer D5 Next ├── Firmware version 1023 └── Serial number 03500-24905 ``` The pump automatically sends a status HID report every second as soon as it's connected. ## Monitoring The D5 Next exposes sensor values such as liquid temperature and two groups of fan sensors, for the pump and the optionally connected fan. These groups provide RPM speed, voltage, current and power readings. It also supports eight virtual temperature sensors, which are user assigned. Currently, they can only be read from the device. The pump additionally exposes +5V and +12V voltage rail readings: ``` # liquidctl status Aquacomputer D5 Next ├── Liquid temperature 26.9 °C ├── Soft. Sensor 1 50.0 °C ├── Pump speed 1968 rpm ├── Pump power 2.56 W ├── Pump voltage 12.04 V ├── Pump current 0.21 A ├── Fan speed 373 rpm ├── Fan power 0.38 W ├── Fan voltage 12.06 V ├── Fan current 0.03 A ├── +5V voltage 5.01 V └── +12V voltage 12.06 V ``` _Changed in 1.12.0: read virtual temperature sensors as well._
## Programming the fan speeds Currently, the pump and optionally connected fan can be set to a fixed duty cycle, ranging from 0-100%. ``` # liquidctl set pump speed 56 ^^^^ ^^ channel duty ``` Valid channel values on the D5 Next are `pump` and `fan`. ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers Aquacomputer devices are supported by the mainline Linux kernel with its [`aquacomputer_d5next`] driver, and status data is provided through a standard hwmon sysfs interface. Liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`aquacomputer_d5next`]: https://www.kernel.org/doc/html/latest/hwmon/aquacomputer_d5next.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/aquacomputer-farbwerk360-guide.md0000644000175000017500000000300414613511524021742 0ustar00jonasjonas# Aquacomputer Farbwerk 360 RGB controller _Driver API and source code available in [`liquidctl.driver.aquacomputer`](../liquidctl/driver/aquacomputer.py)._ _New in 1.11.0._
## Initialization Initialization is _currently_ not required, but is recommended. It outputs the firmware version: ``` # liquidctl initialize Aquacomputer Farbwerk 360 ├── Firmware version 1022 └── Serial number 16827-56978 ``` The controller automatically sends a status HID report every second as soon as it's connected. ## Monitoring The Farbwerk 360 exposes four physical and sixteen virtual temperature sensors. ``` # liquidctl status Aquacomputer Farbwerk 360 ├── Sensor 1 24.1 °C ├── Sensor 2 25.7 °C ├── Sensor 3 25.2 °C ├── Sensor 4 25.6 °C └── Soft. Sensor 1 52.0 °C ``` _Changed in 1.12.0: read virtual temperature sensors as well._
## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers Aquacomputer devices are supported by the mainline Linux kernel with its [`aquacomputer_d5next`] driver, and status data is provided through a standard hwmon sysfs interface. Liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`aquacomputer_d5next`]: https://www.kernel.org/doc/html/latest/hwmon/aquacomputer_d5next.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/aquacomputer-octo-guide.md0000644000175000017500000000605414613511524020662 0ustar00jonasjonas# Aquacomputer Octo fan controller _Driver API and source code available in [`liquidctl.driver.aquacomputer`](../liquidctl/driver/aquacomputer.py)._ _New in 1.11.0._
## Initialization Initialization is _currently_ not required, but is recommended. It outputs the firmware version: ``` # liquidctl initialize Aquacomputer Octo ├── Firmware version 1019 └── Serial number 14994-51690 ``` The Octo automatically sends a status HID report every second as soon as it's connected. ## Monitoring The Octo exposes four temperature sensors and eight groups of fan sensors for optionally connected fans. It also supports sixteen virtual temperature sensors, which are user assigned. Currently, they can only be read from the device. These groups provide RPM speed, voltage, current and power readings: ``` # liquidctl status Aquacomputer Octo ├── Sensor 1 37.0 °C ├── Soft. Sensor 2 2.9 °C ├── Soft. Sensor 8 40.7 °C ├── Fan 1 speed 0 rpm ├── Fan 1 power 0.00 W ├── Fan 1 voltage 12.09 V ├── Fan 1 current 0.00 A ├── Fan 2 speed 0 rpm ├── Fan 2 power 0.00 W ├── Fan 2 voltage 0.00 V ├── Fan 2 current 0.00 A ├── Fan 3 speed 0 rpm ├── Fan 3 power 0.00 W ├── Fan 3 voltage 0.00 V ├── Fan 3 current 0.00 A ├── Fan 4 speed 0 rpm ├── Fan 4 power 0.00 W ├── Fan 4 voltage 0.00 V ├── Fan 4 current 0.00 A ├── Fan 5 speed 0 rpm ├── Fan 5 power 0.00 W ├── Fan 5 voltage 0.00 V ├── Fan 5 current 0.00 A ├── Fan 6 speed 0 rpm ├── Fan 6 power 0.00 W ├── Fan 6 voltage 0.00 V ├── Fan 6 current 0.00 A ├── Fan 7 speed 0 rpm ├── Fan 7 power 0.00 W ├── Fan 7 voltage 0.00 V ├── Fan 7 current 0.00 A ├── Fan 8 speed 0 rpm ├── Fan 8 power 0.02 W ├── Fan 8 voltage 12.09 V └── Fan 8 current 0.00 A ``` _Changed in 1.12.0: read virtual temperature sensors as well._
## Programming the fan speeds Currently, eight optionally connected fans can be set to a fixed duty cycle, ranging from 0-100%. ``` # liquidctl set fan1 speed 56 ^^^^ ^^ channel duty ``` Valid channel values on the Octo are `fan1` through `fan8`. ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers Aquacomputer devices are supported by the mainline Linux kernel with its [`aquacomputer_d5next`] driver, and status data is provided through a standard hwmon sysfs interface. Liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`aquacomputer_d5next`]: https://www.kernel.org/doc/html/latest/hwmon/aquacomputer_d5next.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/aquacomputer-quadro-guide.md0000644000175000017500000000470314613511524021210 0ustar00jonasjonas# Aquacomputer Quadro fan controller _Driver API and source code available in [`liquidctl.driver.aquacomputer`](../liquidctl/driver/aquacomputer.py)._ _New in 1.11.0._
## Initialization Initialization is _currently_ not required, but is recommended. It outputs the firmware version: ``` # liquidctl initialize Aquacomputer Quadro ├── Firmware version 1032 └── Serial number 23410-65344 ``` The Quadro automatically sends a status HID report every second as soon as it's connected. ## Monitoring The Quadro exposes four temperature sensors and four groups of fan sensors for optionally connected fans. These groups provide RPM speed, voltage, current and power readings: ``` # liquidctl status Aquacomputer Quadro ├── Sensor 3 15.9 °C ├── Soft. Sensor 2 40.3 °C ├── Soft. Sensor 3 50.0 °C ├── Soft. Sensor 13 50.0 °C ├── Fan 1 speed 0 rpm ├── Fan 1 power 0.00 W ├── Fan 1 voltage 0.00 V ├── Fan 1 current 0.00 A ├── Fan 2 speed 0 rpm ├── Fan 2 power 0.00 W ├── Fan 2 voltage 12.07 V ├── Fan 2 current 0.00 A ├── Fan 3 speed 360 rpm ├── Fan 3 power 0.00 W ├── Fan 3 voltage 12.07 V ├── Fan 3 current 0.00 A ├── Fan 4 speed 0 rpm ├── Fan 4 power 0.00 W ├── Fan 4 voltage 12.07 V ├── Fan 4 current 0.00 A └── Flow sensor 0 dL/h ``` _Changed in 1.12.0: read virtual temperature sensors as well._
## Programming the fan speeds Currently, four optionally connected fans can be set to a fixed duty cycle, ranging from 0-100%. ``` # liquidctl set fan1 speed 56 ^^^^ ^^ channel duty ``` Valid channel values on the Quadro are `fan1` through `fan4`. ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers Aquacomputer devices are supported by the mainline Linux kernel with its [`aquacomputer_d5next`] driver, and status data is provided through a standard hwmon sysfs interface. Liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`aquacomputer_d5next`]: https://www.kernel.org/doc/html/latest/hwmon/aquacomputer_d5next.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/asetek-690lc-guide.md0000644000175000017500000001013014562144457017321 0ustar00jonasjonas# Asetek 690LC liquid coolers _Driver API and source code available in [`liquidctl.driver.asetek`](../liquidctl/driver/asetek.py)._ Several products are available that are based on the same Asetek 690LC base design: - Current models: * EVGA CLC 120 (CLC12), 240, 280 and 360 * Corsair Hydro H80i v2, H100i v2 and H115i * Corsair Hydro H80i GT, H100i GTX and H110i GTX - Legacy designs: * NZXT Kraken X40, X60, X31, X41, X51 and X61 **Note: a custom kernel driver is necessary on Windows (see: [Installing on Windows](../README.md#windows-system-level-dependencies)).** **Note: when dealing with legacy Krakens the `--legacy-690lc` flag should be supplied on all invocations of liquidctl.** ## Initialization All 690LC devices must be initialized sometime after the system boots. Only then it will be possible to query the device status and perform other operations. ``` # liquidctl initialize ``` ## Device monitoring Similarly to other AIOs, the cooler can report fan and pump speeds as well as the liquid temperature. ``` # liquidctl status Asetek 690LC (assuming EVGA CLC) ├── Liquid temperature 28.7 °C ├── Fan speed 480 rpm ├── Pump speed 1890 rpm └── Firmware version 2.10.0.0 ``` ## Fan and pump speed control Fan speeds can be configured either to fixed duty values or profiles. The profiles accept up to six (liquid temperature, duty) points, and are interpolated by the device. ``` # liquidctl set fan speed 50 # liquidctl set fan speed 20 0 40 100 ``` *Note: fan speed profiles are only supported on non-legacy models.* Pump speeds, on the other hand, only accept fixed duty values. ``` # liquidctl set pump speed 75 ``` ## Lighting modes There's a single lighting channel `logo`. The first light mode – 'rainbow' – supports an abstract `--speed` parameter, varying from 1 to 6. ``` # liquidctl set logo color rainbow # liquidctl set logo color rainbow --speed 1 # liquidctl set logo color rainbow --speed 6 ``` *Note: the 'rainbow' lighting mode is currently only supported by EVGA units.* The 'fading' mode supports specifying the `--time-per-color` in seconds. The defaults are 1 and 5 seconds per color for, respectively, modern and legacy coolers. ``` # liquidctl set logo color fading ff8000 00ff80 # liquidctl set logo color fading ff8000 00ff80 --time-per-color 2 ``` The 'blinking' mode accepts both `--time-per-color` and `--time-off` (also in seconds). The default is 1 second for each, and whenever unspecified `--time-off` will equal `--time-per-color`. ``` # liquidctl set logo color blinking 8000ff # liquidctl set logo color blinking 8000ff --time-off 2 # liquidctl set logo color blinking 8000ff --time-per-color 2 # liquidctl set logo color blinking 8000ff --time-per-color 2 --time-off 1 ``` The coolers support two more lighting modes: 'fixed' and 'blackout'. The latter is the only one to completely turn off the LED; however, it also inhibits the visual high-temperature alert. ``` # liquidctl set logo color fixed 00ff00 # liquidctl set logo color blackout ``` On most models, alerts are always enabled, unless suppressed by the 'blackout' mode: the default threshold and color are, respectively, 45°C and red. It is possible to configure the visual alert for high liquid temperatures: - `--alert-threshold `: set the threshold temperature in Celsius for a visual alert; - `--alert-color `: set the color used by the visual high temperature alert. Note that alerts are reportedly not supported by the oldest models, the NZXT Kraken X40 and X60 (see [#477]). [#477]: https://github.com/liquidctl/liquidctl/issues/477 ## Save settings to device _New in 1.9.0._
Use the `--non-volatile` option on a `set` command to save the current values of _all_ settings (pump speed, fan cruve, and light mode) to the device's on-board flash memory: ``` # liquidctl set logo color blackout # liquidctl set pump speed 75 # liquidctl --non-volatile set fan speed 50 ``` Flash write-cycles are limited so avoid using `--non-volatile` on every command. *Note: non-volatile settings are only supported on non-legacy models.* ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/asetek-pro-guide.md0000644000175000017500000000520514562144457017273 0ustar00jonasjonas# Asetek Pro liquid coolers _Driver API and source code available in [`liquidctl.driver.asetek_pro`](../liquidctl/driver/asetek_pro.py)._ These coolers are more commonly known as the Corsair Hydro Pro family (not to be confused with the Platinum or Pro XT families): - Corsair Hydro H100i Pro - Corsair Hydro H115i Pro - Corsair Hydro H150i Pro **Note: a custom kernel driver is necessary on Windows (see: [Installing on Windows](../README.md#windows-system-level-dependencies)).** ## Initialization [Initialization]: #initialization _Changed in 1.12.0: the firmware version is now reported after initialization._
The coolers must be initialized sometime after the system boots. Only then it will be possible to query the device status and perform other operations. ``` # liquidctl initialize Corsair Hydro H100i Pro └── Firmware version 2.10.0.0 ``` When (re)initializing the device it is possible to select the pump mode: ``` # liquidctl initialize --pump-mode=performance Corsair Hydro H100i Pro └── Firmware version 2.10.0.0 ``` Allowed pump modes are: `quiet`, `balanced` and `performance`. ## Device monitoring _Changed in 1.12.0: the firmware version is no longer reported (see [Initialization])._
Similarly to other AIOs, the cooler can report fan and pump speeds as well as the liquid temperature. ``` # liquidctl status Corsair Hydro H100i Pro ├── Liquid temperature 28.7 °C ├── Fan 1 speed 480 rpm ├── Fan 2 speed 476 rpm ├── Pump mode balanced └── Pump speed 1890 rpm ``` ## Fan speed control Fan speeds can be configured either to fixed duty values or profiles. The profiles accept up to seven (liquid temperature, duty) points, and are interpolated by the device. ``` # liquidctl set fan speed 50 # liquidctl set fan speed 20 0 40 100 ``` ## Lighting modes There's a single lighting channel `logo`. The following table sumarizes the available lighting modes, and the number of colors that each of them expects. | Mode | Colors | Notes | | :-- | :--: | :-- | | `alert` | 3 | Good, warning and critical states | | `shift` | 2–4 || | `pulse` | 1–4 || | `blinking` | 1–4 || | `fixed` | 1 || ``` # liquidctl set logo color alert 00ff00 ffff00 ff0000 # liquidctl set logo color shift ff9000 0090ff # liquidctl set logo color pulse ff9000 # liquidctl set logo color blinking ff9000 # liquidctl set logo color fixed ff9000 ``` All modes except `alert` and `fixed` support an additional `--speed` parameter; the allowed values are `slower`, `normal` and `faster`. ``` # liquidctl set logo color pulse ff9000 --speed faster ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/asus-aura-led-guide.md0000644000175000017500000001504714562144457017671 0ustar00jonasjonas# ASUS Aura LED (USB-based) controllers _Driver API and source code available in [`liquidctl.driver.aura_led`](../liquidctl/driver/aura_led.py)._ _New in 1.10.0._
This driver supports ASUS Aura USB-based lighting controllers that appear in various ASUS Z490, Z590, and Z690 motherboards. These controllers operate in either (a) direct mode or (b) effect mode. _Direct_ mode is employed by Aura Crate in Windows. It requires the application to send a continuous stream of commands to the controller in order to modulate lighting effects on each addressable LED. The other mode is _effect_ mode in which the controller itself modulates lighting effects on each addressable LED. Effect mode requires the application to issue a single set of command codes to the controller in order to initiate the given effect. The controller continues to process that effect until the application sends a different command. This driver employs the _effect_ mode (fire and forget). The selected lighting mode remains in effect until it is explicitly changed. This means the selected lighting mode remains in effect (a) on cold boot, (b) on warm boot, (c) after wake-from-sleep. The disadvantage, however, is the inability to set different lighting modes to different lighting channels. All channels remain synchronized. There are three known variants of the Aura LED USB-based controller: - Device `0x19AF`: found in ASUS ProArt Z690-Creator WiFi - Device `0x1939` [^1] - Device `0x18F3`[^1]: found in ASUS ROG Maximus Z690 Formula [^1]: Support for devices `0x1939` and `0x18F3` may not be sufficiently developed so users are asked to experiment and provide feedback. [Wireshark USB traffic capture](./developer/capturing-usb-traffic.md), in particular, will be very helpful. ## Initialization ASUS Aura LED controller does not need to be initialized before use. Initialization is optional. ``` # liquidctl initialize ASUS Aura LED Controller └── Firmware version AULA3-AR32-0207 ``` ## Status The `status` function returns the number of ARGB and RGB channels detected by the controller. If the command is invoked with `--debug` flag, the entire reply from the controller will be displayed in groups of 6 bytes. This information has not been fully decoded, but is provided in the event that someone is able to decipher it. On ASUS ProArt Z690-Creator WiFi the following is returned: ``` # liquidctl status ASUS Aura LED Controller ├── ARGB channels: 2 └── RGB channels: 1 ``` To display the set of 6-byte status values, use `--debug` on the command line. The following will be returned: ``` # liquidctl --debug status ASUS Aura LED Controller ├── ARGB channels: 2 ├── RGB channels: 1 ├── Device Config: 1 0x1e, 0x9f, 0x02, 0x01, 0x00, 0x00 ├── Device Config: 2 0x78, 0x3c, 0x00, 0x01, 0x00, 0x00 ├── Device Config: 3 0x78, 0x3c, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 4 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 5 0x00, 0x00, 0x00, 0x01, 0x03, 0x02 ├── Device Config: 6 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 7 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 8 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 9 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 └── Device Config: 10 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ``` On ASUS ROG Strix Z690-i Gaming WiFi (mini-ITX) the following is returned: ``` # liquidctl --debug status ASUS Aura LED Controller ├── ARGB channels: 2 ├── RGB channels: 1 ├── Device Config: 1 0x1e, 0x9f, 0x02, 0x01, 0x00, 0x00 ├── Device Config: 2 0x78, 0x3c, 0x00, 0x01, 0x00, 0x00 ├── Device Config: 3 0x78, 0x3c, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 4 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 5 0x00, 0x00, 0x00, 0x01, 0x03, 0x02 ├── Device Config: 6 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 7 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 8 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 9 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 └── Device Config: 10 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ``` On some ASUS Z490 boards (controller ID 0x18F3) the following is returned: ``` # liquidctl --debug status ASUS Aura LED Controller ├── ARGB channels: 1 ├── RGB channels: 1 ├── Device Config: 1 0x1e, 0x9f, 0x01, 0x01, 0x00, 0x00 ├── Device Config: 2 0x78, 0x3c, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 3 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 4 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 5 0x00, 0x00, 0x00, 0x06, 0x07, 0x02 ├── Device Config: 6 0x01, 0xf4, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 7 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 8 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ├── Device Config: 9 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 └── Device Config: 10 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ``` ## RGB lighting The driver supports one 12V RGB channel named `led1` and three 5V Addressable RGB channels named `led2`, `led3`, and `led4`. Because the driver uses `effect` mode, all channels are synchronized. It is not possible at this time to set different color modes to different channels (`direct` mode is used for that). Nevertheless, independent channel names are provided in case a future BIOS update provides more flexibility in `effect` mode. ``` # liquidctl set led1 color static af5a2f # liquidctl set led2 color breathing 350017 # liquidctl set led3 color rainbow # liquidctl set led4 color spectrum-cycle # liquidctl set sync color gentle-transition ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports zero or one color. | Mode | Colors | Notes | | --- | --- | --- | | `off` | None | | `static` | One | | `breathing` | One | | `flashing` | One | | `spectrum_cycle` | None | | `rainbow` | None | | `spectrum_cycle_breathing` | None | | `chase_fade` | One | | `spectrum_cycle_chase_fade` | None | | `chase` | One | | `spectrum_cycle_chase` | None | | `spectrum_cycle_wave` | None | | `chase_rainbow_pulse` | None | | `rainbow_flicker` | None | | `gentle_transition` | None | name given by us | | `wave_propagation` | None | name given by us | | `wave_propagation_pause` | None | name given by us | | `red_pulse` | None | name given by us | In addition to these, it is also possible to use the `sync` pseudo-channel to apply a setting to all lighting channels. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735732074.0 liquidctl-1.15.0/docs/asus-ryujin-guide.md0000644000175000017500000000434314735225552017512 0ustar00jonasjonas# ASUS Ryujin II liquid coolers _Driver API and source code available in [`liquidctl.driver.asus_ryujin`](../liquidctl/driver/asus_ryujin.py)._ _New in 1.14.0._
## Initialization Initialization is not required. It outputs the firmware version: ``` # liquidctl initialize ASUS Ryujin II 360 └── Firmware version AURJ1-S750-0104 ``` ## Monitoring The cooler reports the liquid temperature, the speeds and duties of all fans. ``` # liquidctl status ASUS Ryujin II 360 ├── Liquid temperature 26.0 °C ├── Pump duty 30 % ├── Pump speed 1200 rpm ├── Pump fan duty 40 % ├── Pump fan speed 2550 rpm ├── External fan duty 50 % ├── External fan 1 speed 990 rpm ├── External fan 2 speed 1020 rpm ├── External fan 3 speed 0 rpm └── External fan 4 speed 0 rpm ``` ## Speed control ### Setting fan and embedded pump duty Pump duty can be set using channel `pump`. ``` # liquidctl set pump speed 90 ``` Use channel `fans` to set all fans at the same time: ``` # liquidctl set fans speed 50 ``` Use channel `pump-fan` to set the duty of the embedded fan: ``` # liquidctl set pump-fan speed 50 ``` Use channel `external-fans` to set the duty of the fans connected to the AIO fan controller: ``` # liquidctl set external-fans speed 50 ``` ### Duty to speed relation The resulting speeds do not scale linearly to the set duty values. For example pump duty values below 20% result in relatively small changes in pump speed. Speeds of the fans connected to the AIO fan controller depend on the fans themselves. Pump impeller and embedded fan duty values approximately map to the following speeds (± 10%): | Duty (%) | Pump impeller speed (rpm) | Pump fan speed (rpm) | |:---:|:---:|:---:| | 0 | 840 | 0 | | 10 | 870 | **390** | | 20 | 900 | 1200 | | 30 | 1140 | 1860 | | 40 | 1470 | 2460 | | 50 | 1650 | 3000 | | 60 | 1890 | 3450 | | 70 | 2130 | 3870 | | 80 | 2310 | 4230 | | 90 | 2520 | 4590 | | 100 | 2800 | 4800 | Note the minimum speed of the embedded pump fan is 390 rpm, meaning the fan may not start spinning at duty values below 10%. ## Screen The screen of the cooler is not yet supported. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735732078.0 liquidctl-1.15.0/docs/coolit-guide.md0000644000175000017500000000233114735225556016511 0ustar00jonasjonas# Corsair Hydro H110i GT AIO liquid cooler _Driver API and source code available in [`liquidctl.driver.coolit`](../liquidctl/driver/coolit.py)._ _New in 1.14.0._
## Initialization [Initialization]: #initialization The AIO does not need to be initialized prior to use, but it will set the pump mode to `quiet`. ``` # liquidctl initialize Corsair H110i GT └── Firmware version 2.0.0 ``` When (re)initializing the device, it is possible to select the pump mode: ``` # liquidctl initialize --pump-mode extreme Corsair H110i GT └── Firmware version 2.0.0 ``` Allowed pump modes are: - `quiet` - `extreme` ## Device monitoring Similarly to other AIOs, the cooler can report fan and pump speeds as well as the liquid temperature. ``` # liquidctl status Corsair H110i GT ├── Liquid temperature 32.6 °C ├── Fan 1 speed 1130 rpm ├── Fan 2 speed 1130 rpm └── Pump speed 2831 rpm ``` ## Fan speed control Fan speeds can be configured either to fixed duty values or profiles. The profiles accept up to seven (liquid temperature, duty) points, and are interpolated by the device. ``` # liquidctl set fan speed 50 # liquidctl set fan speed 20 0 40 100 ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735732055.0 liquidctl-1.15.0/docs/corsair-commander-core-guide.md0000644000175000017500000000751614735225527021563 0ustar00jonasjonas# Corsair Commander Core, Core XT and ST _Driver API and source code available in [`liquidctl.driver.commander_core`](../liquidctl/driver/commander_core.py)._ _Changed in 1.11.0: the Corsair Commander Core XT is now supported._
_Changed in 1.12.0: the Corsair Commander ST is now supported._
Currently, functionality implemented is listed here. More is planned to be added. ## Initializing the device The device should be initialized every time it is powered on. ``` # liquidctl initialize Corsair Commander Core ├── Firmware version 2.6.201 ├── AIO LED count 29 ├── RGB port 1 LED count 8 ├── RGB port 2 LED count 8 ├── RGB port 3 LED count N/A ├── RGB port 4 LED count N/A ├── RGB port 5 LED count N/A ├── RGB port 6 LED count N/A ├── AIO port connected Yes ├── Fan port 1 connected Yes ├── Fan port 2 connected Yes ├── Fan port 3 connected No ├── Fan port 4 connected No ├── Fan port 5 connected No ├── Fan port 6 connected No ├── Water temperature sensor Yes └── Temperature sensor 1 No ``` ## Retrieving the pump speed, fan speeds, and temperatures The Commander Core and ST currently can retrieve the pump speed, fan speeds, temperature of the water, and the temperature measured by the probe. ``` # liquidctl status Corsair Commander Core ├── Pump speed 2356 rpm ├── Fan speed 1 810 rpm ├── Fan speed 2 791 rpm ├── Fan speed 3 0 rpm ├── Fan speed 4 0 rpm ├── Fan speed 5 0 rpm ├── Fan speed 6 0 rpm └── Water temperature 35.8 °C ``` The Core XT variant of the device is not meant for use with an AIO, so parameters relating to the pump are not present. ``` Corsair Commander Core XT ├── Fan speed 1 2737 rpm ├── Fan speed 2 2786 rpm ├── Fan speed 3 0 rpm ├── Fan speed 4 0 rpm ├── Fan speed 5 0 rpm └── Fan speed 6 0 rpm ``` ## Programming the pump and fan speeds ### Speed curve profiles _New in 1.14.0._
The pump or fans speeds can be configured using a speed curve profile with a minimum of 2 or up to 7 curve points. Each curve point consists of both a temperature (in celsius) and a duty (percentage). ``` liquidctl set fans speed 28 0 35 50 40 75 41 85 42 90 43 95 44 100 | | | channel | duty temp ``` ### Fixed duty cycle _New in 1.9.0._
The pump or fan speeds can be set to a fixed duty cycle. ``` # liquidctl set fan1 speed 70 ^^^^ ^^ channel duty ``` In iCUE the pump can be set to different modes that correspond to a fixed percent that can be used in liquidctl. Quiet is 75%, Balanced is 85% and Extreme is 100%. ### Device channels Valid channel values on the Core (non-XT) and ST are `pump`, `fanN`, where 1 <= N <= 6 is the fan number. On the Core XT, the `pump` channel is not present. The `fans` channel can be used to simultaneously configure all fans. ### Notes - A channel may only be configured with a single fixed duty cycle or a single fan curve profile (independently from the other channel configurations). - The pump and some fans have a limit to how slow they can go and will not stop when set to zero. This is a hardware limitation that cannot be changed. - The cooler's lights flash with every update. Due to limitations in both the device hardware and liquidctl, there currently is no way to solve this problem. For more information, see: [#448]. [#448]: https://github.com/liquidctl/liquidctl/issues/448 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/corsair-commander-guide.md0000644000175000017500000001630514562144457020631 0ustar00jonasjonas# Corsair Commander Pro, Obsidian 1000D and Lighting Node Pro/Core _Driver API and source code available in [`liquidctl.driver.commander_pro`](../liquidctl/driver/commander_pro.py)._ ## Initializing the device _Changed in 1.9.0: the firmware and bootloader versions are not available when data is read from [Linux hwmon]._
The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. The initialization command is needed in order to detect what temperature sensors and fan types are currently connected. ``` # liquidctl initialize Corsair Commander Pro ├── Firmware version 0.9.212 ├── Bootloader version 0.5 ├── Temperature probe 1 Yes ├── Temperature probe 2 Yes ├── Temperature probe 3 No ├── Temperature probe 4 No ├── Fan 1 control mode PWM ├── Fan 2 control mode PWM ├── Fan 3 control mode DC ├── Fan 4 control mode N/A ├── Fan 5 control mode N/A └── Fan 6 control mode N/A ``` ``` # liquidctl initialize Corsair Lighting Node Pro ├── Firmware version 0.10.4 └── Bootloader version 3.0 ``` Passing `--fan-mode=':[,...]'` can be used to change the fan mode from `dc` to `pwm` to `off` if the connected fan type is changed. The `--fan-mode` option is persistent across restarts and can only be used when not using the hwmon driver. ## Retrieving the fan speeds, temperatures and voltages The Lighting Node Pro and Lighting Node Core do not have a status message. The Commander Pro and Obsidian 1000D are able to retrieve the current fan speeds as well as the current temperature of any connected temperature probes. They are also able to retrieve the voltages from the 3.3, 5, and 12 volt buses. If a fan or temperature probe is not connected then a value of 0 is shown. ``` # liquidctl status Corsair Commander Pro ├── Temperature 1 26.4 °C ├── Temperature 2 27.5 °C ├── Fan 1 speed 927 rpm ├── Fan 2 speed 927 rpm ├── Fan 3 speed 1195 rpm ├── +12V rail 12.06 V ├── +5V rail 4.96 V └── +3.3V rail 3.36 V ``` ## Programming the fan speeds The Lighting Node Pro and Lighting Node Core do not have any fans to control. Each fan can be set to either a fixed duty cycle, or a profile consisting of up to six (temperature, rpm) pairs. Temperatures should be given in Celsius and rpm values as a valid rpm for the fan that is connected. _Note: unlike equivalent functionality in other drivers, this driver takes speed profiles using angular speeds in rpm, not duty values in percentage._ _Note: you must ensure that rpm values are within the min, max range for your hardware._ Profiles run on the device and are always based one the specified temp probe. If a temperature probe is not specified number 1 is used. The last point should set the fan to 100% fan speed, or be omitted; in the latter case the fan will be set to 5000 rpm at 60°C (this speed may not be appropriate for your device). ``` # liquidctl set fan1 speed 70 ^^^^ ^^ channel duty # liquidctl set fan2 speed 20 800 40 900 50 1000 60 1500 ^^^^^^ ^^^^^^ ^^^^^^^ ^^^^^^^ pairs of temperature (°C) -> speed (rpm) # liquidctl set fan3 speed 20 800 40 900 50 1300 --temperature-sensor 2 ``` Valid channel values are `fanN`, where 1 <= N <= 6 is the fan number, and `sync`, to simultaneously configure all fans. Only fans that have been connected and identified by `liquidctl initialize` can be set. Behaviour is unspecified if the specified temperature probe is not connected. Passing `--verbose` can be used to see the raw settings being sent to the cooler, after normalization of the profile and enforcement of a (60°C, 5000 rpm) failsafe. The `--unsafe=high_temperature` flag can be used to modify this failsafe to only trigger at 100°C. ## Controlling the LEDs The Commander Pro and Lighting Node Pro devices have two physical lighting channels, specified as either `led1` or `led2`. A third `sync` pseudo channel is provided for convenience. On the other hand, the Lighting Node Core has a single `led` channel. The table bellow summarizes the available modes, and their associated maximum number of colors. Note that for any effect if no colors are specified then random colors will be used. | Mode | Num colors | | ------------- | ---------- | | `clear` _¹_ | 0 | | `off` _²_ | 0 | | `fixed` | 1 | | `color_shift` | 2 | | `color_pulse` | 2 | | `color_wave` | 2 | | `visor` | 2 | | `blink` | 2 | | `marquee` | 1 | | `sequential` | 1 | | `rainbow` | 0 | | `rainbow2` | 0 | _¹ This is not a real mode but it will remove all saved effects_
_² This is not a real mode but it is fixed with RGB values of 0_
To specify which LED's on the channel the effect should apply to the `--start-led` and `--maximum-leds` flags must be given. By default the effect will apply to all LED's on the channel. If you have 3 Corsair LL fans connected to channel one and you want to set the first and third to green and the middle to blue you can use the following commands: ``` # liquidctl set led1 color fixed 00ff00 --start-led 1 --maximum-leds 48 # liquidctl set led1 color fixed 0000ff --start-led 16 --maximum-leds 16 ``` This will first set all 48 leds to green then will set leds 16-32 to blue. Alternatively you could do: ``` # liquidctl set led1 color fixed 00ff00 --start-led 1 --maximum-leds 16 # liquidctl set led1 color fixed 0000ff --start-led 16 --maximum-leds 16 # liquidctl set led1 color fixed 00ff00 --start-led 32 --maximum-leds 16 ``` This allows you to compose more complex led effects then just the base modes. The different commands need to be sent in order that they should be applied. In the first example if the order were reversed then all of the LED's would be green. All of the effects support specifying a `--direction=forward` or `--direction=backward`. There are also 3 speeds that can be specified for the `--speed` flag. `fast`, `medium`, and `slow`. Each color can be specified using any of the' [supported formats](../README.md#supported-color-specification-formats). Currently the device can only accept hardware effects, and the specified configuration will persist across power offs. The changes take a couple of seconds to take effect. ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
Commander Pro controllers and the Obsidian 1000D are supported by the mainline Linux kernel with its [`corsair-cpro`] driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`corsair-cpro`]: https://www.kernel.org/doc/html/latest/hwmon/corsair-cpro.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525812.0 liquidctl-1.15.0/docs/corsair-hxi-rmi-psu-guide.md0000644000175000017500000001167514776654764021071 0ustar00jonasjonas# Corsair HXi and RMi series PSUs _Driver API and source code available in [`liquidctl.driver.corsair_hid_psu`](../liquidctl/driver/corsair_hid_psu.py)._ _Changed in 1.12.0: HX1500i and HX1000i 2022 re-issue are now also supported._
_Changed in 1.15.0: HX1200i ATX 3.1 (CP-9020281-NA) refresh is now supported._
## Initialization It is necessary to initialize the device once it has been powered on. ``` # liquidctl initialize ``` The +12V rails normally functions in multiple-rail mode, and `initialize` will by default reset the PSU to that behavior. Single-rail mode can be optionally selected by passing `--single-12v-ocp` to `initialize`. _Changed in 1.9.0: changing the OCP mode or resetting to hardware fan control is not available when the device was initialized by the [Linux hwmon] driver._
## Monitoring _Changed in 1.9.0: OCP and fan control modes, as well current and total uptime, are not available when data is read from [Linux hwmon]._
The PSU is able to report monitoring data about its own hardware and basic electrical variables for the input and output sides. ``` # liquidctl status Corsair RM650i ├── Current uptime 3:43:54 ├── Total uptime 9 days, 11:43:54 ├── VRM temperature 50.0 °C ├── Case temperature 40.8 °C ├── Fan control mode Hardware ├── Fan speed 0 rpm ├── Input voltage 230.00 V ├── +12V OCP mode Multi rail ├── +12V output voltage 12.12 V ├── +12V output current 7.75 A ├── +12V output power 92.00 W ├── +5V output voltage 4.97 V ├── +5V output current 2.88 A ├── +5V output power 14.00 W ├── +3.3V output voltage 3.33 V ├── +3.3V output current 1.56 A ├── +3.3V output power 5.00 W ├── Total power output 110.00 W ├── Estimated input power 124.00 W └── Estimated efficiency 89 % ``` Input power and efficiency are estimated from efficiency data advertised by Corsair in the respective HXi and RMi PSU user manuals. These estimates are not accurate at load levels bellow 10%, in particular with the HX1500i and the 2022 re-issue of the HX1000i. _Changed in 1.11.0: temperature sensors 1 and 2 have been renamed to VRM and case, respectively._
## Fan speed The fan speed is normally controlled automatically by the PSU. It is possible to override this and set the fan to a fixed duty value using the `fan` channel. ``` # liquidctl set fan speed 90 ``` This changes the fan control mode to software control; to revert back to hardware control, re-`initialize` the device. While in software control mode, a minimum allowed duty value of 30% is enforced, for safety, by liquidctl. ## Appendix: differences in efficiency data HXi and RMi power supply units do not report input power or current to the host. Yet, the efficiency data in the user manuals can sometimes result in more conservative estimates that the input power and efficiency values iCue and CorsairLink display.1 We believe that this is the result of the use, in those programs, of a different set of efficiency curves, based on internal and unpublished testing. Additionally, the manuals also do not match publicly available data submitted for the 80 Plus certification of these PSUs.2 Unfortunately, the latter is only available for 115 V input. At this point it is important to remember that efficiency, and consequently power draw, are functions of more than just the total power output. Thus, the data in the user manuals is probably significantly less precise than it appears to be, and we believe the same could be true for the values displayed by iCue and CorsairLink. Still, we encourage Corsair to make more of its efficiency data public, which would hopefully allow liquidctl to present more precise estimates. _1 See comments in [issue #300](https://github.com/liquidctl/liquidctl/issues/300)._
_2 Available at [80 PLUS® Certified Power Supplies and Manufacturers](https://www.clearesult.com/80plus/manufacturers/115V-Internal)._
## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
These devices are supported by the mainline Linux kernel with its [`corsair-psu`] driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`corsair-psu`]: https://www.kernel.org/doc/html/latest/hwmon/corsair-psu.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735732093.0 liquidctl-1.15.0/docs/corsair-platinum-pro-xt-guide.md0000644000175000017500000001062014735225575021741 0ustar00jonasjonas# Corsair Hydro Platinum, Pro XT and Elite RGB all-in-one liquid coolers _Driver API and source code available in [`liquidctl.driver.hydro_platinum`](../liquidctl/driver/hydro_platinum.py)._ _Changed in 1.13.0: support added for the H100i and H150i Elite RGB models._
_Changed in 1.14.0: the H115i Elite RGB is now supported._
## Initializing the device and setting the pump mode The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. ``` # liquidctl initialize Corsair Hydro H100i Platinum └── Firmware version 1.1.15 ``` By default the pump mode will be set to `balanced`, but a different mode can be specified with `--pump-mode`. The valid values for this option are `quiet`, `balanced` and `extreme`. ``` # liquidctl initialize --pump-mode extreme Corsair Hydro H100i Platinum └── Firmware version 1.1.15 ``` Unconfigured fan channels may default to 100% duty, so [reprogramming their behavior](#programming-the-fan-speeds) is also recommended after running `initialize` for the first time since the cooler was powered on. Subsequent executions of `initialize` should leave the fan speeds unaffected. ## Retrieving the liquid temperature and fan/pump speeds The cooler reports the liquid temperature and the speeds of all fans and pump. ``` # liquidctl status Corsair Hydro H100i Platinum ├── Liquid temperature 27.0 °C ├── Fan 1 speed 1386 rpm ├── Fan 1 duty 50 % ├── Fan 2 speed 1389 rpm ├── Fan 2 duty 50 % └── Pump speed 2357 rpm ``` ## Programming the fan speeds Each fan can be set to either a fixed duty cycle, or a profile consisting of up to seven (temperature, duty) pairs. Temperatures should be given in Celsius and duty values in percentage. Profiles run on the device and are always based on the internal liquid temperature probe. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. ``` # liquidctl set fan1 speed 70 ^^^^ ^^ channel duty # liquidctl set fan2 speed 20 20 40 70 50 100 ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) ``` Valid channel values are `fanN`, where N >= 1 is the fan number, and `fan`, to simultaneously configure all fans. As mentioned before, unconfigured fan channels may default to 100% duty. _Note: pass `--verbose` to see the raw settings being sent to the cooler, after normalization of the profile and enforcement of the (60°C, 100%) fail-safe._ ## Controlling the LEDs In reality these coolers do not have the concept of different channels or modes, but liquidctl provides a few for convenience. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | LEDs | Components | Platinum | Pro XT/Elite RGB | Platinum SE | | -------- | ----------- | ------------ | ------------ | -------- | ---------------- | ----------- | | led | off | synchronized | all off | 0 | 0 | 0 | | led | fixed | synchronized | independent | 1 | 1 | 1 | | led | super-fixed | independent | independent | 24 | 16 | 48 | The `led` channel can be used to address individual LEDs, and supports the `super-fixed`, `fixed` and `off` modes. In `super-fixed` mode, each color supplied on the command line is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In `fixed` mode, all LEDs are set to a single color supplied on the command line. The `off` mode is simply an alias for `fixed 000000`. ``` # liquidctl set led color off # liquidctl set led color fixed ff8000 # liquidctl set led color fixed "hsv(90,85,70)" # liquidctl set led color super-fixed ^^^ ^^^^^^^^^^^ ^ channel mode colors... ``` Each color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). Animations are not supported at the hardware level, and require successive invocations of the commands shown above, or use of the liquidctl APIs. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/docs/ddr4-guide.md0000644000175000017500000001136614677373312016064 0ustar00jonasjonas# DDR4 DIMMs _Driver API and source code available in [`liquidctl.driver.ddr4`](../liquidctl/driver/ddr4.py)._ Support for these DIMMs in only available on Linux. Other requirements must also be met: - `i2c-dev` kernel module has been loaded - r/w permissions to the host SMBus `/dev/i2c-*` device - specific unsafe features have been opted in - the host SMBus is supported: currently only i801 (Intel mainstream & HEDT) Jump to a specific section: - [DIMMs with a standard temperature sensor][ddr4_temperature] - [Corsair Vengeance RGB][vengeance_rgb] - *[Inherent unsafeness of I²C/SMBus]* ## DIMMs with a standard temperature sensor [ddr4_temperature]: #dimms-with-a-standard-temperature-sensor Supports modules using TSE2004-compatible SPDD EEPROMs with temperature sensor. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] - `ddr4_temperature`: access standard temperature sensor address ### Initialization Not required for this device. ### Retrieving the DIMM's temperature ``` # liquidctl status --unsafe=smbus,ddr4_temperature DDR4 DIMM2 └── Temperature 30.5 °C ``` ## Corsair Vengeance RGB [vengeance_rgb]: #corsair-vengeance-rgb Unsafe features: - `smbus`: see [Inherent unsafeness of I²C/SMBus] - `vengeance_rgb`: access non-advertised temperature sensor and RGB controller addresses ### Initialization Not required for this device. ### Retrieving the DIMM's temperature ``` # liquidctl status --verbose --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 └── Temperature 30.5 °C ``` ### Controlling the LED Each module features a few *non-addressable* RGB LEDs. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | ---------- | ----------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `breathing` | 1–7 | | `led` | `fading` | 2–7 | The LED colors can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The speed of the breathing and fading animations can be adjusted with `--speed`; the allowed values are `slowest`, `slower`, `normal` (default), `faster` and `fastest`. ``` # liquidctl set led color breathing ff355e 1ab385 speed=faster --unsafe=smbus,vengeance_rgb ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ channel mode colors speed enable unsafe features # liquidctl set led color fading "hsv(90,85,70)" "hsv(162,85,70)" --unsafe=smbus,vengeance_rgb # liquidctl set led color fixed ff355e --unsafe=smbus,vengeance_rgb # liquidctl set led color off --unsafe=smbus,vengeance_rgb ``` ## Inherent unsafeness of I2C and SMBus [Inherent unsafeness of I²C/SMBus]: #inherent-unsafeness-of-i2c-and-smbus Reading and writing to System Management (SMBus) and I²C buses is inherently more risky than dealing with, for example, USB devices. On typical desktop and workstation systems many important chips are connected to these buses, and they may not tolerate writes or reads they do not expect. While SMBus 2.0 has some limited ability for automatic enumeration of devices connected to it, unlike simpler I²C buses and SMBus 1.0, this capability is, effectively, not safely available for us in user space. It is thus necessary to rely on certain devices being know to use a specific address, or being documented/specified to do so; but there is always some risk that another, unexpected, device is using that same address. The enumeration capability of SMBus 2.0 also brings dynamic address assignment, so even if a device is know to use a particular address in one machine, that could be different on other systems. On top of this, accessing I²C or SMBus buses concurrently, from multiple threads or processes, may also result in undesirable or unpredictable behavior. Unsurprisingly, users or programs dealing with I²C/SMBus devices have occasionally crashed systems and even bricked boards or peripherals. In some cases this is reversible, but not always. For all of these reasons liquidctl requires users to *opt into* accessing I²C/SMBus devices, which can be done by enabling the `smbus` unsafe feature. Other unsafe features may also be required for the use of specific devices, based on other *know* risks specific to a particular device. Note that a feature not being labeled unsafe, or a device not requiring the use of additional unsafe features, does in no way assure that it is safe. This is especially true when dealing with I²C/SMBus devices. Finally, liquidctl may list some I²C/SMBus devices even if `smbus` has not been enabled, but only if it is able to discover them without communicating with the bus or the devices. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7096386 liquidctl-1.15.0/docs/developer/0000755000175000017500000000000014776655074015600 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/capturing-usb-traffic.md0000644000175000017500000001303514562144457022312 0ustar00jonasjonas# Capturing USB traffic ## Preface A fundamental aspect of developing drivers for USB devices is inspecting the traffic between applications and the device. This is useful for debugging your own drivers and applications, as well as to understand undocumented protocols. In the latter case, a possibly opaque and closed source application is allowed to communicate with the device, and the captured traffic is analyzed to understand what the device is capable of and what it expects from the host application. ## Capturing USB traffic on a native Windows host Get [Wireshark]. During the Wireshark setup, enable the installation of USBPcap for experimental capturing of USB traffic. Reboot. To capture some USB traffic, start Wireshark, double click the USBPcap1 interface to start capturing all traffic on it, and proceed to [Finding the target device](#finding-the-target-device). _If you have more than one USBPcap interface, you may need to look for the target devices in each of them._ ## Capturing USB traffic on Linux _and capturing USB traffic in a Windows VM, through the Linux host_ You will need to install [Wireshark] from your favorite package manager. You may have to run Wireshark as root to be able to capture USB traffic. Alternatively you can give the your normal user permissions to capture traffic by adding your self to the `wireshark` group and granting yourself read permissions on the `/dev/usbmon*` devices. Some extra steps may be needed, you can follow the instructions [here](https://wiki.wireshark.org/CaptureSetup/USB). Note you may need to logout and login agin for these changes to take effect. The general steps are as follows: 1. [Create and configure a Windows VM](./creating-vm-for-capture.md). 2. Start listening for the device USB traffic on Wireshark. 4. Start changing device settings using the application running in the Windows VM and watch the messages appear in the Wireshark interface running on the host OS. 5. Success! To capture USB traffic after setting up the VM, start Wireshark and select the appropriate `usbmon` interface for capturing traffic to your device. You can select them on the main screen, or in the Capture -> Options menu. If you aren't sure which usbmon device is correct, select them all then proceed to [Finding the target device](#finding-the-target-device). ![usbmon interface](./images/wireshark_1.png) ## Finding the target device [Finding the target device](#finding-the-target-device) Wireshark captures USB traffic at the bus level, which means that all devices on that bus will be captured. This is a lot of noise, so the first step is find the target device among all others and filter the traffic to that device. _For this example, assume the target device has vendor and product IDs `0x1b1c` and `0x0c1a`, respectively._ First, capture some USB traffic and apply a filter to the captured traffic (via the top bar) to filter out everything except the `GET DESCRIPTOR` response for this device. ``` usb.idVendor == 0x1b1c && usb.idProduct == 0x0c1a ``` ![Wireshark 4](./images/wireshark_2.png) Next step is to get the device address so that we can tell wireshark to only capture traffic to our desired device. Select one of the `GET DESCRIPTOR` response packets, expand the USB URB section in the packet details, and find the "Device: #" line. This is the device address.   Right click the "Device: #" entry, choose "Apply As Filter", then "Selected".  In the screenshot below the device number was 9. ![Wireshark 5](./images/wireshark_3.png) This will change your packet filter to something like `usb.device_address == 9`, which is exactly what we want.  Now only traffic sent to that specific device will be displayed. ![Wireshark 6](./images/wireshark_4.png) For convenience, you may want to save your filter for future captures.  Click on the bookmark icon immediately next to the filter to save it. ## Exporting captured data There are two main useful ways to work with Wireshark captures of USB traffic. The first is within Wireshark itself, using its native PCAP format (or any of its variants), which is useful for manual analysis. _PCAP files are also the preferred way of storing and sharing captured USB traffic._ You can simply File -> Save to export captured traffic from Wireshark. But for more control over what will be exported (for example, only currently filtered/displayed packets), File -> Export Specified Packets is generally preferred. The other way of analyzing USB traffic is through external, and sometimes custom, tools. In theses cases it may be helpful to additionally export the data to JSON (File -> Export Packet Dissections -> As JSON). Plain text or CSV dissections are _not_ very useful with USB data, since Wireshark tends to truncate the long fields that are of our interest. ## Next steps Once you have your usb capture (probably in a `pcapng` format). You may find it easer to look at the data outside of Wireshark since Wireshark sometimes limits the number of bytes shown to fewer then the amount you want to look at. I generally like to use `tshark`, one of the cli tools that comes with Wireshark to extract the only the fields I care about so that I can easily only show the fields I can about and can use other bash commands to separate the fields and remove some of the extraneous messages (for example Corsair iCue sends a get status message every second which I am generally not interested in). Next steps would be to take a look at [analyzing USB protocols](techniques-for-analyzing-usb-protocols.md) [Wireshark]: https://www.wireshark.org [USBPcap]: https://desowin.org/usbpcap/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/creating-vm-for-capture.md0000644000175000017500000001527614562144457022565 0ustar00jonasjonas## Virtual Machine Setup for Traffic Capture Capturing traffic between the source application running in a VM and your device requires a little setup, but once in place gives you a handy tool to capture USB traffic. There are a few nuances of which you should be aware to correctly set up a VM for USB traffic capture, and this doc will walk through the steps required using a reference configuration in an attempt to make the steps as clear as possible. Your configuration will likley differ from the reference configuration, but hopefully having a working reference will be helpful to adapt to your environment. #### Reference Configuration #### - Virtual Machine Manager: [virt-manager](https://virt-manager.org/) - Guest OS: Windows 10 64-bit - Host OS: Ubuntu 20.04 - Source application: Corsair iCue In short, the reference configuration will be running Corsair iCue in the Windows 10 guest VM, and capturing the device USB traffic in Wireshark running in the Ubuntu host OS. ### Basic Steps ### 1. Enable Virtualization in your BIOS 2. Install a Virtual Machine manager. 3. Create a  Windows VM 4. Install and run your device controlling software in the Windows VM (Corsair iCue in this example). 5. Capture the traffic. ### Step 1. Enable Virtualization in your BIOS Ensure your motherboard enables hardware support for Intel VT-x or AMD-v.  These features are required in order to run a 64-bit VM, or have a VM that supports more than 1 CPU core.   In most modern motherboards these features are disabled by default, so you may need to enable them in your BIOS.  The exact setting to check is motherboard dependent, but should be something in the "virtualization" universe.  [This post](https://forums.virtualbox.org/viewtopic.php?f=1&t=62339) lists a few of the common settings, but to save you a click you can look for the following: - "Enable Virtualization Technology", - "Enable SVM Mode" (AMD CPUs), - "Enable Vanderpool Technology" (Intel) - "Secure Virtual Mode" - "Intel (VMX) Virtualization Technology" If you poked around and can't find a setting that seems right, and your motherboard is somewhat recent, then it might be enabled by default and you can just move on the next step.  You will know for sure whether it is enabled if you are unable to create a 64 bit windows 10 VM in step 3.  If that's the case, then you'll need to look harder for the setting in your BIOS, or get your Google on. If you do make any changes in your BIOS then do a power cycle (turn the computer totally off, then on) to ensure the BIOS changes are enabled.  Sometimes a reboot doesn't do it, and the extra few seconds turning the computer all the way off is a good investment to eliminate variables. ### Step 2. Install a Virtual Machine manager There are several capable Virtual Machine managers that can be leveraged for running the Windows 10 guest VM, but [virt-manager](https://virt-manager.org/) will be used for the purposes of this document. [VirtualBox](https://www.virtualbox.org/) is another popular choice. Install virt-manager via your package manager of choice and start it up. ``` virt-manager ``` You might get a warning about a missing daemon running.  If so, enable the daemon. Reboot.  ### Step 3. Create a Windows VM With the hardware all set to go and virt-manager installed, its time to create the guest VM running Windows 10.  First, download the Windows 10 ISO:  https://www.microsoft.com/en-in/software-download/windows10ISO Start virt-manager ``` virt-manager ``` After Virtual Machine Manager opens, create a new VM by selecting File -> "Create New Virtual Machine", or click this icon in the toolbar. ![Create a new virtual machine](./images/create_vm.png) If you see a warning message on that "Create a virtual machine" screen that KVM is not available, then this is a sign that virtualization is likely disabled in your bios.  Please see step 1.  ![Create VM 1](./images/create_vm_1.png) If you continue forward without resolving this, then you are likely to encounter this scary message at some point. ![Create VM 2](./images/create_vm_2.png) Assuming you are all set, then ensure the "Local install media option" is selected, then select "Forward".  ![Create VM 3](./images/create_vm_3.png) Choose the ISO you downloaded previously, and click Forward.  ![Create VM 4](./images/create_vm_4.png) Allocate sufficient CPU and memory (I chose 2 CPU and 4 GB of memory).  Click Forward. ![Create VM 5](./images/create_vm_5.png) Next choose as much disk space as you deem necessary (I chose 50GB).  Click Forward. ![Create VM 6](./images/create_vm_6.png) In the last screen, check "Customize configuration before install, and click Finish. ![Create VM 7](./images/create_vm_7.png) This will take you to this screen: ![Create VM 8](./images/create_vm_8.png) This is the screen that enables you to make your attached devices available to the guest VM.  The reference machine used in this doc has a Corsair Lightning Node Core, Corsair Commander Pro, and Corsair H100i RGB Pro XT attached, so all three of them need to be added.  To make them available to the guest VM, click "Add Hardware", then select "USB Host Device".  The right side of the screen will list off the USB devices attached to your motherboard.  Select the device you would like to add and click Finish.  ![Create VM 9](./images/create_vm_9.png) Repeat this for each device you would like to add to your guest VM.  Once added, they will show up in your VMs hardware list.  In the screenshot below, the 3 Corsair devices that were added are shown as the 3 USB devices. ![Create VM 10](./images/create_vm_10.png) Before moving on, please note your device's productId and vendorId. These are displayed in the hardware list, and you will need them to filter your wireshark traffic. Write down the product id and vendor id for the device whose traffic you will be monitoring. Then, click "Begin Installation" in the non-obvious upper left corner and follow the on-screen instructions to install Windows 10. ### Step 4. Install and run your device controlling software in the Windows VM. Once Windows 10 is installed, install the software that will control your device.  In the case of a Corsair device, the software is Corsair iCue.  Download the latest version from Corsair's website and install it.  When you run it for the first time, it should auto-detect your devices, allowing you to control them as you see fit.  ![iCue](./images/icue.png) ### Step 5. Capture the traffic. At this point you should be all set to start capturing taffic between the guest VM and the host device. Follow the instructions for [finding the target device](./capturing-usb-traffic.md#finding-the-target-device) on the Capturing USB traffic page. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/hwmon.md0000644000175000017500000000644714562144457017254 0ustar00jonasjonasInteracting with devices with Linux hwmon drivers ================================================= The Linux kernel has being gaining drivers for the devices that we support, and these drivers generally include a hwmon interface. It is useful to detect the presence of a hwmon driver and, if possible, to leverage it for improved performance and/or to prevent racing with the kernel driver. This document presents some guidelines on how to work with hwmon-enabled devices in liquidctl. Defining some terms ------------------- _Operation:_ a method call like `initialize()` or `set_fixed_speed()`, or its equivalent CLI command. Detect and log the presence of a hwmon driver --------------------------------------------- This is done automatically for HIDs being accessed through hidraw: a `_hwmon` field with be present and not `None` when a hwmon interface has been detected. The value is an instance of `liquidctl.driver.hwmon.HwmonDevice`. Detection has not yet been implemented for other devices. However, at the time of writing this, all liquidctl devices with hwmon support are HIDs, and using hidraw is the default (although its available depends on how cython-hidapi was compiled). Delegate to hwmon if possible ----------------------------- This generally improves performance and/or prevents races with the kernel driver. It can be acceptable to slightly reduce the feature set in order to still use the hwmon interface. Especially if the lost features are minor. Delegating to hwmon can be done on API-by-API basis. Log (INFO) when an operation is delegated to hwmon -------------------------------------------------- Example: ```py _LOGGER.info('bound to %s kernel driver, assuming it is already initialized', self._hwmon.driver) _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) ``` Warn when an operation is degraded while delegated to hwmon ----------------------------------------------------------- Example: ```py _LOGGER.warning('some attributes cannot be read from %s kernel driver', self._hwmon.driver) ``` Allow forced direct access despite hwmon ---------------------------------------- Operations should accept a `direct_access` argument, equivalent to a `--direct-access` option on the command line. Warn when directly accessing through force ------------------------------------------ Even if the operation does not (currently) cause an access race, we want to encourage users to rely on hwmon, when possible. Example: ```py _LOGGER.warning('forcing re-initialization despite %s kernel driver', self._hwmon.driver) _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) ``` Do not warn if hwmon does not (yet) support an operation -------------------------------------------------------- Logging in those case is optional, but not especially encouraged. Useful resources ---------------- [(linux/doc) Naming and data format standards for sysfs files](https://www.kernel.org/doc/html/latest/hwmon/sysfs-interface.html) [(linux/doc) Hardware Monitoring Kernel Drivers](https://www.kernel.org/doc/html/latest/hwmon/index.html#hardware-monitoring-kernel-drivers) [(linux/doc) `/sys/class/hwmon/` ABI](https://www.kernel.org/doc/html/latest/admin-guide/abi-testing.html#file-srv-docbuild-lib-git-linux-testing-sysfs-class-hwmon) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7167132 liquidctl-1.15.0/docs/developer/images/0000755000175000017500000000000014776655074017045 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm.png0000644000175000017500000000253214062723141021475 0ustar00jonasjonasPNG  IHDR5/!IDATX 혋OZWزnMF:T. PP""j]kvmUn`_ ÎbŎ7].THd$(QS~6-XM‘%Cw#A8 H AO"m,@Q 8 ٰiS o m8 ɩᑑj#᳟z) I" /u$0C-9PMLܷlmGJ$x EC 8}pW*dF* _kU fT*eU|rR qA[$%{wMϹXc@'8<_l|~ORGn)o(4]׮]}<߼V+ oT2mVc(_o_!O{E=JLޥhy"1*wK^J:?{Sކ_3nvN/ et^W[Sdtn$| A fua6$mkA/#05P; T\ǃގW;(_:B:Bi{b(Htf J,Qrqm #|q`v;I{Nݝq\[q.  uYFz4~%[Z"]bV4g4Lo OD?O;r9k (@,_m@9E5LB%BYӗ%/ #^/v.G4(!Eێ_mX Y,\)mX](oƗRΓj'Ieuͬb-jdU}NnryXc@fsf3O3Bͧ_U.y_l6@,d7:Mu&k (@tiyd2mܲZr^|`0ʌ@ z=ӹmۗM&:@h4`4`4ŐF؍ψ"j7i4k (@Үi^_B̺Fg hihxe)AԴT@ffgb ByŒY1;r:B± AN[[[?봕Zm@ T*Z=666~4jJȧVtƿNFsi/s;mylHػaQ$ N>S<߇Y/CT}IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_1.png0000644000175000017500000012274014062723141021721 0ustar00jonasjonasPNG  IHDR4G IDATx읇_c~{w~޻*I.UlH(EH/ !^3I&dJ+/89sϙϜ9sdKWWW{{{FFܱc֭@e!CE;޵`gddwuuo373Sݻwxv -={:::{{{֞;wnߞ=F P:{\p[uuua,t  dءٹ%%%eݲrv PFޝ޾%11*c+@9ܱ#11ud]@ vܿyK\\26 nwۿJkllr&"`C~ XWnbk6rk.{Nwޭ|Ⱥ  jBDV%L)Ν;յ[^r/! y_CN1RCߺuǏ[b=&8 ֊@Nя@oݺ-D( VMw4Nj! 7***>|%Uu$f4:M>aܾwn[ B1lXO_l?M9bLyDx0b[;T`ڔvd?UGo#u%?s-._1 8iӵ[Yɣ<6GY *۬Tr1ۙdsU0+{}2cA1 N7NQfK-،0v'k]`]VVV-/Pqdo֭Bu0YeT_cAm*۶ezg?"Y{TTʛavb|a|~x wThb7 =Ngi8UzIsV`#Tf~xyK;i9-^ZZZZQQlU)TimSN۹m}<ۗeG參qqb۹(mdk( P**C66|˙kH@ μF0#O4GCT$|`&t(\}fQd,Ȟ<qNmnR.=?!1V.zݦǁ{MNGlWٶ]+l fDExCGYFQn<7Lll$؈ |d{>&&s.F$@ y?T]SfQP~aCI, m;u3Yo&+<$kESO%ݮ}lŐ'rJ`+*[0kЉ4G007Gsf)=ǷMA`維 b߆w|06&&lKPQ  6\o#xzgg7*oq_݂sC8 oG#oU5ż)|ǛfUvz3øֻmW(G1ۑdkvO4q>^0)VԦ'EߎKK/-;SqbBuI-owI _̎ Nx#KVaviðٖl |;`2WbQd$@X]u}' q5h6ktvG&;T>;bܶkLba X6?D u/L]rt˩i:-czzѤ~|^E LqxܙΏ.;3: _"qPQ/Dx/9u|@7 BB6_^ɹ  VG`aњu#/\y:7;?y>Xop@l~j_DDoĜ{&qZ1nVy'G,w⟯nj#<ln}-䩧^^{v'V΁ج_}x'P7ow|(o6='&#=t,-XjЦpvq~tH JXUN097Vr+Ptaarb<_WwUT@ϟGx~@>Kb2X{E\oliA1lqiq ދ3 s9B/)Z)\XZ9K_|iiaH(#_>wkQu< >I+묞]\ƏaEq5s³3?-.Ϳ~Y\?M{!ŇUTT%SДEz~Nlqin#~QrNBg*>/͏ *.Η`Ђ8Ɵ [ jZx,iPBOaBzbZbwƍXFORښ(A; vs6㯮Ud_/#~oR/@ioR/޸D[^揔GM0Q(9@Yӱ&ny 5"\V?-MVIݽIzG._95 @`]R={#O/pVTtX3f?J&&G0'^Dcr>U%iҞ),ȝd*.[v4Zʵb_3GC fpgg9B4}M.8/>o8V{T]翝{_3 J2͝4֢ze "T@4ݘI *C!>BC{=xY4;~.}\X=x7 њOh[K dR7iG"QXޙwf ӝZ ۮ5  ~lf .·B]a6*@j4ɏ*)56B/ ʧ?$[_һ/_6K@9U ˱@g?|!?n>oWXV%Ώھ{gXoy˗/KKy<|ii _ȭ2ċ$& 9 /uzai~nKS? JDI.EQ΄FV.,֒J'& fO]@+ܒEH,8 {hnT|E7疮JwkZ0~:HHM2I` -Ϳz"nS^.%ϣHNC?[Es9gqy@%NKMs*l $X_~},셲nsq+^掞f&%ykGvK#!Nl?5pf&LMXOq߼xw1O/?r◥őᔊ{eS돥EV98Wy=!mOi"dwƵHaeӹ̆>~͖/yxOfiԻ0%S. u_8(/v.,}YyBȱ>#-®擕0XvTo-ˡCM gK໗3 Kђ܆%]Ku8=Id$k#?ʫ"! K:6\/~Ԙ@\S8H葈!lkE`B12Ax㯺p(c&vG mַ>$&a ˪$vYJxd*Ӭ´ir|W 6(%}9*|EndAqy~~`xeqI#YB+*mH@ܧEǫnjϙwW>#Q72WZ8ztڋ79QW蒠@GW"o~m@h]\ZrR|)lTciH`{xѭ C23Br8A).-yB/ goC[gsS;qi^qa@ @#"8kQ##6YԳtӵtӱ8m*&˗/Ac,lGZ^mh,y4dDk/^qr8T}4@, Sᘞ%t,\\\4M5LMFoennxj= w[ ,"X\lv7ɱ/,%T+3gώ4uƕIؑmf2t`:0 3%B*e_AOk7k#vzNvG9{j:@`9Ą>**Ha!CK)Uqqc٩ӱx /_jMUuDL,C5C=C!l@ @QQQTTT~~~qq񖘘9B|xio:YP}:t~Rh >-  }@@`wR3rδi,F=====܀@ bKhBin.uP2l@YV >--T^N*Zs5^__Ynv E`B}*IBk }766644z{l@YV ^uuwjlhn\QK dX;vxH؎OLz,ϯ_cdہ@@ѣG+WJWVVUVV=zT d>OJ?Fѣ^c9Z072C&^@`s hܭ{fQr?xNj3q So t/࿷w+C_/0wT~2y*x;%?q!3@@ #Յo`pppxxxdddtttlll|||bbb|||llltttdddxxxpp]]^^Q^^~O'2 =(}JȫD\Ny~9i_q,Z"9' ?^C V nRyRYJ2D%3mf ^Jz.I5HGF}pܮyA('\0MlvLn+3?xgtYbCrSrŽҝbn̄/~~PW2Fw\*7KjuJn~P;X(ٞj%T)%JC :+zWEH]TXTߋg(&D"cq!q`QQ1^ ?]THl$v (\],e=0 Do?;7wD9<AxYv+ IDATJ;NOkwRdF!]^nu|Zj:gu.x^$5אb';,nr|<@i+zǟ ^~A{9r}kG[#>?[uKPMr3loނʈ;aG*ૄO˟Z b%Z.wVma?x>Ll3J=>k׀,5VۜR:6-+H8瓯7:t8>i '5z>qQ#KmM[RmlQ6BF5 m(B;u [Pנ-%n@\L`^GWV=v~ zZ~HB9@lk,H.Ѓc`@a o$Xl|Tr@lr k @A&o@@(zz &'BX`-A o^ r@lr4B/Io@LLLqqqyy ^@ o&@y' #B@kRp N^|@l: I! 8zq  o&@y' #B@kRp N^|@l: I! 8zq  +{Uiw#{ N6УSUc"D׷\8#ڴN$QFA7߈+T 6zd-Vl*SHo9aEO[!o6`3j:_ @yj{15"6f].Ww(Ekn|&b<܇Ya /hň4} ۜ6=Aƞ0kG+f=۰,{mO@( zd򲳶9ɷ#bD:cu6{Imw윬{2Xo{p^-~O0 mOb٫x#UðU=Xg''q綽:c6>¿H gf])]aO˵r0$P"_#3Qi v0d25LKz~&dUKUijD1 f t6eiNH#oA65s_9B媃}{Z 2׶N8Lpt𒭪ƁzryK=>ցeha5 TzNE)K'< ϭPwmзy=-㬩Q`JGX':Yh2?'šyffuȳ<{ Wt)}\v\R=mYin|mwPC\yMN}W\ZrMatt!n;6]6tU^! 3<8a3oۄ=,./|aG=JnCSSSSsXRWl2{D&BϽ 0]}ӌOGE+BCgL4=f]ޯxK=o SW7yFɴ!<"u'P׏'.ּ 5Q7JD:rʈ~5V|I Cra< &~AW?(L |=ݛvXbjnm4L_<| G@Gj!e܋`DaWG6a8jΙĖ7afjN7y|˳, @(G,O@^Oa2Q-b-cZ]deN>ᆉ>USbM韒_Я*WRi.An杯&sl%_P/x54L!?e^E­{GOtH7-\|G~GROJxw4u}"(2㣫.|H @( =6ô /rWg沄`C$>]u݄ip.Uj۩UF([%؆:LIؙv2U qxriMtа 9avr ŐD-skʠL q.0-"kgQ~mpHx@8M45ӑbbDIl=/pT-1JC+ 3W:Jn#c,:B|tR%Fvd8CrSr}$2Su(AUD՜K*F[r[jIi%3RMU~˩`>pAFSOݳgk< O4:^7^ :P4.P6w=2)V8Xoa 0d*N]f3{R޴φ!f-   |oDMG$6 5\Yf_ t :06qt?W4A2Y%DAܦ6J)ֶ( ME~mm| mږQ3^) |k@7E3@@e=@MA~S4#8M^6@7E3@@e=@MA~S4#8M^6@7E3@@e=@MA~S4#8M^6@7E3@@e=@MA~S4#8M^6@7E3@@e=@MA~S4#8M^6@7E3@@e=@MA~S4#8M^6F_q/1_ΫM@(7z<6ɽBpb-ߤ!܄zZdW] J mQsn߭㛬;_%<Qոt-XujwKaYݠ&~fR .9 ܊<ካ$ CF3jLr})K^~"dl+4I1ʺYWVoAૄ>+?լKϽu;W׾TRmҽպ-d{4NMkۧ>t1QrsJMbʧLxq]ʑ',/̊V:h{>BgMuͼ7MD!C<4^ߋC:GS) 7*/,\qU}K;ybD|d!U`uN I`B?88gqf_OfN>lڥڴ[y7eIM7bhhhU}m#3V&gE!h' QDsUU|Asi }sn./MV˶:~)(:U ay3Dž[?7ֱz8yMJ򓅊@[6 ͠t%D)V[97QmtWV/鯒]7N0̩q'ޭ3ڙTs32WGb SM7{4ʀjI g2O])"/Bluԙ uCi]>ht_~=bg`OƇCæކ[i u=KKٺqgDΥ6Mw!ܵLKgkXj{d 0^:?$OG 8jy&u'!lCGѰ\S`Y{-A0 ^bp8YT$#+Ą :|3$VM$ܒTq =['KmKPtV+D1lUa+6!y=yq%Sև5)W)d #S6)%P3m55;߸I SAvQe|mI:O<}iJVbrDRd:N[l:'Y$qߒ ]MT'S3gfhllǮfj;:&D-dT2`kN#ok}z@Q\hnv s7K;{[^qӰ~LM==55^xEz%#@sjʐM3(">\wIEP[icz9)f1[}249 5"HV͹щ^'(LE[̦ޮP;hz,+\\Hwna-GW)-Q_8QqKwW3fZI-Q ֺwg]rrَHz-澾M)՝&,y ݽ٧4?__%\{pbD{|DKZPAJHXuNa _^/83v݌'R0*߱չOMC?yPx]=uKvjoEL coɒ`3Vᅔ c,Gb{)W èfL %zyQ`IC!aP% -KPSvϐ'ZHCIs&/؄'E;[_O xGWF ]rȎ'I]fB.R $>U]6uCB:,N . nN7[G]W]_5Ccc1p1M umVu砞}(j؞/`&B1W``y%2Დl|.x6ܷձQtISq*Z€,:0 <?Zxbsۇ[^蛞"Q\6:^7uWhLib3Ł6tt0 H?ooW7eNxk^??(oϿ璘}y'?2;Yfw$:gpy]wgK%HÊNQ`ܖN:jC&%Rӕ4UA)GZ%[.3]E;霔J ,V/􃃃 -ZtqWR6yK|^AyjWZlOYj]dcCM*,SrIB*(AQL C+}Yl{k]cüٺ2Vt|mGzݵ5qhx7%Hðcaɺ`޷mvrfhXNM6g=lTӳM'Qc]I@ 68MC֋F3 *(C!k=u>~%8aite! -*C@l,Ѱ%HDŊVikGS|4쯶H|ٌ$aa& Mrho5+v шe-;H) =aoRuus5׹׹kk [%25s^+ 3#N;HػɉɉC=y"UB HR쵄j&:&=}چf=O؀!Q m6CeVGY:Hح Z SϫFix*m3]foqp{g`z&Ewq 6)z>܌;~8^qrw2 7#7)np  XV\#2ZYܛ7L(-U|rlX=V@{y\re]j\msCؼBOY-AqB \i |-S35V)PSw%)1֧EVb IDAT%:$˺Ԩ +;r-\àh Y B/i*|mASՒ0.B/֐5  ˀ>Y$澪ZB*P\\AQ|0m_%kzZ^NqFTיt\"{з=I7CGф#G QW@XXm,bzXG'!f:Gb[;kr;kz:G;)qUҶ k=h⚠7;PD[HߚӱmBOgD!9)$\+Mt8-:G$;H@0YG!+;A*\?Gx BNЅ#VU K 7&' M %Ds^H&H=rJSHƜ!iZcǿ^%%q(EKx-r,}|ct'iڅ Hi6"D)x7Nt):G.[T*֜R 0-W*%1Qfu:8,*zqfqD%{ Y%2&^/W< BO 523&[Ų #!s)vR;(Ul>֮TByE.zYdAj l0>#-w2tM_2TYɒ J8-x8RĈKQiV(Mp Rz6ySRD+޺TJbAV q"hF%p&{p=_GekP_bU"B2%ڔMxaJ[[ $ʱa/ҵ k?_s^HdĻV?xt>QxDmp٢ A_b24Mܢ2[9F&Yh2j:VQyor 'Z*z҅# 3+jA Kv}l%E\ ^^xd|a~`a@llۛpwkm*KE+חz@pDB)lQQ|X"cݣrDQ $v Ɣci^F'i CGTUzPyQTVkSb'5iLLBI$:Y$J B^XՉHc[[ CzG,#cYi/.6ot`@`-  *7;l~*nrHZ~~ntYv\ˉN_MOUd.9y-c1X'S7 WC+[ |d =22L'eXۡ3#P.ī^?˲|okNF'n8;1Q hqm!K1l%A~ vU= P2'~y)U\om휍oz5,\ ezا4AVº~]`ɜֆC&B$>“6ӥ3`d't(#HV^q%SCj 6Ks+ +U-*54L L5QfrOB.̨cJ9dlU'ٖDxѥqMgd3MegۃY]*5! -'OKVI(n&Q0Ř<dՐ^ (>JDҏEG&C/A_ NtӍi6 G!"E*3eɕ* |\&6='栆͕F.J*q-{tqneF]#}/nP H7roʹ,qMgde@-.%CFf6vU%~;^.^ZEףS98k-zHIº҆Z]f`^gpk㎨zC?&G]E^Ʃ`iS>EKꗁ. bu5؅9-Xb m\QNacϤmdXZZɽeF]L]6[jnG#2 דqw)?6dpf! Rr ;IQ^s9(M =}\3ﴡV  zĎFb+!{|SzfjnZ0#6.&TBɏHS~ /ar qXN) 5,n/-|`182DLܩei^#EKƸdUe0/C.#r؁msKgBk)9VIQ,fB R)zQ&xta]i.30_pNGN?y%PRCxG-R墆+Jz~GU9""ÁR9$O]EWibrhӅuJם}.@/ą>,]XZ!t mf=R ~~`4h8щJ))mZYW eDͥ@jnzJ,pz*[*5|V%Q.oRYN6#UtU,^ "AfgFzjUا4AVʊ<,w~}bJjzeNJKH lq0HI&Ń҅04 = |QsK+Fbp[j W4 FJɒU\ 0G5_h҉IЖr6mb -AfgUNNLNGNyUI(T!T(1z%n<06:h14L= T.*d(@ +{ @@@e'B- Wv  P@^ Pv ނ`?@@z`7@@ +{ @@@e'B- Wv  P@^ Pv ނ`?@@z`7@@ +{ @@@e'B- Wv  P@^ Pv ނ`?@@z`7@@ +{ @@@e'B- XGF˞\Ն(}=wo?e#}c#Rh,zta~/J-BV~d 3'$ʒIM˱;Hr.!>Bka Al˶תhSs\P\vSsj_.A1luѩ1kBzffX~uT7i4VG,OHjkhUkX8 =2ٖy+6Ca`w$~~7΍`"븟V)z^C 5\lByf>yeBOwEh_ 5MV']u$( Jz' *G*/;kjZG=L|;" 6w*Z㛥P֯NzQPӤEY?v៷AƍitLqsqI8ndaf4M]Tw{ƚL|IhA.t&6So.(ӛ}+\iv:fTl2fsϛbDl]/͟y"wơ0m<~mHLsfQL]ԙmU:-WZ3W 5CGlXnFI#.;iA1q{/fp&Cd !6M 77`RmgiJ ~{QPӤ EH!C2z;2:긃:So#a*EXd()+u&at:dHp*UX Ðᒄ3f:,CMY FC^ֹ0IsJ[ao:Z-GJ S/`\-'IdAB ==ɑ:L|:j°;,id,[GP\@m75OD19ˮ3nT7 o4^2dW5w~)6<\ c}k0quM- 󈏳(ƭd~f C25}S񫝺Or =?M~\dNQ1d8󘦚{Bp|tTFZuha?~yS 8s]T^| v dyrXȦQ صJLM3&zzt}N)HfS(186:GWE8PH`BM0- )x;:fdBo”)vᅢSm"<تwfP/sW[?> |^}!5ȏ#'\<=8R=@!( p?[wMf;8?eq*ϚacpY8U+ו W9[9NX1|ڳZa# y:sٚnɥjsٱJIKBjøElB_:!L/t5cMhFic42H&@qO'߆2YGS~IJ~͆J|УYaZGRZ BV,T乁e{kSrnhJ'nd"KŰ+ ð(2$4)yɕi}h6~><<}(QpkZ?ئ"2Dp/\WrׁQ'"lKBP A4 tB[TK>М=|rrܓܱ4͗B rP sQ58|ZC\}:=,sfY1';pUxtXOv)~WUrͧn.dKKUMC;Iv7Wh9UYEaH]U~ČCq\3َb sZGUg:sނi0ĿXu,,$ۈbMA$?cqL(s9zݯXxmg0msZ<yߌmW;]iRe aEşލel5Kߔϓ:;>]`c"wFơ1ڑiXlPdƱ8я0cGvqËab5v i}<~m Ӷdz3eu+oƒӓ]@ZVwG i-̋Г"TmXIe9k>;ê]y?FR͵\M5lUVw 2^S./TI ~ ae3DN%!q|ey Cg] g bBm†D7俓!p}b~mo*18ސ{~wA!1+k+Jbx1؂ ڭQDcB}X [J`$Y"܍~Ks [l8gp Gm{9I;0_c^ZE;sӘ8c'|kj|x%!܏EDA튻dte̠*>Dvi11L7XWe2>oPp^ rYe +< ldy"w Zx\]Ia>Haa;^]GUχ|lsO#lߨĘ@&J  Rl#g#/Xڨ7P47ն_b.i^)w)s]?GqBIMx6Jz[A;j.Kޣ<\ &Eo~_]CSI[Jm%MǂS4m~-bI0o5 /_m3H lBѣ3wn%߭\ŊFcaM'~7p;(B@MLDB@! &&č AD@`7h$OΔ@xצ96 47<=J@`%f闋~LY|Kv~À `Q̔տՅ*~#6w l&<`R\ _Zshâɭ=u3eKy?18Nk<@D;s#`n_r5;)C;w<Y;͊\.yHJ6 5mO2vn~ڞ9%[ D`kgSw\Fݮ'ͳB;ntLCm #K^^{ O%wDpBS%Skk]?ue%sKiv @v8?<#z/oߑ_aWn_ػf:Urě/fs(Y3['\9mK.,'~CK}=ӛU(?.`rcdMZB+3/ L0>UFA̼wG@xQ.h1ӯ/^^^^L6?p{\|u&tƶpcOz1~?O~bpzfwMѾ/v `'5nýIMW>j@jO1}\,~]7K:"sk{+kj^]d V`uq8 IDATcrtvqÊښϲo_5j"qKZ͍odY3ײM$zuG9Yi(//oUG}1óIAn6V{+y۪o켵bUu{;$}ohȻ; rږx7JIuLTt)Ék7E+L`swVJeuF 3MHkJe3AOJ$~8,Y@27DnT&}~-tQF=;ɹ>wD>.U7F)4nHrB67JI/n'!,4ލ̩jTeÛE1F$u{e0HN,7SSD!5r+U_V5~Hw8YLΙ10l5,`Y⦦ߚ9 sgw$郏f%" GB/V+b!GJ}8${mnCGVݎ0 G|}yR0tZ~xX)'= BUf˛_-y흈vAgj-X7s&4S{XE:cxT]xda3g;+nFӔhйqV1`ho|YZم@i27GnH")JX]=!0e䕊aiHd6&/O41biN^&ڊ^ <³vCB. ?XR*wX?&ʽb0؂W*O^^Ic11>a7j4Z|j,G(fܣ7vqro :Rֲ|UU`;8j.*C74`t#|$eqVEg;}HÄ俓!p}b~m0`j)S+yOͭ抛$ur fI7=~z}#A e`[`O|Mw"nj[%QEt_73'7}Q$#@M}Xwu=aSYI"r5`'6fg?מvOfjٔ3v/}񮡩M@v-/yiƦrP$b#f]cj"U־;;o_΁̪w#}#З㼼Vw̍Fg(`? K[76ۼYiT3 ?,#2?~ٓ8VUճGVt:PA  #lEO z! `%`Jp]N^#0OCl|G >9xMLYs_.zLy:f!in}&65_.z|_Ol,"[IX~Sѻ;f ADX @G z: X:`AR  xXjG*֊nI:򄘁DJD:niiiqqQ ΕtB+T~_xN!G^^YIvbZoy(_{%<#zE \.wMCm >|_Y5?_(ɳS˞ȉJ6jbsK2Go"b(n#ѫTՊ}cv}GGʊ@1KH'9#`)J&[^Soo_gI#'˝L./=t5>+f."WG x'bx3| ,u1=ZQ~\ǞɬtC*at4z9` DVmźY'm!O?g0 DofD;yDPW>PJބEHșRWjRsj7'឴M$zq4sKGjO1}\,~]7Kx%Z"QξRqbXL^晼 lV軺>}DvÉ/o_;6̗&\|A93.Bj+^l]}֎A|=;#LNr'//D{ Xk%GmL靝HĝniՖ[\G"FS3Ҙ']Fv LeFQul+u?ܵe'K!QEwb`p&w:N*U8|ξn)Sى^x7x0SQu"tQ6ĥ{{{{F介΁qh涀 UĄk+'PmKZ<y>m^;1 un,QcsD1ئ5Kb .sLJlr"^*>F=;ɹ>BL 9r 3MHkJe3AOJ$nz^*7n:V~&ݸIurXqI,RRy=:'_j74Iюgz[~D_]]DEHdW5MDna=y~ԛYe/>̢?Pg0Xy"Wo4ya7i ]vrufj-2byg̙'85gy[~i'/x#oɗUf:s6i4MȎV*'Gn͑8ËLm6Pq(#qOl=XrHnPęC3;9 ,2O!j]%z^]0) mr<%t]njlANDq(+RR`M-jzDi =PI-݌(2BLanBV.֣3/N?쳫Go,ÍjB26NJoZaj|%IgL$"Whѯop8)scQ!r ˹bu~6+9>"@q7ĔDoN~fə֤XHr07Zˋy@bkHlY:my));-S[_۶3Lʐ+-ξ/)+tJ=6BH$:Y/X,Yf8}8'/ԍ-= R_ W1 U'T~m8燇}(;qK3 d};zhG{W1i0O1(d>qx!8ۆbŻ8dʞ+Q7[F7>JILknP']|k:q.Ƿc>d[l)+PV<ڋ, wvvlˮV0py_.@̏WSH\fʮvΞ4kgȔv3b{}C;| {lC7q<n燫c;q=o=l`'zKme{)r [ty}Wbkt Nyy7gkR8+8rרvřîxt|/}UǶҙ蝲"0FO"ONND"*WDIyVб''bZލel5KߔKtVRL*zUF\)'D1 880Y;r;TmsZ<yߌm'^`?Ȏ'&}ȏq+wHke/jZ:uGCXw{ Ցk"~ JY=k=9o[<0k"//!h~+bŹǚf=z2t߉ڝ}񗔎a󊒋ӟId U 9pvrVDGaz1@[(Q: R\բ0$*z ӏr/Ƅ dqb4V7XWe2>U`$Y"~ԃ~Ks V=ސ{~wA!1f-1q8х]5ĘG?Vݏb b-Oa ۶1<ab%ѽ73 }S yyywEr,7!Y;s mcٮ7N 2!]SZsVnn=[tzݮ`+  x@͵r%VK庫8" >fɭHGl;tzD o4mW/cgDaZ訯tG)ʹ9Ct$0מvOf\`nT6WF\SXާoJ)w%q7b4=aZv||\TbHT[[+H&&&/+wyy'fFêǐ~e Ө^g~ FWR<)zv jZZ=K|jN3\em`%@"aѯ@7ѻ(dl@E   @l6 "@@n  6fk f z7EV{zzRiMMMmmX,V7Dn6T@<&zEU*H$tKKK:nhhH,wvv|o&狎W9O$VMۓĴnH6d~6<#zE۝RPrw4? Uo8 [F9T{ikI /[ Jjoow>%BѱK̽u̒^] lkܳљW|6/mL x7N5BK}=Sbxyyy1؞7[WϵDrxSIM%&>|p^"D= ~^V;˓տXWW7;ks}$?l!#ޜ&B$zyl;)Ej^UWK8y<'឴ /Yp.o+׵e'yqMjNɧcxi0g%|ؼvb:7hY"Rlst%CwjMPv6|qXF$Ps/O N[ )@Xg_O7~,͍M>Trjae 67zii,tgˋ"Eo&mkv"7X尞,ztQocf[kNnLqvb~el0?73n ͝v".H)Ƚ/ˋZPXFˠC[wOf":dcH>~uyq*XVa]=z`nD^V C(dmD\6D"ru)goΰz8ސ{~wA!1qm0]e>aμ` ĵ& J'1`Y-}oQ$'ri4 3WIjZәL_=[EPC]! w( j(  zpR! w( j(  zpREV{zzRiMMMmmX,V7D6P@&1ѣ(RD"QN[ZZZ\\tCCCbs{+f@ IIÆ/~W9O@`#<#zE۝RPzP[o]Nbo]]К(*}ǵ0eZ5_ZٮÊ'gDR]~iiIPtttD+oS03˖Ķ-Mѯ %&ѫz}yfggޮ>X-zG6Yk浦 9}WWקONw<88c,zt#B,qVٓS6y._6ksw.`}"܏G}grh68L, D/ 3tR.pqpC >44VyQN(~q; yLLjJ$ >#Hŵ/E:j[Y+%Yaډ O[567KD_N\oTm3Ox7x0SQu"tQ6b78s1#Fr`'?9VRGDE "mBl 9[/Ij_Kƒ3X⦦-&uݍH&?ڣRiˬa2FOqH`AݎY,Rl4#* !>)UYE@? 諫Bw(^)zkC{ ~akeq[%e1v'; Cgjd2byg̙Vmehs-_ X#C{=f^e,s,:):}R xRBbkHE60YE\@&@&ET]]mSc_+#柳n-H#2o`c0iB26x' q[7^I6n*ed}=~L{1&ԇ`!{TLvؕV9zX$oDʹ~,-O}|teA`ˤ=~&}rHD aF%2yWjIOƇWXmLOX+Hy: CNtauzf~ٶ #O+N+ 嶇_ 1 kiiKKKrI%aT 4QP}fVꏩ]#xFꎎ>NH|t:]]]R4"zc /K dGRj,R4Yv櫁:xnR|uN}w۾h7s҅馲$]""xRFqc2;7[^&OG2h3{a¹I l?~}.vP$~ % }O r233s iEXW4_vtԢq=%U° ~Aq7·8{RPW2گuʊ/c_U-oKsF9Oǧ̳z_.DV# lLƦ ^nbYZxyaa03`),gYYqJPB8a xNd}΅p?;lߥr<ϫ굽 /.S|OwUegekesrr Jƥvu-\_4,,OU8x´=| c=j/6d"L&/қOY;IO 1"rq,A2+b+{'wqlI"Y0tGnKWjC74폄^ sY> I.7˛> $.gc"/?|&kۏƖ15m=0fŸ#anݣ1wazdpdlbbl@8;_4#T5+S|yc۲َʇ' n¯}+ݷ‚yUde󢮕 rvt= gs>5}6[srGcXO-NwecQfsaDGV~#6Rb%tHf,?^7QM=yYAZd_ ǽZMODi>nY)H}z`eAΫ֯=ns^,hXkr:GOGvmqaX|ɊNh?~,LU$qWXۛ~۴[oj$/ytYه Yʅ?RsP ~lu;ipC;|AП~SZ sU9"3;7vf Lan"nC7[~uj@[ݳ6;(z aa^Vo G%&;=~:i.݆En|z'?VZE!-ɢ} {؂ՙEG|cFpw~è²; zpI`#aAhZ՗J9}7_ʻz8R'UWf }?S@_۟P}urIEcMD_mIns/C7ݙޥ,&w 24^zH܈;}"kDGj*!/j.1?Y_D؇爑]~A]}ԛrFU+ z{ҕqkzwuCO؉{],UgT|85<;i5{SV9;S u'ؿdU:*μ6PaAҽ{}v?ĴkIDATh!Mc zpI`CT067j^H"kiwނwҝW]=}=vs]}R[ik[S[Gݏ=;|kYm;q${Se#5nԾE[42::yxaVN48;䧆f/,Òٹk[{|2.ߌ]X0N=+o=ɳSG4iӧnmOZ[?<ߌ-[7n/G;ܷOc9QЃ˜~DX"3=Sتh?nҕڠ#n֩Bp}bàz_Bm<[ҍwZ ~}#L^@woS/ņ @pWYvb~[g=p;C_]YA^6R Շ%YY9yEʪ:&,XiZk?7?yYߧNzraܜ)7SGDǏiIEJnHهSl$#>^I5?Kmdn)o }0a A""x–+qѓy-RF_sn' L瑱L&TJL&3~BF`/r/ݿQ.]JO(J*LGFFh fw8<77uRX t$T*U*X \_AA뤰 H@R?L7:&IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_10.png0000644000175000017500000026211014062723141021775 0ustar00jonasjonasPNG  IHDRO@Db IDATxXTW?}5$oͮf03t,K,G&V * *v) Hsga2ws={>s6hT P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@Td~۠A/4h jРA[.Ԁ 2@ d 3f@_~_Glq[jɷj@ d 2@Ȁ2 |/m[NM7__}NuǎhӪ,[B  d 2@0kZYXHcU(ϯ AbI.Al 2@ d ʀں@i|UmX[4 d 2@ a7nܧ]69+ uMv d 2@iAʡm2@ d 2P>GSVV(;j2@ d 2@>G*3Om7 2@ d ΀6S]oL֟'$2@ d 2P6-ѫ7^A(o )d 2@=a .}?bKxX DʴS lSQ72@ d N/b@",TLDTahxvR'D d c`Co5MFjc  uV68r"fMʂmXېPQkjM?b;?t2P tD=<]WZ{ŷ"+j5<[<#v>Z5lwkmULkל)[}Sr^o͔:1OaJ;<=BvFnN:ŦY)1pN\OFVVoa#ʩh=dd$m(XuW2$CF[lU癸&6VSClɆV t#'7HkvXh[$7}Z_8Xֵ/C=ʽ~ֺQBd0c\OB3 mt[w[ '+0WaAz'gNwF"8ZYaad桭%Y'bI 5R4jVyBw|ZOιF'T {,;`(ZM.SKQ}x'ogȀ1۾l$LQiP}Q*(sr!)"Iz(Iz ìaN(D ÐUqh߶ T8gu#Y~*u[FAv}R-0T\mHUizv lPzʻ\ 1ٹtjժ"hǁ[̓%|]e\XnߟFV̫?Q SxA;c6 ̍a n KF# :'{_rK}J'.TT0sO@ 7h}βb#OpxTgnnЙRoKЖ-Ѯ2FI`i67 //E'؇bŹԃ T+:ԚL\ؽsx~i ܼ"&[Mf0FY[q`JK%~IN\]ۦaUcxib߬\R42v(k}sU$]ВJCs* ɀjW(ᨓ`(|W4d T'VZLAEĞ݈ƅU,I' 8)0ֶUQ Rz7F0@oJ!& Swô5G,"ʼnn7CFLӏHu&i{&OiX;hDl (r')JmJXhRʾɢMoy#mo]4#HRZ9MCxڬ0Lvh UF¨G"D'1]Z9NFXڜh,ҍ򪱘{t&63ha>}.ēh3ڕcmh2@j-â*,]A'L?Uge)2i4u=XjU*sb3OHU .0Od⩻9OMk-MKSiN +x4akzE\/QԽ#OˆjroaU9wÖh] KiT+l Sپ*@XDF³l5x'KKL'0 Uτ1I;sl1vtOVBb1l^k9۩*]_o6FxJ*OMMƾs1Bvrr)]%cha$N4Q, ފ9Ş#Q/g, KF GU4"f%Ǯ8Tˌa =또F/b *I7;\2P9tOFq*aCry fY?|t߉$\LWCteR͓ؗV8IU!'3n_G17Ki0t^\ 9YHs(X/ҘXOkyiY*UYHm jDn$g"')B.ॕcve[G!^L3|֓FQnHp8`inJ^uvO3ڦEQT 8@i|lfɭ0Y<Np|֫< :v[q&76 *z^[~& qoht,g޲eh?\ǏQ72 L5zPe| z6O&b*TL'jH @3[pa(42Ѭ Kt[|bj߹(]'nd 1+n6t.?_d mN%Øi `”Օ?A&d 2@ \)@~$ S;Cݩ; d 2P4O4OW3LZ/}'=ݼCz_4u"d 2@jzaN\ي GXwx䍛IU sS[ i<1'ãx < -NZ7c)/ j⇀ gd 2@LcΛ4L^; 8u oDޣV;@_1 A-Lwܸ2qbF ?;wý=`L胇Jx/DVzy@UY6wngޟؾöeے2PIysz1v4S"T7¼ȉ>n49jh2pj(nBSrc gڮR^Yܫ ̞~^X.A^I;;" S /i]l?O)mr/OZ¼Y.G7r[Qn*x+<./Yۭ'mӆqͶeے2P))!&uŃոt&OuoV\u*Cg%ld2(ҥ'M]w+_!HxտCzٍ#ybtí)⤣5rފ;uzw*V3g}=qۡqATN(6#'zjOW\銓ĺ()#2B}+*Cel˫@d< vR[w:pWWԹSZ(1P#.p|]yzbªSHTitn܉4\^kgUB7NG.NP*? [/>k%֗Aܷ'zZG\rP܈x\g2347 sw/2qkLVyrԿ,\XrUU9mmd=ViQqR>97kΫe5tޢԜ 2`u<-Kx&ĝxdC͓&)#i7|j0#>2n\;㼡𜁰4125 q%9=!8yLۀ3p>d/32T7fsÄ !}g-sW,;ظ8i{ܼ ͓YA:e5-'Y-ape@:œ'q)􇽽6^O,gkdpnvpUOG\a"#]2PfeB,"#KiDZGY*렓}SyLvS{bkRN[u;.\cq-QUqR97k΋3dnWXnԍ 2P'0[j'l-ܦC1g;#yo aiEVkkE&嵵QF MljM5%9bǀ2@@g,S݋ c'i<ֺ!e^ ĢqTKpH]b;m<鮒mq^ ]s8w1X⑐BUkE詨yҤa=y*W {`Tw˜dG) IDAT]0bU42LLN6wB@ED܍7<"IoeSA{uyZ㜝 e8mk}]NKF^dfB,%{DVy2;J'mF(D]U)PLX0n#SmG !rFϡ,@ 4O7tK#qH揰q8ta& ;ĕQ89åV/DWepd?=QNb ;C`nNUTp–c³5DbA75ӥH&[=wPis_v͓:5c]l1db 9mDpȁ>qmqƦ=B] XxWbߏcYn[*cXNeνsص/W"qtmH7T.:O=Nh.^BLPdԏq#.4h>?mWS˫α y(S;v;gҡ-NYiY;] gA\7OiEWVqՓL򶃢?f ĉ3p1\+ǗꛁXle9ӱbĴĈ#6!Rsm~x2@ yUecӑ+_8~N_caqbO~xRN<$w!2 2}1h^1`߉u"H+G RY54oxt GJ\<$ס|GcXV蛓(_ s?Ѝ81p*h9a^C 0`橠⧯¤c}>8ztÚcߪSYs{콍Ԛо'i=iíh8޶5ڶ E|'։4"#ik 53 d 2P yh4PTFff&cB0q+ꎃb)/9}}1a RZGeԤoK;x(.]ˡpz,FT9ԏZV5de'.XZ=e{!ЪZþVm-;NҶWbM+V;vEnv\5T,mD d T7fcj5rrr$$PJJ >}DDE`z/,zaϩXMǓ'O4"S`DYթ8|yNs ~~Nck jGq1R-[-dڲ#P"IiZǖr̵Rq  d 2P y#E줧#-- Ϟ=CRRd>|w"f$lYS4i 4"2 D|Q(@ē'Q#+d 2@<ٳZhݻc7n\QFIeD;3;ӻsۚ 2@02O:t@˖-We]wQ(cǎUV& <$jI  d w2O͚5È#g>|p4o޼eWw$<5 d 2`*fe~`ʔ)U#e*@ē'Q#+d 2@<SN5kr:Q(/*`4OI?xlk2@ dThLMv Xl'QSOLGV d ; xo(EyRqvAvؖZ9ƟvC#}fKW` x+ ME NN$qC%琥B%oU*RT2@ 'Ώhݸ6$F IoՇI#X?);q3xOХ B/x|:ؚTXΫkѱ'z8GoF信|p/gy o1M%#~;~40xy1lfGIl,XlICӽa+A&Aan0a&O7$Y]1|˭'U<~4E"WyJ1rk [K+mKgi`Ē)BU@\zSl ) e=!~- d ͓A^V&sOZϿSuH2ħ-aS ]%൑*ԍVrs᫡"?sQl/̹}1+| ^ݡEM>WYdl yjqu9,hcn{x߃ZwwZ~q;G|WtT%aLg9X{]W橘Fz};ԂZ2@/fej!yc|5i{Wf {C"^5vF?8cwr6>u]|@6->#n863_97Y< \;H;/a͇Xg8-+Q.F*998DJיySߧ30{#St#MyRZ(1XZ5Jn=1a)$&Bu+:izIyܭ薁Y[XAp'9/ 8xLB+f%=WVW7xv 4O4O,&_d gSf0|ٷBټysš!X[[ybaϋr'6OZ-rnm߃Fa1.ƵX; KSCyb%3 Or!QQ8y<0+(Yg"1O977&l8{ oeI/. rv@ cm玙\NFВ>p:J<ѝ;G1&H͓YonvZؾd 2Pfe:t耖-["/e2;vX1~ӛ |b3V#n~65GHƍ-ZcۢaEq44Nچe!G <eY"O9;f ?lä(}/WmFט}Sb͓6")3SN`J(4ǁh ϤryR'GZvYwP9)2)cu2yb6 R&˘]H5{'uqd>>.X Vu 1X無i̸ ڕxd۰m y {g-Z{9r$ƍ9sThyD^Q(K)6zWɇ _Pd`tYE!Ū 88m >t I"] #TfYZ7mC*2vE6sqp0zPkUnB `QFĄ ДySCbpwq{W:(II}AaX7/ _V~;( ~Oyd@s58=\F)1 &}8M]yy2 uCN%ې Y'i߿? >S4m 4"2DYL@E0wi{WBW;;w x$yʉۆNX9~'щd ,rpU,:YX;s:V p]>{rP8zFDZu2~ 9gt,f.tXGrJ'fgeT" yy F('%ч7DbA7gJ96 J8L@A?SҞ󔁰esb\[҈Xfh!tF('|1h}.j:&z@&Sb覛H7\&E?S sW|]ٮd2PԩSQR1R0'd?h9B.8'LwXt|0e~]ƅ3G`82Ԣß}a=;N©YC!(D>e<a`G8 \siȹwJd4"Ncq Ðsk:)`'W@8ngCv-^1oz\~Z/Ǧq.~2>.,aK̡ d 2Ps橼N/SP<̓!6m^Crspj {w\;40a /8();c. 5 J( mݻd0BA¹ }]BխUzx]VOf>C1gL4[Bd(ԇ 2@@Iy*EW+#k,)r?zpjTg~BӕMLl &+>/ tA˿}5_uʋioR?b HZy8: ?l'ug80~;4l'h#q$V4BnsO!px-kz5C?@IJ?IO2O2׎Lx}.+KtIS5VES*@T T<6*_ >ZI3M) /B`1h7hcXl>a חq*+||шTqeh ZB`Q6hs9`[3p 8ge|?}'oMZa貝 >clwP~^#=83n1wL~%3 /4&IIUjS*@T,y*LΏhݸ6$PP0EoCu ˚gֻ&4O5 27AT PZV橔yjey:~' <} ?12MkO͛?0L4 - S/N|o[G#2;~SgI{ >帳πgዏ?5T!TzӤvίhS]MT PPNZHn1|_ )cޕG&ސXW:ɇkFM0O3wùgSTQՙؙJ#K@F/E [+7[~T<(-^{ԕw1Mjya9*@T Ԃu<5k ÇrD͛77\bAU{ rx ?3EKj{QA>S~^}*G_ƴsmgȔK|ϋӋp #V8'V4O=T P*@@U+PSвeKnN*#(KٱcG秆`'K p| >/tQe0?A8kHMdd&J)ŌqškpFh S.`Ke-̓|#(Fɰ\?V(2Oӱh7O,?6a.ջ&<Ռ T P*@jS:m{E޽;FqaΜ9ZDW!eM}gb뤞haC4×h:/$5FƵ)>DK~Xy6cF̈́g͓(+ G~?PM6^Xp Ty)eK]'@+k?]f‘&͝]oh}42l2y2XT P*@u<޿? >S4mڴYG T+e2QG^8"g*RݚKSe&O0aZpKT P*@y*Oeq=5n^\T9X{(HE*^JתhSQ8pRRTjgjavITc;V P*@@Uޛ'v6)6R󔗌@'{9d2mꃁcb'x!vMɫ;Vk-M+%Rd6idjV5PZ{^c!JM& M9I<2X+z`R0W$DECT[z %&80+le .7O2(;^7 #,$^+qk ?G:A&Sb̮xQrX˔~=Q.#6uRH>4IKGKahȬRFߤqy+'@T Tu<x_z@u_,,M? ،‘ /]g1Sh6Z;D~iuCfG/"n)d9(x^K=NrXtx<5TZ4|i Iwbֶp e'¹e᠟hc뎁SV"(&MHQi{4G1Q`;.ba>c䞝ۦHC^Qiev&G<8a/w_"?#c06oؔճÉCTllUe9<t\EΓ׼"4>2/,\bH} u'Ņpm&Iɠyz,H#NEVƎ{Ul;AX;{4|E@ kv N 1NdzSa)n=Y~˻--1:I]yK>`3ؤ4 R*@էSi˒!Gq^{Yx0wfpRY g}vLp!ÇrHAoOj9%&D"@PZ'n{2}BBC cWd֧GYyw2nƍ4HX{N'q={LY~\:Av:?QW[ :=+ph=QV-hԅQ;DQrN |hD;o[LD:| O1 "@ D$O܎3y{.7M`V'&>{Ǐѣw.nݾHH\mn;aF:`u#1vq)v)w!gp6AyBD?úM}n3JAIi*ē 5"@ Z'-۷g< q'OBf}>%]ID(nOLٳG%JDžm?!O=y0[q^'Oc0ksq^ӯ#9% q­ybV'VH78$D }S}wD<ǯ?\iW lWĥDyjٶ?O2UA DL@n֜Y^M)իHyw2~潲i|xzYnLGJJ2bs9/RkM7tE?L12./@ۧHts̛ؗQx cvXYKbzH<}JT`wH|bغ'Sjj v1 ۶oPaն<oza髯BY/dY:(HÆޭ],Ƭ'(7S(XݜQs&@c4*Vg=cQ;RD?V"$ddKSzdipl)oxcA"Z'5Ob/qb(""c8qsUΦepx=gV*b0MknnLBCuœHG,x=1f}1R_srU\Uy9b+;TSUWE|d[:`SVōZʞڳ L{ň}@~X+A}\س),z [RtWZ~<+OFBuLL-4N{ 0؂!ݫ;zN?g7=.U!dKDqYx*Jƚvn9/VQumgְsdX8YcGQT >'.]@NPR j-$)DTnsy&ؔ4$'_A\%b붭9fT {/{3x\#'0B*qc.UpoCw E1,Py~+D;ѸpjC93~P s:,,a0\ =38LU Vѭi+}}c'z)8 ͼSx!>S67Lz G oR}X}ךɬLo2&F`.F A1D?L-w >*czڙrV\B: lؕ811=eO_6TG旊|OQY}ҵ.<'%稈x/-'sw!:Sq;4hJH:Pk1c8rS>?а`DD!r<$q&HLL@̥ha.?+SǮ) jeڍ;a w?3GuT?U= rѴxl>tĬ^mQG^x}HV3pELϟ*=.yӋ\><Ž0npX`Ǖ8B lG~)> uzL{/ƥ ^#*h 3S GC7QX>Fn CQ2HIJ1.1޴ª!ݣ1p2xGŢ>RB;> /8kl_ͷ`m6'2Q_ 6&Vu=G&NQ1W`̀058',&D0~h?Xvi[8a8c^051UaX}2 @ƙecS"x!L@fdTﹶ0Yhf6DŦ \򑟸6㊴^Y!?zad"Yl]%떪eidyk\>YE8u05 Kq2HPs'S k)Ve+'x 1rp_XDQցrFaRp}|8,1Šu^B\5Vp<8z,,-GPX?mE+H%(smE\cr.!6BˈGD| _OAlzaMkǹ~%uH5W©)7]7W[ +$EY+gÓoiQC? 'w˩{q&[O-:oeoĂN S{~xR?@>"d#㍬1|Ix%a=0 Oo._ [KLu* cD+u 9}FuWH/*?<^`9f-V-T|.C,0fU kbE&U"vHvCGIĭ XޖV41-sU{˧zXʈ'~_m_UηDhO99x%>z%˗b5wpL={`0ܾsg]6oҳ|,?+ʥi{hTAهpZ ʃ\YquQ\O/wuOEr ,f>3XUDrSvTU^UBd?8w5F1E]HGܬg4}gKAٷ;39IaDl8FD0b^0.K]ϹH~S{;lCFE7aYOLq=k7pFGY"󴘀 z{X9~ft7A̱#~ fVs׎czˈ ЭT*`mሳ!wt C^#X#]7"*No9i"ϟ 󢸂wYma($'_¹Ra'Iﻨ/-y!W_C(.>J*!Iq[rSy*@- qc"<.%^ABfؘK8P{DyS{c(r=K#ٖ3 F@Svv6^x⼟\W! 01.aV7̚= q#=gY:cY9O"~^~ì$+L̬ox,InsDXqz1i}`j6)Ë7nl+y+0*a;`C`WA=L,d]r6ؽØ;c ])e*Ͽ =aĔ~2^jv\K_^ 0z`/w7eo ; ^ r*8n1⟖/7.LzaPt5W 璞5@|povc*[^l2}|?lrrL0s]p.Ey \κ. &aPKt֍`xkp pnQ_XM).~t Ʈ!K`Yݧ~^hY]cSγ0_Zƈ)c_E*kNd+SR[(S WA;~ke≱Tk2%ܵ}ta'ŏQ{GOҊfR[>4P{j- )vLv^Nf=R6eYJKe(PSjgO51*'$KW.QCItKP9j-T&5%@z8&R ;sHL+14?̇mOZUQM|" x@kO1 "P_ ElGCCj=Ҏ L$j+ 8u"@" x": xD<"@ DO7&"5$@I L Dj TM`#@I #D"@f YILH<}&T- D"@>!OQp|Ϟ>Y9K!p*M @IF@ DZoOxڃGǼb*a>rAgqk?|}8o'x՘>}+'&#M> ׋RYH_iRqYH"|_0h-ߜEprtjz в<^ftkocw| &8:м6=@)Ds0Щz?`ľ8>-tX%|Kqy k11|_]+#@22N D!) [9cҔQ=bF !v:n ְqĨ13r(.N;h`_ =/PJTSڡ&xNNQ,ڨ1F1hS[9AGK<g̦ i*IÂhnap}Z`QcL3ԫ se |QW \}|Σ:An{6Se8ӈ[GFBn7O+>[`< |\OH<[*"@ B@ӸcΙ:j DieK0]н?n7zo4Mۻ'ֺ9zXҚ]bJO6]&:̋D ׊Rn·M#@l Ï:YT櫓۸.L׈lp{&e劺(xa¾i},k0d$"@ _Ot]S|xsk|}pa;Cq7vtqHHbBpSpA;}8c\ S/}fMv?J^`XsCAiVv5s#816tttg|6l=B(G<-8JcP`* ΍~VL'4}O!-qQVBϥUJ9_z*O_@'D"@j-z );&t95Y=5 g.g#4|2܎ov ᳣;nGDL4GY(7Epl]W&3_`]KkmM;knx Eφ{I'|*T$#X|UI6ش=NXw#4tH=pn=֝òFJܟ x "@ D@sx'nq+kp&pb!2r6Bŵ%87iq|rWY|H\zOJ\vr#\ `]NLay?w82%[h]6f mְ_Cq!@v%kO@>˜Èa▓ 9"/e"Gw1KB0;6_c2 TZ"@ I@S'f ]w Şp2#qC??KVZ#ˑg~c?}(s(l(.VbSs.;[_u >?ęZZpY,^ȩZ<1Y]5~d Ь@l \N-yxkn xS2+$@I!$D"@FPk}Vr޾ܗطc)v-ŮmKpl2ps^ѕ=pұݗrY9n+s&+\# ?1ưARD"@P1j-?~wڵkHII{"q')ɉ6);t{QU9$T}}D"@pj-u(''^‹/>dXEEENWK(NN߻}|D"@Pj-3T x"@ D DoH<}T D"@T' jz D$D"@ !(/ D D"@4'c' @@* D" x@kO1 "@ D("ň2{W_}__߲0@!J $(JF D5&E͛7VZXp!ƺuS׮]~Whkkjf>1u_D{nSi9朗9UP2!SԎ}{8,vÑpc5F#jcwPJ)ӄ/9i"@ D #)""5*BӦMSjX5R}OUbd8W'@_7lH.QW;@[wƠ(4/WGH ~:]劌0[lZkc;{^ >ƑY}С1 tj^#b: ;"%SY a^$JT,nQO6jGB4 Q]!^1zMߎt%SD"@4R< Ɖʼr+M'!+xu7 vIf/ zaBOyT'm59H#54SSz Xr`oK9J[eA(@ę ?3{F?1AUeuVOJ_y8w!<0kS|0e=NRB"5o_I<Q D"4Z<*'n whwNxWS2y*o_#g sq#p'&Z a*ZC*]I:::9r$޾}˝vvvXׯƍ`n߾=lْ3gcqٴj8<j7u5,?2Whi 6\qUD00ւaoRn͒h5\8 m}m5 LbdԾ;nm:zhk,:~EeɫS G#iyʎވm m6]=eq(4%RA!D"@@Pk;0}8VVVWlmm+yzzƣG*}1r(JE"@ D@xbST7oV;vB@'H<7:H D!i՜@JOO<00cC,OrgG9 D".Z< 6 6D=*6336m߾}8 M#D"@Pkpv҅0YXX ..111sjɒ%GZH>>pqqsaΜE,^XҰ,ɓ<T #G&D"@4B<1rJ"@ D8O˖-ѣGA'+cҥ5]*(͂׬閄Rki0OÙj!D"@$ l?'V} #]8ɴ잕"x)~:\ a^$JJƓxBM D|Q4B<1ոwٹ{nX۶mA" K}v,_,dV+KC{07hCK.ZwY<1є>9^b3 EU>z3#|WO}sx>5T AOkԪ GӶa~7ޕ(;9ǡ}GSNqa'KW$xv uaq9u^i#P( k̠߸ u[cIT8I9ꡡѮ5j7QXx1QY~JDgp6AxaApxR )jm>?Ux: Bzu7$ MQ D"@ M(쫯x2yq/L< V.ƜrЬ>!1) Ip62 \/~+Ow}@kÈ#^\o~HKb_n1ض0@nȐul!UӎI,.z&D"@Pmũxb9q81Oz;wپN FTTw{>]{V+K#+-Ǝçc[1)e h`CZ p1vzŭ'.sp>$Agpm1u2r AZPt[=Nbl#W m.2$mtocoK,O0Q:"@ DyIQJ'",,I!!!qbBI0kRuALZ7A1ڛƶY_b00X)Dwvs]}v%L'몼Ņ _avm[ u͟_a4,h"Mѽw ѦpǾ9OJvzcCO$HP D"P $Ea*)%&z3 i OeyX^N5il$4vhcD"@(#H(ɋSYT#!$D"@'& O ) #S$cD"@8Oz*|||};s6MM;y$&| $>E*"@ M@@R `n-S@@Jz ϟ?ǃ86ݲe BBBڣDSy$T~D"@`8O/M޽{y&nܸ4\|~O,ۜ4/OK"@ D@}HTVyIQJ'fuڸq#'zW/_ (I NWSSp9>PL4 '&t?fڰa'!^8`d -0j vNgTzKCh`K҇XofJo'%\ZzdkuS vj}:Ѵmg:ߍwH-ӷ"iqy:w7tuuob@ ɱЯfoKz=C|>}^f}iQJ`J D"I (JTJ<1s!ܺusIܒ߿w98\Bܺ~;㢃Ɖ$_d {r<;ƺ08|p:/(\5fo$*G[̤ Ehlk#0DELT_Hl귃,g:q >ݱ|=\am>/^DX9޾Clua=pO(Nu# `Ѽ.t7 9""xApX#'OjGmI7ՂNG'\VlfCo#|U GHܕ OAh]Fē%D"@Y0O;v 'dYQܽ}ŦTF '&M 6׭ }n,U-ap~Is WQ1fv֯,#*T~0#'6ĿbQ԰^Hm\tvmM>~TTuhR3YZ;T*X E\^W[nC PɊh]Ne7GI!F~2Ǡ}YƵCMzpXĭX5Ƚݼ>{*@ ba1MۓB D"@j<(L7O`7sPbw [HII.PG>vMNNxR86tY[|뭄5͸c~$8T)4K ZRxR:n訧~aJ,J\` Am'DJ< QRaUsA}V {$HCC]>7Ȇ0v `j,Rq+CHξ3麫EIݒxAD"@ 5O@PR9`O9W劧K1ќ剉[nΝ;RgFF_ι,W .zH6iyk?Ư*Ej3n3q*IozIʡE--4{$gWx@) H!gۣ+ 憰} jFȈK $ D"@j<(LΝ;ʼn%^4W".&JD/sBlhĄ;٦>qk{B_Md 4|r}k)Յ?䉧n Ŗ Mf^=eNĈ=Т.tExr0:V2m/nhn#KҰ!:̋QuWQ 4~ܳ)ZNY$-uKI ="@ D<EBI^JcǎϏs )xz)01$yfLw񂉿޻w/^ߋ~A*ծ1'`84E~HLJDfI 0oC,PrS+|1qGM+ZF˙'ˋ,^umh2Na[-N=-bضL{{=v-񟯵a6E.fA |tcفuXHȈj(@NߛBtUGk؟+߱Mrkn=+l+IZ<œK0qc񆿂pp0ݶ u't3?䟃u׃cg'kpLXk'<ɣBaD"@ 5H@@RRqaMmypHW^4WgOTQA># SњH 22~pi" D"@4o:Ea*'ٙ[Yx_bhb‰?yy/;wlGnֱ<ԷDGI"@ D@ (JTN<1L1qqqܺ'^4+/x &zm^Frw{TD"@P?0O {||<֯_0΁s!)3/ؕ$v2/{Ltmڴ3JZ H<(P D"P %yq*+&&Çs"]VOL4p}||yfNX,j*] xѥ"@ D@D@@RuW1G죖Yt r8RƉ~>FH"@ D@ (JT^<8ѣGsN"ŦڵNl=KCH<} T D"@T<(LmējciO6"@ DT$H(ɋ#T!nzEa"@ D@()++s! k*M,O*=<8"@ D|0Oذa8 B<[@ D/"$/Nm4۷V¡Cdn/`Y>d"@ <(LmSxx8'RٳB1t%@C R~"@ D>EBI^ZbٲeRSrrr?-$#D#D"@'xbqqq effr*&&F*@u x.1JO D#H(ɋS; ggJ7čԃo*Cē 5"@ 5F@@RV)>>TDDDZ IDAT׭[WWWVH0@ My{]* E/;]\Zc4Z23F]m-3oC=Q>R*™%vtӠz*FKS>7OEa sl$E(% D"@ԕ"$/NmӧOb N<1=BCC}||d(~ j}fqXB (!#1vq)v)w!$JS܎ Ÿ u8ވQ-,}R+T#/@NuhbQ"/xҐn"@ D@yIQڈ'>"I2W.gx)1nK."O\xDžm?! ҫ9 k>USNͿ>u9@Aƶ E8Kߩ0mVY(E01m00[>vMѰd󺭲vKS{Oj6`\"@ DEBI^J'~,DRe+}'#{OdNG1bTx }S}wVP!x~[4D,z`#3^Gqq \^} Ar%( Uj NF">> c32V]eUH_~-4-SnI<ݐQ D"Pm0O+MQ,{y\a}8rB<tCG=]s|yקϨ.L?{gQ cI1FcLӼi&1I="(QAzEP(إ\pEwY.~v={yβ3sOY9L߂]l_2uz]~"ftm"W&q-}a^e:h&\-^Xk=w'H$@$@$@OM@(QJ"Bܯ.8 H)~Z]{2D+ְ.Զ,3r2[trk~Cæ-ز6ڍ )OD^xOY~[Z辖 dpC @ %O.\@hhhi+'y]u9?m/} F{Iόhި -{iWX9qozdj:]ҡ~e nl3Rk=cx(O7fl1 T.Q˫QTQXeP}Ȣ.,j+oUyՆcM|٣2*Px=.{aDlj #nQ*tź,ۜ\M*$O7X~.OJː>S iV   9Aҕ:y-ɕXSk,O¯m,Q.k*x!gS]xS]<+-ڡf GF(pK[+|{1~y>} 42)>ꈷ9c~٩Vʴ+0ٸ`O("Cr*z47Cθ Ч|-%̆Ny2QbIHHH%<ɓẺ.BBBP*1V'qSz2)O"l,#:Y#XXM IՓ[]}_tn&FuP;T$]of{6n=ib2phtkcZPǴ.|̏6( 5 SzuE022F!VjkST>@l T9AҕJyvdf/ۿ?f͚WV6 ' PO$@$@$@$P %<ʓhqF>%•+W$qc>I& 3& 'HT+Oӵk$IuzJOOܹs!i4G<@blN4(FWawNqq;kYQ5a'   O@Nt^|}}%q'{ʖMSy*U2?36B Yr¶[#4U)y* !GV闦'T3HyzƀHHHT@@(Z о}{[8wZh@͚5KyIa*^J]Shְ6mƖnTy[auŢ&ź-a',ލP p.ޠBΰT%P*UHHHH%<ӵGA,+,T~)/O@nm`RE, 6ײ̍k>+nx׃gVV41h)|Kk+a |4E[guǠZfVoϲ(ү;,Ua #c   08r+M?B̫0|_Xּ :(,%9'ylG`_V*-~Op(b7mQ};RheIq!Bll<~ 1xG nnsOJAM*|: ;]-M;p5[F_iW~gfuc6gv!I/aѾռ?C)O%0HHHj&9AҕFy*=!O C 䗒Ihj3VTY)1(iΦ< ]ÄHlW kdOzyR3<8i:XìI':a1=x\Fv* ?6@e6(k_Y,0/T1b IHHHi %*UFYkD] cV_-Ĥ^}?aHHHHj 4SI:)'n.SX'd !d%F~6̍KmK H2#ޝ$+O9qozdj:]ҡ$Oy ԫ%ܮgC}`s{JvL McmIEKT3cf<@+K~_U򤈆$@$@$@$Pc%39eIHHH 4Uɓ1LR&MSS˓Ǔgvu ڥ)JnU^J*r/b/=xQ>$X'm5¼A]anS+jSe8~u{:ꈷ9c~٩cyBbl?Yآ#.xE pۅݻC?y/ b>*O آoFp{mq 4mgz#6C~-Ayzyu   DI.OUԼysӧ9-[,?.6mZ!BmӺhCrܪ<-zfFiq_:XEw4Z5=m׃gVV41Ȑ> &ji]4n.~*+lh sqhpf#nlگԂIAX7ht}UF,5E `"m&̻~ FF0k uEri;7' F$@$@$@UF@NtJ 333[.\ݻwQPPtDQWXvS~./9dM]*YPiyʓJ"   *$KT%O)))ԩ^z*=D*\PU*)YpN Dz]а {>ͫvsA*Cy2bkIHHH2IWItXhCܳPGw^)fe`VI jG/b{nsUKNAʓ[J$@$@$@%KT'O:.:$O"/*"8$@$@$@$^r+͠رc$""@y*C$@$@$@%K BfϞǣm۶$DQ/xZ%$@$@$@$~r+ IiR-T;ʓG#   'K B #@P*ƋIHHH 4ʓ!2 P9b^HHH;]$GyzCI6 @$]idlٲE̦Gn,޸qj2 C 5;J$@$@$%Jry+Vvڊ;)mQ:]Xr u`+KTYrG$@$@$@C@NtN\ccc|طon߾LG@uD^zIDLH"(OŲ$@$@$@$`t\iݺuҌSbbS@BBkÆ O^,k[    JS w$iu)O/ $@$@$@/<]$:yGH$pB 6 =z@f0{2Մ<=oGwǓe'7:6Cq.g0:MB~ye_| `IHH^r+I,{W$*Aرc rI۱ח1-иmE%O 3v92/<>][ⳁq|>TGS5eHHHH9%Jry'OGbڴi0aV Sw᧦.<.0z><"§V蒧-O?^~BV׷+q@S+|Yʓ!;@$@$@$@$]i)O{FA!BV2L@ccwpׇI]v_߰* IDAT}.g=?)/iZ6İ nczCkZMe>X}Z=: ߣk06k; L QA Kˆ011G)*6oK];'n.3oA_; &>W}ԶP4C$@$@$@Ϗ.Q3HyW*Sb4i L뾇<'.ܢ+Oe CȮ5I3ܼ?<(43#a w 4km6&zS6vY aٺnk7a~gk=-?}KZaw6ExCyzF}$  x 4ߘl4a֤~ahg z,㙦D|Mo)O%7m>lLڎX+xt-+5󤹷 ?6.jcM!J]>bw}ߦx{Xdnkkj;F=~<ЁeHHHH]$Gy*̫ػeh 3/2D+ְ./aއp{OlmM>Sjo$@$@$@$PD@(<-\P$ܠ xgdd`ҥظqcE!I{G7cucK~L5E ²ɏ7oO}_tzClUn]~[f1=F ox*GP㶯Ԩ-ߟcڮ+dd yk[6Qm&P¬~3/|7sv^ƾ<ءeHHHH@K@Nt<:u /^vV7˓>XE!@yzQF$  x %<ɓ4[e"'J nDI.OU򔞑ݻ}tN¡؃ p߱;QG\ S8  9Aҕ*y$1$"++ iiHMMӧn[ñH7bLaHHHHj%ڇ@l۾ ~8VP3vMaalM^0'TcoEʓXHHH .QSq ׏T!TYS 9AҕJycO/O:l[pIIIOH$˷Iy􌨘 Bx{_J 8ؤ.Zvy> nczCkZMe>X}H48z>lmc:m ZVy)*lC6ܬ>ZkQ)TmICÆ @%Jry'1 Y̬LYN,+$N 8QݽXf-VY?_={vc 8u~AfhF'0.Ğ0o sp73/˖y[auŢBB'U C$@$@$@τ$)J,y.NOO 881c+qXXit:om[kRhlذK0~D݌?cK+1tqO'߾/:!*D.`nhˑyVcZ }`mˋ ħibDQǮGC]\@{{K<$@$@$@$PC (IRiXx[xHh1vEbAX{X:b[yXbdRcg_Cz(Oς*c (IRI<6"27l?0s,ir!Q3l+1znXyR-ZѶK`=g)f9v1sFQ5ʓ#   *!$IJ骒'lfJtҊUp!ܹ̓pprdj53a(u_eW`vw\ON b<^ $I)]u$vs>s'Nq1Q8uΝ;׮Ν;/7Sp0 $%IRJW<@44##<+$󔒒!ʧ޺۷oHOO䈲υ`EIHHHZ (IRZIb$If @ !$IJ锧2F|w[HNNFRyMb"tw(<=vTZ'fKs{OxW\7 a1r)urZjG @P$t\rc' "bo$Ξ;̬g> uB)QvW|c^tbBf8.ٕoAԤ<0$@$@$@$T$I)]򔑑 _Dd$qI$߸q;}R7jm6L|<<|Xii:?>jF0 s.EzB^FEʓa[I$@$@$@OC@IU+OHIMőGbFFf:DG} Q#<"gA C`P v}|UUx/ 6Fz{ \w*3x\E4'@6HHH $IJ骔<=v ^^ĩqɖ5 瑔Ob)ؾ||/ 7Jψ9pbٟ~,W}HVV06]`Ymm^UualT޸_zA2D6MQ״6j_^hUUx.m^Y}St}]48z>lmc:m ZӲ n`ۄ^Զ),MjY#1+nʉcA Kˆ011G)xC]픧Tʓj !   gF@IU'Obc.HNYҳYlWHp2.Q{7zZ^~ػ/{ pi>/(5U,O;-ۀ?{/7=hp7.!Qӯxô!H-biBk qݓ>_=}17Qy/tSIgk?{#A2m|Ci:cޑٻ2>0!4< >[ q7X9|fUT5HHHL@IU'Oif=~L=HIMDI,[j,^۶oEP?K/3gܳK?R<$S@G^,"%'bFF0!>!/'j7'kۘe5>H4> MMaf̬uuou4T.=Oٜvd' D$@$@$@UL@IU'Oaaذq#-^ұ|2Ν<ܽ{{b鲥pyp7. ӣ\Թ)~Z}M+&e*_xg+g; q,ԭ=5M[em(Oח_"?3j #ccuy5ht"""q\t nj{ĽS-w?ڍ ]R2Գ4ǿ-Qv$bwaH:d}Jg@+K~?_̓@07j/闐a$yyJTN;Z\$@$@$@$P-$I)]U$wZn5bҥKeee8x`ц6Jc} ;v͕Η._B|B̯<Ďjϱ&lT_F2Jm`d.~n58ޤv|f*l^fwł5k+6T p^ vK`X 6#Xx O݌?cK+1tqO @% Ȑ &ji]4n.~[4T^E/ǮGC]\ I3i++tlӢ[af j6Ū[Km[[+1CwGrih}_tn&`kR:(:ϔ'u [E$@$@$@UI@IU'O+WƢŋ݊mCBCs- JZyw,{-غ6˥}G+%K;\bɉQ=<鍊IHHH` (IRI<6"27l?0s,ir!Q3l+1znXyR-ZѶK`=g)f9v1sj4f8HHHj$%IRJW +mb%qڰoD_zaرTOqD:?ƍ5Kq{wYTٟ #   !$IJ骒'[ޖpI?3KӦMØ1c뻯$yװyc`1k,/xgt1l /g!ӻa@I3p~R   ,%IRJW<7 "{}LBl޼7nŋѫxx}t>m/]̙3uEo߰d,UwAm:hӑ< ?7p\I?­s(4 ?"c2$OV*.M;x*(O1**.z eg0rg0;YqmۤG*iy %IRJW<5m]ؽ{tx{{cgƏw_aJ|"z^_ɫ83fqDS.? QHݼ6E~OqdN-E>n'n^YФ_Hš#ugMnIg IH M9_? 7Ae!ĪvR|ʓ豥$@$@$@$PYJ*yuwI5E8bѼҲkw_!I+pLӧk8"_'[Oy qYby=0He"oh:ktu(EtdeS. =h3w%w* >M7/zM^&oG8|82S#+`XQ}q'~/݆¼maQb%+pmmCSgs. x~$'X %IRJW G"%i|v̐fonw~#0qamm+x gO-%+ )OF^n2C Xg\ϲ3OI V\\ U7F&n}ob'܏rfd(Oǖ @e (IRilluspttCN"2R\_~8V`ؾ8+ދxm\OyJVlE^z Cp}\Yby(,(Ih9qRw[Ԇz<9%ILF!;#in~dyIe]<$dflTW_Ke$gxx1 mWP(O b=   0J*y3O6n.[ntL ,G?~Ƿ=BzvX<IC z#Uӣ/$/WɿqQ0.6\3ۏ݈8hr.CweQ #͠(W7xԮ$lW n,!y Q$OIX2e 2ϣ - +YAC(SgOy2=HHH*K@IU%O➧Ktڲe SNIp>nlmoBF'wE.G3sn:mQW<J_lקbQIޓm?[Ӻj7(}Sv-{Ǖ_b韜p(SU\Y<[.ۻdg,\⌼4dY#yf=Ws(Og=    JP$tUɓmoBgIĶ{{{XO8.NyS'eKbٲ%XvzBDYz-Kzam[FKW@7o{s܋Ϥ&L1}?_|2%ˋ"wf@噞ү̚QM}&"uOƃأvbSd܁Y;ƉFGAv^f(w[2FԠ}M M~49sj'Rl{hT?>E[^"-YU*LCr=C [Ȋ^k#:kv|k!=h MC~j"l61ʌO%0'M…d,]T{̞'z9/_-#ʋz#qz8S 7 %   JP$tS.gA6-\P:fϞয়?㐛vX:0heE=Q_j1,zô0䦟sa=ĉre_Rif`~P   4%IRJW< ;!q@g%&֘;^~ǹ=H@^F<2qLMDa qR$%JT;@yo+ P$tUS€89b~qbhL= ȸ$?b'?L= fu$LXQy<oJ$@$@$@&$IJ'13пvpOY(?`?`hxlߎCF$mI. :8'SEI$:5C삳~8u3{}KZ4E_?b^v(O%z$@$@$@$`8$I)]U$״};{nƂ?/;Ç DD<Wqf̘+x!T ?55Up?Ow !G\IE=n X=߽YOD>,,ӞW[b\)yu%W0x-ډCۺhb\)*bx؊ Se8   P!%IRJW<zp? S"hi^ڍ5eԤ8w|lӵuEu~C7cۥ[PD%vcMĮX(t,PKD U[-6LTfυ \Y(;sfGYHSA6R!j?޾?L,o^3vk#" ~̏*@N Zgv q>3P26OByʹ]Ա0daiPw\Bc>Ўs\F5ӹ D"e$+yrrǁ(`kt HŨQð~>yd N,&wW(O@]EPjM={M3JETn=ԯ__xN: fŒkWȻ͡iVNWG0NCW*\t"3aZ)vrNP[ ,+krQQnyYwR"@ 5Oxq'6Fب++KX򙭘>m*FCzy-1@g\D/ڞtf(+t,򱼲 V UDh:}'hRhQ!^]܋A[C ƚlx*^ [.k&:7v>= GS7.Z͡,FJ4xN>/r0b?BA#ƾI{K*ge"f:Pz神H*30kGEeV鳡D"@YY."Iطo0tPaώݿ'VZdff ''yyy$Nen : ТD"@xċ˥>^(ʳz> Tq孥$O#"@ DT=$r'Ol9 8h9sϟGTTvx *R'&Ol ">|X(ʳzBhWXHT+."@xċ˝Rjx'"7ƷsV^{RcD5BD%O? q騡p<̗(] 0Ngw6@ho ~ gKi D" Gxċ˕<?0DDDzX!;e0m=ZG" w/oLXZZJ<,ٺHFy8f 0OOH'EV3e.pEPRƱh{ Bw@_SyjW9DqI rrɈ\oZ͠d: ^bNst[tV.j8IPl5g߉~)=nڡawK+- __NN D/B'I\F 8x6!!!fii[!ZqJGAOo(Ưٲ2d$uYsle{[GU݌(iy*FSX?=DGPP!MkNcvDDPVЁW% PR!:.pY ^cRۦiQ O977:L1,|1M1 1EuX+h sH v:) D"@($r%ON8p%W#

-+Qr˂$OD"@9xċ˕<'00bFXYYliS1jғ Iw 4`N|U=T|d>}N0ՁH#k6|T$OMp5\z c4Tu"eh'+ޗ#>(w\DtGßp5GʞɑymașjX|~ +ؑ+OoO˒+ O;$D+MT#+ D"@g%$^\=k.NCfnnqnǨQzx'|~֘ ;wauٻd|)eUr;wX j{}sN#N$$r/ay&h5xa 4S䒮he%"O(<O[Tj1x^zbErp}l#L+nr3DH*K"@ DI/.wTPP,ܻ!!A.bye0woCn?͸[{3 rʱHJl|!%@TYbT"@ ՏOxq'ׯ^ݻ֖02S G򲒄@{?| ϯ~H-HzP D" $;ybrss<L 7勱xr/b޼FTT$xC#O' yJE D2xċ˝;$r%OѣԩSŋNPs<՜+!D"@Oxq'ccc())aΝHJJBjjG+Vr! y#D"@@%$^\u֘?C%\@ THIʹg9*s)LWC.7ulV$!Sxɓ:$D"@@ $$^I&Ȼu4H- R`Fc|D)5a{hd唒6h۰D²ltFxӶh D3If"ִ'tZ9e)z*)b®ӻPJM--JE $Oep D"@j$$$Ood ƞﮉPn1Aɓ as'Oh p֭Rmv[مX @ l4BPlP"uhw#JI}u3[[ @Y3S%!SOGRh{6D*/,B'M54|FöO(]Η}m0Vċ|{ԩ20qwr5O[%y]LH D$+y266v܉#55E)D:.#dk'BC˒-jE#ylh)1 D"@OxqdѣȇHG/^N\ )|{nA2zLH\'yIjpҥ"@ DOxq'v HHH;=TRVJgz,DžGvƆ7JU{TɕDS <}*AO D'$^Zo/.D-w$OC>"@ D|:$B> e #@T9^T"@ ՑOxqN#"@ DI/^#{`4o.fΚ9sg}Wj@!@T}ZJ D%$^FSxY!G`,\4cRZHv:]2 D"P$+ DDlra2u7] yxvT"@ ՅOxq' m?`cgа`,X8Oեr@I:@ DL'I\Skum@خl9?3vJ_<մ!D"@xċ˕< u8lQZXDشZ US:U>"I%e"D"@I/.WFlEg0l更\ahXUm1w^7ȓ+~CBw06rl'YVoŢ5Xl+iaqW3OgL[`$Q6DP5&NS(9 f-5fc%4G:#L<$Cp:`!w~' u D* }`bp*^ݺ/RF~Đm̒G疪PTDp*M$y/D"@xċ˥߸#M]{oÚE)Tk*BLrfP#56qq".m1}c|= iҊAY 㨿 ~QCx_ecwe28"@ 5OxqTaFSO8iwg̳ ^X X %OӦOUf!zy/k36/ 3гuԠ[.1ZOHtm 5ņhZ}&3bIuo d3 }:B#Ծ=E]SzI+wPVj3\M:w1 G2BC52kɓ*K9PV W*%j8ESCC=֜}WR Nn=~cg4L ʎ<-[/_ě7o*(LnvIPTc PV dzs݄E/uhs@ht,M8(y4W]õk7Ņzzj$ >2.pNiY>F&bVaxcۣ~뙈}s&<ۊ>fGY ISA6R!j?ޖyʹ]Ա0dJ_incK%y*"@ DI/.4wO7 m.<60a vLۛ;'9*;KI=N+H o-Txd)6l~iI\Hz:)^A˔{~O2Vb;_j* "Q~BQU"@ DT}N0ՁH#*&zFޮ^X8Ue|Y"Z2VBS#SΠ"@ DO%$^\iXlƞw'p +m;'\6as8{ Xd-?`A/^[nΝS,LΨlv*=TR 5ci{WL۳-'r핑Xԩ zY8=Gd:g m9H`h.ì{S݆R>=:T#o<8TfNP6-,A`D%`D17"@ D|$r)Oݱeg_Y.:`Mxaaav?ܶ7VƼE%V,ȼG0* õp(4G–Ջa{xx`D.cp-^0LтHy*LN}-#(Aq0p8]ۯP(}8gu0y.!:{N-9e\tF75Z7o a:)h}g1^ 2&ax.v'JFY" F ;H XԩRà  pwGKKC"@ DY $KyG٣6yl59[wƜ? $z$cޢ6SSg`ԟ?ZdkCQ(]XxD$)짣lrU=K-U^<xw fӆceSOxim, {u3[[ @Y3S& :K !?B$8M*-*ī{p\?hkwX] OŋA~Bow DSָ/A6Щ 5K-N>+Ӳ<#&"D"@|I/.t4~9;{aǾ_po4vNq9lqxxx?PMFt$yϮV}I$O_6"@ _Oxq9'5XsÔW_ R :Q]}]Iɓp>w pѯps_+<}Ɯt%] D"@xċ˕<v}q |g E@xn:N_ Wϟ{Gſý{q _7?yddd 22Vܺm0%HjG?U"@ Oxq:6u/88pժJKyjZtD"@@%$^FS}*#@Te() D"@Oxq'Jj$@5ӹ D"e$Ğc-gv88B_aώYчTH9 D" xċ˵<{7 /DxxƢE`$1sN%D"@@ $^\n)99bbwGqIt\qzaj BCCY9VcC>ǒzD"@>xċ˥<1OC``ٳgqED`iO{v `2OU}n\yk)ɓ"@ UO'IrF!&&gΜ. ĉ",`iʱP?6U=V+2<Պn$D"@j9$r'O왥|,!b$.ł8^uv?a LLLn:߿_<,_?C%\B 'y8nT"@ ՉOxq'Z^PP A\\ AX% ,]cF 4vpŸ́a5sV^-gY֮—s :4`ǫgwqyvo"t;P<}t "@ D|e9Zju>j^U\bV |\8mzO[k¦e"f/Pa߉!X==귞@avG]1!𱛄v |ydZ^G!ANBN Zgv q>3I ʵkvaכPTc PV dzG Ă4mO"@ DH'I\ɓ=8@\p_PDF/Ft f&q*]}g6ʆOFyb"p.RhػoJFnz_ fS!^K·OD؋|hR*Z*:\>>L|2,vķm AxN.ì{S݆Ǖ8=:T0[b ?{#ٹL:l/γ]<ȸnj"o7@ytT4Rhϸc5AdL nM]Nʕȓ K?޳sib{XK-{C'O,}{[]3D:zc ='VB@(*D8z,k\Vߓs#ҴD*1~/Cd\N SS_ "AtnL|BC<[0qDܱ`/pW̓  D"@'$^\)##^^[-6nSS,]lML:C L>򱼲 V UDh:}'hRшN!^]܋A[C ƚlx*^[.k&:7v>=,3"#\y `OM1vnzkCA(5k~ƾx~ch:-ؠ>D*6Gcq+1B짣VS(4h?7Iwwbv?m@I=&{^/GaT* ѥ*P{J;-QLD"@ A'I\7a6o͛eɶv=_ƌ3~ +,G"PY'O? /^sPy"@ DKw/.w򔙙(xn 6&lk׮L7[ !`l;8Hʲz>чTSeQy"@ DT?aСž߻O7@NNHtP$OEe D"P= $Ky5SMM"@ DO'I8S)Z <."@xċW y?o!<<>;{vwV>D*EHسcgr%D"@@ $^\n)99bbwGqIt\qzaj BCCY9VcC>ǒzD"@>xċ˥<1OC``"##pY\x1Q!vDړ-žorC||PgX}s[KIG=D"@zsô{+t_uWީ0^u/ @S"@ D+I/.WĖ߿3L9&!݋={c GOa%^Ύ]VRa޽#[݁Muh FnyHzmt6SBN UT6!x,@sUԫ[EhÏ2-Y8(Lş3:B7[.( ~_˶HS?ب?礮9*S Rïn~Ѯl uTYAw.'y⢡ D"Pc$+ybk?臣G#lAAAشM d6ϛ1#脴'H{ FeyXH:XFy*xq5k&\ @xhvZ` d3HM F\iv[njEB$V!)qyo Rb7SO1iy§T?u83w`:4D{PT$OvPm"@ DT\bVԞZ)=ڡawKH,+S1 "@ DI/.Wmd/ FHHYZZr6mFҟQzal* +++I]![(O"QV9KSA6R!j?ޖ?$-OY֠H GTLĬJ? ú;1ǶG3(LChԸ+#::>vNY/OL5Cѫ $>ȩ\ PR!:.pY^< \v ׮7z`5j]`cAp*]x6]?|uX+h ~V?.Tѹ _8 :^`˙W"q IDATZHNʽ _Cs>1崧tspmdhpFVIfTT"@ ՖOxq'6Fب++KX򙭘>m*FCzy-1@g\D/ڞsN#N$$r/ay&h5xa4E򤦻/≝ ZpGRxZ`ikUЄKuV6-Em؜*%4-OZ|.<}=IXySв0 䙩bU'y"@ rN'I\[mg AlYVKᆋu2d|!<7ofHc2ɼh߶9κ M>vR8d]G ~˗Nm]*̴=vBF'eU u[(˦h/3^͕)OAl\ޙGGu%/+)e +yJVw^l,,3fp3ـqclnl4M  h@HHhD撪Jn[T+ݺUJ[kns߷}'a?|3s%}lsJl>vܺla.?d߿i$N$@$@$@K@$I-Wy8v C$^GkwބQU}ƴw ÿ| nEöп7xvYj.|Wt6?<]g}`GY}%瞧i_!fA! mbpmr?4Obk   LrU?__~;6:pm{o߆x}HXXXߣX3g~֟♟,þն3:U/O;1_hzޛG]_x? ۄ/׍9#cKގclNden@ޟ~xo?GlU[ i|-mn I{+Ec,ѓB?s͓w8r-$@$@$@$fR&Ij̓[Rr_<~Çȑ##>x B0o؛?jj7i#ڋ~XXH]4Oc{   }Lrՙ'+W.!6}|k`0YÚuFڊ~XXH]4Oc{   }LrՙGqq18|| |M<;YuԔ*R,cvƦF%@.1'   G@$I-WyqUHHÑɧVvJT^y R kVX̕h''&ƣ Fb}!w 3h|&   $ e4cPWWg9h6{ Р` J`͓8$vB&'E2Cb}%5@ HD4ω0y4 Jj sd7TJc0I\'ޤH4 tyYGƅ`Yns 9q^|]y!/j_%c^`͓nq79rB,fƸ3Ԁ:5zAyNsU(ssߕrvҝ<"WP3RgŸ0.Ԁoh@ˮL)3UJ㭢1[xp)h\ΜMľMQ1#50=/噙%yN^xR]* (ٙX@MOEL M yy /}<'(-RoQ` 3n(͓ٗ&GnhJv&tscwj`z4 e4e97%yN֗1yzL81w'CM&G.SWɄ~z .r&gj74zAY>9Ǘtoc@4͓ YgO VRg䴑;6FT2@qduj@RF7VJ3cFԍTd䅿-s>SיߕĀ),Mԃte^ז$N9mNrg*Q:L,Yp1. 5ܫpz79f؆irue)h$&^S[ ~Fd|–cD&jc `g"ԝ[p88N &;Jv&hQ1#5N xl] üy+_;Q>: <'8^}Vmxb/Nmc^U1p2>J\ RzStyg%1yr9P݂E'oM>1"yX„Jv&\,ƅ yY-Ɩg7wPغ,/AIӘ$KcPD9;mLMJ&<sPyU=^>wf^-1Ą EF!R{MS);~ƛ@%1yr5 a[m/=69ttV- PVoyk{ٞ6mL{/- ?Bh5&}S;O $!K7zք1 \%*l2%x+vImw )`RO+"(hVlދM㻒oh#H Sb^PH̃X(GZ|Wxyb/yvc®^BXP B{ܱu b 7FDN3L|8y|،ۍn'yQI} .+}Z$mBTC@jlw> z]8%ot}>IGh:DF_m!0v:?#Ӆ#BJ[}WG  g#R҈I-ǀ)S/* ^0g utzGi3 @Α3]&$A{&LtWq-?DزcBǍO0u$;7gk`>=s 97xjV'9m> W{<iٷQRr4 NicҷN!=Y(j°_݋୸j;PXՀ(zڳ}2ar8".S7h0P%x (zU!9h(jrd'i.]}(cd\$4O-aXC3C1>[M-qᗑգgK.ɓfLM=\y2P=oeb G+GxL-I<~fF#Pb0cxׄ`bkxxb;cS^ znto~W3@qduj@1 e7q|R-ٍF&Qr [<)rրj#h$WҰ#,[s'_ n5E0v7[b_&, z'9RI}<)T;ji:GlvHz4j)y4 _Z\68+q9<壗K@4O.ܾai;Ԃz$䷌s<؊XΤV qm̓z=Rlz:Jwq:P<9M<:YUIڄR\y։WX'F'r'$͘$`ԃ@^Q^Y< &ym5-@ؒeX⇗>˜<0/K21hB®S:leDb9Y m Qt??QC5S3%; .u\ B ļ 7[ 6\7m@GxR6:`>4Rj=7KBL(JF^<cRPXVjHN{ g&k㊯FA.A]I h!v-F.(.Wj7t:_Z@ÊW񴦑! i3rHn>k+_@ Ţk+H53w"0` eLn EkQ?(U1Ԃ{qE  mΏw%; .u\ B ļ<ϔi"XE;byh"NvٞeV|KţCl㻈n ǫ'yDQGI}\.3583GkG : `44Ijc:kwm0pu=،=)LCXq7+g4l%֯8[]*S  Ti&bƇJI'LF."xQWiM0E ۘps5DLLI h%yNq t?kc ihKFL<wߕ4GO4$4O. 霼]<3dgb΂qa\Ѐ339ǓB}`,Ї<<1Ѐoh#H Sb^yy{SI!LxzĀE{ledgb΂qa\ЀgkNQ㸕9g.S͌<=&I!$wQ1#50=`z,Eܦ*xgx$υL,"grļƃmuLJ3 f`v7OY TIDATYY0_f9{瓳;굆YKT+z>ʥcűgiZoO_Iq5:ggژ f͈1дYvCOֿyw~cYnh Iz5ߜ>3֬i}\zW1[*Nm[Itsbo_q)Y`k ;=1hNY+sZ)1sݎenXnycu.pG(_S"_<.cei9)klmM_qT{Hh֐0{ٞN9N)RBQ˗/;h"==WP\@ZԚc?<cg$64a;8Ñ\r ؔ)1۪Yؤ̍u0Gs̱)I1 1g)':8vNth=I&qϓ>LO/sh 8=IWq8d ~!.!VGBNz3ǚxTACL7@Eo! 8R/CW>ߌ@#NS-i.(=PJ4׹Nf>ܣt1 qWV)Ҹa"JZG]bm2޿^|cӹN5%:>3OPj7vW2Q$兰)0hMʑCJz:X[$HJ?ce VRe!>h5Z={^|6g>47X/(dN#ERuj3LA//ja'|>r"/@8Q0ٿ^Y# mH_&!9 M꯵ìB@EPWj4n2Hfff>u1)GZgzS͜XaĐys{9G1\O;)-,9"_Xp?u';x N"HR 9/fR u~EA4MUT~蹇n=w A9]7q >j617蝝b>c_M<7D9&s6?s܃vu+)NnR:uJW;N z^ٳnKirꇻwcvFx =Zn6pvMŝ YoN唚s?ƫ)*6Զ&q{2(˹zb_"D(PAVF5#C[n?~J-f>qW`Q+'$R MG|.Q_wq#qK[?X!R(inmmuR\\,)<0LyysΉJm4%ys{JNLD/1v} ;GIgcd/$k )Y4E7XOqIs9IА$iHY,;^K*DZ")>d:OqKZꎢ#AQ_;8*5lyGH(=EIGf )9!1VG7g3~}ߒ{jR'G|w>[8H)6֬Pѻ [BNˆ i.,,E3++KR=J֭[% *˲ Al0keϞ= ǡ"kuؕ,x깇ث朤h-I]3Ug44Fχ“!ß+hK^t@OՅ Rcu9>jgXJq৲|q^U I^Xim-s˂X=Mzy?븉RsLbHlخʣLyGjX޷w,K5.ǧe-YZ G !/-Vp&?$4PK=+|7F t\fggKp\5be] .H;L,.Nc {F+ȱWa4UjW"R9*W<|RKJJ'rrrz$`X}왱FkXmX _;?CTI4B*^R*#f#Iӵ6'J}-qJtRPXݵk^FŋܹhUjSǏW3x)WʖK(,,Tn  *w˯^wuw9D:??_=yZHάfj:,>Xµ`[iVb3 >m>Z|vʟc4]AW=ݦy*NJ"wԏs>7)8Q!55UrL>#I[xr' 4Yb'G2Ӽ?0Izc?TS|ϟ#㠦r{I$|9$Ү)n֏,Eȣb~#%* (䭗l~م): };kɜ֙b~ULH6$`޻P|Zĭg?ِrց*GS[L=Ah2kcm~Iq7̿^hbm#!qsjljIfǍ8# J.CRse $I괫11B~Sb] !cE2IV-wEDlǹh5T{"& ͌g}g.EE'?3,|>DڕO/3k_WĒtӅI/V49(Ѳ>UdԪ +`hJ*(z'qĜb)ޱ|Z(Z(BZ![?~f_ҵp#΁"1A9-t ?{oowvq Jiq+봕P;ɪmjR&7 b;ӟ$Al?8|L-A^ʧq'H㎲{SIRdYZʴTKݙ -I~.b=~y ;i\Bh#Dd%I~乭YOjfO9ұ;4TxGӹf,J-_}zެLmLȕ6AI;a QjO/IfU۝|v=¶ۘqRRHM;GM y'>&/MA+mAHd3_z"a3񾮵jL$tLQn4mJA (>!Ujכ/ǎSCUf A{51_. `nD/}~xUR䩈X f*m%Kxe$9FzѢE ݻwQkfS;Qqf]OnҀטR~o) a=2ͻWPG>Cch$I:Z+xJIۥ;Yfm cO@1']T%LwU PQ8rlI=o]k{ퟋA\XqNo3[9zrw^i䟣"cO% 9ćC\!)}tBbP͓4.?#\W@!y*lF*uGGGp34Y,P}>;ΜܒRjl^R#J-q$2ͻIƤ%Y1EAZzӋ>;y):(t-"`͜_ȧwV[7?ѤӚ2 VW`2U-pz)\@dާ@+5Al-p K-O?^×KlpDM%7VBlMśeJk1w+\3eOZSqb< Q8ΕDӤV6&zrG~AF(T8[D=uT;~oJ$%aR+ZPyZIŋGNa88ߝ H:t#!:׃ uWl]8Nё3ܖ>!:S1t8 vs9NLHuѣ-v4WVt ]inmmu-.. Dyy<;˧zYqBm2:::$@@i8N5tuuuFS__E.**ww`;%q{'P yUZ@Mjkzxʿ_%%iZzj{6+Ө}4{H ԕf"Һ{VV͆~'k-rtƓKe{eT Se&)|ig {|h_)(;mpvZ'Jq\KKd999z.l6&s!B;XFJ Rho1>-/cYOk̥NX]DvZpU: *tWڎ[#5@9+++(%>Ywuud(%GGC$2Hcs݇<J RhC?폶eļI>G{Z2hz Ǖ8"--͇kuuuE$KKK>!\~ʉƑiq˦,d7 QI/ϦBX|eNiX|j*"WEf,76$-Lsw>}ӛן~H`BQ1:Z`Śp 5-g IZpěng}ik8aԢXf:ܹsgۋt:xzS2 rKƏ"@{xDžMz u|Weddd8 lE]m㺒_F{x2a SI@LOh\'} 3Ec**i=<M%B:w BrWdslF=*$uYb+3P_Yi8`uJ!OUa`"Hݏ{J'n`"I 9a%D8L3I>O_!F A.(v@KᴷﮗQVm0'= IDATo! Hc nI<sp]VA?;WxLzOLR,uB8Rۃ/H-pyKp\űB~̣6>cZ/,1PӅ& A|ͫ%%&i'/߿WjW:;;ܹSXXXPP>QQQwt'{xRN'7$Eki}LEϻ9% >0Uv,Hi9>pcmpy"sN 8NU>~뭿eq08_I ż˗&8ɝ?f&i֛gY~oi}R[((5jO/ӓ$i^rQPϩY(u F @x?3H#Ji ~@/f1F:">; ~``n~ϵUv9H_v`|<(K,Ѿ԰|!{ک#}LǚgGi_iN k:S'|pU>աIP]d~%2Wr^Lg&"[VSP kROaCJ=D0_.omjH6D\A~CFN50Σަ7/`zsBƒ'ώoYo516ennӣIPo&gw܄FS/&vH3Ukv-ZC-{vW m-) Mx:} N[::Ԛb›Tܖ_yPj{c|=-j`9(IR I=m,`lRtTH?01gxss FkJezG Ě`{T8"ș˅Wߤ3ͻϼ|?'>?mv];:j xH2 8.5oU0;ޚf/s1RNztJӮ`o_q)|P0\uqq/]v@΄Ug9 rє*p!}z}9XentL cg8pSjrsc Ŝ/=}]/l֓$c>Uv9ţ?@nZj&GXq$qZ:ӼRVΜ&'?]`8v?AvS*Bygw”(\` U3&!.!VGL(hOYG\$-GS24"]9XTjfx~Rj>@k>X3 ןMt҃Ui@ 6(uO_0zl6\;*g̲}OuI6Vgdk' dx>v6:Ӑ浊Bv$GX!~Į`ϯNgO mR J_'5#t&!Z)/Gά뎢|y,?ީSQA!i-?Q۝Ѽ:zfc1=}cU$hH4$,_ V5\Y* d)]OwP/s Q_(ԸɓJP )IѾJM II3:#2gԶeRm]{;rMS1*m~yQs=u۠&/ v ie)!~|Z֒u…|y,DGo\DAhRv/~šP{[v [pU4^EA!?bCdF nY(S>ǽwCC;|e0\whʅFt|i}VIKд1&%kwދ J͟o:aAzAKkMqs`KM3u›4 6ql4\{"& ݒ J@aH|"Y{jWp, ] ~/dkM}p(u81$`Nё3ςR0@``N=Epp@ `@::IC(JꊯKUȞa:#gUC1 lY|fa]lfauMG+sϑN-m~oyg)왢YA)t~09swLӽ|4JXOF AhT;ɎH_ou$[):u; Ee)*xR}z=w}wxϰ[_#w:XcYcz-IS#|vI㎝$?f MݬNBmٳ} !c?e+fDNdFՔYhi}LcB'gw,Lk qzW|LatOh\oGjvh3isֶShZoOtꞰ KPYpfhZk,}}D S'A\gq2 [ gDhZgNug䔖4+[9QB̅ioR|$8IIL#9$7Ft Oe{ h(0'Zs|72{ɑAD3H::53;-NO:)鑳;W)v9䔤$HݜVa$dJ"H&$H>*>! A 0k.zͩKpreTh! J!rP1{"?n} v}ICP1/ 2kbkXylJL$@x2 zHuėb$gSJV\% *~-n7\$G{;JBP{6(uȇA`y4Rrһ咯J=Lq(c06:|%DYcp5#9go8(%'bkI $w*PGڌ h9_`9N(v۠!"rc3>yJNz7 zJ/R){UF^(=NC~:CM%'l?LrtI|yo& ~c%BrFIvy`.>C l!|˛.V~ 9>(45iHc. ]t/;pf$Vu>ދ*$ $H ARZsBb,3~}V̳Ra"sn*GD]Ӑy:(U7O.'f8v?6iuĜZEI԰ P8vS][8/$AF[M(_HӦvY ;\}Ws$s˖!IҢ7>4]Nf傋j89')ZpGnMpj*0o! poX˗13$k.N~nRQ1(>*1)),6Op` x%JQ(6>[*9-st }"PPO9ڧDӷWZ@C+j8N߂ӗ@$m+X@E~:j4^Ly T-g w*_,I);+lTMgZ/I FkNY+sZ)1sǎwnlMj/'7L),L(`(u`y7I, Esǥ/Z+ iIRq+t$ASge9(?$%! Rc6y6mwrYG! OFKP]*n˭/,d0ptӠKg9"RS8q̇+i\R.&Opp&H(XEMbzBqt7km&vcX@nՕ6V"$ȤJ<ϠPQS+d֗@ (@}Zz4*[!sSk0>-ߓQoM|t^1$輓m7(R';.ǴYf"zyQ ;u=(WQRj14N(upOW5T4x Y17D qh>eҚ_\bz#Es^Osͼ2]WDL[,^Ԭ(W>2/@@=xhwM\ YQߜ{r<34dSIRI\p]HESQXA6=ǹ秗ɬ/<:/ΔwֽIX?t@DP 9 #Jv!8r0aG:B q@#.0` Ž(u؅ @ RG\a@P 8 @À;a2p#Jq! v@.d0G:B@ R]a "(uą @@DP 9 #Jv!8r0aG:B q@#.0` Ž(u؅ @ RG\a@P 8 @À;a2p#Jq! v@.d0G:B@ R]a "(uą @@DP 9 #Jv!8r0aG:B q@#.0` Ž(u؅ @ RG\a@P 8 @À;a2p#Jq! v@.d0G:B@ R]a "(uą @|rryϥ}lOA"0lOϕC/^9b=Cc ŽR۟cH\~/_ =cse/#(aƦ>XKO߼ژmw;yQjk9USRù wM@RK]Rjlc6ua??Fh' 1+E]1e_Q⵳Tqu P2qR<4)RJ+NmmlՂ 脤8#cߝ16$-~^mExI  xWjiQjk -1Ps8If]ħ96e̋~-|X{\8hyZk,&ݰ04 ΩeeZR.J(2] GgP׿'-7jwL%QJ}y}nZ^@|gJ2Tj+?{r5O?ޓէ?~aO+>j85 +_[5l9g/Խ؝ePj&=[z_o1?_NKDmrE3%xbm+ssNJNG'-y/mac_cOh)֛3V'Em{|dqrL־=5g4F퟿Y. /J=䦘Cc~]蹴AIX8 N F`B==׎!"G >&_SKSK'7!il)S$)ma@ O  J ` O  J ` O  J ` O  J ` O  J ` O  J ` O  J ` O  J ` O  J ` O  0O!I`B!I:$N  @$P 8@R#0` @C2,@!JM @HɰS@(56!I*IDAT:$N  @$P 8@R#0` @C2,@!JM @HɰSj|Ǧ-jC @9zHƜ" ?RG@OߺhvhM 6}> J$J4 h̳p჻WOcA thԁ&M,i!ݔz̖y3Z)9w}No Sbڔ0{ώ\gʼnMiq}T;߁@Hl7kS~yx~)5BiKo12OsEէ*j_=]dq00TYMC?W3{aOH0rF>_V)1Eztbe~w|l)aظ@vWؔ|tQ捷E#Wc~{+;|@KF>]ӭڥ)coȋsg?96$:yѮ;o \O:̡G RO@(uC@/h <P3#J=9^ xԁg= &Gzr5@ @z@L(xAk @ TUU1 CMMM@@ ^kkk b PcccR777D aZZZXb|ɣ@ bf5440DY  "nohhl߾}7olfC@VSS344q zJ\!.pH"q~aFGGy0CCC?*//wm @ ܻw˗/CCC |M:o8n X,n @@X,!q8Zkqr=22²,#|@Ueّq*.D;kq=  ,1Gd(ʦIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_3.png0000644000175000017500000011147014062723141021721 0ustar00jonasjonasPNG  IHDR)ѧ IDATx읇[J_sb/vzM(`PP(#Uv鰻Hmپ ",^`6g&L&_ϚVŒf?j`]0#{Cu{Cb/7H]lKD(_9C؊dRpD(P0p_97J3@sN^jR-(?BaKƚ?(hHwU%U6Ifdf<L$3H!06[w$m ͓a*ҔƜh|Y4-f?4%hJ_,?ZPĔV4LۭGK_}w\qA 7I_6}?e#aέY;ͱҍm(Yn3`=aE)̦ IAD V!+z`dr 2H$ y B_4єuVuIͼ{D~V]SHJC/\"AA/4VzA9CzV։FѱёelT}Vgbgl]+g 0'_山sZ߉U[ euݸj5#dTVF8(1&OkD8ҢN6[V#Mbxə1~Z*/J"" +Wڴ%&4wxf3"c mຠrcO.[]pAQdv6lwն5ܳ2TÏdj)ŤņUjjyE)AfkVmU0AYglS0%5@@iDy q_Ea*;}J"}Ty557X85tiOzlh߳EQsK[תV !wiȌ.|BQf5ڡXXpH|#\t&K;q{X ATyU+WrZpNZ@C D}(T"@, @AL0rS ïU@,0W+ӏS d@2PyUn]YvX!oϺ To,֕ T @ 2Py8 T6ʌD ><<@@ ʫr~@@5ʃ TR_|{W_U&B`QW9NGD?p/_|,`'@`*wFqs|^BٙCsi@ƅ_B(L^}N枅Z`$*ϕ1&y\ڷ?7= v΀~ 'GUY}R^;^]:Wg&zl^jڕMO:Jz9@s$%U^mCW"\vcQ^]otȨrvkǥ}'4}]=-aOOxݕ y/w&*Cb3Sd2Y*IT4 0G_Ls0Τ<*[ힾpA>Ό>.xlT~-1vį:T71333:4]ߝRarqY4G|?{4}b>9;vd2adw" /N9'J-synޒ=0|?Ȉ7VEM |\Ͽj|f pNᬿ|_EwHd2/N@YDyYff6ֺtE{9wff*' xgW؍'9=Vfغq a˼ `O~ս+*g錄V|>˙Oz?a`ƷǏӹi K#z!d@'/d=x<99ŋ}g$#Sc'x1?e_jIOϤjS2:5=!Q;;= /hTRH^klrTredo~gjjhp͜?vU` _dldxNs?A&'ıpk[@(++Z?_p Ega/@/#G}8IJԛs| hͽf J)[[ȅx'Z|khj3UQalz }W.5v7Guazf"'6.Mx>v}:?+*h쒉qG]1 o3CRE1~mBT ޹C'f&x7R44w+jOj|x>zTWCĎλ7;"vHAOeTٳLOMOOcl3)0=55NMNMs?  VRe~{?& OMwe%%Mq( :sybyyd}iSAx4҈*Z$Qa֯>rj!n~ .+) W3?N x'0Ojx$|\ykbC7Y02_)9(RykC鳼FhM@C9wU\*F\ 9Ž[};q+1=ElnH$ EDV;uĄn[w?{wcӎ};]\v v = \|BݽCqC#זSb.:5^y%;,΄`JO{3m0=JJ;=SG8k KWM suh`K҈I+h{b"}oz6_s(g"O''Ғnb#5 SewJ}>L_c )/~RT}1%UqP^Vt LOg]K~}˹<8M:B[%HxGlGȩd"Hy$ TydMTçD?wً]'ͺ\Zcu|)PvAbj2by|`Tr,udjrsVcC+'f |qL;TǏ3SɏMRT^aiDȲKOy;hZ哤d,O{7(<ě߼4R_SҧiLqbck/Scd0Q9B9=5U;ifE׶oYU\^2>3TW&q&|'T@n\T;#I=J-A&\{oۣG[^v[sF-&?U/zo2?# ?yݗ)%qkԀAqye>Vwm|a8{OcML}M{%ʣrT^Ai+ y%[RnDLr/5V"[igED Bڝ3>"-{Hy1:7>MS*/bZ`ta€?Td,/ìTTQ&IϾ?]ɮ pP)bWœ HJ7&vHO#WXsOvvw_<¶m}pO2_p])nyy͏XNM ]l}m> 14G)q<$훘{+14ʐaiSzZ7_c{񳩩Gټ qʧl0yiD҈.-Y RBQXO~ Sm5=3U{'G.9:Y/ ǜa9SوiR}23ċU^.Qy9Up#*޴o}q~-Į+l b4ws˻ v rٹyg-{u`/0YҪ:>Nsf_Tdb15Ǐ33 06-qPg [PaXbm?F.^f#4,U"ЬN4[Ek5szzENJLcDs*l :H'GEEs>|@?֮[nun֮>Nf^b w`zk]N~Ӻɬ)WnfN~7QŪg'8cK*&Wv^=253=,lY×<OD$RZ~~CXvk,&IG¶7OړYKnɄ|ʯnETӳ3ńx)UDZzn 69ݰR8 h 1[:߃NLr*:x|b7_^aӧ[fɈ.GWEҟKMLhn>WS27vk'197#!CX拀* a#6w Owm2uilidih]l_J6ٸm }&9^EIE#zzGŖwY'_ YJ^pS3=wSd<ӧVF}76U~x~jiVwёrJ#:"d@7SFl.flGL}naXD B614Ad0YQ607 9~ Xa閔@c֮;Mv04n`CCj"P?>~r 1_1Y̰eq+2=2x㧜Y:@Dd$Q! PWQG?N qYڪgccme0s8-Dĩ$b+'7/{'N:y*"&]@߶G)@"e7qةmɺ ԙnL3q;Q(Fd-bsf#f;Y:X:;cmaC4@(C@ٸwfM醎dCGd"U?~aŋ펕`I1r;ь l=? Uo` Ht1Գ FvϭE[ohݐ@e( ~LeIRU>VIj~t/_x2((X @ (p*Ï߅l[@ݐ@e(x铧OL[z)1^e<@ @$immkkk#2iʧʡ2-onH 2Uyߦ&e[W*㫌? U]v5Y Gܺڵh7@@}ݹsg-zںڝ;w*@"eK_X$V IDATrw>~BTiKr>/d'? ؟ b#c?h%6;v贿X7oI?M4:~lyP?4J?6O%ykn":Ѥ_sn>s-.ɥ˪fQ8d@PV==wTcGO}={9éׯ_ymoo޿7oz^~_t?{%TW?ܡ'zÕM>tUQ56عkV*wriIE}75f-7e@ m***+ORVJ|G a "+4c6ݲ(sHo' /Ȇ|ܥ}ύQ̯u8?T.Ϲtŵn'V+ 2WR!ucykNV[>uIﴽhS(_}iIKOu"۴Z5QC 07ʪ2S^ZZ&rb4 Z^XVVM+MxR_&wx<+r˛5N)u GR1Pm6/㕌_"՗WWE795# tysԂou>$="'?oQ&o+;@HPV卶q#DڧV\RZ\R 7ۗAƍ4%G+Һ7w/v͞lNü/-n>UySWf2b +Cot >7W:csYDXh /NpLvxjys4ENf F@Y_n/^}6op6=6sn.%T8y6gwAe!vmN^}9 *WyW;/w_EOb= w_" ;.;v0:/|DSzW|CJ;3 )XElc5g%^޴!Ppa;@`>oWF5Ϸ[PMz'#O>uGg ou1I6"X!H *ekĆTH@̍}UtS! ܻw|ٻ_7`^|}R|e}x+Co:u5~䟽S6?uV!ʰ?{_[{f\Ьs6ͻi=MN魤P?޼{+B著JV |֝(X~gE5JLR/BFv<@"0*uOuuUyyyqqq^^n ˸hs%%"rCoort.9et;hՉ/Xs<3\o]I fמdt'i-lOYLN*5z!\C8 J\KְUSycsOEl@YK|8 !˙w T_˜yPq<,ve%\-|p`5 [!C]9al2M= N;HoȇRytITY];總ّ1~ =whCoՑlH7? Ao-#{q+=oF?Z٠=7  <_KҶ za~ Ags_ EvHg69Ku8s=;/?7de×\yΕ>umQӕKVנ9>a i{r}IdQp*yvs0|UyVCftYOg޼údSC̲vde! I 9hy5oo/:jS(Z.In& -FZTmt=.2֣(|jOi[xh"]ٕ.fT ]r0Wsα,L=>o3ezeS[YWWZ03iHPJNؑB*xg # XrTg*uWq?OR+OQXZ qPXS5tՈH_;o_"8xA:rɈ a,?>IIq d <#ZW^_#,8>k3*Sxq?/xNr܎S)4UXȅ7'Qh;.6 F|P 5E^|p K\U9{eA%ES5jv&%3ʼnJO/Xsylw("ځb䈩9N:=ᐬ#f$M55(oū뗫sDK`ﭷѢmZ3<[x֎"/{XF8#^^xA'xBLUyM[(tG g= wG<n!?_ų\I{$ {(c^bLQJ9t\q1T|3PJ5(6Q3/Svp0^sҲ 9eqv"y)b>!"*Xh`Wt2-~thx;n](vyy;z|qkC߿!="  LW5k [tvd<y>#y~TAl3Mp]Ɉ@Ho6-N|*$қM~/TxHk:JRЈGmH;2Sbٯ3I~{d}e6oMh2H_>c '>E3tϝyy/t#]/A.{RM U./ҙJ;[&M ӶޏqӶ>F= pf '9屸@ YE%/?{VwiMcw_1 @eGJ(4ˠ +ˋܮpYns NIeuY!gDO-,/[nV)c"0(n.)*I M/O8]RY%<&ⓈR$ C'v+m`'8hS( 3}I"ʣ(MA =*D5zn7fTahsuo5gI4=-hu - u96Dj544፽q%}UWW::TEOc Z=.=SNN&t-_)~1lO'\rWLQt`hu~ ~ED"ߗ`+/boa7^Jy1>R7Nh ȷ]`;˗Og;f9gjD7s:pl8XWO;2NV|0ǒ`7 Pyle6'J y,W6fS+~{kP aJN Tf( TDlT  0V TJ4#8AT^X P *ь@@Py``5@@%`*fRD[@I0dkCT^u<@H<@@.Ko@0 \< slx@`,/l@,qK| \rF '*@%w_⁍@%NT~7 KT^.X@x@@⁍@%N3LJsNZ[԰ `tep>_' ,/71?1pKT(E熑3]W\/r"QHX WGt&Y \ s;KZ5CQ|#Cq$Î/8X 6(BK0n} Uy5p[`r# 3kM;tN ӂ$ZrI x I؀]jjuF꒴m}w?Bdנ%9J*Ofn%A tamqqZ%iFˣI`abeG'S݂nԽ o/:jS(Z.IqY8Í-t)$2&mq Od8; ZRt2*Cן_8{ >7:t!M [)fG8,E7y+(rMg[qF1izwPElFH#Q.&S19|duއY.AD{'|XiQI$bTXh}.|AXi EH!`WB2z;ғʈA!a-#f[aZ, vB&kzAe(~UXEފCzT2c7 *caH+iJE\ - XzJFe]@AJ.})oH8\Eo>au~ayeqFd>:NeIyXUYB;\lvَf_T[]Ǒ.tc=XS. mxsNyc^%k⮍y׷8iY7қkt#~ʣ ~̾pq #Ǯ=(//ߒNʳƺRt]O7QT|eS4=(3ۓXT^YqÐۅw)qk?}p9ЄD,4ncX.3+ eI((+/ό0vl.e/X(*GIJEњ8*2$niUUQV-q({lbt~,K=`%ՋpO.T=xgNqL)(͌" 0__l#6Tg*uMѩܺ Eǚ&F|?=K<1fuƺj7Al:eHb@hy=Ű_"Rd` omgCQN m-c|Ҏ"fEfdc:K9+Ty,UdsY8A]4'ӱˆ YavZaosb!M#蟒o AP/qѲ?Uf `I \fGRLE(IHG6[]sD Z,g_ߌ:]OK%/XSd" 1Vӈ*_YY)NT:OgEJi'黿N84RkN5Y&G&t_s2l r'ǚ4Id CԠd c/nӶ(e"{Mw_ےNf ۋͣB]n6s}>O^^ߕnPW6mn׽7c͉ .6~pp e6  RTEW-‰*^?^-}|-麮1ŠK=eᷘ1t!,DgTyf{Gdz#Ry% !k5ΉvqJ+ 2}%' !=xQBG|?s?vn]/G^0j IDAT|;Dum)OdV?ny\.rUa;l#^$j!o~J˯vw_U֧w4ӥIOVh;TE=ʼn!N& e_R9q&MCuITk1|SxBJ"tͶ˟682i-p#//nצoMxΛ4)6k.ٮqFoԧ4 =S{sLEUϺpΛmz[vo#VTKn(%yxyEǺr÷3$Uu ]Q ELHVg7OIiM~_DiACHX.lEdvLOwK= fhsSaZƒ>uAⱉ EkSh]ZGd拊 r, '>鷯Pu!0V.q! *d Sja>Iv[&JxƹGl n.1mL7*\ʳ[fWW:iL={+I&i8dYl@g"7]2EdsEl˞"Tyi/kc]WSӅ]* 1 +s/b4 96  L@̙v@,X\~nϗ_΁@eO,.PiQX@}@4بts@,{_E6{/{t X~ 43X^v%>L?aes<~[Äoz b0WVkU~t`⮄H#"KHmq \hS~6t2Mv;*DdaFڮxR͏5p%#ӳՒ)9aOޚbd  { %+qH>nGމa71~ґG袬+fcĠZ&-$ka9oezI7lы!l@ CɲUȍ]s։jle0K8Yg9U/w['|W C ל?9~#Dx̒ yyw*Hêʂ0:bWVFk~\r E/<vHUX`iٞĢʲcd0 1$;|,v T2m`\.zT0ϜSVQE6;Ǔב.tc=y,'k;V~K:E"l[{ufcMP=cw)Gr_GPm  E%9+WXSi!wK"m)CyC(*A ozS겂¸ :Rߐbq@GTiݕّ@tsG#bnCf>Wsα, $'G_a}Yxn>z E`UEq͕xs[mOӣm >:c] ⠜||a(RqѲ?U2Vk˙XS5tH)CMlPN~FV m@*VӶ]|GhI'U;M/hޥMJ~“TsN|` ֳc]mOVN-ns% GTvrbu:S{3M~Y#U:"[bZ9zT8ޭ]OʑӑĚ^ kpܔ6gI'fE!2%a l):˄lX􎭘g(#@ 8gU~e7^۝jn9y?Umwp^[ /U>=i!\=RkN5'oU@jar$6qbvj0VsPytz!t_s2l (RT^z<>;B~ЇXg[N?bH}ID?dM J@ƛ3El=7P2U^yҬS su<%yޥ0OKB au:}E<gA5?_  T^Dov$y*ߙDK$a[DUSW ӌMR y}S;UpF #lu+ 1ݗerSp0E]&:;ޝp&b|Y.0__Ƃ}wQ#LD۝9w?GףN.;ܴ &hS_j ?.Jc"UFQY9\yʙl^3At/*U/W [lom R!5?T4OPg0sۼiڂeU:\aKٖN S~@;BӲe00B%PyY|HT3nIّHle>O 0v(Ɩy t+5"}k>EgAԆ_ g;@\^8SkwsO^t_𵻹[ipeo=UXVT VwiMcw_ˢ(twJ*5_ho?YQqQ#ˋ۴ݣJ;W"T]z#3QcX_3vE3Q-S * n*SBdF9QhAnWVK]3bܩ4+ hw_eb6҆O{,1f>ᘛ *+sf噅OFz+9jݾ͔L1=ͻ-ȖL0+ktzѣ3$:~U2;Ix]_HPTZR}w;y3).(+{=f!It&%hMVLpcVѼ;uֱ ռ9KPC-o15GyJNt<"6 $zGH<@DW~PģcX=ɞnɻ"sDo4`54)F[o<qE;q6~cY#ڮwEAQ#Ci>vc2>qE:Im}qG3]*DaYDneq*D2UH3\aG7m5yF鯅)(K{4c R& wEd"6(}!JG۞0TFX*"oR| L f/z˙O񑺣Z෯~n1_-c\zz=chWw448J-dFv̜⪺[=i'gb,Ssxѓa~c3x V{!\Z-&Dq9/:^(~siytxX-ta}yn5r'J0tx->3?y< gpppllÑlx  |yU ~ ~b TW@\^U_@EEwu,*` @@ X^\@Ϥ@ 096 @|ʯV _>T x*E痖r'˂7.п}EӧcccӓcccݥMMMO\mdGLmB9 a.-6/& OZ~[B Flnhh$O wWv`[,EO{[N|1-Zpc-T"~XCvyX%z+.%/LQ,K9 wS'wY3E&UF/q ]-[b$s2 v4ٚ+_KUik}tXds *MulˢrHqk3us~]pEho$8XZ\)%w,&*J=u@͜xDb q~C>\l~lG_)Sɪ90G{YuuEf9M L-/.큩.ya}a;=oܮ|¡jjzSv~Ţh\IKCؖEɢ~̡WDsQCi_dUsG"wh;Z+ejjjjj;d[(9TŊ&GlϢMV8J,vgdSM+64]>}M(_oZ-f.<SXX>}<6?➥ᔓ&:}4|cjZZ shJ ;_GveQ2Uvn ˛n0:sZo|CElbɖk޸xnkeL<wziwla1ŠnUg}<9X$vnAhJ[hiڤ杪g~!M4wEI+YSNX o춻4uKJ86)2]7Wd;I˃kD+Kήhu(zNv?Iڸ86"T1c\4s'V+_sd%Y‘B-?uhd:T?M[ s4$"։7\#M]587vgc`.?38'%=WVD!Q,`$FBRy>}ϷX,jڥn\'IkSCVmt*=:C6/Oxt>{#})+O=xxnlOFa1kv.Ԝ߻j,8RJ&|wͩy2ҽꚊse~`_DrRyd\p`ml{9ֻʏcWz~:vwzq{[\++[Cbί.NAH @<3TTTp[ҥJ*Oۦ`13xvkR(ܽ]w#n,](+fN M8ppE:+IwׅJWE-ֽl%6[;jR{ Ѵ]:[ҕgOӗ,*lB+MkNb.9Dg){<śuQym%1S墠beļv+nntV;U剚+{\ %$@`ؐ$Y\\j,e) Z̥G*o4__חW\]c"Y[KmP:)q;Oe)d^nc)HVyWQV՛s[,FŒʰ-7틒Ee*8/UJVQ>7i}(r ڧ&lJ,Ui۳h V8J,vgdS}M+c,˧IRMVvW>c݌Ѕ\bJC/JoZ,ܻA TMSB7]qACuwuzX3sKB 7FiYoʧMc7@MR?'3;;ҽ*+춛Kko5vsUxqgMrfFlO.,ɨ #I[yww_Z2DZZ*k{κ0i*GHBE,sz=n^9GSyl= LmAAy,`u"ek8PS# 殎;VD:vzo<8((t͹6v@,oE9?i7( .Xf?2|ʊLjk،"sMol\'eʙ?KM:*lީnk}شJsX$+mrR)Ǧ\|f>О+VUT;*1 bhC\մ:H4%n}z3DUstIT.9YIzc}'VD΃{hhG7|~~bP.ĸ*O֦+JI*]N]Gg#v_7ֽ6wjS[+bn|̚kbd =qyj' vû|xbIe G 6V;KJC(;(D8K4ߙ$m I9@ @`4[+*,Wͽ*++]zuv]TK+Z+ۻ|T#+Ҧ):Nиqy.8YڌZIvUB!KH5P9 qy.AA`[T*/#)U*U[[ @w'I A'NbiEWuTߕh9B!Sըf ,UxZm gebdvj5$EGKq?dޥѳ/xHT~|BgPўEZݫ'O.Y>A|X#,H=fp(;v&-+{`q8}m!%S("QQQuz`0:PIxyyĩ$ݬJ& Xz DRkI\, ,3 Rt#=x-Ii9Zn[378cك@Bc8@*$d1KRy>g+8Iˏf  # @$l68  >RM&SuuF{nNNZ2y}t@@% a4U*U]]bX, j}6A뀃&@g9}O-l6sئpPy ʼIu:zˆ{vdq&.?0?|~׉{Ay;]'FX#eδV%G Flz}yy;SgAV1_/ԀլB\\!U~|w 9AMo7Lyyy{\p[.a{5}\!U~@@8No>ޕ= t}}}uu -7.KBbWt Jq[k;4.!a_:_&bk}tXds5(MulˢrHqNȹvx-OI,"*XNڟ!}BôSflfybv iY~aPqH>{וzʱ":&BAZh N}8:L"IC".¥g77\?nT!9!Um{t\񈻰-'cDͅm3*xD." s4g|jjEѸxUyJY36/,.>.J29dތK+D3~.&M_רs.X-/8YYj.>X"۝M캓Z5^9kM(_o+[tPUUSSSSXvPlW)YEܤѢ$#oX1Fr:}4|cjZ+/bs }|<6?➥-.qD-0XXܽq"=gh4yĎ<|.93X-f.,&*NϺfOhrxEFo,ȸKɖ'Dd3V|]΁qyni$\!4%FK(@*=:C}L\J׋ʓ]ʥJHjpp֞7MzoaNűЈS墙{VVA! n[,owZw)|u9-ʸVO*VgFn{=lfbD{Ξ8B! ]q5- *^9wdɾ*=2]q*?FUĩT$=D|̚kbd }˓ڥ#6[{ؔ%H#gV)l㰽5ot+Ntӯꄗڃ_YdC7M7*O^R& 3P.IrD41kctFW[33ʛʿ~{tܕCV9DM[[Je$JjkksW\)'w1^HsёREwe RE@_!ސjTWJfw/Kc&_-ȸ{IDGѮ%Inԝ]x)A}JCI53$qjʓD3E \+ݿtx͔`W\|tDuE+J=<}S yB.N72-VLw@o0~/gT{Eg;ut脆akmAR,ψ|RIɕ-'5K}֮%QrQ4lֳw7RwGI3JX˓Jcv8Ft~^(FZ͎,=|TIpP4t^0F q9}y6Yˏc))s⧉s8clӻΜ* ,zN]d IJlJgWIE&q<*Afd2uЛdX,vXr4"B(W@@-%ؼ?oiQ-ˏk<E@16|v@@`ʏ+x agԜ˿͌ځ[ˏۦGADlE3 QqqARO&?xqܨ$#@x://-zOˏvGA DlKK 㓀cNJ5>x Pqy<}}QqqA@սWT@F[QNlt/@@I1*QCycp0j `y4]0^~tHJDi  0`@KÉIr/V ފ% 7@@ oEa|@Pc<!`@BO_(!% 7@@ *|@PC!`@B*(@F J"(i  ~';R #bM L$E}y(j ##b?( @FG_36+  ?їR T~5\"6O/w(@Fhh "XE}@F  (G_clm@p<ފ `   A@ #)ї0g p<} VA@ 02Q<J}yA8?ÊfA@@8Py21,p±e<Dl@@@8Py2f8DQyΓA<†)Tw0 <<†)#AӗWH(H*#l .;r ` a @x'+a@OUycxذ `T^y'  <a@"a@'1Ɔ '#)N@@@l_!&HF  a@"<"6|!E#.r @y S  ;l@ bp0  A@ qy 8i!@兠  +_a@x%7>wj(ys   +  _d%IDAT  30pUc?tXPoa@%7_ް @@_ b5T'0  `Q<aaA(CkQ@ &A@ 5a@#ʄc  xP3"6±e<|Gclc  xleG  a@x# oa@ :Lo 5 >o9)      Pa@ 2lPB]Tʰ BB]Tʰ B@F(  |1'%a@x' a@x$ aj\UUhǦjFl@ y$<{y `6 >}l"A${fZW _X&./A\~F8 FC]jc U4>ݥMMM#U4h46Eݮj}!hV6wDЗ@~~Pul~~Puc0ƗT~HPy_IPK2L*?d@}AA.2L*?d@}A4>$C-b͐2T~HPy_yпkX*C2h|HZB凼~?Gʿj/B*?(,Xp-սܬi?m;<_ڱ+EOZZ4of&Q= 32U^pjUyǷ U`ŢU7G*"yx{/^y5gF($em [BOԮQlJ̲eV$-/e™SeҐe;_RǾʧeհ3YTɞ/*3O%'%Rn81]]+ep:so_QLR;w+_;^ u]=oʿ&ͥϤd5@[_4756?9|\B0\t9%\Aӓ Zͯި5xrْ6C.F!@*/2ʱU׷}}.WŇc>x|Y* {W.)ԑ}!sg3$FIb+?GZʏDijU`Ћ'f̗){X}o O9~Imy8TЛg̽~QpM#66?ԞX.S%r/ -hh^{T[^@RZ~#)/KWewm \{LkB$״xq}ݗ⨁qZwTGv?c3ɧT.4F_[n)xjכּ}&MC=/sׇJdjJZzcĆz(c,fiԿzݐ*t_-U_.Mr\cUyb+XG}}m,!*_[pԟnդJBZTY][*]x(^,30bm4"s7}KzB䠶kGj*zӧkrNJ>ڟMNzѽS4>yT_kO|Zڕv܎~Ѡ9O_%逸'*-ԙ{ i2H,]H2H^M^]~[>qD5zAU@<}4G׭ODIDbkި{g5^Ҙ0""<:nKFvƲ:PIHW߹0:L, Z6="69!:lKX|_+ϳKOKߊJ>~2ҭO^OVII)i'Ϝ*ou6Vk4v0fsUUոBTUUē$h 郉IP!A!C@utttuu.h&eUχ  c،  0ju&Iߟp@"J?${Ǣ@@`pTI'|6x>>|lfBس&>x׊Sv=f<͚`n ȖbOg3t+{ Mp3:--V/尧0Frr}3|~dnl-ff󳮺ro??l.٣܏n=ǭl9mbr+#]Οs~ -oO` `?ه?a?woޔOS~2I$ǓI&O~7H{'菎ޙ_;Mv9g33h[!)LlN֖YLN_&3S.[`cOXGdI&30{-[/f'9=ZV-{VcNWlfnlQl"S[3GLE 4s@tlS @10Ucs dhu-v:q3l~Wzr+5&~2Yl7o n nANay;{3yXY?=l ]{܎Ix3Ė`6=3ZȄ=*RVZ9 F{9d?+h~4I$'~/)y/ْgj[,IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_4.png0000644000175000017500000011656214062723141021731 0ustar00jonasjonasPNG  IHDRټ} IDATxwXIwkXW7]+kX  (`(b9,fTA@0f ='3 (q=]uuΪA۠A4hоA4hЅ1@ 1Pk`4i5l?vRƍtY^e氲|@ 1@ 63ҹ˯E&L+c?] SSݩ;t|@ 1@ ":qOn075E6m7hРDݤgO HЩAC 1@ Lz;ofу膪7ʼnD &7-c׿~F=u6I61@ 7>vc, }FN®(DXv= +F Ql{bWOڇv=h= D ^k ,^BH Q^VZѶ.8X{]@Xџz]ލFO5qgcx< ;8Ʃs  2pn^~Pa3)A\x)02-7PNKG ,x|= ##λwBb^_nEw&( q|>q~ ev>6T],C#zq(1qnA~^㐧#3a56h:J"3pz09! ұ˩{Ÿw ! pg w=Np )=ymS=P,YvK<r=v,'f#l:VTӽ^1n\kU!W'8vM:v!2?AP'j jn %@$05Ӟ#/qz-:cpxNXw4(|z6z D(|}Θx>'؅i.:l; YO>vQw,k@d<{HL'Kϟs;{ް>1cOq]ؙ`ٶ\5YZ#vtB ":^Z!_?+.gb܊;YGL[X3;a0B#ª:X"^”]1㤬g]pkwx~f![𐉸) cbc7}ws u|+%L5L Sq`a< xuc ~`_$H?A(H8(Ua_=MTj>DW s=&@Z:Ά Ɲa<uGa[x(fB$ٖť-\z|fk7W~RM d`i%C,zpy (Q@w7z$ Œ5UcCQܪ6|.qZ.96<> Wc`H\멸~LFx΄]x/TlV|)>!M=;krپjc76I qws#)+~zY`w\ifn,^|PW,= ~O <Á1=峆 gؠ" ΋|Κ~)&.<\Ks\orQg"'(t~y?ivc6寻u[e[Sn:a*:76~QT{l?cHJN!pѹ$1@ 3`ȟm5.5 ; ; ;1@ f}nO?0LUȡ&j\U$8Χm,|B U8&rC6UsUSK b1@^bm1i2@NN-ub z {= &?9ŝN  SK b1@^bmj1i2@NN-ub z {= &?9ŝN  (/1@ 1PP{dd$h!1@ u /B> b &QFNj1@ HI e]7[71@ T7$$J'bG ף`Vwң1@ uvvj1@ #ꬰvdKkMQ bF 1PS )agB~^|+l֯E kgX].Kݜ`_S΢tF$b ac֮Î{`y{`92\[Vh={=V7QF}7'&޵Yot9kvm jxDž0p L84}gb]T:a8OIw_y쯥%HFr;w~Gk Gr] 󨭢y#"QVΞX+KGרSZ/Fwcȶqk/Lб87wF}xyp3'L}7FØ(DnqJ+ )1C+!Jt.qNpON$ѧVgyk&f?~PG> f[XGjXZ_qvND"p~3<'/Cx Oߙpܱy;S!ND 3a,ڹ_Ws|(ey}(/a9k3+dŽ1`,>Y`]5ٳO/FصH1OR9(GyDQ": q4dԔ5>sazǟ)aЪޝ7 LC$)H(@Mv]ۥAXO(*.tL8nv8E(H[xx J!"}߀ I80 d---ƣ'O0oy>t!cG K8%Avba柁26o;Q σ,*a6H2 o3ϊ+v:Cpc&]WحۿR>/.NR m 'VDwY8wUmƸ͢˘%(*-EfV(V&G9Dc[}9}=FkbA uDb1jn)˃8%(BҊڦqOr dY 3V̒w{Yl6WT u*"$&cwH v_U8k8 g%(lDֆ.,BYy .TLʊrp0cM\w9}ۯ;_M#+oqivo#qʋBtTIpG@i"JQ&13 0Lk0썾P*Ɂ*kh\5[a_}][ o%kזp9!vE#&"/[ ^h)E wӰ#_1Qy RӞ+ľ@s-&VҾsZ ,۶oP$DiJ2HPƝ#_ˮ)1q)/]KPZ 6^,ڋ"|HhqM?DC\ !ӸwLk Js`uIaWfST$%X^lqTgϻ D }i UχCZw੤ CʤM-F" #R&ݼCP@dؗ) pv48>zdUZ5c'_a0+\Fi[+j: %JRS|n18@R&đC|uIqG2T9ߟ}uUdEijhB hb`a߲u+KJqLeY>#斑iFO )2`̴;}!c%`f/]&'܈sC!)I+&)8ąK<ۖsTxM} e(~ TʋbUȓ@6z-"ٲ@:(y/sԓ;GT5~/ξyRes.3XDw~ NQzq#mW1 <_|R)Α5rQ" ,HWMWyDʄ]*^0uYee8{_1JE~S Sj~IP" xrO7NoV4ŝ~Sƞ|APaཅsfeʭXi/8sv=p/@RHJq蘰.G赩Ue=걷ZOu!r7Y%e`WKJQX .4t\Yu^BݼMް,.Z Wň ¦g%xq=y< bIo"sDv>$ظUޣW-v"[uao! OU6vfڧ%(~n2eC˅Q/.GH-#Hď:]?+mLĄ*-3of/ZK6b X"],ـً _\q!`"Fi:v{kVJ6z[7@`L{լNaIZ17z(x;Wzz}=%¯3`h6c`asuu'|~i_:|꽅}eg-l{2Lvg/YK1mdXe:EPvě*ex>b wR;zd`PB&JpJ^\b<'AQF2j┦x$ %e*~SP&zzꛏ1bHrךvClb /%v2 }0yKtk($H Vy,|0yoY${5|2v3s5,P~,/ŵf]Cl(FN5 !}UfixP2ΞgSsGOQi\8M{ޮm)( MMÖcpw$BCdI+N P"@ s pӞ 6/q=A͕4W2!{V:~yᎪ Q G$Gˇhz-=^bQ8梴8%x.G|rKqr$^s.Ӑ#)µ7}ŏ_5 S/Q\^eo/(FE4) cNip=Sǜ:XQmWAP2`oڴ"ĹQ*& 7L& uq =M"z?L)\ꊭ;G⤟#&#n؝ gf ǻ̽UG \ƑK:nM@lAP^7/_T\*^i9xW"]YK{k ## Nt\} >I~;_Tb]JFPDN{uP BR7r|陯,ҵW׬t]f_(~YC8\ @17$M0:''C|*̨߭|"D'b{p2vDr{zWH(Ӈ1juA5۩tfi8.d3Np79;S9W9<ԿƊ/E?%    8ag!ci4FNFo$\ٚ57lxx.ۇ.#<7 $ܲG\=†B#<شyuU8-yp+K%%x1x95W^Ӹ1W2$ܻo"[ngᅐXd#$:ãn'1l:rP\V\=wE1n|Tbz[A*k6< K )xY#/+STG lG1;)5|t؛7biB!=}~wxg⥰>ll^n@.e_a~ %"8$ m4sy}կ7+ƟC:?=E>~0X׮[s-GN&&nD9N@_ 0Ci\TmupqX9=N ř8sv(#v[\PXpY`ŠT1@ T}5{sxql !c`=d rƅ/l[xoFb΢aXk7:`lꀵaXZUutS1@ E +W"+- t 8']a5\`1ŬhڍҸhvoԮ10}:aHLn1apNq3]1i>fk] L 1@ @U0Xؗ.[W_ܐ ~a L` ԸE\:1>^~=Kɥ!lamFfkUCME 1P0X/^/^pXNęW&ra毵 w,oQc}ֺ*b 2`/XO>ӧO!w/pMk3aYcO<|fZUutS1@ E yHOO~ ^UQ$EX~We1[bPfLb ʀ>>U6_o'?bWTlc:b " l$' %%ʽj ;zgcf1u1(d3U&1@ Ue`aws;w;-rag=r6f#"b ,ӧOGBBw9Ξ<}ֺ*b 2`O:7n7-X3֪ۘ: b.2@n8v. IDATyW- fF]ᛆ1RExZΛ&v OW`}.9y6l7]Z`{"zXuy, Ag zR eu7:nI˱\xyT+os1ƞV.hSOVwY)긙)   S8<|P뒖G'c?x%7o 33o߲gݻwܚqiXv`?}K/#i]mFf6Lӑt_.r"}yz'ϖ t\za~pqAM)c0= rhy0.z-=?.>US[;U>1@  hٚmXbb9\:1*U?9oyVCi$N`HUӐ {J6 6+IqAMqr>\u@>d]ބpO8Afy~Ώp GkMg1'?ÏġbAv󇅗_M[- p0@* 1@ hg`a7n<͏BDD%wwnl&]""o%Bznd׳8]al54 _:w\Dꎓm nZb|ޅ/ A{vT %` ,͗5 4mA'Ps0|.wa":,@]ww,7:aeh3[ziF@'OP~CIGkb >  {㋰k]رl[HaׇsHRgp ;ζkezs窮#8to:\Nށ 9W\՞ϾYyRoWaK)~e/yiXyaK47\'DtT_/aOZ̏b҆aQ! y]1?cO=}aCC5rL  {Iko, Z`A!9xHmjQ8vip/s: }V,3y{ \*vH$-SZWV0?= 7:nMwt⛞®q*MáUlP\GU9 }+p;1e?҅}!IgQix&#r)fϫ5 7:mLؽ88&tfQg].F Dȇ1@ 2A^N s~tqo SaZͱT |蛎xOsip#.+[^a0SWil a @fƅ,\,y;w.zȖ1wŽˎ͛7v…`i4P]͎0tYq X1i.$w>˱xt]vu1tޖWdgaet[uVwF{ǡ$=vށ2L++&=S[zjڔ,,m͓*}拿7k9tZ'0P ]85.sxĉ8{,.^ !:*cc^GTTw;]VMCzA %bTJ}AEZλއS|*.>VژِKH0+=4^5Ŭ IWyN#5?[**S;AQ'g0Q}v$j&|N1PuH?T%i(CM10Pn2}5NV- KζHnB@bƅ qu*f#b@$B?!ߑb S b1@^IJ&b60@NN-ub z {= fmh) c!b2@NN-ub z {= &?n+O' ; ;ԉb $(H6P  ; ;ԉb $(Jd? jj~|@ 1@ ]4hХO,1@ 1P a 5b $(ʮۭlŏ vvj1@ #HQ0GiP  ; ;ԉb $(ʮۭlŏ vvj1@ #HQ0GiP  ; ;ԉb $(ʮۭlŏ vvj1@ #HQ0GiP  ; ;ԉb $(ʮۭlŏ vvj1@ #HQ0GiP va&TILu\> =)QKD߉a St'lQ! j% ^ͣaԦ޳%-A0k&aH+)嗿1.HĚVt(EبN?}PT/@ 0;,aaij3pÞ@Tׄ]zfrb"›gr`bj~c'"E0qi2Lf3<6\KÄWa&/IO68k!"5[QmL̬x зYAai:"3+\/mLpTs89މ(_x,dG׮\SUk4 Q) T 10Gq5]Ǿu =yvkcg>.K}>aA8st;6G7X&NX$.]CX2~L͇`uꌁb9a;"{pLZÆ'afir6Dݸ߁Ca5j3nH2W3aڣ',\vA!W}M{jFn /]W̑ɿj0ow ò  n 8by ca6a67 =z-fX|1+=Epq3;!5B=tgV}1|Fq0UXp(Y*=blqa;YԢO͕CjbI:bP٨.jvK0ý{O̔(\۷oaO 4偯 9_}~=W>z;cˈMam:; El!dH8aͱ+ ř1ҬaL?#֑ RrMuS-I{a c_%sX;h>[BX`ҡde0Xdz{} #+ +U,`m ZMrn lMa7GL;H}0joT 3Ƀ[M4$ϿIžh,gk#PX<Yx´'B_ Oqv /#YLao! o,GlArN8`a٬s2XCTr-|!Wf/ 3E.o` L<(]ח0Ӽ(=clĶe:SqSC"nոaO=gssW x*caELȽĊ`Myћ`6l Dz##3r0؇w^*f<􂉩)z4)M 09a䡰0۾8c>KT@ԧz >1R&.V]9\=z`8W: >ɽQ.BcxEl1Bog>FRe h^ q"Axsi,fJld>;[a8O]l܅f}KeNaQ&W:VY vZ]}6B1L-*cQV fXZbiTv]pru3sʱ?V]HÇUT=LJ/l8͇8'}+w0\^1L`Yšx{q+)wc8Dz!t+h٫NJ`plFo Y>_,*K1M yc &Gܻs ,Mس.4\~#L]6:W^]l*8!:DMM KGE|(LzO**4:*c ۪LbbX6A6 lq gΰ1=z6U%Zq#bq5=x g @ }ʭme*x _!L!3n[Xa;25YbY8=oz4d;|v]^E:GK2tFz;xi CbS*veRe/h]{vk𴷀i_W+cTUe!v8XXAs˥$t)H %!=s0Gn=M0 ,&Q8~.9B^y"s8FXI'khm>|/A`j6+.+(*ReKL",:#Cp.w )gC0x]ނ,Ϟ*95+#V@.-k7dg~ 9'2 |~N *oF݃nfIN#m$V)N͸MH͂{vXy5C,¯"&۱y)L4+wϫƐS6ag-—p|l s>MQȫtU;nV:^Q*?'&;qa1ÄOPy,}v0Ư TvǺ91 ={̦?FM[EOރ0}N 겊EW5N[ ?PKskyvrnaKع)WyB1ǚxvu )?y,b ?- ,8`$Y^0F!#1l/"vc> UDB}z?t)x裫0 =;&+Ss/F"VEL.Q?rU (b b2@7O 5 ; ; C1@ #HQ0kGRς  ; ;ԉb $(Ԣ;-jŊ jvvj1@ #HQ0kGRς  ; ;ԉb &wE  :ב<Aqv5qC(J;Xddd@"`!Vw~E pw\\Vqgϟ|!'D^-ž/^O1I  |laֳgϐqXqXeqag2,G {H$$g=It+;@xƆM^"$ڄ6)s}j5)!y@8>j*Iʫ$/3wHYI^!p|le_gPY@ \DS&)CUG`ghxvFFHC[N?@EUޣ5viIf} bj6űkcX:>1S>$ڄ6)s"쒗^{L<A-MCM1tcJ *W Q¿o4l~n&\M+jJV-* [fzuOA/agj ˻}kxKK_l?l_.+Fq"< Xa=/w`rT,Ll#ts|ySSyzû^HWf >MY~&_DSTyz[V; ӗAēB k?ўAhʳ {mgbkf2W#z-w-n?/O ͪ>UWUMGuU;? *&%X֧Vit"fQOXD+ȱ.+Kpl4oOKpsmo4=m=~0M3|g t:(ˍnmH+*)©Ш+ A9;%U|$!/Sќ+hO[\{ſ C~?x.eC0Esx${~]Îh@z.6[5UZW3mhF7ė?AwDi8oFME{sMeϬq&Vri4=\=ߒGЭiS =R k/L$QAs]bL:'AҦ~pl ؏?6(Hm ƳBP,sɻCX+;I뺤XuG#[aHaӕ6T:B^YP9:,AVߡqÆh/05Yc2?Z CEsٳ^ˁ KmBb2a3D*q,_ #cy@J%o c¢7[qP"!|#,GBZq DzAp{!VR ai;9H:`afFbUyHįz` +>prUlXj<-B^dz6.I OV8v!1c5a zq4BaKW4c +uSѯeS1e\ āYhuŔ IDAT7x[B/ZXco(nDmٯ {!hy W8;]B@P0ݏɽ~V|VR'oq&- eU7>hG_Ih32RY܄hfBPwHGZ4c2[S:WƘAAp`Z5m;С ߼'$Sŷ84Gɗes38vXz]v}q4Ƶjn c>++62aO8eS.MEyi,~uo/? >2@geҡ;E_sReˇwnj]e@:]g\ĈXb&M`ʣ CSѾI3=]Sa#d;UM}PQg"MHbTapG=Cf@x-ܼ;/ qkXuξ+G q9SD0]5"D[3Nep=$\4w1VpM܉AY?-|1W^{[а3B ew/BdR~s%} m շ=X;x Cw7??8!T>L*b=f0])Mý=g0%@^j8lf TtY4Rq* İ€ݏQ/ EZhaGQ0~nSQï9/w:uac} x62Qf;VS~W }#0Np2_6{8o(^:?6iődq MYtֻe4 c}CK /Iۉ_S/-D" . ŊxGQ\cgoߖ6Jt- {gg U QvB KW.|OguǠb\7ްh\/a\)v*;;&z$ IUm`LMLLajj=gիLGb{kipٕb{Kragi^P" :Slbuh$R<}HBRt[G`ʑ(-F`bJr1-_zoKC_0xTx-~ JtY}~3+7p%q~L^+%?I=?F_ŏ{o~KWpXv/>'*AS F> 53,,9LMyNFVFKs*/Ƭ6Pۯ?E žz 6Ol&pCo?awݻ<@FoѺ Z7 &['6YL|ۃ>{|V-ha/4r ct k,ѰtȺݱ#)67h V5aוw9nD/}g0+˹RV ;sR?o`2ZѨ?ֳNX_W2qaZ:f?_ c4 O>kj *C?~;_WShF6̯5mY]xB(ʧq{;_X ):"^ߚA2q67d/C-bxĸ:ra:k|'i4^X}cC*&<ÏG  ~ Ut+x-v1f=\}eό@}*5l\ kekL[0 ?vd$Hߚl#k$+!d}>9 7Q6$/4^j &aWUk-fk;(Z7G s0$M.w Wy//Zi'u<}yޓWm0Q6yNk0y|jz/rY#.ްl7)hv9˚WVɜ7[+LH4a$`% Ga/ G_KLz>2 J%(zt.֎ؼw99t%xtb&Nݎ33Ej=BoA(*Eɻek 4qP]ccjFD&a3 >lV]5ܬ(]Ѩi [zCt(@7d'7/v5:7oe1H+WfvF/@ \\ ecOY֬\ Ƶ+qtB8t UxMcѨ&LItm7iU7eu*y c\oF&_|@B|tF!ܤʅ&hEK8,9>6?4UL+N <&#cQߖhh!L 5lM< Ka>*`٠i<?w6[T>JY# l ?خFptBb ^sjiW"fGߵ ^!!!/a lϾY{/ <2?R2/=mKm].]kjQ6o=up3>a͔!07rdԟs7pfbF|847-,1nnx?Y[b$axEb> a9zlF}bj7(,]$:eS %k*C6_tw:4sCe3n sc?ŕN+4j u 4[еwhsQ`}-A>i'%4My6FH6+# |ʤgS;4}_qsԶ+h4i%I͗<Eϕx$L@92äw<[(DgL!^U^9?hؤ9~7+K2i :ZsnDAM>7>k rXZ' RU7X%?vz2T_'4՟sGK_"{K4jTVayh߆*+K[_a'u*^xte'ϱ྘,.vOEB/ #.asL8:Y.Ys aZDS'Ze ;@h٘&3p8&݀S7^IsksY#K:fq p,/)`e1 Gb_x/cڈ43U_ gU_g+z1ffkIk/#D*KYE+ۯξyɈU 5^;~p_ vjG[A:7'K d+uzm -~/]ut܂CFKV[TCjpb}MaOB"pZџn:7O^х$UV {ۦ-0Res5ՙ{6[XDZu\ ?`%㮚p4kƚJWQlcv|/5yxEqw=9ei{h,$X&%D{UET: {?naggw CvgwνޙYoq󇭆T .gU,Ёh;նVTهf7Vur< O@cؽ~0"@c5eYYF`GNc{#`MENck˲#x>錽5;`F=- ^b?W?9SO<.fwR0#S&xUUU4qozx2tg{WWJKKm. #0G`] fmmm]|.ya}}t3թ>E{zUEݶT! c@b3ׯ_ۖ=lFkkm?c/zxj,V.¸2 ۷o1޽Cgg;-fb?DWU.W n 2@Ȁvع@ :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.4v;3u2@4v [6vd L 2@t]Gb eNcgN :bƮ#1ak7ævԎ ؙ2@ȀHL=Ù d@ i d@G u$&%K$ IDAT3lfԎڑ24v;3u2@4vVr8s d h;: d d Q;2@bNcgN :bƮ#1Xgd 2@c3S'd 1@cב̰aS;jGȀZ i d@G u$Ź 2]h4vfd 2#h:v3ljGP;: d T+c992@L 2@t]Gb2nM j1@c3S'd 1@cבje{,32@Ȁvؙ2@ȀHLfͰ#d@-h4vfd 2#h:Slp@.g6'aֲZրu+sڎtlL]6g}0,?R>-`l]4A0IJ϶#!,Z3ĞMƠǶ j˵C uLuSQ@5t[:Nس.5ىH T3vR$Aደ)\7SWz]3wU/lqM}Djԉ]C1pbn?3 Oښ[3΋BHp0lw8_΋~]'!+Y?U'?!f/ʻuk׸95/&["?qQ|v"!0E/ŏxgvɏUe=_$t&tMIKYb0rZs1KR#n[G>/l[c~$N-\vrՋ%}D ULc%Sgn>P}oٻP Ei)‚L$^e{Hy,1FcklʫPzr9N^Yq2Upy:up19uu{&QOvq_8ߜoN"379py0V"v9rE(D^X!# Qؓڞ4x;ʏ&Yy͸Ei[r|B#:r ue?LS[{]5Õ>VVlD7֛r*hl/,Fd|=7"Vb4ҁ EN- yoEe_7v-ePkZ:=#R#nYG1xcܚb±d?m|tݽcCc}U!!B,ҵg$Nf(#6?'a-V,]ሜ`,؟3jc7ֶq J3voYmH:_i-._+MX~x!'=[QO H\tDUR#nK$ݖ/6]c4Ì*nwQ,ܗnz`w"4?ۍ=ƠlJUK.`ͱeKeGckz4s,:aP?aT,g.6vszhKO]2cTT֢*{bnږ98^54~݈یݨ]x.wnv*ڃn ̭EmI}̞ _:Zvq_O‰uXwzdTC3v+2Mz]eœ]wnܒ3vE-=ES1C1 ؇wVM4%ϱ;nK=p6]Ɗ631WWحV5e`<#/vV߻{|u7ư5=bwEyg98w.+QYzǶ dak9|ed!/\-AY)oq-R2 ye>ÏsfdBpmEa>*x눋9= nvWa!JTfa Ӱik~  f>.قA#oШ $zfG=x]深7P:;9 W7+^QKa2 D.E&_"n}@N;X̒miے:Z]qѕW'iRI{_bEL$BԘ08p!z:ٱ;rẍ́#)O}˒q Xn'R<ӌ]X- 12+Y4{> SpQ }iPl|&XkGlD/'qXvHF0ma%W]/)w3v13l}_bpӱlj  %ױ9< hrtv7]$#k[_bPM )jێuf+hAc1imi#rrLZМs Fad[Z'鶤"ƾ}^$qIkbC:߫j d&=Ĺq2Γ,c`hl)ԱV ˨؜a<`||c& <]{Nl(2~8 S.rwyMVܞU(j{|=V̱\kXL`s2u{> 8=-ތ"azhIZ]`CmϥKѓlZ}=|*|S!Nl.E )Ԕ )} Od|H3"d@h*I$d 2@t]GbNl̅ 20q i d@G f]]]E~~>2@B _ dCc'/**Bccc@ dz{{܌bbÇK92@ *1 |Y*.Ě1c d}}}.ǫb⚺J3J d pUZ!1vd uԹrB :bƮ#12# ZfNcgN :bƮ#1a!2@a@3nXݻwcÆ ?u8teU1*wby&d` 1{bǎCsNg;eM۷om >tۈşx>6ޯz _:Ɂ0AU5$!6.粞ohlS?Nj 9p>߰-7TW1Ϝ8u7ۿxK X2<Ƌpl|[Ƴr4xۏL\NI-\Ffzglƒx޹Sx[=qI_}yfq.!jE{ .Np6y9iHZIw7vK3S⑚4t9y.Nŝ^8f&$rc ?'cɀ.ߝqs[\j9Ųŋ{z.n7{zzl7xj, oUi~*Ŭab~+=F֙ܭRj^V\E|d=xMxF3ihhlFӋؖh*TT=Cc3Td]BBB*_H;eO-/ysvGxֆ6tZ1蘱_-:KW6gs;f 8u% r+G"IzřŗXNzcϩF @}޽¥̺ijh-Qwf"R7ؗ%8?c1c˂w/k{_{m1>u9 xw WἾ-=t[>K'َ%и?6[)x1C}gm*o&f[NC<9C4؝KC+.66K#mr+W_:}>&r6WK='7ـ3v>(%1+|+Q\v᲌&wܽOmcݞx}mue}9^=KErbΞzOqtzSˑS-b샎v\)y=|؝xUJ\?,(nHVIX<\P{%1E?ct5zj9ehqmœSw~""L1^45Ghx7.!P-n755=y}׭x%igZ[xkdĝF46ƏLG.]ŲIY^,P^nyۉqKp?W({2N4GnH<1/Kܵ:$󸛹?XX].)/o샰GճHE|ydxXmENrڊq=cA.ilb?7cee6K..!g/@Q&Jm@t]7(`vhRLp}h]x&~F

ԝf],ˋkc  d πQᧁcՌݙ) cE j3@cء d@mh4vߨ !FP;N :bƮ#12% ZeNcgN :bƮ#1]ޜ2@cNcgN :bƮ#12% Ze`LGo?o^nqVzs d f`Lv3O>d1{!&/,V'ۣY0Bp*єc78^oK뱑&zF[1C]Sl 5xMrLOgY_phف ʲQ0"Th,?.(Xjª':t{47aC|o&$2kpzz̉APp(v, #%vFs591#b)omɝWͶ)a,!e~_MiH2v,[+؇fNVx}onE@t|Mo&S=sB)0vԶؖ\^{wҁT=򥃒RYrU:>ً#+N-ăb=|5 [McW#zF|'MtylR{iR]斫,|:~henjl/=&؏8WOZp!sΌ (|+/HrD6Ѵ]:()5Ls\ G()ꭦZ_ұRޤJz^r'!mAx IDATo`ގfH*"1L_woG–8EctaH|w_LǬo <> ھ_ǟ;ovbp az-ǗX>/ D`nDC`^ \dF],CP0LKL9y?auL$B >ol9붥 dQ;d50Ey9K3\?5E1 7|%!K={b;>?p[ +c8\@l4l525fdFpl^1e@ 07*!ᘻG+{3 +v,;?a6%e[:P΋t{uv77#ys$3rޛx%mzz^7ɞKrc϶:Zue7Jq`CwIo@7 n{т;ޛњ+ُtú:=B-B?kbZg IW#4t;_\4!fk#N2i+LrspFNG,hCF^)*+Kq;af'`ѹPr|%ؓx yEw+ -}Zjxm ^څP:\ie)["MA/Ep2v6pjp1N^Yq2Upy ̎C{ Y_VS0E{ Vs@f~r3QB4V^5%=\/g#n7iχO1K_W{!>|8éϝQ< mʿJ˩ؿȄ5+o2R\:ʈ g8IajD3.i 3v9*YFs)5}8: K S F,o3~\˔ȥnMl7aEǝ흹!ڄoS_EZop#6'?\hn-Z>}Xk¾!cwlW8kb9OqA]u Cfpa'c ewBa4jLc?eH1Nifkl2"V]I_Ih+޶<9~Ba(˛>Ѓ#}xr[V $#twnL}_^Tޮ{$lo>?\}n s$`x,6ęͨ]P庴>AJqc2ʜ3m^\â8fw渎4abGуغv1fF#rf f`_~N!Ρ`VsR@שxccaZ +!"FeGcc]H]3&h 0?l {;ã>˔ЈAqNң̍dnMsݦX=v(-K-7e[#q\`N+I=ƣϪpq T3bߕ4vY7?Gp'jhS =6zkJqscZ^n.[cc~ţ:*Lo=gcAe6tmyؗĄ͘ݠ~g7 !ƱGWc!MZ=uXuSQPY ]꿱iBJnUnDmƍnTǮiq<DJ]9{ɓ'.h~'kfA8/k.,e ܒ =._vG%+sH'ݶZZXK.`ͱevhS<^8!>$mb|u>MЙ8븄(ޛ>Pi7qNo}X)nnL{j?acn¨lOKmn!kꞌґM}TԠWCk5Lʄ9oq7 _:Lۻ:RrXo=KePmonBc\i6pQ3(G4%ϱ;E9Ec Y8.NV_.I],5~n]^*V2Fg1cŃ|۫\=|bviYmOj@U%RL}G˟2jf;rWYGo}H. %U3j6"d( >q;#A1cRFۑi\]yynMjꞌ5DE9nn$Cſ`0.4_1+y"%yW^ E1l)~Mkynِ`: %W2 V]>#Ձ+a]]ppa;qrbl:G/ʏAH,]ӏ;\;KEq.E(g{?PӎQO|ed!/\-AY,ߢpUevtK1CG)V.I˒nKY]jTñ-1YzuY&OxfF%>[/حV%m|Cv)o7ŊaA.j V1VRaaŸҖ4fΙxUs FD?HpؒvÖx_bPM )jێuv~=΅)8ƨl}vׂX9[.?)ӌ]X=/ !AA0Fӽx,c{EgU8NV+dt)X>y:0Xɵ[$l\(#`N>p<(xb \|2v8VC#ڥ<8˞$,5"PX1"6Ǝ rcL;w'uL],>'^_ ~Ƶ|1ط8ۯXes2"fTXyb|iVI11cq·ٹt1e$ k*GQ+P .ziX:^a’? &ϯ+rtL ' )VqmYolO [k"l"k8L2n Scƶsqf?&.:0^tww'X8[D 2@Fh*c 5 d sj.Wql`720id 2#h:Y΢!d`<ؙ2@ȀHy8 ;: d d=hC}h4vfd 2#h:s<2A32@fNcgN :bƮ#1EO,P2@ƃ;: d L 20ؙ2@ȀHLfѓ;>ԇ `NcgN :bƮ#1#98C#//~Ơ?@Y.~jj@c<:8a(EEEhjj>~??cׇ#sg>ՀNcĀ7664sOcMMlRLKOYU: e/G,Lۗ}b95w@Xw}ՀNc{ekkG70x5]̥Ru)/ہh@c=x;Ni* hԀbZ'~@cUbBSђaR@L`]A}JNk&~@EK\i윱M%𥮁 J:g3٩h?QjK¢}Ih4vUb`ʙ`+݅?>Š ϮK@4h0w|/<Jc-=5PoQxR^C]A}JN ݘP#Y5'01~Ȁ p:Z1&3rSZ?XmhZ?cSғ@49cW2}r6,r.$< xF3xMecЅۿDtAA1ae|qLEc美´i$!vubYgB%26Z``,}G>CRl+iu2ь7;MicPئ(BEY2EY+ʻ?ܧ}v E_%ZG@yi^Z0/QS݌#+aXdA<όY˰} 8e ~f#"Ch$+ۇw(<3̟P 7"1봾FV.] ) 6՚4vDp /3'ˆ^b]U8v:b=F(* p?p#>:E~n|27Fcݎ3ů?;NnC=7#]qO`0|}>vF~s8V̍Hk+D!4K?ˣBO7fr&Mn+ŽYF,yQ_x18{3,Acw-.A},݃.1șQqrL˰ ]T)K$d*q[؎-؇͕H޶EQ#bػ{okAMנLN&د/$%qpЙȘQ~|B#V`,Ǚ_ K|<A:ʾ\wWF;Yc;꘱rr2Ѽ8F/s{{YaoS9ߞG88'W nќKӼ.ŋ7$x!؃9+ōthHق__8O[pi^Z0/QSՍG fcB`_d ?RX %b1 ]Cqu->Ǐ=(< aK'KP} 9{ +8F"쌻<]n:ݬ@i[ތ@(x5|]WsTo\ME.# m:PVRgsb\c(i-{#k:"eWoF6y\5P0vqg (+H/K&o6BJ/S6a[9(?{e(9-Ly!oXVZ<&~&|o>A^\΀18!̚?<Ϣ= BK񶁯9mG"f-&Q`͂5`>mP܅o NbXf=65+0#<AAA EXXڱ8\G֏)[\[o?W ]$mK cnᘷG.z5|ۈ>-MWɸ;lAҦHO@QE#WW}c~+( s*Mwrx3V>4hH:?d@F6`1m? D;?)4u@4j@ Fk@]A} ;3v65Mf&$W4vuΦ'lz ;3v65Mf&$W4v{UBSl0b*h4vUbhԀ6l\U9ܳЧG$\by"~'@=z 9&sԠƮBb TUU&5Pόz ՀNc{ԢGPZZj3>hK|'f(MMM3oOu7#j0V;vo6 1k$Ͽ~vY_v'5} =՟FNcWiwvvݻwx-ܟ;j0G]A97ґ2@D1@c<3(hy^d yh4v; d d+26  S;: d Ĝ*(ə d@;: d d+26  S;: d Ĝ*(ə d@;: d 舁!cw?|cƀ 2e?ck QDa}IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_5.png0000644000175000017500000007713414062723141021733 0ustar00jonasjonasPNG  IHDRټ} IDATxwXTɺy9>'}ɳ8N0D2DQL sΘb@TTD$# T$J$ZMGkZUz_]UV3l3lذ 6aÆ6lX1@ ai3hL{Df= &FF056|@ 1@ 63;f?^gokl`qGi#1@ #Fp?|uaÆ)_O?t1@ CIŝMsƍ#AD=2PCBQ(N10`C/?+N8$b@{,6l!cnji%GjH b`0\Z~1Fu $1@ Ā 0->l,9P{g1@ 6/ b >~_6?"vvZQ4@k51N}vy8?&a'a8_%%߈b`Q6BA̕ŝ4LuMN®54W#Cxc?)"}vNNNN 1`#uetNI&_b`02`8zzslZɽk&0Q-Q68Hh0Zr"xe;r`c4r TZ";\I5'a7gu,.AȯeS 0Z"SrYĈpݝdZ(k->nF\CRPS]W9.gOy!N+p${uv P%B4=;"#~E4W lK,0jHجǝoHێIF4:"^Hqe|!xeIiz>l^q\%?C1g o: w|&~1sGMYWns 8(MJ PQ?N?,LFT}G$T/|?naom& PKib/9b$VŢ^ D}16,6s%#b 'bS?9F!q~)c0x"!3va2}2ǮD͸k+kW`$l[!Ƕs˘܁F#d:#a pvV}-LKE+x!Zύsuܳk8nǑ9Gs ?A8Z4kck b2 ޸[ 8 "Lq ZxYacCTĕ?ΞWN'ݧxUOC2$L8'xY@&ڒU^@@Ac@ٶon>r(x|mnFʁS[xVh׹o[XWS#~-_،q峎WaY07 !jJ 1~)&##U cRQg 4"$(F3x?aGܳr6"6j=+_%~h_drݏb>_(D՘(]Ů_q҅-o' {9gesspy$F,ArGr]xPPJT4ϯFݕ}h,`Eh~vNcFbh)Af)o(ݱԧbd;fb$q~#`8spvވ_ a/ʢ䛏62jWzO]Yj>"X};rnԌ6߶d<@C <3 eb=b=s!{m(Hf(E[^}?~{}j4{K66LQ?11쵺rGE1 a]~R]i{O#vtâ)Tnw6>P} q#qM?MJNk6pQZxb T϶j3R b=3~#EUd`4yM*$9Χ=Y  >&ثpL/ʱ鯘z1@ z SFiC  ; ;ԉb v= &?9ŝN  SO b@ aף`;b8 a'a:1@ Ā1@®G;S܉b@nab @'''61@ 10Pmb b`h2:e$ԙ1@ Ā0@®'YS(n1kHIةN 1G Q0uh$A cz1@ zOolz)S̈b !%Lȿ_zm[`}3& -[1v>|((_1@ !#mpڿ>Vb~X—pߩ:v{c]j w=V앨a8 AlFmOĒk)xHBwA/l>jU(Y˰""[m_wfa_ll6Aut76 m("}oP_g gGW[\hftUsy/6JR] u}>bA]g\LL$Xwe>7]oy;?v}.մ MogBroۛv[_Kp[tA;eZqf[#>;@z$ OCsL|S>20}8WTU;VMHށ /ʝ3<"hzՊ._7 +0bCWny_B>i0_>pfV6c@ka7[}ڇ˗W#Nrfe;k\!jYV5tuuHu@ڄQ5G]=4/,Z {Ɵ-EKg^Gwa~RV; }>\8l_vm}%X3<nJ8u[ Љ؈hRr\e``p \rx%EZN6Ftt6cvǧ18^ׁT9߇0|{N)gr M"ˇ0b>:;p+ >_Oo!'v_M]E2|E@Kd !@:x_u:A㯃obEJ5m{;j~!ZN!=o@mjc_ݱ!K} Z"3E"tvuل{hD`#:;hW6{(G]D bE,-`b^Q(T(i̺"I x"'r_Nĕ [>Gvs':DuXI7q"wr%)n~ή6܌*Ktmʶpz9MQr+~;E)FhSzNt#hnĦ1Gف 3 mu0{=Yf !x{5paC{G62_+ Oϱzk.}Wa 7ä[XT'I1Ow)MhmQ"/Ro ’l} /GB+?u xp[rOŪd$v~a/Q!O YJۊ{uͣ 5O1!-8T]y_^{sFRBVN=;سOַ# u)-T!N  YK^ob?ڟ 1W~"G>NB c@ka߽gZڸyF31mbns]9Askrx_Wg*n0Fq_ vn=kݑ錪2GtQ :ZB o υuרyu>]|h/ Ƌ8G:#%[cRs3v|%(Ҩ|=|=[8$^@{FpOim8}f#$_cS6g~).5Ep Z;,1NP]y_>ߞ-+/dM{o4F>E]g'#gI> j֧7qMޖ?J<9OJ1WhvƬ;Ş|Ac}]`۪ {qal} IQ獚 g^ ;EtL!μ~4{;ؿ\66TO~uw;um`Wtu9Yh)GY.W^wcv^>Hܦ0XO/OEce+2cSUs MF!{#Zeەgk#}tt R-mhˆ/V^k$0O1WeC4gE>>].>1!; 困pF,XbXz;ֈ%kcj6@\(-a+%mMF}D- 墓/aMq+7<9d7^<1&vWpR]lZ90+ 2}.NebUF®q9r9~U,Eװ{B~ؕynV:eano aˣ/S(DiZ"kmWޗޅ}{Vr5{6 ;:pY4> /dM ]1 u'Ҿm;  \ًp!h-fz \ ܑh{Уw硴"J"&:62a0^N<(F*=b<ԁyI8xȌ/ 0UNb^[t K1Eg=5-GѦ\}J?߯kc[Mau0 jo= ԁ{wz,|$D{ X$ᶰwn]lgf)@W{fXkׁvMlN1j9U'WصaEaڗg2Y3s{ Y2L[ 9K>;y{O[ =_67$$8#:+9ajA(m+juvr2ߘ6_ˍjkƱk* ^oFO\9d)02tog#N<!a?{hĊʳUUWm䵱 ڀ6g;(E:Խli=^W ac/_oѥhjChYY5LO塳seXS#vl8-gNT}1WN VcD9!?d h-;w[ފs-s[L9zs!<ݤC’j;s /:#>--TuӸiw6^S75mg ĩkFFFK'ՉUYqoڄ8ʞoWbXO #4>Kp|H~6nCWIYx#!1yCP *qn>/XnQ9q+UmůYy~;. 鈝Wz :rS= ϖ"=0^I6+W]y_.U ԁv!R/>SKFv:QZXe NM-ӄz[`|BK8/_5UN;Vd~Q'2m۷C C s[& `7}>lSuM*쓍եs!ӱk :!:9 +9HLthgOw }ܵEѺ<߮e^a.Rr`5/OV[߭OGu(;!EYu=ctz3!M¬ٓKgpSges6XMHcŠԘ1@ }UxYZRn]Ydٗ ψ+7a4SH2|f_uTb "Z QRRmӵ^yQV'o[[n[he1[bPfjLb ˀ¾$( M^]Y=~Ů?y_#_uTb "Z {`b#??ߣj ;zgslc62[bPfjLb ˀǏs8;TH]\Zf_uTb "Z  mʂ~ݛr.-_ju(lƄ b h-CVVt-e]|2ۘ:JE 10 ao IDATEa5+iŘqnt/_ sYy%k?_:]TZʃ8"Z"##(,,q+..KcPQ^ׯ_s?Z]]ֲߝÛ7oOώaY KK_q<7fͫ;L9_ SieC4Fح`5'g1z{2o᫯mpXUX}(ta Na>K/bn%BʔI1Z}gFjjRSS%{[Z4H哦Zscj WkY.X؋F0[..̄}SxED:~\)?{!)7|iJu,_9?Z÷Op8W"/?Ƨ.HrU\#0"0Lkl}Skb ZgΜvQrr {ܒ9i:.md]+ޒ1ɖ$Γ˷;}d;;Ϯe׫٤ioKpoK5h|"߮/1<{ryƭap$g=; k\5W1";1~^!y,o&3oPcV] -l𼘋_WJ;L؋tH|XL/ 5?)b#?b`ZاZ "x/ qKLd dśv}{\>d5γݟHKldj3T8EcXߗJGݧ!vgc05fwym8\|L.4Ȏ)3u=.SM|Yw` l.+/T% bf] >Gd |.H"ޯTg1@ s6z"x{o'\ wav<^Oa*'= ;R`bLt^/ Ø}eq/UQ9 v-⓿/1-2W pFɃWXxᛆޝY|6,dRסP1辞= ށ]n0w\LbҰZ޻_÷g3 ~]qFl?Fp; gT N|>+q8VbѺTH4zݭ %?}`ʁx|;70@.ƁT7?eRԗD\'.؄ydcy YH؇n쨃D#! b]]3ES1v5NΈLIQ,)11@NN=^b =b]J~i$F Ā*$$S'b#H(sUO'11@NN=ub =b]J~i$F Ā*$$S'b#H(sUO'11@NN=ub =b]J~i$F Ā**F> b .Æ ;'@ b S:t1@ z zCMĀ. a'a:1@ Ā1@®GEO1@ Cvv1@ $zLe^6ŏG `z1@ z S==ʃF 1@ mHIةN 1G Q0={?1@ vv1@ $zL](110 a'a:1@ Ā1@®G^eS(~1 HIةN 1G Q0uӣ0hyK6;bΉ| P iOk;+=o.kLߙ& '[@1~1֩ sh fLDNp2%CP`*v&ר '/xj +/\[1~xL44l CVybzv}rP:MĀЙ js&`3~nD֕[6]s07^EQ qN8ݤ;ЗFaAw/R~zb#"^Çh Ē710Б7!etL4ӏ;t' >+qAlikCI^өrJ#l@NftDZ`]TW; 3-abl5S#; 0y VKӂ}^0wdlIlso *aj4p.DbL'X +ON ב?>.S`jhIS&nG(Ҽx|t UFDjgt v;N$n Jp}w \L``<sbQS"v%W(7  L6.|p#s7NbuLD=W"ARcd|*}"Q3D.J@oKPG.πn1+ !ga2/nMAZb͙kF f FzZt"s22⋽VnD"4?> gؓAN6Ɯy:gLh EiL kCR9BV^+}_EbdlIvǥ XWQZD|ɴkxf;Yg*><Z#],) m1E,r"*o`).I )Dv߿ڊ/B$?V[)vU1X`fad2/Ra_&4U8̛ 5c*ؕGa],lCg3 {oBGU5}}g@7^tFFX)Lǩi570n׃MHC.wēFiDp*&vn?~ݽ a7q&P`xC]R]P~pܒwb~8^$#.PG}ͻWTx!nHf xOdrvk◞]$Ad,#Q-t&.,kAѝ)fb+S8mMQr{ ;C|(֠7aׄ˦axX-d~;1.ǦCp0ǁ: /ٮRHԕfḦ!K'yK{jPԈ. ML6V,v} {_i8x (J ]`2=4r4?AaaV:t~Z 9 k0a *835>CY-)+3N:#}Ga :6#|a}g{m)t3nDڣ|<& eIԋkċDM@Fv=y f;ױ+n(=!y4:mcL4쳿| +:\IA{폧ѦXSb@7.|uh> wT7j8Z(-.; 7 m l~,43TTL/ 'L&u4W,&\^f nv.MŊ\/a/=  ؙb5~bS,u.$nar® /ҩxsjBz{دC&NC<0f*j!ݳ8-hVgxx= qx+DPkkHCEػAvEqܝH~,w076ĭ$ܻGa"7jl\Ɓ0Mċ։~#V;MKI.\CfN.e;AI;=ah숕G 1-) 8{>Jзau?7nx|7EFtt߂֘b#5 {_®_+|`dm+?.7lԇQxj΄=½ =Vl̈́]Snj ~CGIQ})0rXsiHUr>5Ǖ㢼$"A-n8bvE& =\ˉ&sM߲@i:v Cݶv0b?Pc:y55VH< ϩu7Sy/zOS46hl'Ո̖k[l D'6^GȮ)K&ue®Z)x&Q*oa/j򕷕_v&t)IThN 00k{7, {$_SlBꙍub{򢡰DBTdE d4X3WԽ )»\uaj0ƓnFGqe)+ HA}" Ɩp IxWۙr;F֕o ; ;U} MjU{Y·< Ңi+1Dq!:SPGPqh‹'9u^EүQS-|:7!2` ]wQGd#b`1@®/r#p/T6?z{g>m,`-|nq RcN4H "Hة"PE b#H(bI31@ 4b'1c1@ ةwG=|b =b]O=NA1?HIةN 1G Q0wۿ-F  ; ;ԉb v= >8^hE @ a'a:1@ Ā1@®Gmz71@ $$S'b#Huf 55)))zd ==c1Ov ;k322PQQ?U׫@G>-񫬬Dff&H?lCHBD'Hu((//@254b*b c&//F:HӽPC[Huؘw׳2 -`V^JO >1@ c]Ξӿ=0Xt0 ;1jb@[HI{Wb%aWPvjm)=1$$fw, ;5=5tЖvvJ3@GI;omoJO ; z$KNtO4'6e] Qv%a[ƛ3=13a[ؙ\fu/'/xjGXLz zGSCK) `0n<YPAqN8ݤp}@E®^꣰WWWbS?0#a>O8 !vOq sLao4[d qWa%Lrj *aj4p.DbL'X +ON בF5${/WHT" L &MŚg]g#Py ,mDV];Z`4 {C{aoG_|dk>:WW;~`:T~: !.?/-~s΋iGp,u7;m|ۻ%|O}vw' {|C hǀv0?؝0:"X 1+7bkeQaoDo <~x;` ?~OSĽLC&{bٛHNM~d` v 5/bgt$dU7* ~6ؖX #0y O ~' ѳͰZy*3'cKBD'asm NσU0қ%41y_s70A^iL kS,,/-<OGеo2a%?b䢻hhy} !nO o6w+ě'?bܱ߳U3K /$#As#W;b@ '% "4c@7ޒ͎&u4WHMPl0~!NK5rdXf,43ϡNUfdݗ jha9e T$'~ HӰz1u?nx|7EFdt-jAjkLn{=vz+\AbZRq}4? KE&d Int.Nxnxܱ7rTC>0ŸD=E IDATpt* eG'淿o7 gHMţrT|0w PUf7\yӁ0ӿrol}k:~ZvmO#:6;;]٢w c ^7ޔ%$[UM0=Ri-?a4q"-0kuTse+x,Sn^)rWV~u79P& * ur<-(@t˽o_DiE3$F-N^ &P1$Ly9\@ um!闰BPS]ܲl!a'AѶ@2@®T{_ a[nu%}wd c/<1@ h ; { IL-$`k`SDHIu(}g5t0BN & 6!N4ao5a$vj5i) q $$:Lb:l!a['0@N޷0`d ;5ؚ4ؔ8фvvvY &1LSIMiM aסUW1`b c&55^wa]Ԥ4$ ^k|σELŖR $:vÀL~'1 fb v= &ViB 1@NN=ub =b]I=u1@ SO7({M^$@cu%ZԤQ_YZuvs}/C"q2@®h Ǟ@_`ɴxҋob;\{ٖ,+/x*i4~Ta;5._il]nڇ[v]6 Xn3r?6"(G"{,| eo !q5Iy T4=~wZ!^]]K33I+Pu ξ5]`m9!q-g ,8bv)t[򕋇_ Ȗ9cz#@  pd$!=>_GX[.>X2 1/_#aE4;>zjN+]!_6/47*{곶#aee֕] { 2/gM%a/<0J _"Y< 森5ҏ'OyBk $>*FIMY=)[aVaB35 H sskl]'w\RZұ+P[]gYױa-9a9Oƕ8o\W\Fd/b?,c'Ր&8IRل(S_$ƍM>bK,R^=nwŬPYG=-Uj !+_A/N|fyvp0< ׋Dv[`?e \|NWcqxLY N>+pAf 1>m S,Uȶ>naS!.m),;Pù7*||DĀzHup%L0l}q<9_p_hgbܬhDss DMƳȾnp]Z-$<$[V ղ9oM<@ّVx8K/;"Q/y*\{gc8"=`r=e9a) ;6'`ƞT4}磢jP28;`k|5Jv)r͸܌5= ma4z0v"Z]ؘ]8qg<GtDz?ZЉ(+Ѕ*YZ$|J.Bys;[ɵK1Z}g}#o*_jm>$Z8KT]a_d%y^07xS sA"|.u.ˬXDEƾpqAȵ" Qpuy84"i<FaH<:-:r\xHBc}6N<8<^R3 h v9,]GEočNX^+g7wy{}er+eU;/ą%{ƌpvو I 7o.9{m~e̝PǐLyk5f϶x<#\Q@b< b  j zB%~544ωY}Dɫr>K4B.`L&Oo4CԜnؕPgfu^zS_^B}9\}yܔW.;MMo^*"^b3g",ͨ^^<n:+f8RTV %<WߐX|0*QYVBֱQZ6%q͐akK3*46AMpvߎ4UΕ}Ar+qB! {Qh$H#lz` K[xoō:q͞s`k9n VxYEm&[;"d67UPx TJͪ <-bcȬv c7b2[m=#fx%1R;4 {i8|lW!Qh6nl9[H&/=Yܳu33 L9Kwjܬw7Opf8ZO5\%7qlV^|A- UϿ9hc s)prhMA xcVJSPc-o[o.RPjd[y[;` sɰs_}KWgޙGuTj_ԩ$UNsS9;"6g@ 4<6`1`cc666f0xm0Ƙ H fl!Ybܭ弩ՒPү.Yk:tf tbwr" j ?cDžĎ9$ 2 I1tH@숝 00`ݠfFx+0ĎI001 j&ۜM/% @ vn .F B2PPPcb&1 2m5AL---xw=}=kg,//ѣG}/p{&lH@6*--#MkW-c)..fndc) v'BW-2F薱455:,oʤ@0з vnS_pL$vk,;L};Ro wmb؃#S{;bn^/,b!S@=iz^"G߳{pҽ=xa{ObGMK"EH>e׋~ M($3_7g*6)=SאqdJk[2O'h}$zd3Dז+ə]Wb!Sp=wE%j޲}^ٱE?ߒSM3?eX!ePwՀ؃5Qoq5;R1E*gbVъJ_Q=iv*eF'͟v_J&e(&*Z Cr֫64S_eh{;N [7w?пtr` ZŞ; 60 WUje.ڧj?iw"kϯ1CEn=fd^8{FEү|SO34rI5h6Cmm|w^2e<=փ+ޟ:%bD]\+_k\lf[[5)>{nWW^ܥ3iP4m^mW|!1ZU@7ފ]jnj=4{?W/bWĎ؃qݘwx2^_٤b_G6jZ"St{% XZ0wR?eQ#Aۊؙ'gHp*{H5~ԏUxN啭Bo>;F*&{δ_ԕo\G/xU| UgitBҧ,{-Z,-Ͽ;_!}=:zNUy\6_oIV?U4{ă3![|2j[oHQ jR5~>Vn-8*+ڬG*->Zq)ʚ4_^Kk_i9}&*.ES^זҷ>jfn҃ozRo]2m|k, 8 No0zƛ֡R_I|d bqpJKKwpL!e,7oԹs"2WZɽE6N&***݂W-2uحzYY#=555}ɓ?52FĚ5Y[MQP XX us'XIon& @;s`` AlOk|% e#v: A vIB {v;bw&{fE^<]{Aٱwl \KUY_挜k幻K3fꇒbܯumӭǻLcGNT׼y_6)Y#4o yZ;B![sSWJg_[S~S-yn 9T{]Ƚm[Cd;0]]bMgD&}Z:p~;SoOUEgo,M[[oӖ7*9mg]v Ib9ճ')w`k7²V'+))IIzq6UWh鬱JO >D._Ոv۠ 5!eh]^&gρvmx{F \Gݬ{ OBBWi^[d{w:[7,[SFZ2Fi՞  \ک%y4bPN]?v mMh@{b s'y- 5TZrY׿c9e3fkgYjjjUiPC-8BϭحKnw4j,mxl'7޾w4lJ׵Ыzh>Q z][w4''SKmiuYo=F)Z 7|}{DWJn4z mꤖTg״w eL[q3;spD/:1,twqX7^jo{CUG?VNbTIS/e*&]hrJtwg?`1-ъU]pUffmڛkF,UZ6~V xlP+N5}x͜pIu~U~*l|ffvk\PbSU4ox?\6ݔOt;[- + [ρ%ZY u:t&~Wyʞw޾bV_Ѷe/ilp M<m?NW{r{(;֧OV-ҒS4-U)'답Ny.x~bܺSZ::SwUtjL-ӡi|եN=e2.r|ݫ?/ȱ ujcpݫIVڱ U ^ʞY{>U]C{]۾>(1{ՑNEc%~7=bS 5:T ?KŅn4z[Gy|bC|DWKnԎ=|Ҭ.œl-Jo]ՅkwαOh=~f9Zgy7ԜұlρZ5h_՚25m^]y^߽5V9n *ޫ^Ӱ]UWO ?kNfrr&iCudE]_3FkRçc7U^7=LEZ2&KWхrߺ;”=;73bYy9D)i5v(*FFIDATuq^G1Ԫx7bRdŵ'}+/Ҽj]N𭝸olZzz- Ʒ꫻NXG O՛S5$U)C;^x=xkJaJ>&r{{R/))Aa<$hܹsaI=}#'(]'l~vwvXzzG7zu]Wpm=@=3vlo[)&FCzV@f遽 ow=y;푊`$=ݑe&#vnH'atgHnӤ>PVW3FAwK&p;+v@*H? ;cD*} v&uVW6Lh}?zНҟ$\!vΊ&T*7j끓m$1=h+{L7U7IM@Y>C0F*ݝ/jDM􌚺0dB3MI^m(?{u{}9.#vZƽ-4^׮ҳ#ӕؤt6E404IGn=߶bO]|Wh|Fbc9m:w~UGSblbkōXWj=7%oKێz=Tŏ(.ejz  ~l;z)ΕsħiLJ'KG^_׆*택y2~+:PxJŧoëʈMӢZ&/ӡTppޛ:T1 tښ<:T'Ңv1ܵGg4^GWNP:ּX!Uֆي]ӵVmЛj'F }RR'}s!W÷3'byj|!+٣NQ|hԦ0FֶWDeB:Sڨ{(!yWkdL۶oyu,%=W_(~>k<Wwapޭ|GMOLۿT2[I))Jy_އjiIҴu]@&539^37]xα#@Q}b~&.jЍ_hQL MQ#!`OHТ_jUf*]ϔXƶb5b&Įt]|QgӆEtUVy$*>9I1f֩bY׺Z[UWاJ-MeZ7-ٷ_44Wkۼ bզ QZw'-kԟLZ}{đ. dnk^Zo2??q7\٤ ɚ{++1 ]a^%$譽jB7Q띟.ڵk~nb>+P|Jv4=}Bo}P^} 滐I׾+4H)7Iү@l5U+߮E'tFj]Fd-sJY [Fݳg}5=E}[tn{ب('ih3uq*:$(7)5V\R3zw +tpMJS\tS3;>NPԬ4[YԱJ s;) z~AMZb A1qJIՔwEmG0NjXuqL%$WT_:ڃ'x}S|wKԈhc=V7uk\> #Zەid%LY[`zT:;;DqQձiՁ0. i$=@6Mꁫ7x+zǤ?T ? =aOHz;+v@*NLhTn{|$biRg>V=G*HmRx" Ab&=iq# WݦI=poTۤD";bgnLh}?z0GM:+B9RA*nBOT~?@숝M 0FAOÌIB6Mꁫ7x+ywrgaNz`{ZFGĎÞ< -C˹sTRRm67nЙ3gf'#z`_-#{=@=;wTTT{}}?>PgPnݺ%km3\AeDz^Þ=bGaONe{ǣ YFKLֹ1nUrip١;5 Vpi;bG62`%*ݻwOwf Y۷=~v=bqRoXP5u.00 vʈwPjLae#v00`ݠf؞ bG$u@5m^` \;b' b7:g%01;I`` A$y)=ĎI001Hm?|;jwԀ`3' (bIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_6.png0000644000175000017500000010724714062723141021733 0ustar00jonasjonasPNG  IHDRټ} IDATxw\Tͺy|;>>̜s;q5qdT"̢0 s0 $ @̑:HͧVwӫWƦy|+U=<ߪ_UZ̀ˀs0`L b063fZ4[]nʏC>vvb 3ai3JgoM? A?@ b0# i?~믓0@Cհa`7S b X w65/ :݌zdTF8Qb`nOhNpdqH Ā p?rȑ;|F>#b0+WbA b0L4ށ31@ Ā91㕿5@nN![ b06﻽Vw"jODX=󓣾Gx7HcF|vMWMN%pӵJJ~# ۿ@ 1;gׄ96hhFI$ztou4NIII؉b z6RKm w^&{7L&_Ā92`3{u giR6vv@1&uW 7vUh|I?2m k!Q0Tbc9Rbo4pFLDTp8 lƒz=?Y7Go A\NOK^"b0 #-T.Cuؑ[I;6#Bx̱uƴrK~$2HbA[?dHC(H8( _7GiPR=xn̳]+TjF?  !n8r3XYIW{amb9dJ.ؙ[͍ƟE/ E/. ACgLn>h`xGA}=mCFK:wV"vx=p ^I>&a ܂ח0mb~OqeW2|ѐb' qZ:u9Xx^7Ťyx4^Ą\J'a4L;\=+g#rnCFCAr[ąEiX97_EiR҆GܾD&ǤհRbc\dtYk瞡3y&R+ vJn\cY:#VyC!6٠R̗tbcJOlgL^wcRWHy7atM?BLYM' T>h8F=ALLx_^#NQb]' ZRI1|c&VPzg _LNb/4p lBr Yν7yHe4`7]yqM?bMk O?Pӏ`Fw4t sobY[-j$h+dz>F-)3ަs j)$`0ϼ.DGe޻CV+ U|4>@Wԡ$^J^C{S3Cpgb _ p1* ;Al0Eג1̀1՘$$$1@ 5.G8W᲼~[lgȫm*1r0Χ=Y  U8&tU>#a'a:1@ Ā1@nA;zb SO b a`R)wb3@NN=ub  b݂w1? a'a:1@ Ā1@nAyS)1gCؿ_ b @gee61@ 1з?? 1@ @duHة3C9b  aBI=ٳQ܈b a'a^:1@ Ā1@nA^G# b $$S'b 9m}7E=`1@ ĀS΄oKa%#n˶Mߌ ERE$b0w0·{ZLXKbnub)&^0l|M+{%}v.8LX|%$4]g|g3{ag#+cu< ˣ`s#p(e:$+s\0&Yɍ]rLJH"_zj_o)3a&J㖘h„z5H'6}F޾\~a%:oLŕ[p/;W姐rr98ܶ_/:-2=ōmjCs'<'ojyoLHK༰ƕ*x 'aӺ|[$a7ƿ$$Ā)0kagarvr oeBy3QW]e8sKk,BksmO4i}26/z;@o 'aM߮sba ]Z_wi.fVx|I PkiJxs#i>{zFg`yQvjB{Fc}$o3 %4Y&sΫmYn:͙n~ -NbփطI 6v5SixQ7,B [UͲKE{_MBr lZ یH ; m(JO"ہ;8%uMC& .X/$- K0o{tyV)x⃬-Ky ѱWl$?ĥx,oKKxe{b:lQӹMVֶzmU~|wHļES;veԡ0-‡V$E_ğ w;|q"8 k[0n5D>\y'?,¸mX+d'+^S *˳I}3>~kY J[,sm+Bi_UXcha_y?dž9ju:.VEVK mhWbFu߄eؽ2OyǸڛ}Y) 1iTqw2w9N Gh|FY69n+8rT%t!6xmRwP^w w\V WYdHutlJ`k莝Jm!®yI+BDA*fIbfr[.# Xp_H-ho{'Ci^%q2ޠD&+Bvi_9} -T&EK[+Z;ֆ6էv';u5hi.bo'r6%ʸ曐7"#m.ΘEt_'dM+ZjJa!,ڎS$lם_vZbz-w ϳt>˲D״BVVs/q1Q|B;_~ywWmx|㺲LוYHojÇLU)ȿF>@k%*;?p'o4/3,oCUaNhX-mFSc _ mڙωU>kkJ? jIko7棠TԶ4oԠM,~SiGxs?Ȋ~;cha߹kgmԅ?-/6f/e<'sW`:o%M A`ܢp \[> M)b:q) @ #u6Uc+ld6Iz!]OWUyB΄]*'1hcT阎Y?_Á-4/p6EbTb|bao­llՌ7sWv&F*Á8Flۥ m˪~v]Z[zᒺ_6}~U>2McIO˜ka}}e'& t[le0o,ZKl[db[f+6~[~#ոuk"^1aD˧ qdMY y||lFj<ٿ\x\)m⦂U?;M]bو1eE(ނs|ͦq}3Z?!:"{h1H ·כ7(_ae/ `ۖYcl~hmvC>55"|YxS{~ dhz N|*6̹E!辜Ƕ}~Y;yC z5b7ȿ wt {[#,\HZ6IJ1Qnq`SZ#":ũH #(?j ̹ mp;#*SBa7& HE+1[HkaDp$+Ƈ:uZQy,0PVh\;}qz=' hǡYv5ܱFF럤 7 _W!h|ˍ=E~g1V#ڤ8_v>o[ȫ "%0>mXI8`X>Bڍv#oM}-M5{ی72^B"0V}~z;ޡqQѲP )Lوr\$hJNE ,5p:!Ʃx1^' V1?jw@¹<gO[-c&>'TCe/DLص 5 wPW}'plxTI} D@.Ik*@^Cކ>­}k5ja~,WR^5g&<,~' _BO_þwho"v16\+@h#$WF'1|h+#"r\"77(tʢ/v#xJoٕ6}>r=숝hu ?-NDd=Ʋ^hpR׊n`O#ϛ_U' /> ~ag aZ8ʰ7D A< +}q/:N  -[nT*T&ᄝ wZ=&̓Ĺp1:aF)}uNGHD3g Oܾ{w=Pp<q><=+6cpG|8]epRIk;ZCd߃{k/4cm[5S[ {[w; +F^lL ?V#-'TӣAv 5ֆZ\1Zb WC ^^ƚoM .cf6rU;(okc { +v cW1..߇gPQ5>1H(~wL߿,CH~9IUwMv$FOڊZ c.N}i1k)/ha߼ hh焝 8&fbԸ>pgi_w6r_qֵޓb6n8y/x%O\䅘/DCed46AC}%}1(d35&1@ ]eha߸qjjjF(E{:\a'ۦc ==c'nBagMMy1u/Ѝ>捭;m7·Cئ XgU}Tb "F QYYmcpΉ{&m2NnqQE71aWKֺv$xgO?Q>pa/lƄ b -kCC|r I= VzFaq]>׫gi4tYXu$-1lc>T1@ }}5x 9{ND ygf.AW/xP-'gys6=PlA!1!b*F +K|r1*[Wё~gy6f#R1@ Ee!!(++6^( &mt|}־b 2`/^O>6HwP =~Ů+iQlc>T1@ }=8x=*BQQQG* ;zgsd1}1(d35&1@ ]eha Zazw\x/lc>T1@ }}޼y(((6_YUWlA!1!b*F 9spmܾ}GG*QOfU}Tb + IDAT"$FvU|L/qy%XaBt)\(Ͼk/K69oF-*1M $"+~8=]Włp>[-=̔@F9S֍|#1xCL>}^W^!ʼnp? Sc[@d&,+ ˀ>kl!/>}s+--ųg c^~oݻwO~娨`;_*|!߬̈́Ϲ.1?KKQ]~˲xkc݊ -$J~:=Ş03pfn޼dl991.tj[qXоZbXRt~ a/hK"(Y,-wvT0[/Ky {@Fn|<\Dz3w042*ևp&c9a-i2&Hʎ@P l?X)(a?A;cr؇_0lPu:{_|HlL!!QH']|B dhawQQHDFF-=˄}Wl{\:=γ:Q]ldSƛ.s+_uJص/8?ב矗\']{C=w^T!īu{$Zci*$L؃1&Tst'J{ A@Nٱȿ#/u%&,>gcRL^x ~UA/b0F 0zBL1l2< 48-2'Uە" FaJl)\6qO&,N˒D=CW*Fܝ 1!V8w=,7_Agݚy:D:tt|K%c0Z؍I\Q+apuƮa׊ѻNᇭy,?~̈́}BcدoCkl_բ!ŅM˞: q4F{XؕuX&/!NUeM&Uae!NaK}vqS""NcGiQF}jt+&ty)*U?Git %py,,-SՊD8qJp я_V_K8^ e#•Eqyyܹ"M~F1_ ߓp?Jy ΅$dn%\/`؁O:R4 oj(}i) ;oۤﭼ)wMŏG F0`ras푍gP;#*TH|ÀɅ: Tbjb a^* .1@ Ā1@nA4$F#b zvv1@ X$L NoL~&sfz1@  Ӝ{dpbHIةN 1`A [P07;a3 ̙vv1@ X$LsAm4!bw a'a:1@ Ā1%mb b20`r9h#1@ }vP b a`R/o)~?b HIةN 1`A [P0{GiЈ  ; ;ԉb ,v &v/G#`z1@  'zzb SO b a`R/o)~?b HIةN 1`A [P0{GiЈ  ; ;ԉb ,v &v/G#`z1@  'zzb SO b077 q&Gb>v=n0|svo!-G^M*TWM {Vbb躗|Lȍڋ#%'`?㞯S$¾wn\Ccv΂ ;b|Ht}0y躷IG :ΌIt =|BQ-ު.Y=4gl14M]6w7b+v}wNSgI~G[<6;n"3A#0tPfNJ?bH1PMը6#sN<ވ[asQp빲򏾘IJp*dB"Cesw0]s+v=)}م}ĝx-N~^d#c5FUS5ު8='|uǐl b+vR(l|Y`~T'+N/VlZUq[0VuQ9r8fG9'`ɾT)8 Ӽ]`km A)T5 |h%x<½rѪb6a2qTkL VW*zGxW {X\`c kDZXsdr=ee~uk cх -w,uGXoxZstUuHZITņ\%5.E_<S]#r}qcPexWF`mޏӼlg ѾXq,R%ciK#Z;#=qv _e:*PTeONuRz=N&=p'6lQO+O-9 YdM\t\}NaW62"p#;Y))p4ǮANr EHɾH:6.K|WU#5~)\HDff {/ɩPEQ]3Gpn-TYTSCqL%)>ng#1kYc3Q\9pk4:bÙIhA^n !fRqE0v^Xqqi38`]2KqaW:վe]Ƶl_–s&:/V._2?!Ur+cPXSu_WL NIiȽ#a#Lnf"v,8Zco=}BɉGv^pC ux3|76L*i/ywuX6gBNuVw"QYkq^ldԍ8 b#]F3O1},INu+`FUZ#ʛU ٹ Rx6w'FیÁʑ8҉4,iE ]0#8b8MbvV}WV.zPY*f)ݘ)lM:#F4*?1<',ZךL, cG"5Gv|h|'0>|㗨P{U5̱Gu& (I9% ή` ͙]ވm`;a'eZkpXYFYc0,{'80^Y/س{X8p1i~;4 ;/o]Axz`_:mU6w~hy1Y 1GeKe܂spjPfy܌v9>Ư|$+gc&TF/v=Ŝՠ+?L+c7`F<*ĽlkmC ڢG;5r9dooh(D{'nANa`^a:v pqņSV7md5(>s#(^iUn5aW;fㅃw:vc&`w~IĄsr]1hdxN 0ry|`ϥeesmw1]_܌롰J%ߝ82[;+2>|MuXސ__L.lzV_‰:ځqn"a/F. 5ѹ?I2A`O5W;d1mXŪAɰ|6Ep}ޏV$#Vuʦv:wW0s#tp2rrn>GFQq̭y o |@(9yI=>uBt%pӴ,LVŠo|h eMƭ{Px; w\agSszw>@&C޾isƳɊsasU $zy v ^VsM(wר#R΄tج¬Gv_X= joO lKpp l!42YȈ/Ūxq0ȷ[]o~d1aMd!+ Uk^Xا/M5tC鷾5aWܹrLumE#Rv a$ll0@z*H̸+AippqoJ\.fo苛[h\}®??Ed/6\YH(sB{Y9 eNUf%fD†;ЯI; `5<`&,Ty1@ ?&Éu;9z<.MHGK-n6֪~bD Ā6&:0qg?F{ j M;hwͅgq:熩Kv!T[0y~!SM IL&CccD"!A!"b ΀Ʉw1@ ]e]|b ,vvzF 1`A [P0my=o)Ŕ ez1@  ^]O#b ,vv1@ X$Ly[^ϛbJ1%c a'a:1@ Ā1@nA4WGH cz1@  zޖRLbXHIةN 1`A [P04 baW1zc-84c?I;G`YXS)1@  {]e ;n>V{c7oY%>w;1@ ÀɄ [C5mvm'qX@YgT+t]-w,ڳ!u 7S)-Gn^I,lJ_#aJlP_|NuѸHɄ5wL+jgIjPv'9+՞6y9`"Ep;&홨eJ`#&·D=Ƥ/^:/zӡ36V G>jSCxS,Mc5>v_LwG?;ƤەkP_u%oeI11nEu ;w}#^?~`iu q+f݆|cΉǦCBaT DC{5Igވ[asQp8銽1>o .;7/ O~gQ\4ba {qێ ۴q-DZqۧ8iBv)HÛW`Ok̅پP;VxDHooKH|wt]77겳+eӗo7}hS:g3dag+7hzZ|!{DX9LJrȥz`9s5b%Hsg\W HQ ޣace d_*Jd|FCUn6XtC Wwc({X۹`٘0V=/Y l4Oge!R|kw=ceU K"nk&zlG`mޏӼlg ѾXq,RUZU7#a la7b,"^+g/6<Cws#<Bp+xQCW:=m1u|Tw0 bI<`;ix"ظ cc]6SD%ϣ1;rj!Ugn*6$ub-ܝakc8~r}>Ů+9v#o*.S &_L"[<õFUF©j;6 Gs}gʊRo!/+w_4B.]3Gpn-5ȩB.()ٷQXxIC0aq3Q\}7zWY6N'!;.Z/{hi>7o(: v$"wxd=f \ *7֗L;½JҧK##n^D-3 Ⱥh펽yS^ªōK1뒫ҾqaW:bC:7 p:rsRq24~8ZPڼ+|]Ƶl_– dCZ8KƪrY.98协5]ͺ+zLMȆGv^pCuDeߢAVl ݥi4x.}nS'''{uKž8-agaEj,=ɶXtUŠJZ#ʛєU ٹ ')GSL4>,9xs'ʅShq8p[d6Cuk:CFDԙ kU lwrlFBnr^c0#hW4,vԃW1N!FU>E+HAfXS1zybq{5{-?娿>c+(6v I5X>!(WY"5GĪgZ)XBO4}=ř6k$~dA-fSl]}T<XrXg5Z[ Y~M IDATp+kب6k f%Xz (I9% ή` ͙ twac(ܗ1~l#ZVwRyY v(lbixzury#n͸xAW+a΋+-9^9s7WҏLgTI I=n3=׸M:[Ijұ>ᙨFO!p!>}JRHLwB~ 6vNuy5868p'J/AIyl?VΘqG؂b=7 #v9v.>*Ӑb|w*+k}pA^EC؍Vcc{Qeoo (m8:G»*$ȅY2-Wq#"\ ^c6/bew ֩vt ;q1uT>Ov|b6>1(z56ݫ);&yjl\x<>H:=69Fcꗟ~97*k5'^]^}Ҋd;b!Tx~^ry rvN0r_j'b,<…s޵Tp__S f;h>j 1~ղWG;gKpp*jTx勆Qe!Ċ1>v\G"TCᾊ]v`dZ|aa+-]_~ rbDgTeɏsi1zy3`agA?:]C+6pF:!vct#i8wx \-~j;/8|9yȾ3Q#Qq̭y o |Zإ] [8H2Yˡ7[,ul<r2rvFg4Z eMƭ{Px; w5uսc耙j/DX z*@B7`G @ie\1TTavmvTV` >ř-1 ~^A~E*rG` 5b UVVsv W{EK l#F{bidaǨ4a=`m;o8yNW,yt5pzpXrN)ŃV9Y>aswj(_\˞5?{I8_ֺW(^Oiu1Ã0~lFofX;bCr1~ÏHXu7,lnN4"jrCˠ̫I`ʁS$_UyEdbVW[5Ví\+d LR>jQe>)Y^?ņ" B-p_x=1(;:o軹`2agd~hůx{TMXODS؍ si$b[ T 2 [PVl|D}[t=^1.Bk`=FĀ2`ܪM>+GP}5nUbTkFsn $;sc$ȑQlf-uqMBho1 TWWws˜jȉͺJv o 9:/|Z{u1--LܫW N %1@ U[/妍b@߻sSU 6oJLvvOsSnsE~H߉ {?tU1ELIة'O +$*'$ځ;5@]fW>|p햚 kHةJI7 v;oGHصsHk#Mv2@®[vSilI3 v;oG,Q?~حT|=4gNF 97|di X??~¦vEWV{_'~^쏭udvg,QܺgC]Z]k86Vv vL%%82!~MZk0Ç pk;8Mޛ_#0`t7~AXy ^[ȹwG7Lu b;ƫVkȂkl;$!2Dإ}3!d)%'#J4\S5P84#]f"A,q:ܪR[HF#mb@?!0|G\inY?`44)V]o')v:d+_<^GLܝzXvEcjz(m|qQ X@aoLq+5Žh;_5tuew)4#>];Pz6 G[#b]WOe (qǖx{#T˺UyiB>"G,B+/ǷO+o4&/ N?uyYoƴ/?D7zfK®"}Xzmm+t:;C؇ { 9СC1~oƊ羾+bj*{cD\,]V}Kmz^{az.|.o|*A5p=QzTd"R`CxnȢs .3z10c=%=QĞ.V"!Vγ7EaG-Ɔ(^,PCb*3qߓ X۸G/$ --ϿI3{:p)w /[V߯ { Zu~}OLĴ'S\rH^'#֣`cEdDvF,t;bDX? ֶ؛S$P֓ E)sBNb@b@ Xf/ѥ=sSvjH-! {ifH];L4bFٜex.$E!a/ ;5mH~bȜ an- vxIةQ6Fl! v;oGHصKN iwR2'[/F͵choV6&vM}G1԰-$ڜԦF!']SGs][+o&j3/ĀE0`.O62~ݮ/m(+6}n[AmYT^> YMET3#]=?g9^yZ@KJqnm֞FI[s(pr+<&LŢuQZ31[uLh !"il +N0gJ\%}pF.Gԍ|F~{MUWx)벩;q7fIY =/Daj31s=k@wk 9N{alf?ă܋X x+i9]ZY=}-9ue$%mB■K=Wz8]TY>!S1YowܠCkiLּܷq}/ڟ{3{Şd֊-9s~)غCLUekk;6KumLNҲߏvobM l{ci,M\~0}:qcܣ}I,벹xNiVT bZϰ`wa~Ln[kKA{ bqAꨖ=olhg\mxLMzL.p zx|]*/ƈy3;Lަ3ً>{mZwkߴxJś5nRRˣNJ 6G/.XW'o,WX3UEڼ5ee{pTWss4e<]R529kǓ4a^ QOݶVL=.=]cmSOs>o.Zϖٚ4u(99MNFVy]M!>_ҰxpgMO3KA㘺 ;i}|6U2_is~v1RGGqc9#xb?kr=[fn}`}7ލu귾T|rxRǥi}+w:tj+J?/Kݿ% M^ڦc{Uu.]bwΑ;=4>V734?uѮ{w9J/d6Eו+W<>#zKk9Gz/ӺC2tW>B_pN{8&k ZzzXMztgի .^bn z V85& dW'p⋁a!ڦjmUKGXkMMלK?H_Pʸ؛NjtM]]8JX`ḥoqݩeZ1eS˧LWw]+` 'NS[m/ 34#]ڧu1iz=?1oz^?ԫy&{^e|%ǥ덍{b?jW(K/{O{W{}|ޗs^Iv]w+{c]/=cIw#$6<}Χ>Gj5j5 ޮ-WZև:} W/fNЂOݮ7.Lq^K~9ܐ:Ogf(uk6+UYV^Vj{[)T聭͗>S._sklkK¬;-*_3OʘJ't=Ut; 鸡74v*PeTRU~le*]Piy_:QU)=qx%oy%>/igq*=Gsԕ1CUߢ3jh덙㕖14}o;XT yٮe9zsx-\[ [/1ZLfSoOugt~6 &11 DN xE1:'"u0b#v00`ݠfF*V000|@숝00`ݠfo¦w` R vNR0nP3#+ vNR0nP3I7a;z0);b' b7J{쇕 _;b' b7$ᛰa` b *bUZZDuuu~~H00#!Pmm?Eϟ>zKΝؑ `% IDAT`` -QWVV.O:ꡓH A[|g2[+uK[gΜU{kٿT`DWOv_vgױu\v1 G]C"vycǎ\{ ߺ<Q=|Gubt'jTL2&UX$Z+fgױu\':297Ѱѐv횷~}Mvޓn(ur+}Z-P;Kʮcrn𘁟ԊZ DﭭAE:yo7رZSg=}zC?܉iK҄Ǚ]j_bxym:KxMXܚ'r&#Vq펊s>ѬIM9hSٍ*ZMTBlbh_7@<6.X;*F /_t}#ܯ׊߸}Hag&9̮۟4B{|Sbϕ[z_߼20zFN%jzo$čVB4-X ٸqO*\'jtLhlKyJɄs3^s!9ru}m]>yۮZf[7Z|]zoRُsXԹSG \r{;UB%yUZY65"] .#;U|N[H>9.Y zUDNuw`֮*,ٳ5:Ħjߍu|)"z9.Cfs9~P'.{xǽU*;ǏKɳz!(+)1f~ z\9'7&*б=556AX/01حPп-(.c>~ʯ>Y:[IZVnT:؃=AՍuo!&:m=w^ֺ/=nXbOԌw8?;6:.kDMRs|7}sT2bh] >'z;taMXa\d镜++^Do\~&Q>?<53]}и_J1/ԊZِ!'\SBuuDh6E7+#GNӸQ/iTLbc?b?nkONK~9fkoU;;>m}={ckw>zwTe$)%sb4iuI@op>)vg%5^1ȹ;ǶV}wc-+.fVfX;O(}j>&_{{(d{9b20$bp… &6֍w٤3P|Tm<ߢk5.6S+׺Oezo]?zo\4pO/kF|ޥpP+{z=ฟq}V}n.%S?*EOٺ@;zkbIҴ*zVqY3XNCWc2SWC"FssukہMIЊ#{PKb\쳏]cxCz3iKgzr|;kK9k qZ'O/9z9kn@}ٳ>}ɩ-ZZ kS<{WD>{,vjkQJw{&׮YW\4}]7ev71=|&ovY՞=U3zd0ngPqSY!_jbl]OhU95Gq=G]hmPأ9.g2W:[Jy.0}9S ح<~XUUU.Z[7YBͽ{ ۮi}IIxW[*^9KFR\8-X_]/]j׭uze|fv_f x-D^6M+7~_˕bx%ČR\$-Vۻ2^յVqǚ5z[y_J=^W^'q>u,U/De<wv:|U{yS5%մiz|rMNQQq):~٢%<ޡ=b?x/hǪ755+Iqie$je[ɋ{Glz:mbwx:Kևuyp欕8Y$` |^ odCo-v00bbo߾풷uǻ:u϶Zw[7ݹsJFH;xiS}X'OO}(KqS&_02'\\\s6Z3 q0;ȏ59+EqwyWz_Ys5Q; 0I:2QDg#v@5D=|5W0CbG$u@5see0Çz9[]]TZZG5yϪw zjb``80`_+"b~yeenܸẜ)>BAkknݺz@ͤ֡ pc`>%u{Ҽ~:2@X[[.j$5&) 0|0b.[ <Ϻ Qس|N=a@0DDVsv-pUO LBÑi 0|=# lF"NHh0݆EF"v&}؀Ďyd1mMOi8 vC&@d6pÓ@>`>}xL ` #v.#™qy vC&@dnމdLOa1Y]9* m:t~Qg#v ac l"=:wP6hUz#t(?[eoWMF;qG+@h m~lޮmy:vCNݬحm;5gN48?B=OݼS噻8ؾY1O[޺|Tk|(7v;ڻpIW |mܘm{TؽК45G 0/@>؟j<+eg݂V tlK .Hu]~[ttlұ+wTaL``كsڝCER޶bvX[t@g<;Quwm (:QG)5gެG쮟oV{zC5 ͝ɊʄɊcc|`Jmޚ{SZ^zw&m*$ggZ)RN}{h)`wI|}s)S֥9RP}`'Ph06 m+Qznqw:O;x^r=9y_ZwncJ=|lTY5ݺ+j(C9UXsA 7nƵK:wOp&Q0BF"v&0c` !@M?ӵ[qI5oםeg+gc~ߺKG6v]wysnp{KΎ-9[򔓛]\9Y,ҟ򕓝jo%KžD_z '[F"v3Nցpc5 @ CFgN u<5sGE~e Pѡ{WΩrn޾[ T6,Uc{wƋ_I-=`b _jؒ99{ڣZH΄00r@~= H>zL`@ bFH$1Im|K$bFtrj$+uR3?0VHؙMF``H^QQKg# Peee0jm ;xaBc y??tuÍU?ޯuC D`>爈I.ᴶZ_=Jf7nܐ"onn+vj [Ýu0`Hnܒrd='Gh5f/Իh=ZSj ``80`?t{߈=wNmjCm`` DĎع00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u@5DDL5 ]@숝00`ݠf5=2.V600=;b' b7$%bjMabG$u@5Ӯq1;I`` A$G/Skj 0`W;b' b7vM DĎI001 j&8zZSk2;I`` Aʹkzd\l`` z vNR0nP3IKԚZ ؕĎI001 j]#be0c#v: A vI"^"` vNR0nP3+a` bLq10ve#v: A viȸX @@숝00`ݠf5+a` bLG`bG$u(>IDAT@5DDͪSYYJKKYuW`ߧ ~8ĎÞ@M#P*++uM={Lϟ?#۪R8rg =Ď{V7n@!$hձ6d6AD"WK_{GhRiSe򻕴ru92TAຆ=\-CcbG!ON#sk'%nb*;`p5fD#'P'ۑxI0 Ѓr8=&#v! RNXÑ ?{&*8?]&?z02NB숝{@*He8 c c G*/<@ԻWmEkzm^F@pzL8 #vVb`IQk?w{/#]lÙЌA]Y>yaHj]k_Dر^[K3I(J1R伨f&kz6mBx\81=xvUhCR["Az׭Z}3%Y7] ;ĎYGJGKo\NъOY+Vثg0H{g5[)񊍉Ql\'ВOUIH{Cm;UF{X=O{FjdgBO:]٪CGunnDKBI׫9)Wl-I_ds>Ş:U>=gt~bGZvc gަfktmk3u55+EqX_{b鸫x[UJhNHդ菳zW@T?1MʜHݫ;:\d*11U]"v-;6IJ%K_tN?Ou}c܄s8bO`Z}}yRI >ҖTc-n).yVuM_OQ¬tQ&靝7AH 39w]ס5jZf574#-K$@=kok'c5}w2*U\yZuugTgheხ %dM|?O%ժ8W_/WCϟEJLd.{_kU8MT;YqW]oQ0ki7j!v_moZW"{{_}jv>sD$vtt6uRfjc.֖/))qηt_Wݒ4aiWɊ:wөGV()i~.ǥM-WMVʒmo~~fvRgE*Ѿ591#-{JKw?CYO5' opsoUO~F׊}B{/?G$f-T*66vR4rbxtXKhZmФIlkK~18ƸofbŮ6e]U)$NU\[*x;8ubxX7jV׵ rcw}rrYAz§7 [C7xb''Gn׍hmX_ymf'MWhlF]__5K5S.9w/^x/iYZ-8zX&'*.sVmܣ*;~XrӺC jiZ>U}3'ybE?U|4;Қ8v@%ZT%N*>ʒi=츭Rʏ*yEQJct=_]/:Y]6&htT~,G%47%Y p#e\+z|T{6tݒ5q2mr~ueny_3E;nkT%/ eCUm]ES30:VqIBU7~jooMzZ}I_C_u {bG!O#E%&=|P<#XuԻE=\"q#v@5{¿.``$0;I`` A Icd0@숝00`ݠfbXC}`Fa` b̑D9FV\00ĎI001 j&)6p>``$0;I`` z g?C9ϟ{IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_7.png0000644000175000017500000013047514062723141021733 0ustar00jonasjonasPNG  IHDRټ} IDATxwXTYݙ鞞;᝙NOgvP"Y((bEQT0+F9!&@1!JQ***ArPU yN9gS5lXϿ6l? ݰaF6l,-b b`0i4j*飏>1f&tА1@ 10H`4z1?Kix?> b ut?@O~M4aÆu0Q?tJhb q2qgCOgL7}edtA Dq">h?~'S'8dqH Ā bذag]ƎJܑ31@ `aitcF,t1@ 3\@H>#1@ &pHS`j(bp0q\~AX',7XSai0]Gv[G`鏿IIN`cwoĀv1{C{쳇qWwH.p\b!v^%w*#F`Qy[ar&tF9(-,xxcGid[(%%$::0]q?J}Im\ǞߙC]=p}z2 ;]l^a a x(5UhjDs/LXGb}t B>؈M|TG8&#B^9=է- y?.>^SpI~Qg <_(YА?J5f 7{0ս^Mz?ޙ?س2q\5w?ٶ: nj9vQ14?!:?HB^)YbHXoH^G5hyz##xi ĂR gON{Qza3F Dg (:ut7'yg[&^D\ٚ누.>! "u7#q,lz|G,ǔﱏiu|`/3P.9ƣt0z Db޸輴gݜ@_`!O//U:Dc0U3/.cX{$JP؄["ދH7qXI< {q 3 ]ot} oS&ҲH?@(H8)Ea<ct=^Yaܸ9P+w-gCݲEq$F⠇NkBKm y ڨjH2EPz7".=nOFF`h[(jP NG r1xvP$!ƍR^` Gq@LĹk슯ɶi(~C⪥>; ~_.Y]@_~C"ǁe9HQ~R'x+WzF[l@n#B^%sįIi?>~s;Lĺm@[ЕbW/zfK[{h:9&ൔa9[kZ bj5DC>|IQ`.h=:weGԹVHhDtʄo9} h}2g|Ů3'K,v!Ø0[} |xHdH]UtFyWVWbKŪIY K$Bl #ut 8F/ i)&ʢ7lG:W j> X‰*aсsx&50wEdܯD}3-xx7;gZg9s{mQF~H*f(%bQ_Em foF+q^a=H=" WʲcmNڰ 7 z *{f?IJNk&pѾ$1@ d@mդ.5 ; ; ;1@ ;f}!Qd`yM&kwLE랙,|B Q8&W}FNN:1@ Ā1@®ET5zb0 a'aL b@ aע`Rvafw;1@ (2@NN:1@ Ā1@®ET5e1@ | SN 1E kQ0);0s;ŝ OB> b 6]ž Z1@ fkiB> b &,)#ad9b -a]KI̬)n7bhHI)K'b"Hص('A c2ub -b` ާb l2`1@ b`H ;Ưwd[ ̷oَ-0 ܊kb`E  b`30d߾G` bUX!S~dll $Z44/Grp(->`ڞeWS0a` w^Y|{Nʋ|텣83 {fc==GiCI{55 a yl`3Ԅ׋ҿtk:}® ll36'gj4#xR}1yO.uoV\_قƣ/p**+ߥ8Ԙ*<*^WT; ~^SԈ!][]oA-L-wnӾ~}..#h*T8UttvVTx#btL@<\oex]8|C«ىƺWS9OMX/qHp;5D͏`6hlo\_a][}i߷\t B>nCxh}CK@f3^VكVM[copg8O]\ }kߡ>zg=|uzҊN.En31ý{yWB.4zW.a{fV6c@ca7 8%aou9th/.] @Bbe{q+!l^VUtvdm J!:i6\X;4`?[N'^{w`NVQ'|_ϥ}6^1[_^Y99(oNE]$BMM NGVH$BPax<ý. l0N"A-Jފcް.[dn5@фDxuy ~X%Z;xص'gQG#a6+n]q;"-4RewUG%>R*yk # c#FHW8z_+$} 772յp]>G%mJcӿ Cږ]c&1/zC -&MxM=]۔n)zڪX<C݋φBӁƬ5 q0aӑP}u" TmHX%v}2N@Ll͋Wb)8~%.[ϋENr].ַC̼2HP؁:n9%4h):D OPِ;mhݮg-J&I&2aYvQ=|71_sGud {UG>OSq8[Zi^kͬ8Tvtv p?m8VLU;:E[P}%8B6=xR7*;@p٥m&z2ٻ|"z]: @xZzLd>'lNm`C쭂x_ Q-2͎T!1nx&>z`%,='ۘ)w)a6y/mwPցG)q*~"E{qKHl9 gawbT:p/Nd$veV  ŢuT+Fe2[֖&,fsڛ.O_ ۬ԡ{l ߜܶNtSVlӆ8CǩKOI4j_w9ןmqdEnb5Pƀ{7Zċ`%pYKŋ2Ŵy1} ǂU`f.\-^<;^t_>[z)kt˧(nZ le cnz(>[K,/bA ;gKo\w1! #˺A$8 ]>U,J(íruWQߥpQ"lu_|)C z7qGNT zǩ͊{ؓ/ ޸vƶ}'m7^݋%EtnB!j"DDŽuzWL뱗QT?šCPAj: Zh4)+}(v%װy;14Է)7%6%ױE+2RI'{=PlH%'~ y ;vz\۹ۊuUoMo_ؿ۞rdgq+?KE/\N$AR'n{q곢zͽiPd}f,Z WlRXflɲlv, |Yq3병醊r&vh\)mk2 l^׼yT?ǚVPNVxY1-p> b*a>8 ׍x+r*/vQU.bQ^ϖ\ŮQMlaWDv7e!6k}s+@W)Hv"-_z_wP|A1\۹ۊu]GyaaZ}ad0N_/c6h6bJcasqNҶm ܥ0g-f/Y#^s|a{f/YY/s}VckBhWuDU=Dm9*V5"`Ɲ5U9Tbd"ZF^W'Cs|.DȌ/UAb~kt*`ZOuˑ'#+l\a&6 hY!&clom41Xx_7>$PsKmYZyd3Vɿ,+ :6ȆP*7vMX2LۊkA ;g{_)>m ]9:~{X}3p Q\hQ wv6Mxmт;qo5?V}ǼrU.5քcW{LD"{=Iy03| V-w>Ձ|E!g؄QVn"W֞󞍼&6C|[QنgFBC;?EHcn\Nc2ԋ^# vn+7 9Z;pv{n-hy=V픜z6Ìf1gN)Ty=cݏ=(ƈ^wA3ܹ ׯ!|-OsSp6ysx2Tg `3mE~PX3ovwPGڣ9u'Qbx؝ WW:UaZq#cn5QstvKgxC(ǫ6`r7^2أF,AhEaC-rܮx5үGRv 6ź"D׊z1}Zj_X\1g=ÕQ^Zp,WVZy}{6ǬԦv㡨 D,ӛU<$J0kY_Ik)ňn3蹶sγw:WEl 5{c/uʟcW˿$mqAߕ۩tD\cFAQ9@z9w?Ŀ=ӌ_Nj'r4m۷/ \m-Lqk:s.2agksWY[]{|#퐘hL[ݱE;df!!׮;"8d*-Wo]V$>ZxNe f=k7^qk*Q؊x-xݭ3>{ kJ*dhd::߂=|٤N]6֥?ǃ6Doh@l\V^~ϘsnK8jk91}ߨ\Sӡ=pV477I,L2e;́lX8z f`jtQv7鱯|VM8pCnva0{8bagl rGKsjlA!1!b/ [P__/^/Tf2&30vm<`4]vچC{o03`mvDlr[s}fFl"b h,6nDmmx:_,bwMqdwXO/VӠgtQ&.]q4ٻ~ Mlogx-rfP L 1@ @XWZsEVm+[_|Vӕ{+g1u G1@ Pd@ca_rg馱*2Aڊ~>fP L 1@ @XؗEQ+wŮ?R>KQTT~e"(l >{5lA!1!b/ b EX&쬧&B=ld1t]T1@ C}…/\AtU=.+_fu(lƄ b h,Gvv6s-X3ۘ: b`(2@®Ej IDAT`5B/q `%;0#ot`Pli|mQ}ib`  aW|&+khX]Bt5/} Lϕj[(ϗE&!f^/Iϑ[n0F=$WG=0ek738"ƀ>w}gx+_P]]xxͶsհlg*x">_Ç(+-S0ۘVMA|i$ΗT~x:kcDl/V>^¿WK =X:~"Nn2UASC c@ca=iHMM>4aꞽfYy_l]#v#Χ2KBӊvz{X$A2vlۻhɷ[bsdK2,9H{%)']$v?Ez<;5/>dl>by4BaZY}0 _0=ѻ 0lƭCH1<Φ;XXK|$#\κ[0gu#Wq$٪cͩpX0zݧ!ؗFbRl a'aL b@! Z1@ e`ذacBb  ;%41@ h$ZLʲvM@0@NN:1@ Ā1@®ELA=b 6$$씩1@ h$ZLʲvM@0@NN:1@ Ā1@®ELA=b 6$$씩1@ h$ZLʲvM@0@NN:1@ Ā1@®ELA=b 6$$씩1@ h$ZLʲvM@0@NN:1@ Ā1@®ELA=b 6C[ϑ|v?$<}4aqVɯF}8U:VU_Sܻ[K$n:ާ^1@ h!C[؛sNk&Pz$ ^͛%7}"dhîipۙFYzobgn)Ncϑāݟ܁L&A!;W`;Ǐ1,f`{_ao)Fv^)͆_pZ4@U=R>n}Tݏr6wrn2i [ K#=HAVzavn!RHCVeo_ 9w[n_uO~%20n2kR;$F4?A՘ao#CyAx^GI?,&B_WƖS?Q>/m.<`g8pB/vz04^qn5׷{  tk`ۙp!胪zJ6[3#);G_6ꧩ}zm4NXQ&]> 9wU˭s_|hik#E4G<}% 1e) 6KI=Z7Rmsї epůOb%|NA>#|OInKlp[Iq|Q q|B h[vxM(ǾgnsݐK}'̖l%.;BsPv~R>ߣ.\h[d;Ďv8 kύZ yp lt'@WOE7}3JcV&0&!lpNN- o8O6Rq}՟_K _ov㹇qYkcE!x"C0[aӵBԩ?5fo1%|30.<'@u8cE&݆/H.*89wRkcjYC=]aCcxauYHܳ}Yݞo)šl(zCMf7-~b8aW^O`8(o_6+ƭǹ4zCz=Fq鋍.ay]*M->4ȇMZ 2𖄝5X:8 g&uH9Xu22gd/DSq8`KHsw8͸Kx~"SXh+~AM  KkXMCȽ析kpi \lRb *%FdDl>-<^2yX7Ƙ>i Wqldz 5z3+,Yg1uz<(w]US|Y:ۜ< .E#3wpp \v쨰/68>{"L2R%7~Ki[F#\Fט@gK]jdǹOx]&1@ &H0EP̝>=<:K7s m5 wnцB 1@ bi =IpZ [ί hm CML݈+ݣ\L b`p3=>El{1@ wptwKPgib -b]9ԳLzJ1@ 9$$씩1@ h$ZLt<%b`3@NN:1@ Ā1@®EY&O=%b ޜvvԉb v- &eoɇ10 a'aL b@ aע`,짞1@ o {SSJJJZ1@ @zzdm':! ;3##D?y<@L^xL쮸Ϟ=b<@ zxEEE]c;~gy<@ hVa(fvvOy`m`ɡh ; {@:<@  H>W9<@<@NNA]TYy@=@NN®W9Տ<@<@NNA]TYy@=@NN®U.z+St?C ޓHIص_[31zqt|k~'l}{jXy@-k| lB+~wnJZK=`M{&2(v~G&{A(w?`:W?>G_u؀*JG]Dv-vIp\?da+x {{]!B~6N'q3t;5ك'n?^s$Z(^>Xh?A `a~Lw sSSX;̄> 5P"7 '{ly 4ʫ&d_ ~|Tp%>댠GaO 03#ġU༺[ &L"$Z(n"y\-]+d轇 Ps/ wPPؐp6E`KRc fg##o-w8/_ub ףcq;(NMC\cلF3B} 8O~ B$8^/!. O03Z4qn F|wLx99HyQ|zGD_`v`ލ;/OlOdkWzA hHصRoYЈ;'+Z !윊ql5<dzRa:tG-9oDVޘMN>}|XLʮX`i]IJzʓ_y@JY?|;Y;\,gm+ع 8>:j.`ҟ?Ɏr;.~wCP5/?>MJ }a',[lR&ؚϯ~6yo}h(// 8f,nFa{k>؃6\!nԠ,)f7_|9O TŞub|7-F kaB> %K&\H#hHص^؅+q&&f]T  ,{w Q\ /7v){`n9N^GrfғpYҫt/~Ui}ӏ𛏿æ}fs9ZK89񈽺&Das:2bb_ULw3'܃Wse?nTni,&kPRS0u7/ 7xMv v[|ؚZ[ ߏ߈/Wȹ==W蝳07~Qx*$9nltŘS|Edל_o'p+y9qJ@s\w~z$<==mvm7V5,>gPy<@ $$j?D3E HIIة- Zvvv-*y-# IDATyzH>NeZ;{ywzee%؈gSS h 8Q&ld=w)_D   $s |I$b@=HIi b@ aע`R6^6K~"?Ā63@NN:1@ Ā1@®E F=,b c2ub -b]I٬z,D  ; ;e1@ Z S3~׍gL(x rsvM$4 U!BƔI3HQ{5/,G^xڙB_WƖNXQ&vcܸqEO2-v=Y߼Ga`uהz_Хfq}ïpD]v*\ȩ:ׂ|da=C38ϙ ūkb7uH4XaG2xݧx wf1l+, {SuW:TB&]u&F: WRw&+u743+^Gge1xȯJ:)ԇ z L j4DT x677n1VDW:CX*)i;<cÈM@Fr,3(!;lg-9dUoùR;s3JoɅcOICrL8xYBo2z&r ))|x%FFqZ_q8MU@tDb6a3(ͧ@ $ͩXp'{Xrncϖw%-Ԩr|JI%еѝamnϕ7IX=^'h0 n(2)?ƤᾦX} O"0j w FSPT_ W=Y"MsKPefsHރW3&>c&\yu!҃`{ߣ,IxW,`8`%Gᒟu> J`qfz;Xhb Uך1 /$Lߊ,cO5EŧX+ݰwQ[$}_NOd5 1@ (a7Zaԧ`9R1_p,}p6 ?Vf3\ԧL/o~v.&mDv@|APҔPl]#]S,8#^fb;V]p2䗈ŷ5Xz=Gvg:Dzd[&w YFaFա Iaョ<C[g YdQza vPvFOmҡh,HC*+mNWZ+Ba=v΀oLkyp0GGNac=ld"bYG $j\Za19z\5{⿼ o6Khr&L6M<zCz=רq<ή5aG>>Sl@`!62+J qs-&-vY=fVxt#}=U3p3k`&qBمf`[GT?P:y-(k\MV<z1RyP5 ;N)0MJF܍=.zX^Ħq+iH ǎE4e nAWH=ğ7>P 2'G` µJ6ovA=&#~Mq`K1ʹƜ'nsOK!vwmӻ(痘cMăz!bx aW!tL^};j MU (s(Ke߼.)O aו"r.'ZujLx WĿ6~xq&|O T757 ,qy/Ze<`C3[xĕ\ɯbSFz3y(893+gb1Uw/cWXCWHlߡ8Pݫk^ZFM&4c #Wȹ2kCX4 ۇks)tM ݎA0ٺ {B k/3a +9a{cm Ib)9JOBT>̷5.ܭ{x5Dm $;l(6bQ삷{iV!Iw=^:i$!Bܬɜ3מ=sr 'A0N|RR8s8[EUQc>8o t)t7B5R}"'lr6} ӰW}q.㨙b ;qD,&}zF_.JHYd3xwB}! !y"xW^ATC$/)ot" }n W @6$ܭĨ ;{?3ߏ}9aW2¼{B L¨CS`Amzs 8t #6,CMj[񠃵oȉh? ɪ;{R,4Y'xm2gϫ'<$G4+Ģe/+DJ%k3->6/TŨTC__?ٙFoEWWBH:[g~GbU-bsΡF$ $?.F}]$F FfAt+ .@$/@MC : jPZӌfˊ'G1}js} sjn+>f#Cƙr\mx;"t|z>ݱ<ĿZDi>DP1U#+Hȫ13V}v+ETCϱAhr͛gYU=m$y'{=?_+QZ95ހ(Vf+]cW !c7uE3bTf.׳k]9}iR1ݴ ת܌pEPZlH+ LfnkHsRqj'xY1 ;tmrrĹqr *4r q^!F Ņ LNxiFZbLw8.Sקs8EQ74eR8ˌY ن- lagƋpP,EJ44x $qghl CYH'd~ Vۄ}5r-ia%jSn֏1q(2<=_+V$wdŸ,S ƫ%\vcg܍r3 >g6~G{H񁋳ީ +[& , qGܭ !RqMSb?X mPIW=E{^#+q1Rۅ]YF[nb1F QsPHH538,/ʸiliC1Ocn]MGOkf#^t*G^C0Ww\ꋄS1p>fv6ΌQ; Fex\6,cGU h0a!+\h5Y;wޅ3Il@zAm مp®\7hAw{d Gd:"3NjW棴'z!koэUH.; {}&xZx4WנCnD5g6%6eMA0px8P]=ӘD _H /ؙ ˨X=賂v(g]Eޗ 7`~u.Gӛ8!7͡)};㼶-̏Z u0IU}$3 tB vg/L8w.YQ{a\5_ ABgZ䙛x_ZT=lq+5+y_7QE1bS:vS T(; Ig C'|#qi~vHm&uM W/w3=ס9j*3^Ƈ,X\Fy i/k Brg3h9Ho!8[bygw0\r!B)kSؕx|!l؋˯{4@͖`Ɖ Vϰ?ăǁ 3y7'@8# Y Ĺmk(? ^T&2j(z W,f®d>7oMIxhoE$1'1'  b(S ۑz%l 13@yAWBTg5+CĀ aAIX)CE66.}ϯٗK yHI(Z&bp Hș)H|@> ? ; ;E1@  9OGt~1 a'aH b aw gR#e 4$$1@ 8$?%iB g"ub b݁HYюP1B.WaNu|K51@ Mا*R zݸTyt"nG !8c w.S%@ƚpVHpN|N}䍀0ڑk'c$BuT&%7Y'.W[IW.p9ېF5;ݮvf9زy38@o#D~%̀]#A&>3Zxo\3qZ4qM]ά]pq1e=M{!d-$ o;Q|<7 UC#'xWՈa+3l5p OG'"9a/n6omES}roahb_n8}@$Vʀ];`O.P5 4ߎ$$rUܼC9$Ą@سxc:3wW *R"]C˻8;rRH~؈)VA2 I6 /D ז+ ٷ(M ǎ4|tKhy3ߏ](_aW WR낰7!Iz(ǎWWE}qj(ep#t@l#oІ*+"{+x-|\!R*5Wz+B@v#oD8AߣJah`"b3`'aWB<?C(H3138퍳yp.HSo5Hu,hdoGKh(pFڋ5"~x pxy4 i$.(Gcc%@"َNxJTW (Nc/tc~" I0=- ˭=+p{vfc^э&®FVHbsfZnk> IDATV}~˦aƑ'_R uOL)4JmC]?<ݘIQ`HS#ы#>YN iƽCp=i,uXX ?ǀ}od9]o_+hT̊xMf'?Ξ2˼p`a& H1 {ԭVQà|Πi^/(sHc2qL.l{-XAd_b!fXQ x8/)N6!WWL9J!~64ن+rpdh ¯@1Q\|if_ko SESGPR߁Z >O@Ecxge8+܏BԭPwd XP]#P[3tBǭRlOo^(#&©QF$Df543 }qF m$" {>bݢO[v ߦ4j|H by nnprj,<ĸۅppզ"|GV;|RTϢ<%ݏm ^5wR6uO&:{{u.>RcAmdbW[r9=p~x\@Z^/Uiw\ϵE ?`pp1)U.?8%]!d"nMX+kvc%=c!guu88Gpv79.~W aVa'|Dpv?q`O h9"n3ΫPz\}p*gЬźC\oC-u 7 &qouFh`%g>®Dj{Хt 􇛻3tzrB¥@$bJ֯~h>,iZ~Kq<"I3 J} I\x&Zi=5=1@ ~bz:d<\p?4 'Bz%&اQs- "qNgEuc꫊<3儝Ly3s*T:L7IaƮU3wA_.?hl;/:jPWp& \_\c}I 1r#s8遄WFY%.@' )G~2bB !R!FaƮ=jT{ێ@n'r7+U5\8}0'+C+X"agF"y M##ןkT8+9:= =W8W.rDYo2u"a@l޼].yb  #̹NFLR(d#j_y-1`vHˇ.4!|4c.6kƣϨS1@ m ®_;x _6P>4x12H)Lieb b݁IʢYى vvԉb vr#G6a12HI)R'bp Hșͮ,%;b a'aH b aw g:rJm1@ +c"ub b݁IʢYى ^ؙ!gŞp?Hxx#ri< 53pss@$oh =EۈgaUC)EJ{ОQ%tXAmb`Emv&=Fׄmԙ3Qۈoblr!܉']V7Ϊy7٩(pڏ/ \ ‘:i9DIϜ$pOqE "^aze8;}:|kx $qg 53xR7_s$$1@ k]3SrSk0oK ASG'>|ŃX)YB]ER&Wb?X m߱nDs=[R˿P'z^ap%vcf1@ Xcv4JχB ;E 0-6ϙ7 yY^܈CvY4ތ0 [h ?W\(=b ®@5\Sb}bpkUբ}1rvr7]񖆛y(%p#> "Q(ǵrZ)GKW#H <"ѧTcŻ#L4ww6{b V_-lcxq%Qr$r^V4cgAPc-)G"C=xTո>}?v#`םZgt|kpTM6?p6ЭH:.b _/9G#bb6-; [-b b[7Xvvvb b݁IQQ,هC $$1@ 8$!6Ҍ  a'aH b aw gRtK! ; ;E1@  9_D4"b`iHImמ ٟcTX~n[==ưr}s 0iY| "3Ce[Vy<3=[/g~Rnfف]5;%;0O͛@<~ӶT3x ʄ]58W*Ǣf;JƭŤN˕W &]I}ܦ?$T λ;5*A<Q+h=Ǩ翢 lB$hzE ; Bxǜӂ T#m\}ފyVvCpx?aXά]pq1e=jZwu3͸U'7ħ=ےy̴H|f6;"shBQ-Y~S̺Td9X]Q~'P5ɘ j1;~kYv:?qoo9Ñp?<=m*!ヘbFΩ0'&+j )P1B$EH=zuvm `wam_a n5Hi]777Mw:PA2 I6 1@ef"vC*BoBa4|=! ݆U2?QvʵLje{/uV<3 1>xx!4,ޘ܏Lcgg wVq I2CoxE <8f^:~5!pE ]oOLU}抐MD/Gޘ'~~n*3EdL8d"d!^2"hC>*41Fli4{wYktO]S8'lc&n;-?65&^!1&ۣlL0HE5WX1 gU}D@ĘD c8Ym{!󳃥| ^"<bL?Vm2?zg lsv{-Vj s.vJDTya5@Q޾>cX;+GFҋr46VIJ $unGKTWQYߊ6=:)?>qhp5RKmߺ`>mCj7ݮO( L YzmQuSc1_LmƗWMD){ ~N̵AH%t=p3Ff,+VTdʛē}<ï-,neh)CԹh4B$Ѝk} KG 0ЬmEsYr"D?`k4Ov~;x?O/T<3Qcr1g\_b!.Eԭ֥SWSj*ݙ!:ag&/c=U_aUAt@XϲI݂o@H9~a2;W4[ivdzK\.flcC7oނ-[h7mڄͼU|_%`8Rlyó89eix(cVR%ۏ*hHEzӌ2c+oY'Ka7m+.i D uĬ|j{G$kIG\iMQ}}P8l5CaI*%^_W y~(X.SEamwww $H)[5KaWc,;j-&$jlY]=Pg^-FClm}.lY_tňzCö@ucfR)B!pV#<7ߑ`dFE\G/) Uf!aO垐W*D~ DG1Mm*s ! zAW)T WBR V%U JW_x scg j*XuZٺvCǭF;|RX: !x`_Gxd&H>4D| !؎'lj@ Bs.OYPkZ;{[ [}tԦ0}@=YC _`c. ODlJ[gSß088!3g egW4c_TOa_?ʶ娧Pw|_F߱oknL=G nEvϼn\dRg)Ğ؛]]i Pd >~"\,<D7LM;'>*`5>]H8^=]CRt MNA4bC-5n\APhQ^ 3x_B vL1HYS95^(7uS2}3Xv?b{+uZRT3 ®ۓP3O tE? 6v6P ѷP F,-F(G!2%nE$JSg8pŬȘ5+S8DQ7s=mskmf%A>*YvV.Zc7d n_χ2ښmj|.JF`iT /uoSl<A5﹏0>BZw/OaSGeR<{jTDq '"(XKx45֗-B"u%;@ kh㴟/s;1}&Q'cݢVl 7 v '077:aWN")P0>_GT>ӌ>8^oc>~t~)矅_E4>]lCw ߘ |Y r}LaB k^F?OwV{W^`=k?i F5ވR㯴U*peQA=ӄ8pC߾tzdأ]cȧdv}m̷bXR+F0??}(JdqY^FvَHW*Q؀\\~Qjӆy'}! 9 Gi;vNy3s*T: s{!koэUHMUA(ۉ믪Z7G`<7kQp:-P_UawdŠvū>^&+B}C sc ;8eOp= >\Ћ[^xo8{lٌPZU_쌝}5MMhB8g/M=ٵI;|hP}u;7В>;3umf&>}|ΠGَ["#iMw$qt迧»@Hc.)4,FPY= @V Y)0Sxx*>b!b#XXH[#C>-@ʾ0yުüZ數]Y\+^_}M]{IvܸIqζq2'4( ~ׅ%`=C 50p[vspN{g_{!7 ثa 4?S$;ߏkۦrvtW rD 3l7=[Y՘)@ZnJ!pw?LAnBZuS T(; ItTv.w;'&\cETa/esw8pk IDATُtK=^}}_ X߈Ɉ C XKegNȗMb g'g؝3灈h֯~[`?(cxkU8:;'VE @͛_l?x-1=H|ƽ b@muȧ/w$"0޿ӯ4Ycַ3ʳo+brƩ25C;[w0 2ݵ꫱o_4>6%aY0 rMmhÕ};)FA:M?!"<@ bgB:_J#e,b 6:SOESRIXb`= 1@ 8$Dx]!1 SN 1@ ə VaLWgG]IgIQlx} aVyig0RP.̊vr sü&w˝͛nf{"C?ǖ-oln+ԮN<%`waOr6Pvr7ZtL|ٿ7Hlh{ sf-:q xqdU~Y(îWJmnr+SjxXR|r(v~oj׫ܖ"vmmDCaʞ6r}ok<]u_p[$vp-W#WC0?+yFqTf ԇ+.EI~?kE7u0 QnJ?C{l2V?X cok`e~64":;QcČ,(K8'_9 p/naD-ZZ;H}GoDu$p/nCazR>aWue/ 4KӴn620m5eg)M렯m$+_$aCAد_Af\`m~Q_d2"gqT́2kC]8Q֮N4/@GpRۋޏ>@CHpAH~\2a6aW"ݢtAo8{DϡV\H_:uD@EMKsq.J;k'1f® yGūqdzyaןkm6 _hPҚftt4]V<<9B$g<,򡅭WS[v0;nn!T8x\3D2>au96"jn}[_/-?K!|oZ_}mk<]4:hNv;a Dxg18W_K<6~nOogl[49Ho2]D)]}ga@VWmnZ$ z#\NH]#S_΢Y'(XևVhX,{Υ`>f/i4R-ߛA_Ou!Ana D{%,>jϬ~»KUaggKgQ*//;p-oj wqGbv!YptCUx]h.G>AWЫTCL qxRV(jf 7"OJtt2l[W+s=J|nm7a؝PY׀x 3%3fdk@ukkkـnb*- },؊Ԝ 46˫ r6-'B`+7D_|twT % |viіχ o*?paxaB:eι &KCVcZܲpIu׶SpCXքhlg dh+_C({)5|!|":zn/- u{ny?p6Eߨi[dl~I;;EcOuT#H@'{#xPcrApuCV -\rw,o\W ,$"ǜ"a{rFLRF)Qqᬍ!9OfDLt/E # uvErnR~up-[ <; >z`w_ڋ`.lIfYʇh8jŠ<{T9[Sc*;Bj,/?m{5nvz[ ? ߉g  й~F0b?/l5n6麒Bawٍs{YV) |: =ea%[ZP.Hp 6tH,[m{-gPtXY~`%bv @(!3+'Es^v  N[hj jJ bEX;T. *1@ ,5FnaϽFP Յ ~ʎϯf:B¾@8]165ؖIPJhҩ4c%a_ +ni49l@X5a|v$vv<D]_5$?x5Jz! ~H$얆뇓lH6$b` Sʋ b aw gRdȖlF6#Gc"ub b݁hQ'fR1@ vvԉb vr&Elfd3bp4HI)R'bp HșuR{h&E  a'aH b aw gRdȖlF6#Gc"ub b݁hQ'fR1@ vvԉb vr&Elfd3bp4~jܺuߌԓn02h`qm>B~'_&6m3⮞PK%>N@'Qz+B6A+ϯ >o2d/p'V&Ԋ&$yǕoo݈nf<߁v(m'*Ϝsك /vVܯ^¾iWzx[:ݞ3v[nz>zmd 1@ e a_V݉"WՄYCך+ql﨓mе" ' FQvhpBþ3{5 &hT<0/7p8GAlf eG_>ή8 3vf#& z9-|?^ .dj5a_x|1T?\"(d5@cfXE[.#x{q/B|$p:cxs]Y'tc#ɯ&K^؅GsEyr~ "aEq}Ξ'Q7EmieEoG|b0Vx<.R=pɉ"]}5sh~ he[l.Cui.Eub( _ov;žzwGg4Ό>w7{3l EiHY_!/qh4j =;\}Σc^ ؑD)]}ga@VW4M6ee<5a}z֧AGz"Зm/wzq6-g+oh~# FwޅW A $ՙ'~=Mm:+rξW 1DTtȜəpƖږH1_ )UP0xy oC0P>A8?]s`!ʑb*DJԦlJBj? Μdta>'3O]sԸ}vZr[o>lfFe,og>vxH*g;f~Q`G+1B "K:,Kah=C&g[nBƑ 9& ;mAh]DJ܆}_0V=}x6 :a}Ʈ3y9 ;E4l;-}ʳl}-vQO!é"vllb ra_Vm;.<'r7Gb8v%XvY[!<\yv#{ NE.!B6i/b&Jp@Ef[T|RW0.ϖE3K 4 Mj,N_]Ǿ̏Bucc e3ZBNEܓm6¾"Q)L~ ;bzxm>G(MMM:{5<<-ܹsjnnȂA5vgbGΤ~d̓mN3f I 25nhSu yi޹t,XIi˹)dpGjIbGonrZ6FCYMu]w5/ vCzLIЯhqト[@vvkh'm[خte mLS)qx=rUPQL-_M_O{w}^_. 5?$.`JWK[Z4nZ2T߇{$\He\3uj:zTGGotҍj>V#LdĞ1 .wqQԵ1]W* /֎M+S;^ TZ߯#b\7zFMTHEe~x>9ۭ rY*4ac:2u8>&(*W•~~sr,W]K* >?:#G$nOHMThҳuFMX_qo{8G Cj߱JTFSjj{RNʐRǔ}^2@숝K05oUAռ6ynb cHWN6is::ZTm6)mjڿS|J+Z7,T(jV:{_t:]WhҟN?D]b+ӖeSuj;F6j'.mU,xS=/N~Y$//R1Ϩ?ѿ%Hdى[}#2v?'?Ьl>^ xj?끧jׁ&>^_41n-; vC:li6czCyܧoM\~AX9r5ƄٴOIzOCWi鄐x;ޤ򭖔iW.o Uj^ľ\*,,[P2Skv'+!Tzxif֪S_c\4Z؛cu}}-k418CZ\!Kv7^2@숝QTKw"*ڷQ}PTbhkbKO^mPuFB c FިĮɓ:ySmՖ imNMNBea3|L\^bzb/ڠ֎9׹8uUD&~>Z[T+P2YOv^&;וݣkǙx M=&Rc7k2\/8MsJk;tGkPIQt,y0<8 ~%Ďؙ 1`|C]uSUTvN>]Oko#i#DybGˋ_u) .?:42[wT xngD0ǎ;k5v.M :-.n/wsy+Xtx<Uk=$H}l``<2ؽOkB LWۡo6n}uK;7kF\P牃Z54mq<KfhTrDMҁz|፹ iuoї6伣6?V#o+nˤ2$:zIm5=#˭z k]M'LR-xMǙEQgg{uSa?N%{#.sHxIeqm}:?JMXo|E<ѩ * z g@l{ݜ W?>{S6J)T1E _Z#taԜ#oR$rTYPAA *8]>J1F6NjGna*)~e0Η{}'fT@ M_wx$YK/p)eXZzP{lۿ^642H*l=Sk^2@숝 1Tn^642 7A~/a"9*T_Huu;!r5)~Ks mL|RA*~"AbGL`CFd֤~/b7DsL}U q{ #v&vC ~C#2pkRz ԙfSOPFߵ_׍*Ա>㦓 ̉ 25Ď3 P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*@T P*`l׮ڵkvuh׮]'.Ԁ 2@ d f@_~?>`u2;2gGG89qd 2@ d ,·:HYD㪓luC`پ=j@ d 2@ȀI3B/ݺt]]vb -Ďih d 2@0Wml* פOF4 d 2@ ma>M}:wˆ,2@ d 3 Yv>o?;Y[?@2@ d 2@̙s5`meUn΢< d 2@g@㗚q_Fdu͙W5 LCD# @1&nӺ 7,ǃbԪ" Vƕd͓Ea%(I>UmL"Q\k0O-[uikVl QX9wuSb[ Vk.E1ӡT~ϣX׹1uEPxm$:XE18Z#*%Pqr{ꏳi%P+R\%]3@Ҳ6"_2??2RQV!VZ}h11ܥnԍ 4=?zE[AέawbS=e=' 5fǀ|l® Чhd[dr6Gcd+Ѩf,m0P 7_ZyIi HKե0ˀ6CAZթ4O*#''rM Tu(S" Xjdiӥ!{qбẒx&WBHDžŃЩuE$K[GJ|Jzt臝B;FPMW}MzԐzP2l g7U W]^φL_ND f  XXϒR t5\,2 Z]Fls1xSUB4eBRKCqМVNssR y "sϓeW-C*A~NQ pk7:6=fFV&GF}D M@~jщ(A l8>ӹӬ0Vj5r/|#El&&(y~2ϓC}5cBa`󰮰lD9- ax:3i&!LBj4UZyHd]E*^܉Ta#]кo,JBqn:RRRK2 Ә+ dWL=7C\kkbUP^* 0R%W '"6yk[b*qxV4qlVX;_==<xje.LZb1ThWd &b]mQ$ݨ"{~:۫A+ ) ɀ|!h%?zqs\*Aţ^e^f8v4cn:yk͓e0O 0O(!["6C'fp5C0OpETT=j3OvD Cwg1 8o3KdU EǞHQhf ||f*Ss  K! K Fok80d -NFئ$?ea,L^9R*|c]h>T*9BVzVFU8P˂0Dw*| + YaB"zN܌Cz~1J<>GL6"V+ ÷=tNe̓`۠丿sLq5K TѩV1yqg\ǏQ72 s{Pe| z:`  {RC2 t_oA0DPh%1 ,]&d 0]ovhxzrd'@;RԘ kx :'#6m(aK^XBUgՐ.2 fĀ"axuE*4&TGy2#**oa 40h)م(J؋Axp C3cs;<&~ `gpopiC`ik++4L d 2@bsav a&]E͝(=p2@ d 44O4O4Od 2@ d ͓"5Se>A d 2@̗''^e d 2@ dh WuG d 2T <<* d 2@ 0@dHMTz2@ d eW d 2@0&7O2@ d 2hrV2@ d 2y٣%d 2@ dh 9f2@ d 3@Dī d 2@ d @'U~ C ^υ<Qp4*2Ԍ2@ mf7Oslwihjke&(´Ƒ}2@ d@f7O'$$^UEώY0 L^k͢'7?'d 2@@2b?5mDh~=ٗQhh d 2@b(;٧M1mzM)5%d 2@̕':h^E'6ڸd 2@@3@TOǙ5=piS'En2NDL?uEO凌 17X^'d 2@;0IYTg;~x4[Em(zEJwbݽ~vH޹"8K d m<(?苠CP(K P75Oũ)77bAuy/BƦY0Ul%}'eYqϧDZSЀe d 2@4 t><; Ϸa1v>ˎ' 3&I}?{ҷijEH<[臔c|;iݢҶ4P d 2`: y*(7[{QEF.%}޸LPU6cqP 7c͓A?<$ĉdP$FH#-NۈmEɣp{X(.˦΍~?ֱnúeݒ2`fo^]AS#g!%gDLy֍koHՊ?ܪ<Fh|T$k`(D: U~MEm5O?načDȾAx0S2I'Xa)W6SJ{gX_oh,7To,7Uߺx/X_> k&9S+ 1mcݲnɀY2`)='sDR<\󐒽 vERC+Ds˼E;[[98PLWIHOsb(~o{e&Lu۬(|S%~Θ7eS]}] ѮcJrrpLC` X޸<Yё2q$GA~"_7Yi[&oM%WwDߺc=d=a>'7_9:oPK"d 6O;N[3,{-DJz1 .,z3x6?vd$n@౭>Kp7Of)XmS4qzcSb\#juǃ!=Q O]1'"%wb]уhi[Fyǎ͜2'kGym0 g.]sؾr6>y}sb]] JFN( d 4'fkH5=Nq)|Rb֞}Yʴ}jCr.}g0썕k~)m>F g'7x/±Ψ2/hptp{_x#P+ {S>7\aOy8U_EJ<5VU\z Ⓩ5J;hQ%``OWm9i ;J_~uǬHGsV^+͈WcS_<%#7/4c¨Pi{UTiҶ"H=b 1Lw}uEkCUd dG3zxcOxAI nt|rb7 aڈ6Q?2@Ȁi1`iޞx2~8q 7O*9ñ{~|a qk/LŽ;yfz"T¶i<oɕ"s5͓d\N\+ǰܿ&?H~{0ԩf"~ct{!;=Icz;#r $$(,D{;KۈmE"];u舙GS*9m_W ʹY:&ѴƄ :i8o z{EjLSNh y:{sV<(dI?XJwg LP\ ?4?jN]/1cz{lDA 2Y;[fKEὭr*X:X|Io :'`4O88%q~febr$^ن٣îp7spԶ_Uzl 'aë́5F켧F5N xoy:pWvBM'\ܻa㔗C0H]/46b[Fylnjf\ z9/nk7pyھ $6\ [q.8Ƣq=BX1mAp4hV]6b[HD26L >prp{?ހkiʩ;Lkby*))|o)Æg}׾Z"IX;JGy~Yd 2@̃0O"R$N~~>rsst%''x0 +a>y?fn:0Q"H+y  d 2@<:׎CAY0AyWEi0SCjH d @'arNn:Cz6 m0RD\F 2@ onH)ECh8irflqz;B౞ d ChdHA %d 2@ dy=HF d 2@Ȁ < )_Wg d 2@@0@Dī d 2@ d ͓"ɷԙ 2@ dyyU2@ d 2@ `Ll:C d 2@Z''^e d 2@ dh Ne_Eqdͳ9YC&"[Q@ȩ/O^(~wU܇52PlR|H+?z HtS÷#ETKC| c8(յw0Ɵe%d 2@͓yTk(y&*Q R6FpdN5#]s!yxgdGY (HCYq c5'ˈX2 i"uritSzR2>E{lȾW"yxl+MM'UD#4>y֍koH$k`("d 2@4O:)i*}-.љjQV?IJ-nm*Vx{w$*9 7U4y^*D [y5BR_U}$M/ QFܧeCP(Di><(~ ?YRM{0O^G{[ }aݸox#,~Θ7qIQ( {;0j.5*d/G|}4Q^^ gX] <32?$h+3q`;f@0m䇀$d 2`4O:fp4O"//H%-?2-dNF-(-+^D+"\3:S}yꛧ1 *.An AxQٳ;C:șәI hIQw߈asيzy0V_N6lIyR}\p0H290|?}fERZN# A.j8i6&5S5t*v d ]LF g'7x/±lMI2O.2,< Z2{ٛКE6baazIUM?O>n\tKǬ4Mq<{mv<*CZzfj6@xwi:eƑpOX\љ9e 2` y*S&<Yfw "Q^t:HFAUX⟾(k5ߏP\= "T5OנԕyJjhޖH^>CAqNGX!mݎb(O||_M<Ր Sqq<y~xWn#::E_Yw,И̓ u; }_?Bjc F͞/3TKmO\;.# Dci1G|{9'34EdR䆛/ d 4I'U|8^$!iD&yuMJ]@1(:<+F#gRQjȼ*AڨrwAօG(W'l/F< IbrzSyH>D5_86Tc/P29s9d.L<巰 V@qELswüө@NݱZ~yj'E"~uŰw4 5` oG/`l Q,S +{dvpl)'1ޣV@V j?GlA {a 4O&\W=Y72@̟2Ok'xyI݅[{`eFyD":'DΉSPh[e>d-DtO_׌2%8"I*@ykS4S3] #T}Z%MFVx_?BZ4*NXg:Ӭ[œS^ 2Y;[fK{H GAo7x.AI]<lǥ7 _Q~'8$x)cxwɀ quxL)!Xs%CL4O4O&{;$T0Lcv7f`P(nB`D&ƭ>[ш#\<)aXW^?/nD_Ŗi8lⴓ.<[G'WGmpB1P,I++˗̊8\F_JkǬՐyb E\ƂƂ 2@@gMg~aɵ<$zH<A;׫;cAvǁX<.p>@Y9xw^hͽKFUHc=cȉ؇zJw@UNN̅PN1-*? K:'Qp \1+yyiJp{/83T5{bF>U^ū;2@Ȁi2`r)ٷ2WEotS(ͳׅ<ӎcM<#Ϋwd 2@0)LcGĆ5f2@0ML< 4Ӗ4,^ d d$̓#nFZL'6̭0s 2@Ȁi2`rĉ~;|h׮QH+pppȓT1 eEջFDq[jOcc8wvIn^Xm?Ci 4[C$<5M1IoiVO7+C} d @cM*S.'Yᗿ(y5xz} grJ#VV߮Obb}i:~y|y6 mÁ;pXvR@4zw_tª5DeY89 :Ļ ߓ}wNп_G}Z)l~1;A߹'Z;HO2O:2_aB@xcnKtmD#ؓìT P*@Aht+AA=WI;w?Āxx3:>,su;~tsԼ7_Y3k#|w/<1x#R#jNξ~?Xq-$n%~+p5\ X/_i?`ʑ"N^ AXX.pm* IDAT~=1yS'-3:4SԩIMUfS*@T$y m`ܙQ}L߀+Exvy44i橅T P* y1q&M'o N/h!H2~8?f#u_kGkc;1lO;yUt~?`# |5WX*2 X IwƖI"T P*@hjϢ-8sW`ݟ{¨Jn< ^7bFOg&j)Ռ%Wᚘ0⓯8ꑚ #XWZo<#wRh8x2Os_l8yN[(/]MJ Z #O-3BT PTS@@~_/1yd̜9˖-k"҈"Sm_YQ,=>|]|_DǞfvrD>>;xi$6]OXS}3`$J%`b𧎞XyIgZ򖎻2OwC׿|?Ͼ<ׄUFTcd?w>+mBY-24OT P*@Q͓8'NGU=]FiE"/'@c`1j5Ӷe :v#$U/LdT P*@ۥٛ]&t\Orl~Spɋjl[#[3t^^|{{a}IXt-T P*@U͛'1xm1M&5Oe8(xg=dpbէh :|macyrk ~WC_Bwg=<1rl=W cYqF6G 8z*T PP͛yPhRT-.kO ¨Ʈ'\j٩ň> vY#=RV:wr+dv峵CqW}Xm2&;5c, toT P*`fm<==4jb^)0T5OePc^up^<"Z Q)ǀBwe~Q^yR#jx8ي|vpr gGݓPs6Z̓Fa8ll`1/=nv5Eʮ'_Jd눞Y/}=<իT P*U͓i @󘧪H&"c ǁK^P\2QG0A;8'_en r$KT Ҝ f& _T wu'2s8 JQ}N*(yw8!\e!橖{;>CSZ܎ P*@Z<(<FaX71$>,reI'%O^XvQ0~Z_J<)o-z}H9գl=I~m Eq ."ߊx"^D穡;fJB}s\ET PyM~G@'{PVXp+^_{[wú#qfѣ $G8y{:JN:`CUa{ʛ+h]%2<)|f5{U|mhVl۶r~&rE HFPRl{ !> Yܜ P*@m<˱z>EfV&݋Y_B``TU|ߋb;H' @r5.By:L&.S@{}dUE%UI % }o,>L;+(3Ո| ^&Jl/҉*<=ag+١״(-" FMɾ-S~kcxw`xunb.Gy.- {[;8{1m4Ospl" !IMT^WLvX}K,f"cv27 #2tCWc^i:vuDd}.Dz50 P*@xkh)** cǏ+ݒ3j$Ϝ~8 w7|b*>N#GHۋtcƍExOo޴)0A|&=AHUVD,p<֭@M]YxpS}^zm~no(L58鸶[ ';8sȮBRiyHM~a{)r9&^ge;FLžʨˌ[0}8z8;^?"FY\v %d` / ɩT PO6O"z$"F8mq3E\ č냘GDÈcp>sWXR@E{HMjި$LLT P*\ y*ICVS^^.2|>Gzz%& '' "Q#p?>#q+?˗awfZ۰fmT6Z<,*@T (`IL YCDIif)3+iY2?ID\CDߏݰ;~#W.3Xa-O9U+q& 4Sq *@T fmlybqyiJtz_z/=~ B4H#)uJ;Et_nm;I* <)T P*@ZT6O"$={II0 0PYٙ@Zs$''!#<|{u.]~1~SWBYAu 5}5 0h*˲s|d;Cn 8u< (\S<nHd5rQ*@T0k$x+$?OH)3StXRGEK)@RJs?T P*@{?'/oTD4Sv("`[;bł4 w治^.<ᶩg=z-{ &.)ȸL\vw;[ cغO.d 99 g#fwwqS!zo(>݇(4"Vy*W샃)Q^{mRV @IF?dl!6616@mju $$$p7xR󔐘蘳HNNDzz2.qB)':g cǗ/g{*Ӥk>8|ߎ:>Yٹ穙 D' 9"rT& 7m\wCpz/XȬI }j=F|-a9?F)'OvsD"GeR&D"@ { %%%/Z<)ZcǧNrlt$9.^ǽ|2\6d/33瑔^=knmO?"0Ga{<5]F 1闖he' +"lr7|`KN{G^+ fee!//VҦD DK3ݠxb󘒒9d-ztN<0"\2Χq=Ml>s.#55 q =°,6d:D"e{'@#7D9L 6_7$eZ "@PMۯIu*xtF #1-1' bQbq* À[(S9R^"cյj+"@$j;%Z x:r#chX3wˁ*W+\lj i^ Nf@`%^8c_LƜ=%1qj^yYSTU>EwBв5= 5hѪ};4L5C,K'9WY}m ~Yb7lO0>kE$.cmy]xE7ޱϫ(3Ì<Q+o"ҋohL,fa5tޗ-|1>c|'^)T) O* ]$D%'uE9#:+ {Lغ쁧5u9X2Rx*Z?{.%@ު$šWq-GTzTS[\k2oa헰YkpBصlAxo0Rū"~O珕HRc ZjARb7Oa`-Mø"5{i tifz.x`2XCש1|bOA&sLS6lO6ܒ@^ƜsםȗZO'}= LJ~'*#~rVn0qd0B?F :Cۯ1f`s1l;g0عqð9nsa1:Sa ≍zSsuJ@ FEl\Ɏ7yr8SӐ{ CQ$g0n[ۓo Y PkЭ7R~bO@16ESpފQ?6)x(f0ZuG#~ƢD-Wz$lh4ꄹ OL/se!(D"@z=l_ 1wT̚: Nc0k(F<0l;0{ i1u8L煱cԈ!߱;`h9uCvѾ٧DM;}O0xAe4~)W(+]lS`l [[qXv& uQ8K qq_ 7]4,tWfc,DK8D87x9ʜQ5Hi>% O]s8nȕOWASh h(5"@@%kԣߘa Bp,x?7)!> !b8kPqc|xNppv;k6bF ѮD::rOhiOCu+8o (/S91T?  иqckyH{EΓ=h`lO._[g+LM:`/(C_8b+168~Vx,g{c2p]E⩒E]>?@rYlD"@kdĨ-C0q<׸`*' \7F%p_zaj7] .0t@ [p]W K OfCo!@ `M;t~[ץ(~&7naӐ.r"'`4ljʯ`Vib4Y5$o7Į;HY @|v(/OTJ"@ _œunn8Ʀ82;ca71 #:fm퍈QXBXy9^\O=yQ߷vuBPsN]֍\"@ [p%4Z<ݻw7n 7u D<$F`Ld̡+5:Dē&Bu>r9N@'mD"@@#zܹ6{≽ӧx;[/e`abߠNOu_8d=PA# P6@mn~‰xҿ.q]%@⩮,"@ HO_:@SD* D"@47⩬LGpq5t@$@IKP"@ zL^+V?mڴ)'wέ?=㪤&OI&D"@!P/ē7'XoSuCRsH<չ*Ivd#vD݆XWJyRF6`oCa*tM7K Z "Piz6m4hb"e˖h׮4it=77zId*̻'@3 ."-/'U/˄%FoFIL@_ڵr@Ίt|Gꫯ!{ǩK.`Iq;r5kƉ-ZpªSNu滭bvXG xFP+YY~ AK%$ϰ l@1F>.07M`fnc$hl` oƵr /!/Pnrtl3 VBm1z^d>|\a p3,‰@}^AclZYpTi'3xGX wp,GExu|&`zxmߏ̯le=T+^'MY5ܫ2܎ۅ9#B.p|ߪjʧu׎br+{ `awH>g^ZįkBEQj#xkӮ TX~'uV<1_{/^pI6ɉS*G s9̢?VD{k"Ag]V~[y 0. W\ܵ[P9 '}o8X ߞxq=a'S/#+0fYx̴6/t~3k8 f>8RhTNvR; D"!bZxu s@RJ"N~B xIYf@x?ȾǝԃJ:izT,pik#"6gvG?KS.EIT|u"zg &&%ҧy/\:Bü?]h_S[RM˽n#ykƥ<t@>Uƍ9z4YӋzذW^^J p-8.>{癬.dL~Z3 7$UM}= 籘bq&Y_k_}&"R'0qcx#'= 05L'1v솻y/'V|!8[&O==`k5`"#oUDG sK.tKlC?sWl,4S+M"' xJ"BWlfI_2Mgo@>zPy^}=(éSs; =6Ba{,1ԥ',m5G3+{3n#e QzMMC !e'UD{'i7ŌI1,c3۝^: \eik#jykٮkX)"x Ή'f\M6|w޸| g= Z7?/~tg&>J1f0_G!.q~6vDڡicj?zm5U.l97I{xt c1bx*JZSp䗪2UzF9bL]' s DGF>_|aDn[cItTXm:@bb$vK+֥HIe{q `ILj.vl%ڂ{Ԑ'8ŅM܊={bq OKAֿ7lmz?cz!o X?bql,T'iܚ!!b,Luq^Ebcu#?>#=@> ĐLU P,M)=ouIR.\2,UįUPFi5aOΉYuR<1!xl?sL*cǎpJ&y۶hf6q8fGCU1 uˋ QXP _ m-z ak౲ ,v O<#*<9 lYi2b]87w zYn cUO,d% ^]ں&gM_U[MKƲҞ P'Stt4Zn-Bcǎժ6ds>c4nRièX.}É0h _wuƲ3 pBH^_c "+zJrҾ=L.¸qC4j <.Fw431VoL b8[M[43lu7WJ th0h^IuēT<}$o{wkX ݈[Ƅ^tX]T͘> ְsp\dS}1`Dost71 lajҊU'#iAj;C; VVXpR*V/C>.5T'Sĵt=^p9b@4c}*__2 Sl1MwJm=_'a_l(O3.rgL?z<ŗ5j9V\ (<#յs?D ʡuՉ"؁ZTaFLS)$_}[/V\VUY>J<ڵ6Z(auRÛH_dٰXYSnt^}z)x6AwM˴&ab&O8^쁧5u9X2R.4ܗWͿ~>2U^a%L8Ziܣf 0%g_ho]@Zj*R;'9{a{~ xx=OPΓ\ HW0b;b|],0$"*:OS[[ 6'i^B`^XznRpxTP^k^6lZ2^X Uv$U5L u*ENdW:T~,|6j⯦sZRS;Պyk󿦩 ee~]%Pdž1Q^nmCT<}zؼ'އMäc_OӦ,Li*YF[ȗT;HPWV&ptOM6Pt [)1U@^b-ŀ}dT*‹3>Gsk!v| 9)pP%Jpu?c"wy? J9EUЁ4U F( o_.\X "Yf^-04d2DB0Cc A_k5rs<'Mi ߔYGՂȯh[MDkW,WqHBcHjȴJ(ƧF(W샃)5|TYvQwP|e<-OE#:<'L肵/Z+Ո'ˑk Ĭ``ZC؎8ԴdD'sX،\VWze m0(/eJv,|Uu"Gݰ= [ 5Ưvo#xkծ5!^ Pg3/Άޕ/_ |}}!3͵!'ܹ\d 63.1w\*?lO, ~lvY,|mAc8SfX? w"I5m2#3#\z@`;f͒Lk_y0q36+#?iYۘ8rX1VB 9r=(W?Fpcf-1dVd<2"Og)X7e,`j*uowL=,YKPqz-©k~*9RPX|pnxp4Y1ah #R0= Muci2f0謙X+faU⫶k= + Yjk_yF4֢]s1a"'xj۶-ѭ[7|J,rLXI$*>*;N<Őo#(yzٰ a{l] 6=midKobM68VT!fؗ9F\MVEUWE<ɋRS\:~tK<߲SjD"@/^<12QĄQ߸z=z4wHAҥ<͛7kVRt:ΜFdIܺCLFNM$B$3GX`0 AfO ?5 :#gpSB*͹QF [O8x8nYA&_a0d  O{G@hh 15YElեOZJ垧!X)H ,_kV;7#D"@@{4n84nx眝9Aw5Cɓ's֭[ &y]CuS߇⣆hѶ#=Dwq|t*7W]4T~^]هAuA4i kWf)d\Owß?Qc4i%w+KSgV0h`JSBASUQyz f۠IaKt{+םQ(NxjL"@ Dk ;'WW*PЬY38::VsN矸sN{ 1So J@( D"cZ}Ra+$Y"@ C@œ %[ٳ'h O4C D#PgS!@Z D"@>OuYx222pX\:P2 D" :#ݻC̄931sLa~Y0#?D OU YYolb23Z| D"n ĄӼy}v8qOFDD"##:eamA6;ZJ.Y۸q#6mڄ͛7sn˖-Pt:1mذ,TNI͙$A'ԤVouuLݽb\?g. G @O@@@<;wr"!m\, ~zSO"]GJ˚n:Vlسbطoۻw/T9?6Y܉6D7~xZE\rM{"@euB<͚5 lj*=&u=vgqV/C_T<~k.(}mV嵾^TaBx. CU_/VGZNrrna_!A~N23ȝ(ˆgJ/UixQ/ 1mY\r gSRl' 񔐐R„kgasT4֡T<9cG<HT.* j@ os+w,=Y0ؑ)Ҵ/<\7ϑLp\~U{eT ]x t b,cBh^݁fp@|,EO*9MݎsH9e^uDzՏ%&Ƞc:$D P'39L߿_] 13q)r߹sr_ð8X\o~J爙f=N\v]WtW1_T>mQOZAZiYY]wyKe83 0J]h^JE*ZތBEP}|A$)@/ѡ 4BϾàRO<{|c%'5p_u֘S-ʪ/Fa{yS!v߲=𴰆_0R#';KZjOeXlQU]ҋ:&yܤ#gVfPΞ=`GPTP)K ̏(4I5mb ޕxQfh{,H0LQ{O i#ѷ%LL "cX8VS۹bΡUIqC`쎙{=Tñ/?.=,`j"]{ydN.Ƙ075-\|`ɻ^*?s {[K_5rӢxWW',jRqbcr3;|J87F7lӎ4I?>/^pj/"c+-].7~_C)ː},,}p4F DPnN'f] }a81Kzl v,saaa;]g{h% ÎY,.mu#߷û`KN G bBh {UyYxi83رj&vmA D6&ڣSp $ ѥ1ƫ_`1 .>F D7uvxbɣ厽0QĄP`` g B&7r=L`vѢE]Z!@VSD"@xT $utR<-_@qqq9 s;wIJe8C35V}g"V%V7xH D"@:ꄒ{:)6.11Q1qĬ۱*/\9v̮{|,mDH<Ք'D"@P%]I/1LcasmDH<Ք'D"@P'TIt=Z[t\t3LW旅a뭰8߿H9u$j (D"@xT $utR<1JL0c@AlnӒ%K:eaXXN]H<٪"@ D@N@PRuOgœDt@jZNI"@ D=P%]#+$F.2;; rĀj ԭ6ֳ|N(7⩰/^ġCu9s6L D1?A۠q"yD"0"@1{l*0XqS%]yQXX7)44/_ SyL"@ 5#~333JxuK^ IDAT\47oʕ+\+s琚ʭĮ_v3F0%@uW86{0F D}`C7UI5Ol\64u 92 (&c&٢+¡c"-OڒolL;mD"@@!W'TIz6k/dI&dI.[ .&rssOk5$JR D7&W%]9+[wFY+<O$M=_41Ć?ub=P4,獛YSs~JI DJx:s N Ai+N<1u*y4DL8{lYwH&B%$^?@c"@ D&k:%XӲe8Ì?<}X.{px!璓8D_81Ć^Kj$Mj#ASγ!ϊ>}j+'Tԙ(KmvFy2ݳoۢA4n? aщKbh?ÏȰ9uGEίU߇hؘ+HzPV8?xwдZ+|4FĄ' WeH]b &" G9~Ez ju )U+{O' PD"@{%W'T)ώ;pU$93K~MDqwPQ\9s%tQQIL"FK4b4b`w Ҥ#(Xa׀^v?w Â93ss;ĝAPܾq[bBs&$v䉉ֶm8!+gd#8{ۼgb(9X 0o֦-1Lrk.&D",, aaǭ%j6A15v|B?d1 jXTo>| Gn1ceXj KӾػn.5-ZcґO~ *,>f¥L ^DaT-^GOQӴ~=xO)P2Z `7 Ӄicj} 7h]hY>ȘI  /J D"@*/$Hbiz%O>>>;vja>y'ޝ(6-n'"\Z46u/(({nI:6&WD 1`}$wk|F_LeT@UF;,J,k`[+|Ԡ|Z{ֲ vt'uRzrrw3|hL 5#D 03Q\D=,B3zO%YJquyo40+|7֒IOyRaL.2rXEb^ !L DN%mz%O7niJ'r\Zbc΀-L6L-Lsgyɿ-MX<6yL{h`mҴɓ+kFh1nUXyז)\W݁L9Bŗha\cp5G_OA7桅9n 9pltkq>SìܤզoۃM0p{0~L|i i{DRfDoX}yl7y'Br2ϽUúMW"YVM huj;"@@u$ X^ɓLLT¤tDISϞ=E%Pɷqe@"˗/s*f aUX"ŷEӥ8֦.:͉i&U)ZIZfazaZF]rC0s"$GAkO̮M _lE(,M͟ѐ" ۟"vH?D4nD*nL0kkHJIrU=w<p`]q>].W5RSqP("@2~1QڦWĂEyLvT ǖK ll 7[{n%997nB벬Gܳ%-afliy9Eo)().,vF& .[sJXt?^Qۼ)mz\vyjTTuӸ~ZOP#>򔛸 _ՖCK?6@+#<x-3csm}yulov391ʻZ}!%0o12@X?ȈF<QGWOkp@CҥaTWXE1'H N O+HAy=\:wǠ q[Ne`"Ft4@&D"@JE%mz%O?A%NXP6dIsIuƧ&C j ! bB'eL.cZ'kغRPQb#JusZYh{~l /0O%\WMNuĺD^Xۯ9Ϧ?Hsߪ&(V!YcS4O%0 D}'P%AҥKPP&$-,{gw O? <BJѹ;aw뉑k*x 1z`O8;:4/ y7ЕBI"D"@Hd{ػw/(ڵkgTz).&2#G`ŊX)FD>/ ʓ"@{=mz9mO/{^) I?jmbpJJJ¹s8g'M^&$Oefp/0 D"P*{ !A<( טjH D _L< $:Si ajM DJ$4']I~ՊSnzZI DDIhӧO rr'@TH2CT/+I"D"@ʍ/$Hbi-OXl<<ڵkE*$Hbi'O1˗/Ν;!Ɋ4ÇTIDHJK09b"@ Uqz ،^L<[ qPzQyjQ y0ԕ^0(vAei!t9@tTsٽ/N!AK3(y"1*Gc^^^Xr%rss# OJF Ľ5'Xƻ}mQ6h>e%K}s~maalڶJ2KNWW4W<ʼn9nh0p!Τ{n)KEȊ pi),Mai9IU;4<ɓI DC% &JB FRRR`NXhr?aaaE9ro}ѤF<I*S:ڠUkMpdF3=Fn(5罺s^y>OڨmJ˞kbopjDpAkZ{s(ڿm-Jyq߃dIhf)GJ 72G' '0Y]|Rr5 f` }pY/G[3kLKWg su"/@ F||4y ħ0'+^/u476pz[9 Q{54U'20 D"@JM@HZPiu,D/ s۞/(AnF{3SL䯎$MˊF0^#Xn^g_,b, nbvh;56&g(k|W ډG)*0;_#y. "@ D 6'ME @-,]qMh)m*۳MYg砙YsxĨ' k'lM[,ObyylujFP϶ l,ko$}#qtt7g!Un<\Q D"PjB$Vݻ.54|yo75 _,^~l`P\YTXT~ӘGѫb^ѷ]by&.זVmB 1ƴS<Ag狖Ux᭑<^Ƭi!t9@tTs@o1QVID !yPԷ CgT|\o=*>Z\pڞ*`7"#'F+m0YtŁ0XܰTan·XYzov#fH*~ D"P}b m;yAy }_-1 la :`8fk"y*.S;آ5-0݁޲/s˯0~ > >VT.mO{^9=Y8MڂHńvuT^{N#<,6Ic3qZ\>eil$OK^Gs D}& XzݻyAAAXxq^G{)y$IJ/+'7MaV&hw\.L!-*}Ӻ01 *xy ۠DxRDܦ&5jim4s'yISC0WG4g##cX}z5^+Vom<䩄Oš1b9hh G,6므){kc{t8ɥ'~W I;;;"?/֓ݵ~+F{`¼d/rꃮΎptsq4eϯ`"F hD D"w~!AKkyb#""0gܾ}R[n)33S:*Tu]{1sokn8ұ\\:ӈO3CqeZlO߆ g@t\}o0ݵPc(H9q{;& 9 ky\(lyzᆪEChTm}roapmxFbFE}v b3'ܓ^cXqyI;0sB͆/{Q* D"PUbLB~uTzz:7w=&`0[!OW7c#%Hԋ%u9yo$.?bҒR-J9W C@Rns;܊!#.:D]P&L("N* ͛7eCDbHHV"@ D@/T)yjܸ1u;i$AqR {}|||мysS8jj% DK'WT3fԛl?$ }9R=j5m4;$X|VP̚J!YsZt$ORB B DJ#6'&A%œ9s8/jqbƍ+&NN\8ÿIKJ) O`p9y?C?# PN~q\NLDb5<~=|{9_6; hh5"@@% Xɓ'9qbg>¤.o4m#O= nbvh;5lд[۩>lv-ɓ!!B<܄,17]-O{II TA D'{( mkyJhӦ Zj۷oI&Ej޼yZj l&OylujFP϶ l,ko r)7?F陊ATQ+rkC"@ /$Hbiz-OdG&Pŋ&uу)7q9ow0m)pb[O$ l43Ř] qrO^(FQ=uT SbJO Dg{( mkyԩիVq&PlMc@&Sl`d^HqSN}St(S1{d{]StW0/}^g^(LHʄ_@ Ta"@ DT~!AK[yJJJROcŤM 0it0)'>7I[{ Юn1yjg1B3zqo|hx:6U)^|ؼ%mé0܍;Ξa[S7UO#vQЭ1W<¿`D"@@)b$Moi֬YjydG&u Mѩ5Lj@-ڰi%,F }tWMܸjYsX nx8ͬnZb)1N,ͭajdzMx*U@Ͼu#hKc#Yҭ9W <Ad#D"@~!AK[y۷/@|4Is,y1ՏSszZI DDIh^ɓ1$\4vjL>,S A6*M DL$4ƍݽXua<6mZ,_J f-_@GD"@/_L<5nޛ,&M{a%@TuVe 6Z'D"@_HJ1cƌr?&&&oe(}3`–[ѻ yzzs,j85"@ e!6>e z ˓]nxL4?SǢ<+<ʻU ȇ A T@ DG{ B-S+4j/= nq/L-{'9^o._Fin'Vy*H G^)/R *"@0{( m#y*y*V/EF"/5BM֘tVIz lMagx\UJr&}D`0n-Q[sAhI F||4y ħ0's l{0(Ov"—g:Z>x#/ԋN Qs`V F.ݏwg'pz0  3h3B^KI4aTu궔ZF D0{ RJy렛/aQNQs.,B+c :[xu=?z@4·pV7S%b .#D"@*/$Hbiz%OƘ1cF#>}:LMMKW)Ovq*4O`FyRwk6E >͸)yWٔgdG_u>cXu}ՁӤ-=x_LhWPع0k l,kŃg]agcc<}~`'?]k'alhKX[}6 CX# *J݃rWT hEK"@ C%mz%O7{dy6mڴ|i15Lk5MkЯKr*ONl`fjgvC2DRRl:SKk2C/CISH's\N-aRjֆM/1`yYǻѥ1,ᑔo'r 6Q X٢mXT"]z'6J' Eua…ecw'+ Csk^0v7zB]S_@5sʌ"@;{ <;fffغu+޽4Hʩk@ώaDzt D"`~!AK+ybe2"""rJxzzr {fIh+v:YiY5M꠵$+,z\Hd8}.5_@%/:"@{( m;yCM6"@T^$;TkK#D"@ޕ/$Hbi%O Zm(/$OER_@T;"@ D] b$ i2e ZlU6ۗ>D] <+A85ZS- DI, IT=meI'ͿVR D_LXjjj% DK%mz'Ou) &+V`ܸqH$hԨ,XP0&O,OVuyc7@ȂfD}vq/գJ"@ ՗/$Hbi'Ol:G} ?@QL(<"~+Fw 602Mp8$,wsw򋵞<Da{GZNS8X/+6TO ]/\*"@x~1QfptNLLL_̙31uT8V"<φ*¿VR Dbx$4bҤI8qbI;w:lɓ U^pŴ6c nTHv)q[ʿ9݊i{I5 ? Fen^|aRȓA32mOSϾ?@~P>ߥ&y][a$OEp"@ U/&JB F~w$M6Mݱ,x}O6mRVJ#OgWbXO5k >#dlV_aܪ =Ϫ*'H ũ-VmџDvY3 9]0"S;آ5-0݁*F}InQyˈ = sG]SԷ^lb[Ir-BkD"@@U$ Xӊ+8Ib/ Peff&֬Y;v _p IDATv]xuq&m,Q0E#6ia[4>e-Ѥ, U ;\F5jS|i?; 08| Z6Q [XΘy:x!1N.MĨj7cw'C&oQyŁ[¬N#|0;BQեW*۵Eb@t9@@:4%m#OW^Ž{4[TbVC5".B`Ҫj* D"@턶<-Z-}3yK8$OǚJ"D"@@e$4m۶q“ YLoyQՋSoj- D"P= 6'UzBdd$^x\/;۳gO./'}@i<K D$ $Hbiz'O QfMnԈuaylذ0{j]H*?N D ! &JBRgϞa߾}YE{-{TH#@T应D DbI,MoX(T  ME"@ D6J(*V |q& n$"k5%r&@T@);"@ D!!AK+y}6$^ٸG@`N8G!8xsCd2=RU @Tz@ Dqb$M)#3o߾AZZ^xO"9.^s>C|pDDcr8JJI䩔hw"@ D !AK+yb6Tę!<GFήP^X~|UEA^= $yL$D"@7!AK{y\zO=AD$^)??~~~޼}[3A~Z&md0GT@']I~D"@0\b$Mo)++ )p\SkDFFb"ܻN?ǎGdT||d1 jXTo>| Gn1cE\?ti%y*yM8z2V"@0PB$򔟟 8r(=-,4wII;T>|޽SknQgςM퓍)wXZ!g8xm\&Ѵ9#S䊶-lQ۴&jZiGwy/y6ͭalT ~nj<=ѹѡEAsaR?dO;IBk䙉X3/oK{~EnAj[Xvnt%2Ce"@ $ &JBNX`;wr/eNYYܻ7 W$\ILDh> S~شy 6m _ Chil۾W]텺ApodO@SS9ؕGr#:ܢ-<)C\hw1|Ctl3~o72^Y/8{!juȿ"4,~NbͺvznF{3SP눍,(4rHSMx|_-M)OfD:_Vfϰ,dMik̉.>S+j cp{oƍ giLPjy$OeQ:"@@%mz'O!!ؾcV.u" 0/_ 5!"2k֮c?XxG*sn·fIMYg砙YsxĨFX->KyrAy +K:\Dz-`l.ԒS7koK@!O786'칑XH׷=M9ph.89wÀӱ? Y/җ8g1ƸY>?M*$]GAkbn\>7z-z)T5&ݿ]&EjY45)lß# q8 /KC~/:':K\{D,?zj QS4;/0bmB^ K=f#vԿ\$NtSqI,su.fh"@4ڼB[=;痒n&῔%55.pC7L[)و[3 q腅)Iu>p }zvAqT*dz!q  {;''g|d/GwGKs5 ='@x5ŝS DXX8P`KddpArQF n_}" Qmq=gl9Tx,zg7S}4[봽b2!;OG!:GWS1˲н):͉+i,ޏ<pF:߅<<;T8p/aΣpA~?aBz/,9[WGw>w{2<>9 ]x/G~B㻰ga|^Ɣn췢Ncnl=Lgdܳ])rAQ<=: oĤDRFT3"=х $@-Os}=WyKӱ:MAW#yKۆS!a>+wDNҩ'{;8Asgd?ɕi7,=:n$Faw7Y1Y )iLE艪[As2n72ŗ'd_ÿúp3.1afD$F,uHdb r5W@rNB`` z.c&yfZK{*l߾_U1e4;%X4Y-#XXE{WLqY1S~ݧ,T%teKfFi]|f7i/)$pYݴpEaFF0?ݭcWӎ*G Q#mOs\n 8K ȓe~c4{n!RNX;üui&yR}ˑ0G!Lc`>wƔ }fcX/gev_FVLES"+ ZrކQ>ב^:>[[) 6&r"@|$I[ӆMr*E,xPp%woV<-[N7ǪCEc:nz߅nza5pQ`T<ј ]ѭ ~7'(Ciť?]dS ln}8.CIcx,eEgGI$>!u L+HGڛź4ks9lZsu38a5r0\%Lda~ Y]9R0+ /q[S.^~ŪJP˓=:I, U.?^rj. d/pdzonccotcW/;.pwL4 }Xɓ)M1&ej}"@ N@$iK+ybKIQZq#W?k2,djC2{,g_]ēO+sCr (xQlȡ'ڔhwƀ!셱t)+XL۵ 1=>ήp>ο)mO<1. )3q?&ݜ8 vBٵB= ZɜI\&c.`'|Jb`ԟ npu=f^B\90zrI)|F+?M]`\ȤHE D"PYIt'YΝwq5\r._B¥˗qUܾ}?˗/+O\$PT(o,:ދhowtd |ߑJDѮD"@It'T VLy{).{g{SJJ ?sx_AFFo>3"P9򔍀yѻ_/tqdeꉅA)4jO"AD"@xg$I[^;2 DRIg .~ܽlS90eC Dp,DI(" @RID D" X ـ <pQՉ Dy@Eq-?'Ŗg~I ,,]]kTD{"JFfY`Yw( qѻLv7 PNcB Vjj   RiaF%OIII$9BdT$AA;ٲe+[n%dnbbI:YlZQ뿓KA@C4Q2t̨:}C/z{Bؼe 7mbm+,b@\_nB^A@A@"`HJ 3*yxGwwq RS/jt8!pl̦͛ٴiϜFV/I&.Ayz>~"  Ձ@idQɓ$A&(IާHH[={[]H8աmD!OըDQA@A@xF0, UZZaa "..V3ࡃ_K@SyIA@A@J%CǪ✞831=[XBv"CA@A@vj!OO<+#j_Kna{سg7Gr/?4٘.gO98ÛC+|9 JZT ͔{y=97JHN$! @e(M 3zyz1*|ٽ{7Μ>{gOi dw06n`Wp0II<|٪ғY?;-lDԬ׈ 7\B*S9\p?L3ӭF<۱!O kzd}(@!2=!]/g7JnJNg[;5Yp`Ɓ>cV__/!CGЍdŞ  " ŒV233u6GkJ8zǙ322;tXcG`]P{{nvdQ|U䣺F/qϚ;ضn2f5ԯKK  @U(M 3Jyl۾m۷qidk6ii /Jҹsw>IC׮[k4:xҰ}  GӻALjէȌCP>u0&u~Mq,`;jɛ:.**۽h6}&-݂Kb~noQ?_ky *)lцf5n7y=VdA_ǥs c-BywY65iɴꇥ0] ^>?g,{.J~MP84" kJG{mi[_MNmPZc2;\WX:b0Ga^#S]칈E8'ݚ N̿SgO(X;uaJ2rplQ(q3 Gn >!7ˤNG -!O8MŪ=;/bLǖ$S-<+A{~>'c6L,BJ% {c g {Eh,DAA@!A*-IZŋJNYw9Irt9N>MLl xR/Y®?EddWٳ{0\?ͧ}o´r4kww ;% Qsta1$$İeZG>.o܄jq) K؎% hAqIĨ1Њɛq$n'ގG@ӓS_w;9QdBٳ})m?㭺3N{N|lxcZkzM@x^oMi6X4?=c'1A2:d<zoFDxdrq2n;t^}yWӢnJ/U2FѨ֗L1Ыf _1w&4}Izm:(N>8E4֘MZlM;Iu/aVy:DGkBBp~JMalS#w+K=!Ґݏ`&_Áx&8Y3f Tzqɢ3KO=^6գd =MyWXahҫE+O8(:ـ8 'ynLWwM_G,,ZQ\&;I۵8i^f>I+6 ÆkSTi%E₀  `HJ 3:yڻ7+W2{>]`>{BC{f\ݻG̛?30}gNmRՑŤ(}zb14(oxhNK<4OL002X 06ycgpFg;j F4G>~Jg33 E9[)3ͰIMᷝ_lo?qB#G!!ѣyiiJsSfM;#=Β)Q֯C-<9ujb>RI7觟yГOj6$%%l\Wjϓ12+jx+߸U KQ AOג2Yvk3M^mO(_l~'UX|<͂] IZΉi|v "T:jD`c;Ȼ?Π쨳r8Q1ti\!oٕEO5Koek-犗`6P6,;}CwY[^j9/)O;XZGѻsѦ`wr@anc,wA^1&ӧffX93`zw{{q)lPg:zq2OgpgG,ZcfiAKK j,UǓ :+['vUTyaN8M@w+-O`sr_q<1ƂAڅ5t%ffGVWBG/A@A@|r$ntpbfϙyѭxXx5E3kSf[{Q`swX]̝7I}+˔!1zWY\(OI,dL^s4ߣ|1yR~2ymĉw ~ںr#%OHIBqOuǡ.y*ѶbGA@Br$nT$ -?kfdڌL6M#ScǏGn|)̀ߙ6n7 oC~j^+Izӭ[ʽINKΝ;@y \p|C`r1#=Ɠ#WgrA6tUERGA@^Ir$nTGZ 'Wz"1ʓn$#ͱmQ<_f@<7$+%sJ~]gTUE,2.8-QX6x*!ɏ2I,Nwg{vX(8t`,ʓU{7\(,lp zPJ#: ZlDW-/O6(-m1:=1reKӀTA_e[*E I1+yTs;z:m-䄶~WY5 p?S+rуUY"Gt0ЫB^剛{5!GAt$KSA@J' 'IrB yﳼ'.|z\xǎqxbLŧIF4vOPVUtcd֩:Us77>5}EɨTm׈w-qq-^_%Oj"&a}ӫ)s&{~odNТºؙn6-ҡV˗gkcE8}n'OؼSk%a{[phWLޕŴۀQێuP,MfѨZ=2IfJ.ڽ[FiV5޵[~Gފ%05NB 'Ճ(R)nJ)Fr2FFX7K2ȤIe},呧ʢ[@>lN:Ϛ!jrSղ O=9%6]AJzO k+&.h02=#_7+͏JZ f$A@A@" 'IrBLnՐ/ Tvжa}Oɓ'lDIJJْ I4iSƹ)_ -z2[#|ab˲žlb̩ b/ + yg=OG_w}}--*Y4bW[ZLS7f04yfXڻ|C4bVMOG,̰sfyBre(-͜=NJZ+@7o,,yX9|ϑսOuަ͜'~]2 y*q%KSBA@A@J% 'IrF%O&&&3RH5ZjU \b=[`R.MyRgnhy?Aqŭ:Tb=YSm;c.yRiihvE6ʈ  /9I 7*yׯ,[/r=˚`tIqRK.դ)r8c[+V]**Cj'ε &>Jc%0_mT A@NJnݺEfJ|<þ۷/4u)f~K7k{+y -U\Sl7QjA@A@*B@NJT*={6~~~MTR#RǥxRפY|`Z 9w=BH]A@c 'IrF'OA*!'O1*")A@Ax I\xǏʓtL|" 䩲HtA@A@0^r$^-LJÇgʓtL:G:W|% y  ?9I $7TO.I y2A@A I\x"R*F@SxA@A@$I.\SuleQ*'J˓6{z3U/Ls:bIk*pKY}@7 Q* d$}t IDATI.+W.jJMpݓYɚZT* œnV^|r:kr45uU6ʋJ[}m~^ _b5=osHϹT\yU4t~Lm%"@$Rcdov ,|v1pЀ*B.} {$=:9bma]fێ犽+x9J=Ƽ;l˼YZyjc0]P(q:%.2 VѵऽlpKV>G7Ks3,lq=%nͻ-<&3QRw%^m#6}~X\̕^+p'Iine׷D}܎N[.,9[ɽʪmp'^ɰ VrRVfʊD͕Ht0Ыe(Ny^ aH{9l>e–Qش<ȿJ[GlFv*Ŧ7U~HX'{ѷs;lpa{Ų_~y\b*\iB|[^>r$nTK˺xMnɴY?3%g|ϯËIJ|qLg.N% T^%M6twͭGco5:s򮰤5NȔNzzy] Dz/3, U.|,:Vt/[W:7YfBav[e{8V;t^ܩD;YɓalswozChh(GOK33'Q o,߯<^4M/LJ )|_dInFP f@䥲5=8QrF]xZrr`9w mq65 Ty> .q..BqRj{S@:Y#F.&Sjʯo#~Vg,!4'#VJ| `(3q3si4zObNL =˗u+e\>c8&m'RyC-jFbk=;9$2΁kYy1\tK!\-X7Qg[[Y3jQe҂[',^rX5Y?eŽkVu$YYV6W'Ri=A@#9I 7JywNn7/ݾ'Zu(zL ј}27͜>ٴyn;=UcSCި%.#Yu-b WlV4d\ɝ˓5Ej!,:P0^}vgITADa8|^5MtaI\Lv3{+7zg3Ggxɕ㥖,mM CF܄=z,˘6kS9ܔJI}qzV)Wv0q*[;dOJ,v%͸h)顂!A.yIa~O+zk7Td8/Ճ dt@eSXf&c+eg5űlRŢ(^mDZof4ɕqi6? W:ӗP[?v~OTlrSmƒ1]H=O#GаGľ0 [+O WXAVwjlʌ:OлI=U4|n`IU\ɓ*| >t'rJ>+Pvo^;ơ]3dTڞ/V\⯦$e -l4{' gϑr.^:=OGw¢X8JBn"5CR9.-aӍ[#b'-ѝg|/5` ]mO[gMx'˻~ms2)S,ASjH\>Z.Ƭ=cܰrg P/Q/+Ory3 膹m/8v-H'hdrhN/JWVsqbؼ$`ڱLy*:ʯ)<.뇥S;gA/NdݪNek<~n6 X~Vs^F_{^E}nJx"x20+nM^(-:5s#.yTV|a{%N KG@NRHG7:wu][̱;wOK. #K< 64>, .Am>T _DoEzM7{{ٜu/^;IXi5)ONbߘ6``p>my6vst#Љ?M-z.rΟ,tF={V`e߁nIn8դ/Ł^E! 䀢uk-qtƤOF7kxm;__uUվ`r>OU#`[j;I;ڽʥ',;9ϹK֘YCPr*ftwqRs]Oaǩ{ +OyIs6o4(18:$uOj2}::`af31_%Ke=5v(Cmͱ`0PV5')isK˼.qFS PU+0C*/ 9I 7Jy=0kJ9}F?cF)ܔ)pӳFIhս8_ɢGr{4괖G<5S0^ҟ#CFKs'\y˒޾C7we1-6`te~ X9IS< iFs{^]3Zyzy74^|]OOvΌ`rX4c?U0yz:J@EXt'QLSV+Ko^Y51O ĤP{FS0QA@c% 'IrF'OZ0z—: ^~Z`_`bs-5gsf/lI߯ӵIZ("2*nݻf9s޴m<4OL0AZSa,Z+O;P֯ Sԓ%$2N7_ 5kno?Mhjq+TKykC& Y pS8*:'QiE'=FT1NȻ[mUߕTU))I\H98Y*ުD /$Ʌو%sA^zķ4t]_b>;WjV{RL7?ltM惮J/7HgW`^!I^ OtVAW>R+Wu,kŽCgsf\*hjdR14Էek;>UAJ I\QX6l3 >s,}-3+ iLHTIJJ$%%lGlvߵ`Im=iީ&Fv^ yI`Wo8; K&#OS+OEUi}Lgl=ʭ]) Wol.E/9OKPA@A I\QӒ5-ٶ[9ټ˂|˸I_j+>Cv۱pOL 0o3:w ?>4kZ3Z8+ȻAgz5k7b+QT4EZ  '9I 7Jyڽϖ#혿gX _s4=6%mGd-s*i-[lֶ_=<gӊRӁ+w;|/ފw׿)|݅zop-m?b KfΨѽs.9],]3pqptA@SePiyT}^QrA@$pLΞ=y&BBٶu+g放oi_ Kx#<!OC$_Ţ ! 'IrF+OnEp6n\Ϟ=a F~\81M=fPvڥ9.'/œ⋏ <=+#'!O/Ǖ,j! P9I 7Jyg_t۷o,;.oرcDdIGxt}[_`ӼEZ\'U"$G$ոE-A@$I.Ir'm۶p dL‚iI'i ˸uV%qx{>7L<ql"VF^AL yM}A@$Ʌ 1$p'iu$c6iAիWrJ̙C66<>_}2W[ĉJH?RKaO h;H&3 -x|"=KWs'a}Ұ 5LLmѿ>6㽺5yxä6~_,:#\eQcU/1O^{5jԬIyw3\"=sȓ7*q~xr qJ5c'U)u̦ 6ၬ݋Qi˒gngSe^.Jh\3֝^ P]3;"O8H:_(4O}i }z8 ΝlɩPٷg[A@$ɅϿ&u[!,&cQ6CoG񌃟|﬿Aa턎|V-^K.O$큙u'en| Z۝ƒ<@ez0hw\K߽:/zcYXV}:GC)+\/cU~y³j>K\AI9>yCX9\sx뚧 gq&*k캪Щl\1.] qa8^9ٳ G¶dxH•V= y9P14z]#==TiN/G Zc~)/=iѡޯ'L"0{Լ lbČqnaF{U@iIC$@$@B@fd&e{z`׮] l-P'a.x_:D>S P3fz(G7kZoX~{laQ`ٺ[--𞧢ìЇS~4\l1rS<)ŵK8u@cxY8 A;Nc"]`cb G28c<ԲHHDLܤ̓y CFĨ0?3ga($mdظf S tľ^CТeKjժb{ 6cg60$F:uz>j (<GOAIx] 7g+_~Fȇ>3lay:fU'qBd Š` /:/Jٌ^VݰhnݺeQ^E<<}Ts=MCT) cyN,N!f=G:<ƶ:(Tq qXX ( VM Vň)*K1Rb(^.N. Nϯo= xg%_G1W)$=%Oav[t: ;{qaؿc6;Ou<~B"pIZ4 vz);qz S!B NV5/]LC m?8ӉI9Q .`gEC|aHyR^mGqX8 v=aaD' &l/VM(HSeߺǢܹ38 GYDIq 44ΝADfLkqۑ8L53i{z>," 0 2$+7)$~i&HlׯǼ1s8^QYn>Qf @~'ڡ%>z4V":?WlGƝp4, Øv+5Cg7BdZ?D֟bƉ'R<53Z%]#qxeOov _XL ìoкGOD"<ﵭc4Xy8:h3 8腐\ -;0ځ=QBD#^3Ʈ Dĉ`,^0ba\o_'Mtf_U303lt 5ȑC'al_Xu9x\܅I\atqB\\#D{/>.6N0+nh+:EzBqi(ɉ>NfT@( `O 6~0r%W(E_W¥0<*1/=NQ@TChI$@$@B@fd&e^lܴ7nM4ۊ+0ud|?i(V.u… 5uE;=ѮeC۵km'_IJO}0Wn{_E_lcʟb稿+o r FdTk8L7ϸWZ(o#x`|ѩ#Zh6o_]+}'uWѲ]Gt5fR/6kNt_[Q[ױ1Mj:'MW=U'om:~~ӷJ bRGo-`SmG}fA=їbSRE$4q16igg@$@J@fd&eDP.]-[ƪ+vZ[N͟?.NŹC`%:O/K ::h4OuqTe5?7tWӳETXHH:Yə?~ 7mZjj?> =a('m0tpx,\+vD{#y/1I= ډ]5SLa4eɼ>< 0'2$+79T^^;wn#((k׭eKl2L<:cIȸEyL WvrrĴiTD}_pܹ{=@} y/ pIw:eyOH Iy*++CqqHCHH֭_eiHK=e(S,HEp _eD=Q?447P(h$P_4O%f^i[>_/̚ѣ!礢(]s2F YnR̺\ +-X˜sit4Oؤ@DdeHH$MƍBrb8e_Ep;sgر%?սN<5,!' wyy!  %IyQGBFHTzF:0zhڪ7Vn3CP@ Md)'ER0AhhLV @$YI%H4OF@3]T/sE:s=X466֌S! 0'2$+y2\Sl "33S5meWFFRRRi  ' 3Ifaqj* mlzV(^r?h4O A!IHHP(2t1┕1'xHHL$M<ҥB@:tĉclhգx-/ KgH=P@p9hcԉXx '/Q=?z_~bh%g>?bIɭ~ 7?Tn> #Iy'd#8s v,kGj"Yz޸ zOOe9Dr8($@$@$@$Дd&IVnrIL#GEXqՔx;v [6-ñ`7qIlGfmD۷*"S~SR4O8x: H@fd&g=K;w"44"fHl6/<܇aI*c$,p޽bX`vܩO/Y_=YPf;q'q/   hNd&IVnRȊ DЁP ĊM,͘1=ث̓kWe GaCcܹbюhOk37e xL۔(j, ys)OQXx << 41IyˍCa1M,///Y]: zXsz3gf_юh/-aa)GCYm h,N KP0x}Kh~zCRʟbW^- |yU/ʟ{gh+Ez~gTI͝k)5O%Wwg^~VjioGać`r#vZ'})l mVW!ַ4OR4|HHH̆$M}4'su&UoXtZu*5OQxZls#0>GiCvƧcBѐ < ao+WeʹT;5@ P@ӀE\r2$+7)$~voզ=7m[Kϫ,VՑ+c;'>=:.(Ud!н?m:}=[fWM+ߩvh-+UJ.c?:F8z~ڷz -;CnG:V]2_|Zj;}%y8e{M 9NIf|վ#&>G#8Sם;R]*zmE#Xt%ʞ;}4w')b}+Q-8=WhѲ%ZjU_WKraNs{ctX=Eb DEϧ YQ^O󡁵 IDATT?*Xu/5cbbh-5@ PԀh@\$M<{o߮M'???TnӧOp=wЁEX0o<=c۶m}ľⷡxSiƚ'R{ SoU3Uɳib3n47oj5@$OJd&IVnRIu ˒Wn9{DYӛ|dbO~R_[nԯO)ў>/?t*@%cDGן-вF{.7O0KW'YSy]+9_ѿxٓp}-_նc=ꟶ?NWҧWS4W1˷^zM{rr>{L:#N-JHĤC;sh$M=GG;xzzV[xTy艿vhU߄k>O+ŭQq =0߿aJy a~bdѢqI0O"DVuqxeOov _sTN|>;pD$ƒ=a^:Z}OӫS>`>^Iʟ`[ѲS (DX0?c@D#U!<1N:z'y>#3Ir2O.]-[ƪ+vZ[N͟?.NŹC`%:O/K橾̫>(05@ hkIL̓XNcXa-hӪUT1dP ܧ@[>_/̚ѣ!礢(]s2F YnR̺\ +--0' ?#ylq3[i^= <[pN#cGb,;vT$IÑ'X't~7i=m:tUX9CPVWOоNj?3􃳭5 9CAA> JP4Nr yrRTik*^q;t6)J^3vLR2N8+sgv¦B< ;p)|1Kgtb:<=.-GnzQ&9c\unNprVWխqWai&6ruI4qluʞEG'y6Y`qئ]%ؑ'++kX[[{Z9{{t܋Qvv@& / 3\l1bcK pr w}Q.ߏQ45@ 4k f{iʄ$ij2d ̓ݤԊJ1}]K=v*"r+OI:6 (- Jʟ#tq/~p7(ڋvsUDfvNp ]ořZSQ`ٺW,Q&x4<9M4] 'x,`Cc(yR*E=$m3.aT"aL}=shT 3P'<՞HLv|B y2'=&IB='f2s*nN*͈Q|'¦*\yvzƾ+g 3G \4e91W\ _;f%Gp--*ojkty*)c~ uSX^b^}=#LyH !M-h 8  5@ PM___O/bhj%d&IVNT;Okh[{0̶/VmYC7"7y:6W,,)U&!&9irl3e<%aq{^|E$Q+$U,g^Mnmw3qQ^sTp m8Bg6Iɝܩ4 LӋ'ړYI<ΓZ('w5VV69b,>FVM|1W \y f8l}ANLB|1>rhn9~3$%\iDI}`c7Ymg| lA8C;xدτ/s ; ߇e8UD@H P5Pxϓ<$Y9͓%߱`4O"e<6VVRe;cqb rroA{Y*UcQ m]ܮ}0vJ$?P%J 6Vְs#,DbL"jM"*Ѹoɍܨ'sIC ,66ֲ/최$ijɷ,͓^yjj*233ihj222bZ^fd4O[Krc/<77 */6vHHH " 3Ir'y|IHH,$i,Q%<: <ՉHHHH$Y9S9O1<5UI$@$@$@E@fd4O?DbOIHHHX2$+y243k4Of^ L!=h@a I<x: Ca8   0e2$+y2hoMF$@$@$@$L饅jNhSW   0$i̜͓̽G$@$@$@GrEʍ LModHHHHyu&yuE 2jjLG4OE ~0 jjjt5@D.jjj0@4O@7]065@ P5@ P/K4O4Ojj ͓^q 5@ P5@ P5`0 3\t FM8065@ 4Xcд1hr$pdee%%%ȠI4;wٳj 3"j45PeffdT*V?yyy}ѹcP?1hr$FćT4 'Nij4 r[n05i57O4yS784q4d\jF"gW&|4>1c`qg >4OzgSa^S\h#5@ P&M3M''<͓\외15@ 47}4} h$y#=Ɯa.LXȑOGh|.LKjFLܛ>qg >4OzgFA1wiHپQnjis4M2a!GjG+rv6hv]͘\ìb|uy{mL hLk,I>נiHپQnjis4Mc>LL'A`,KjjjwCPCAp]'CxշN#gtˌh$ɵa8~tA)P=#R[cLs[(t;p^qٷ~h׳|xy%2j|4h0iN?E"F1- u#'Ƚ;w7T9 F9ιi'@Tأ'\`kM!F:4Ou3Y˜IKƒh]q;'\HOxִ?ɽ1Fc@L+ |v.}H[8qhd5ϫ 4&4O簨+fH(r:Iz-Y?;C]O3N 6O NSp| +Fpj;Nvu aڟZ67zd/IDATQE[X#~ڗUq΁ zZd|? "r<.?)OU2z|xy%2j|4{!n_0ϗ$ <}݁KHGnhNŁ}/?DU)rq+$BvD CP"o||`$Rmޘ\cPP( nTN_ Gv?*GocH.?| VRB\?>Ax\[C{Exj -k߅8LQyBFFrl]c8s6<RGؕiF PPP=::OkڞO3ɸt)'样+?Ry#z&\)\](Spzz`U Ʒ ‘kN.)x0%y8a4zs|! ۧmo(ki| OX25@ @'pb2PXK(wpH>8u#WL+#>{'!#3 驈9^{NvHp'!>~8u> vC,D֙ 9qn{Id}%zLF-}nQsxv-;Nvy{W ^ 4[Wwt/|pA)5ܴHĕc=GQ?쎸V>4_=htsm|QP$+HN-uc~؝^5Z3%ϐ:G0O]tMvSq6qưU'gؔ8!Q?bI(qkOpr +q@TꄉWk.ah?O]]0} A +sij^x'jS@C')F圪ļW`_FJ}qͨGDރ3 K8P&λc~~V5=82Bw"fe'S$܉w|!v/Bݫ⧇}I\j1Ok8 =̵/R>͓2uU_ qޓ`?`RoA_z!*1Ogی pAk[y*D9~uu}W<a'`(T$5bQ s*~@ \|\TaX*S'e1nw< So.®㗐TcKڣ^]6&y7c+R0v59On S-Wj{i1W!5]>6OG+~j\zٽaߍ;?쾁?q\)T~Xz ӵ iV瞵:3X+Dj4{Avza_-Ip1u!OGTOIG}}p폨kc- -pqرcGk7boVܗ&3F'0v]yR"I=kϊTV'1LI<S]a4Kc^# aWܨht?4;4HմHm&ܘ)\&X5@ P7O\?Xnyi>9H>gyPGOS뭚k* K,DPM ژ|I+a|]'"X{qJuce<aζMHD|NLڞ!uFC y*H`;|:g/^ƕX1ҩNTRb`3c3l1zy}SŷalWkFanEL9>CIxj&%aje"D P5` hxDqN:N`D']ƍp+# )8{-EN"}<1A<)Ȍ;oLY~(Jn>/}8wE^5\z|[ٲ 1&hT<>q49_8/#HUTL0$^ǭ8*p6K{t:^Bwc\L4s{4&4Oɸ Bf;(nqf ;ǽ (RL6ְutEɻJ 9?)A1~@7Y[= ìݗ DHPDfJ}_;㽐U$z >Nyy`LW/&,=WiGc>Lpac`Q5@ 4+q/~~)ZJ ޾;7>D. tNx{e}; *07 5mO0?CFR$E^^D' ^b$B*kПJ;qL 0Qk4V 7vEl}h*.>R }|sⰟ|K%x4&4OM7H YQ-c>LubLjLA7ΨBC$׍ن1I@a5ݧu=ۅkXrz>'E/B˜)\&X5@ PH7yѶ5L"#CTe!>nRT0va~56&4O4 ѩ)c>LubLjLA&7b"fkD 1Fюe#ć섗95jXT+cb@DDG|L>0jh, D⮝p7AÚ(cb@'qL0X+Dj4ĽwƠc@Dđ'=ybb @RԀ)i{'AǀIOl-<7'&,/#5@ 7}4} hh8G4OLTL!QaCj0% 0qoĝ1h4yCAAx= *W*hL\WƠSJJ 233ihLFpSNTx|jLI.]­[h`?k\x޹cp4yz!TJ89c5Ȏr#NYYY]}aE P5A||@DaD+ DٳzA'޿bJIFM?{5EgbE P5 `f\& ~2Ʊ-_4MnD*Ǐ#nd$:qbQy#@ P5@ P0 !>@jjj5@ě9=jj ͓LO6jjxyy 5@ P5@ P5`h DGr=y75@ P5@ PԀ)j2P5@ P5@ PԀy2)^P5@ P5@ P/W4O4Ojj ͓_'ojj0E <<[jjj0@4O@2E>jjjjSE_/Ȁjj/G$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@$@FYj3IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/create_vm_9.png0000644000175000017500000026041514062723141021733 0ustar00jonasjonasPNG  IHDR\,0 IDATxTTWî_~OTh,15/ رK{[4{^b QQ ef޻0𞵎L٧=l٧D NL 0&`L 0&`L 0&`L 0&`L 0&`L 0&`L 0&`L 0&`L 0&`L 0&@&E%(Q%JhXDƜ d 2@@a@\{ |?bc7lܲµU+vq  d 2@o6P`l .IsqrBfШaC6h2@ d ب)ٔ<ظL>̍2@dk]E?ڗ(ٔ\CzЙ)3%d 2ZWɝhS)ٔl2` 4lClƔG|i9XoޱIqsuI:ӳ,Zu瓝mJ6+V6d$p8 е~6Lk[^ݽIǮqKܽ;8cߦpL,1 4v?>f;yYȞ{G&Y^2^ssßv`gXu]G:&%Q˘vϏxL{@ʱ]=VA3یmx\% /c`cۂض!wg#g8/-x)d1]!$N 4ZDžD⃱k|?$;-cYDô}qT([>s<ܞ#7%͵uZASxM#vd ;+bR Uא A&Ai~`DX&]e&)%JvhAD&J!Kx{kNآkHɐ۶ĠkeDtijDH)K1_=\0pwQDLC!ǻME[ki]Jn1EʼnamT̉9eaPxH46^B"K͵aպi׼'ǓǽpHtS&Ӷ$[-/$ݰ~[rמE\l4^޻S{hu;He<:L?Gp^\K8Atxfk:^D^?8v;qnU⛥ٮ=&mD`HF!!=^؄~5-Sx lEm Ibߟe\sAt\<އ9L\YC_}-9lq6E)9n?lcD_>|-y0Ȁ:MujKcriύ֏Ov&~) ؠ "8Apn]M."}_G}*tlli,""44τђ-E<ǓaxLػͮOvzH9\s.޸ݏ91^#@&%[s\Vm8MX)ٺ_|#49AYfb=V^TG&mr72;oli>c%%<Ɩ.>ȮOfј2+>Kcg'mr> LC`XԽ)lml /DI_jN+-yw ;௽b}2ocqp[/LܡJ<B=oL+f]}nkc Vk_Fd7h;[[5Kk@9R3ZŏY>ٹڞ89=7yc 9{e@Q0FfE &n+ A1J^- KO-GCh~y4-) L[v0zik\8w#Dz!jYT<7mnmcl Y=1+R{I/84Uf*Z_@R<8g#{عuxj;Xңq.%[ D,yjIvgԹ͹~È9H;OyŊ34D4&آ_cri]/z\>-Z) Ƣn|^UwמfUs.VՑl!-Eh$5]5/ b#U-ٶ6ve v`?Bpy3_oP?c66h;XߞYTC&İKtZy?Յ"-x/vcjocۼɘ0u=Es.GnMCc0[F>gc.%6ݾYO&z8H6 s*95#/ &>âph#uѸ\܈@ǦwU]+ (3=mkaU}Eud7yn.+pbno4tppl W> $Ų^m0lyTgfQk,tkitCݾm}CaaN1'eX߇RH>ܺk7a[b!x"TJߌƶEet? eHb^da q_ud4bMC7l/F!.tzۢÌT>Nf\k6vwPK0]g8C]}Ά]+yI/#v][lb'~1ƌEቢG 7.Z5-bF;F;琱ȍ';^#^G#!_YM]Oİwh$H[3zpAh.u>^u-zLzU}N;sj f(>=792 Yfԣd@۪ee-hXXQ2@ ̀kc9>1aMɦd2@ d1 ZEd[M vhS*v4,d 2@7.;}߳ą ק#J6%M d b̀OiM^Z[)Pwk?? d 'lJ6% 2@ d ds /j2@ d N(ٔlr%d 2@g((m_3@@_d 2@( P)J d 2 dW:832@ d @KL&gf@ d 2PdG2@ d 3|8b㱳ł 2@ j(ٔlr%d 2@g((< d 2@%_d 2@ %;/Wr%d 2@dS˕ 2@ d ds_d 2@ lJ62@ d 3|\˕ 2@ dM/W2@ d |fK?83Sd 5̀ 2@@d ^` NLҟ+բY2@Ȁ` ^@61FlVR%d 2%4{QdbծX<2@@lq'N… 1c L6M,eDY '& PY 2@Ȁ6^?e˖رc8}4Ξ=w2mNL ?dbծX<2@@ŋcǎؾ};mۖVN,É glVB%d 2@ٳgc׮]0je̙~.e_:;PfG J6+V튕 d z}ۇݻw 6`ѢE*.ZkibMJڌBek34+um1D$aHpYrmK(٬LJ d@"!T?ZE_'OZ͛WUFͫd+"k TeCزl IB)مT?~)Xك> :%&Fd 2P((=sL9r[nUr)=zơCg e2y}ǡyuL ѿJPD ?'F]V'G_̼,j4rR I3z^UXYo1`3( G_PrY,?TONꐢ_ѤvE.SnX{M3JKj +˯Pyl]ZҲχ+;B,(+KERsMV"!8*ڱy}}RR- iZx($r%J%RRR=DwYkN"\*B ղ2YGŁ7g>Lۏ; >HRYK摱(q\\g6\k2@ ?^g͚5d.! XY\(FҽzjcL^6LKrVin2!! XqD ZO|}?^8{yyaܹ*-e߿P= eDُ]D!y;bsXUF9P5 l器iY 3g\C?HvBFצ?jrP;T.zTR-2 ,k`_ƺ%[85K}ff077̥mcxxhu|N (sRF,B!ʙ%{C ^܆̒3Օl~~8/<9H~9ޘS52}.Břج7 9?/~qPӘvBRk߉t~۟  dh3P%[/oݻWu#j-D[B j"GQV,?WXxCfEְțd',u9>Wo#$5*]%Qò6fJ)dOGM*&߿5ULmボ]s!OeGgd I~2#cY}]]#+u.HXxsa\~#(RtZe끎#UI D]gvMR!}ͻ1[ZK @l!~~~Vl1t.]k׮fX`"EYL~MY 9$at`?50SE!t$]dfJGEG2mIy8&]ov.>ld+Eױs= |N5 o2U+\r('bhfNk* ,g_ glҖM~HR*p<%y7_<% ӗft1IQsյ]LƜ)H|u]ޅ^!WH}}6n, d (tRի.\*c2X&/S?t::/ooط[||匭a)2 jVWYϭ .P⼸O0Ų _6'[vc[~?7l5 :Qv%[lsd.^<:i>Ԙ0޳9=恕ͼe+٢b؉bĉYrxClkDYR⦿?芑Y6^*nY쉕Cvf냭o]SǤ{I'>G׉מy ~ ^\Y>ȑ~to1zY9Gy2@^׬YUupp0.\"ZE17{u[lr?)}cFuiZQy*h2k.F"Kf`mV[v~Q,95quH4}%J[E?rE\֎le%4hߖ.G4ѨVEX, ߡpDiٹ@%w1~ZÜ^(!}B4Qqr\=װ{D˕x$&eoɮ3+R >jr)֭Sw5i X* 1{XgH㜱>ڣt׌."l썕#]8ҡ5_R>E~8+#V+u ujZirB0B/٦Xq/Q845Jò|u8tc?NMdםK2%ޅMVⵧªgP*L3h:?cC IDATuFg>˕ddךtBP(ruZdt5]D5N38^kI B?oyhy|45igWU޹]ߺsis 2(y@~Kc!n*Dž089J9/Knx*78t!6?¾2%KV. V&ʵ{Xc܏Ty42l n Ś > ?AB g}Q_gz8#R;ð.V?/0b-A=N“hi]ϹQbIk](/? smZg>̃y2@JvWYށcve[0LTy ^3¼y!>$9 x>F,?i]@q': J%Pk°h0P wT*!OJJב8 m#ŜW$DܼH_;*Sppn-Z9#0.r9cb{Bz6o;~禥e d c]|G\ d%!_ZU||_d 2P P @%uAW\?#d .(y%pU|"d f]U(٬  dp1P$?´S0{g<K]*>~Q|2@@A3P${ͿcX|1L\`(٬  dp1P%{1bd3ߚXxI~R,\˟jJv~uP W/*/2@(h dwsCo>2tpQpmLqߡ*>g "c*r6n<]1gtoL:?-ϔܜ(Kfe]Е5O B/ٿ jíg,s1pb:e"FjII#r&ٱ\c, 15NտyYh䆥/}R=ԮPʢj8ƚjIWIJ( ڼ4~49mSZد8<V_Vxz۶$;y~X^f  d2PD%qj̤۠M%{prmͳ̝{C? 6G2@ݒ=b0,\ m5MX64+ Ra}E2̑EU*MKvq AH]UU@}<4ԕj{BXVk }q-fw/OLv6DzJ?u˓2@f$[Lwo)uFG,]6Mr}K,_ sa&EOk L~--e BQ2p^rM*JW2j$n"De5L<'I,},'¨mJsŇXNEw#Yns8utV=BŅ~ˆUq( V_8rCk&VFa[ U8"a6ϱkk:C}xfVu{9Z\ ?FEX. 2Q۾=l VFļ~&wz`Y!rl҇0Y-XeźzK %^yD˚a7HN]?RO;Z9 %o{L;x1G112@cJGSƢldH~%m2<=GI ,J\31CPQ +QƦhWn'!?/ܐAry&jZŒN+R3!J[I]VʢC#0 V +qrz3\`3mUuILG¥Pa ,. \"L&=cԼ\Chx-L|共ܩ6TCFΔv^|`d LN BG]ҕlO=h?w>i." U0lMܿ_kūTuKvM}qs J?3h[ mCΰn&)!DHvE0kW3bLI?np8gfמn#fЭHu˵JмysYC&s8;:u_0__Jfb( jJ4.0qZ˵6߿`h;|#~Ap<g^h2%"h =Z^=WWFѣU,ΙM.HfnE&':dRlѥ#lG?T*SeJx[̫H]x,T'_:}Z|c#.ފ8]D?:QЩ.r;SwM+ᛞFud'C֨]poLZe2?@V .`ܾq&l@iwU?%7%\k8m*:wkl N7BnScRdl> oN7Θ{깖H'{^w^+uxt9GNbn֘Z}\l:ÌK@>2`r-|Fdڮtd851̬-8sgy`6?ċ(lvZ]g=b_@vfh*]s쓝kAWs+ɹ]_u4[@ZM$m_phR{<4b^xbT.XzdD8;Xٿ-FmӌB6sv^t?<7<7ŋla<6$[4LmsE+-}IqԽ?ժ%aY;4z<}ܬJ²b]^sK"@؆0oin#ΕalRKy`WM gZC)٢/eaie%7?;EBa7֡cK8:99mvBΘt(R}g& ۢ}z-3Jeq>\yqv:{kKp꟭ĶQT?T18/ /"1[{L9 RSl. %m?(PVy &+J;wTN]91OKv.}}-;wƂwC'lɎm1bA>Xx!ƣK>pul?6_Wg"kGo8p&mm}j?܅AmzaQh6עI^c_cyJ6%$ئ`>Sl!SRT%/'&)0iɎz_(ys ut% v2u䩙pvj.ԴؒF8\9N]&Gڌ~0nU {ݝ#79tM!QZ$8< Zpƨ4#Dh?$S)ٟcE ^LZ?Tq[L@;Slp!2޵63 ?1rKOLx}4!pxJgr={^ 6d :ep#wcֆ[%ϱ6h;lG?;썫B|֍VVڡx?a ž81 xޗ=, Wޤ@S:͘}5:|%_¥Kis R@ d JY1$`MKh[AgC7Ҿ;/8W {}xvrK^$Cus˖priN=aGo2)0pezYK{1 oj OLEV`sf=H0 :O>1M+L<2]0Hnpϵ$;c8Ča$gͨ e=G<fBȀi2@ɦV2= dS[b_ qqqxu(lj w+V̈ 2@ȀaLVO<ϟ#!!A%B;l!u(lj wlÕ+TfC d gLVmт,$;x]e˖]2/ϝh( vK\G$@ι`ˌ d f$%[޽{UB>ZHk\VNw&e$V] 1O+BI~R,\{!O%p ِ2@șPdZsddmQNDSla!JvΕ+XfD d0&)Bpn޼o͛7Hs7nFɓJ<lE)(&ེk5vuڼJ[V΃MFꔰp>}+(QB=9sK $;-8πJ02@@d$%êLŋŅbǏCӤ+٩0ž"jtg}pF֢*S}|c:Y> cAiXz](.&<dR PpAHHyy#d f$%[\&Ϟ=CXXJŸbB_| Q>OɶT ee5L<'Il!6Q>$eld E2%Ea02@a ?(LRMgd+> p\ m6t\_.È6/ZѼys쌹OV7';Mz[\2C{ Wv5A /tˠx.:q:.džaݐ6vh\5C{G+gWt5;oKՋ9:`ДV/`L |(ٟ0ln&PPvQ%%#99cpfVw$0.mcɁ q{֭Z1SB?N"> ޸鄢.2l!,)"t:l)O``+g?dWv |oK:Z1桵HxAS3:äO}#Iy$\Â]y}qX6ɞ٣-D.9`Ld) J3ZZ55}q1 ~xrAg zLnAV}vB(m8cNV RbaBu}EobK, @퍱ZcH+ѹ &BY~Q}\=9l'A&`>B-hڴ),--QD fQV,#+֣7Ɲ;5_>NɖVn}yd*VǩJ vwkwp.<%#?$ZlDGG8:;9}vBΘzX'CH7~n'DW\%i;uR&`@l]###PPDQV I&zJ!J`.d4J8slRW=|\_:_CC}dFMwٝ *_]hV\m7qqQ/t"#q)y;E6`L  d#&&&9e,,,rI,]$0ζ*lxe$lELlȦamiHE!p@,!fOvJO1mᛳ/ fvmI;`Z17ZuDGźƵk1||`L'P%[t1*C{@4WeKL%u12v$;9mU7llPsIm,9(.|= ą㮚WqE/apa+2'uFSp:OF|rHv7p?mČi? . gޘoDХ3*E=f: Hq!pv Uz4.=ǔv8Hډ_& Ps"c*r6~9v:b Mj/d&>dPG9M=_ӱ;mo'8K{9GUHz#\:ƬCҗUPԜ^hR܌[A>0EhR;Vpi->ZѢd覻Y$@;\>!TCǒ3}f;Bgl!o 0&r%;WIqv=nW馢x P*Kì#d/[,Juu'^E1_cϸaW Ke%<kdHJ+ۺY ,ʡCo ЌOoh ըss+Ten&*¬di>oy\eS d8yL 0& }ܜ3]5ʾ[Fw O 8剀ɪ B?q`QOT!5Ci8uovuPں!^tDѬDǷwoH ~`Y ^g:IGoma\~~v|Mvnr-e)E$`L c\[Z˺]<-bC%4wAZrkSjc=ed{jxNEGХr8/ҕErnXLlZ :_Μ%;s|`L'@ɡ]7Hv*.oo&V2fl" GfkPB9T*[B^%)J4!֭1`|mK.DMZZvK:ֆY:d.^E3K]:| 0&@KPK 'ݾDQiTMqG!H YO Yrh1z|FH kU}/[qAVGre0$[2άèvaUQPS-)W Ps 3&( jwvDki٘Vn}2Zʛ@ lkTe0*;\ ǧGJT ״vE%J[E? u_hrP֩Җm1-J;lvSql^?(sP}p0,aYEym|on(%[; >fL 0&d;w.` I@NM`L 0dk^{}&mŘO  d nd#C&]mҧ'an?L'ʼn 0&( J(ѸD>Ns+& \Q| A %י`L qn8| "%P2| 0&@H /^@.{ˇ.R %י`L qn8| "%P2| 0&@H /^@.{ˇ.R %יp~ ?9W| 0&P (p"Ÿ@^>Lm(mAg[c ˶`{2aE#x%h{E>']0(٩8: ?|c 2UШ;.j}c7wei{_ GA5Xr}P9UAR0+S u:b̦ $\UP떂9XeW1~[6ۉ7-<5῔l&>[y`4mY{CQFpb@^>L_SpmQX+\,*]r!9F=Qli|f9HvN}e,N.sع?2GWOP_˱a߾Ǡ1ɾT>f;l";@ȋd;88`ٲeB0xOe2EjSKNHL^>LBdk{ɚ?-b)NQ}|#ܪXglŻN%~ b9/uN;VE.1[cu#,d/qxV45,Bu<lE)(&}F}v+puH4Uf%K71xC( !Pn:eX.#TѰW03/ܰe~ƊHx/MjWyR*_ ?5d][x-F5* j5"9iL]$I~R,\KR*$j*7pB:`nf*mVV͇L 0O@^$119|1k)ʊeԔ|# N ngB.Ri#'eE:2~.oC_ ZRѷJ"wGѫfy8Je{i{*Giw42zo~.Jio# zWDArF!&Z1T_VEd2̑EUƊ_>bh ;ׯn#=aU%tPFߢJ݈2\ke0,+5UXp%Me :eg<*?99 I6ͦB #H"`APAQ:*j@RbRC F /EDDjB?wn.:sO3of}g.IIl^ԟj-ﭻSXSo Go~">9+V/[̌>`9s#+⋩u/rqu;1vjbcMgxfBPPe%gϞ嫯k׮4j___rrLJ8wzJ*T s&:W % T$W-LN%umƥ-ia>4-d[RRI)=Ank .*1w4RE3Eхgkx=Пw\*/|c 2!8j{UaXr&g&%D]QuWV&-PPNϓa']҉5EңP=(& B^m=H` G\,E7 hrmZic\-ڭdRpat ;/1hg"P(  O)+,tȯѬ3YgY:=u*x "6x &93mA}y#]D.ނk-PQ=聗ynŝ:UNDj;G4\I=!|0'O4;Ny6%մ gpj\"E+wת>?985^όl:VW("Tm3w$ԫM؅=#miVܩ6 Zenn]YOፍ/-'ɶ& qo0͔B@i) \͡ ;4Ӫyj.dۚq¯dd߆|8}5X8mjh<dȱQd#C-6-Dl4$p1W>ƛdhy\'g+bUﺔ |',![wm%mղ,ġxJ_RMc=#>a~u cZEim3فJ]:If*rO3y,5<_^K^vЯd뮭]bz`2_k;@vcfdb3TC>̐FT`!YT(  J_ ِͱ0PWt<_nvƯ'!ܿQWi3.RnJ\6!۰E+h xF6䭺~:? oV .ȃuّSY1X3sZW¯7j53)B4]jhq~~!6-k|#g[>JSc%1MK`0(}ί[ תfBO΄gt *V&ػIq:cE}~m#>>x$pl>6Ù$'mdjYէ>n0h&veר&.qCKؚL\t܆# nGh ԾOr+F.,cV4u3u%qIlY!5!fR6'n')f1Aӄ;)#NBWQ@ y޴i}5ѤIVjXnj>٦ X6^k WIHTٖ~+0F2mjGW`6dK.!-VSLcIl6G(T d[#h ?^GىϿ7Q-@ y]_!{y3I@ShU;/')ݙϸ=QKy%@w\0<܀3 ɍPY .?^AwӸjo19r ?-*D©C~J4;MC4lKX~KϡODm?]wn뤮ɻC~%W)l >mG@ n=VuH.vAhUy<۪7sR/e-NB@( 4dgffOV;775k>@cBvQ^g=&6,*h _yn28E-Δ &9[e![./=Q/nK^6K2k}mjK_M`IFq! Pe%/_ƍ Rr S[mؽJCvAVWm҈T2-̣ 9z(} 1vǑ3Y2ݤlOZUEU3dZ#"oaT1{|^> BG (2٥1{gY6Ey¯5?J[yدIBslsȖSFګ:]F֤$n65bᣣ &9sd)sq˟Ga>uC_v4*ҬP6Qwkj BJAҖ}Rtϔ)Sxgskq/ kϔa2; nP]UP ( d&= &ZȖSs3?__CjNU8Qēo;62ǵėmH# (_BBR,Ƕ-Vz$ (LrBv%6LZ!wǮ+fs:ת~匐(0<ήG&lw+3C IDAT*^\̷k]a}RPʌv!w\;irnꅻ%Qxd_;Ŵ-d F=*Lrfڄ일Ӄ1&յj_8'S-,subV3?ٌm(h3xj=m!v֜L{2.oJ7h\3;^Akݛ9fǝ\ z;רˏ{` 3iְ埶a|} m֕ٻ{:u9fX@ 9i+1$1 @pV4JCH\{_em?S \kEvnGu0V7u|ʬGШV|5tka.» [lbm:x4ɩ<|kI7ԉv-zФ*OB;3%,z>uV*7w2†ȷiVZOT>{x:SW^hM}p1.p `H'|Es~Wv!~5*d0d<^DlSLkokh7, KKwݵ]ݟ Dʻ3cZ}ˑA{ڛc*dofN FPa≍^Ƞ56aA3?$ }*0nvĘ(_'_(}щ9,ICߜ@L|"/xjOnIѭGo~">9+V/r:FV6Ee݋Eu;1vjbcMgxE2SgJhDZSu$7&o'~T_"f$.!^#)z< LH\/C l| ,:o#|ZeBYASMʗ-P4ߌu$%n`jp4 Ч'66lK%--5_w:<4˩6$=nm6F4÷!N_M|&~O@Ped@&eǎDS'Hݵ [xkU NM\5s^s?J]HP (Lr6؂̔TRf\]C l@-^^.'BC3uM>O-xUi>jDk:Ġ}U 3Xݍ5ϻK.6睟s@"bj52\mUaəo`oZOXr'OoǓMd^M[_oz>!.&>ن4J 66!ļ֚[_Kf3O NU{ݻ +diqna`.Yu= Aqת>?985^ό_Fb@*A|hh Oi/-'q1烆~N@1]f{8( dK~ݺC>=9 ΝMlfNeˣa\.c8ϟ{{iaq0! 鼤`[qb{GrZz_mpM17WL ޿67PݵU (vqyL zń7!;0CSZd;ۮV!mwu(q~6jotWXI3Hr9V14QRH%"߼X!d1i9R{8( dϛ=;SS8x0CilOIYf}x`Ȗ-[1 UxUlL3IV!08= όhj=ddS6N &9mB6Җy69I0 (^Yi6 "%YTɾ?m /r4o׆qON]hifhϙ{`b 6DցϠԤElIL&~RfD={ LrZÒlpPyס%lMJ&.:MGnS L:l7*ML]I\b[~HMu"T0͉IYuP4a;}[d;ݮù:8d;}`֜{ީ[Ћ,Yrp/ gt *V&ػnel'vio>MrF)X8aoZCGObhʔ/N?5i;MEXh?Z%\P {̝;Y3cbDZG~-N8)EtرlZKL rCgÊ;]$W ې-=]bC[ԭ&ǒz̝Q@~F`~ҽso*O+,|+m)6?>ykx_Ɖi[@T^T*_1~Ι{g(^H!Fw܋l׃@nnxT~K$='M3*KƲ w܋lߝxyS>#Z T5̛ w)<>mG@ n=VuH[}֖AhT n+m.6LӮ^lP@G%6*Ӟ[?gGR{Ҙ_ť»d.>l;]b4"m᧥Z[L7/?y@<˗CCpS5-Ͷy0Y[yTށ="#j+΅@pAl n|6pa?njS2uNbʴIDE-,l^Imxcҡl]N1r w' ۺ!$g,d%[v ui - <*Q/nK>& l) dGn\l#G|."*/ظ-!xj ;8p~pF6j\V (Lr ȖQjؼ˙{l&4>fOIDP!+ůsP2>c6 =d3DBJLAfgΜNځ}LNB6-<AG2gzؼjZT (mȇNa+M &9d(c5Nl=65s˱ <|rq˟7֗hQP@(JCT,&LǨ1#Yn i#`%DaZܟ,o _ ]C'at]I%IrʈpP@(  JLA^Gt۷E >>">q+W^f $t &z ȖSF BP5Pe ORtH.$t9{Xٮ?]J%IBP@( p pAlאYXoP@`E@2"\(  .m+,e &9d)#…BP@( (ٮѶRV@`3Q@2"\(  .m+,e &9d)#…BP@( (ٮѶRV@`3Q@2"\(  .m+,e &9d)#…6`H&=Ch$ J@@h)QfW@`l9eD]rܨƑmXdy'# J!s}]} &ZBv%6LZ!wǮ+fsw:ת~匐 >(ZjchRuwNbT/;* j5{ Ȱg2]=bZtaiN97W]ӒN|^@IQW( (C (lː҇hPd0U6dr6hl]=5}zu&%huy'Xم>{R(r|G߆ܴÇ#'9˪ʠfC!X.Ջ0aHzOۇ%'g eHS2#1Bp@]̟??gOL_|̿Dߡp;O[(d0Yd`vaw^OJ&x͟WQ<֞j0=fCCAO''qJཀFx0Ydgu/rqu;1vjbcMgx* P@(PZ (2yyy|2 ƲcG"Ogq:x=?75!bRYCb~Gx  & .lAvfp*k3.lI[.voU6 ۬xiIw}ԮOxӰg]7/q.vn@]ryNHD-ȄUaəg&%D]QguWV&-PPNϓa'E5,RM ޴xh:42nAF:kh>Zwe /؄l\EkN3>h9>&l ; i=F7"v6)BCT@ y޺u ̙;wK.Gd,\Ҷζ]Le{HN7[V g ׎ʻ'-P2,20 (- )ȶT\G(N\. *sz&Mt b;U*7#9z2DRݽn*ÃrO"z0}]{q쟙z TxX\ Nl?:;; ҙ=_gC~^aDfMrI 70PYt=ȶ)o;l ]%-n'S_{{q^A \}pLf W(  < pAׯq!֯W9r#hGbOKK w30ޫW׍Z>ף˩†ȷiVZOT>;:G!Z!Mm0zտ4O%IN*[mro.htppKHr8VgiF5bKxWëN.ٺkh1XY0#N]bcyL z43?,Xk8 .(S=oLM4k=%f}[x̚-? ۸P'N]З_k~^[;fA4Rw,$kt֐lu_~e@ (Lrلl}69I0 (^Yi6 "Z^o4q?Jrr<}K4hi+wɲ t=_Ȇⷬ!0kɆ;l *MMYĖd7-eFNHlclC9neւhv;HZF"_n =Q8˞-X`yU6mF||Hإ.-|m3IN> '5.vէO}aмM˂QMn]7C]㗰5)(6 GЏо} Wfк( &}c[ԡԕ%&eT.H؜ o_O&Gn8 \E%\P {̝;Y3cbDZG~-N8)E? s L;ocؼ| F֝:@t֐lu_~W餮b$W7ې-A%6DE xjiy,(ś wkm'©" IDAT(?{9'޽5K_Q]0c4 ƟuO-#" <,^xR.I![N\>ShU;/')ݙϸ=ƼKy%@w\0<܀3 m!}?*x}%6N*~Zx-t3ԫ䋧W _^)w4 Ӱ-a}-=.g?h2bw߹|ǝV9yb(֯1c."gBԧ*<Ni 4<^m՛9 gͲBPTPe %#zλݺg3fNeIL:)&f u% ][zw|Ok}i>q[wO?)Ԭҧn+ԮF偗Uuʦ3r\J P@(P@ yȖ.++ 1jH֭[CځHgdCͥ/ڇЧxkZZ Ƃ z i(ސcӖ3{H@ͳ*5  }o#V;9U=4z4 XMMpp:Gn$&+>hL֓r?Ny*y/xEۧX17-VS&:v.3ݨOgq:x=?75!b@Ey1{y.1]a!tݝӬՓr'aҖߋ/ *^|+ Wmm2ʠQ@yWs杳l*,S1MN06 BU@ ,dK_>D#h;?t%IN ېˁ۠3ֲu:!ՙ7xK?.ܓ*(?GRI3U3X'/m]q'Lwu=C6ESG0`NIyAӧHwagr,Z7=Rn"s)|4?Eg | |5m1m 7v|q陜?dҹ&]X- ,0WaT5 o:ҵY|_598E4*ɨ]w `8/q7x6d%_ZIcٱ#r Rwm/1vև٥.$g-LN%umƥ9[0m[6 ږ./;wB48r o=G`nRNB~rl4 TU1]$[En/G ~ْ?us¿Y*6w4R3fJd1PMa |ndeq aKx¯5?Z.;٦ߜwfΤ!4]+LrFچa؁lb?>^mo7>`bNi.Gy&ѫuMT*u~m&pq;̞L &CYM읅m|{T&ŝrrO4qIGOflrwq,@afEjGĒG&>n+3G@:/]9ƵѰ/lӧv:ǑpƋ"ęP@( x (2 ׯ_C_M>=8rG"5u;11Ѭ^Ŋd;C[&IU?T>wfJ…K, kv]Kh}w?c DOźYth&Q ;ѣ!F4^~=߃oX@ǣk$W[[mro.htZ'f+ikZosqm?hV!7ƿP+6r؈pаbg=&..t1n~T꾪`)VT2"RK={I;|sWiSA6w7 ^ԉ"{$ nBص\0c1y?3i 4ŻZwu뷹`~*"B\..(S-]wd/;w69ug-EsYp?CS>٤|OXGRGݐ?roN &>mQJs bO{91$axUyqs`&ƴE=IX="= d0Udc\hhHJ-|7/ ~r3)?*[YǼ?][gd~[/)0?q,ؼ*S[Mߵw!;cR-)ߒsop'K$:듭(" NƷy퀻ˋ_nq&0{0! b) gxi1pC:?tOh=ɴ5]vBWC+J=lR(  h:TģE\|6q[$#dGm* K$3y5`Tj u7z?.T8sw~>i)ZH;+d0&d#m m> 3yA-śf.stZ;40zXw[X/ӋpqQQ4 +2xx`X|{gY6E| vd{b,k&^PԵTbRCw-؈c6f3k>/Tݳ8kQ MXur&[z^!cML,[7Ĭ1\?m /r4o׆+|>?M۶o<8vɴ{IʹӅovi'ÿ{= vsH7>8Sb[[!HL&%2%FP@(hPe WZܹ5{:3fN1,v;~42yDcΜ]$ ̽Fn\7@H[zـl-ɯ@]e˅ם 3DRݽn*ÃrO?S$W;ې-%6DE xjiy,ͼś wkm'(?Gy;<]6=eꯎKyff9쳞skq|*ݭch^"O^U #.=(?l!0c4 O@j4mSV(S|F94w3.̴>a :xq‡nkM MëDC\Nie$Sc4={/9iQ!BP) dK@ݿG՝wu峁 a;f̜ʔ2aӱ#Db"3t7S,ujwվ6׋~βtp{TF@ZS_m.s^/?ž7 pFW#@w\D{F{BȶfgTf6)e&[4WWj;vobLZړD ]e [jj5]\_QCCw #!jŦ}־1Pi㥢BPJCv^^ `¤ؑȁ{9ydQv{쇢$g-LN%umƥ-a>4 m@)$=;g}*gmh9#oysyYѥGӡ7'-=MAˌ䡄0*,y,S |63)m(W;۳+YI F]D3 4^tGm* KDwm%{fɢV"kx=ПwyNHDٶ0s#+[W8w C_~\d0M(܅W1[?ee1PMaƷF P@(JC֭[U$gΜܹ\tp>rD ۡe9PV2 (w;~|Qyy|˲1 䜞I/5^gEu0"wf%rI9쟙z TxXl׏czT % ZwS>7~ꁛ-kҍ!'$FCw?pH^3ח󲯖kg$^ac 3 IDATDRݽn*ÃrO"zvFȶXLAOu`R/Q,>nj wbֈP@(`W%\P&!k:|էGȱCn'&&ѫXl'|- ?h+עz7b=GvH%I,[mro.ht7tb&=gyz9n=Q$H U+l)O}%m]4n2~UiǰUb5>J^X]dXj4tO36sP@( .(S-]wd/;w69ug-EsYp?C3FȮ0m'1G>iYA3>8}-3,X`+d"6R1}b e sK{'@SIK̍g\,8ijf>Hr1w;਍gq=ccL= 0$BI1%!4C5B 6SMBp7|F:ݝN'ݝe2sXJ>J!^1xpV6T8Okk@T,jd D,ZlcBfSj 8it@6R !S!b$RAjCJt/GyyHی}81%*yt†GL<6Y~_x1t(@ X.*޼yD];шqn4.^u֯6)3dkcLŎ]wڦ#>6j 3 }"iqCqZ lZ^_An ?Sx hvr)OaG Yh00jf }X?G9T(EG("/bp͋o0/ X(.߼PUͧL %h\'lq/: <Tx9`;?;p Ҝ\.|rtz1'KC GX M8@jVxJH'8cߚwT1 N1:w{_E6DS ¦M߰fJeXjoƲ.bI(ENW2?oi&@>!:MLg*59>w Vw7 ZT}3 )}g.r\M&.3m<:|z¹]=ѠMOL HECѪ5ubY~m%:7݇FbMf87cʨﺟ`hg2&cf)F @s@dždW߲+f{'"ta{Hn-ѕ9 .O»f7*o=׀=jïO0]M&͑W@ XdS@=~X~}:Y +W-ŊUKrRر#ٖZ],e4?G٥)!3 e+BDQ(@ Xb {nlܺuYYY:aϜ=Ysfұٻ`?qmA9.k>2MMG) 3 D [H(@ .*YT#k7q%"Ν;Cm9w eXKǞ2?0ǒN?ϳr$,V@Lgʜ@2d;Q(@ lC1\`MI_\\K"d9rѷo?< T,2h6@P#)"D̴_Vm$T!BʐDQ(@ pUAZORرc;Ϙ#HOOŒe iඍf"Vڂb:Pd )CDQ6V'rJ~T /^>ù m?mJ1I!ۉDQ(` lېX.( 3 BK~D >`> d N+ 3 F `,mȈlә#m'Vra$G|b@b:`ə\ >@|}@ Xddzm{OLg"Mm Nį > 1\@ FÿٓD Ͷ @Hc^z+3Rwl@ƲnV[b@[BBt&KBQ%}PZ=A^|1F-6K׆6;=}.:q#!7|T%Ŵk&"Ǯ 5q·^s1Ԑ= G@S4 V#q\k?9řEC,LzlȾbTPcXrʌ 量BBUqCKvr)~<7nF#wאR`I}bGF#(NLJ.*?E7#uNg}yq5jPy)j>ǵ?g=X( }E\Ll$H ?\xNZLgzX ٭Y i=6dӁp4NױǟGBqQ 6T~|iLF}ɶO  dP)~ˢfh\EFә/H-T^zFh gxũ%_Ȁt_ĎP+ dL/$؃H]0KBP\ Y<#;;A]yzRAVx2G'vl CjR7Bt@_ `O7#y޷HZ8[7B!2nGB(S@02~FHߵ;rF[!=(sӡuHC_Be*BvY5u RȎ;E9(>5F̤X ,K>Շ=DڔmvkE1؟ݖ:#u(J^C-χI/bx`'9 i5*L gCC}(IˁZ)2doBS@T@yG!nN\*mg%i3@mlAݑu/SHZэ.3e/2;"nNIxZ@E֡zS oz])۠ ۇ'_gH}%ٚ6(Hi8fC  PC-σ t=H 3 ːR7 9;z`ꨈY^! d d[&sx,jYJya(pH;|%YPrQ(|KV e6cݨ}}pC6?j:aKG4Bكޚ,P|cEwzә4P`?Mc/BezhaJJdis8hw\~oR{O+򑽒U*mH!=#CP=GC'PK c. AʢHZ2*(dYtGg G8; R_2. a^g(gN}9 BvP*5zN^L0\jI(3#iTkį:U-N댝 .n~H9jirNEaHٴ e!WlH[>g i&ԯ"VaE O#qz?$NQ!.W[hRVBsV>`W!a(K^!{%s1l 6=@Uţ~429 "5TC2w??}N6"s[.zN[7_mĒ'~X/Ɩ@z>s|+${U>b 쀌XU:RQ(;DiO8IS& ZvF)*/ FS,ȏX)#rx캔㲘dך-|p;daLvѡ0nȺuI" ίFʬ.r q:> e~>ٯ,V8>Z2فl ȟGL`V"Xؙ>F4m}ʔx!ʦ=G Pz]Bةɋ4#Brx!{4]>f`C[G}ُ { _|omb|}m8J(dP5?:E-C&~Z$Doz7i˺)eYfpUC65}1.dm oP !l|$}c5ǡVs!qv0 ^ c,O(.8B(\d==OG(z-"×awj?ȘIO7$,7/3qҖ=&_0MXQ<^,dTDdPf.™ ˶|ZȦ8Ҽ|[i0%~˻g$G_#ajo֯\ jsbc\q$.jȦ* ņJ=ɐ|@h1$*l!dԞqE$(S"i$6fy\C뗰4+\|TFZyN4QL&=6RƃGƭȣ .sK`RNDb0uq1]NEht&=.Aa:_GtvZZ!8[q9H-vŎ "+FA셜͇sя֟Zv)ک"iQ ۂCڹx n"mƁW7dI: )y\MP gyN[_:BNgvCӯ11ٖOz>:BQRjDL a().B&fdEBme4;fq<.crtt&}.Bv5em!{X\@O'5t։f)&|Ec~0dyrȟn0WXێ?GQ ?G"e$>멙@^|cyz)223pJQ9_#qO2~+ߠPL$٣/-dS7fرAȏ/:1rAqDd9G9?}u]:#~"[c()k#kbc*m#5M.#i]1,۠1+nEHjQߦudm#z*lSgF@z+rRCxfXN>`|VȦfI;Yd[~H=Cr9JسGZ Ѽ3()+ }Pp?(nUKn`Wؚ5V Euh-q#ڂAGU(&fG,qX![4BIZ~'d]Nq\evpSq/`n$ے?,O! wr!F}l/T4/pr.hL?F5U%ZL:,C6 ҎLUk|lBڷLAFTT>9mA&B2n^>Y34 tX?Lj ^27 z(N΄ZIgXSLGMRB-ˇ"9rw~Ôy"P,Xs`|Td 49C1$ޅGP%9I%fOGn]z*B:( ҇>+ *3S_ K\.l!Sr\ 90lZ~x} dYPdBCݬ)F)cLB$ckrx١;_&\Ȧ4%ј[Q4% jY^Cv*H=p%iLODi8 A0ͻ:@v!ˬV]]pUAv|pg:"+,98Rp]n~4pg ';r^H!3:Fjx<);uvN|$ vOAvy᝿vE*=ol7n*K >]7Qk},p4zndQ) fb_cF~1sA ͔coL|$j d:2ތ]MJɸ/>Mhj-v<+ޟb ]PPM(SL=`;eݶͳPO!b:5s2;|^@Ls?a- >R Ȏ9rH/7o}}?jjmʊOod#` T݋0 9 s&; b*5}V{z+ c(e1@w՞_m۷Bo h$34Wg<)ʯءՅo' v|@ XdS#չ@ yJgXy9f앐( {!uKZŚ7Gj7L7"i 녆k=e]t#ٖĭƚO>V6odE{d \ל),!lXK83Mz9w et_ʳ7)t&o]ʫߓ|P[vDZ 25THkk}%7Dg1\`]*_9t*L)ݶ0>~ e=? |LGRI}K(rKJ)ٱ(ރ/>lMUl>Fs-jZc<{$0v (h#f5K IDATOjSS)Pe?Gt;[RN!tL 5|mD'z >@|@E.*N܃攒gȿz$޽8gE9#3&K|.*Ȏ SzAYؿA}`&~Ј3X D$3.g~ >`> UXb׻t&S-FDQ(` dLBsәNT^Q(@ . M5LBBʐDQ(@ pl"!^xt&l!evQ(@ ؆b* 6%VK LBRl' Dm( dF++X1IDBve2NaAiD=K;!dE+ dWt+lB1Ib'fDAaDu^^=󏳡.SF`K7W{~k%wUǞT,Uϱ7$n>h[,=A=l Fu+Y;X$0釖: ;}{s:`zJ3PmL݉kN&ȏ%Hw<¾`l=sA=ɸ2ʎvިpe8h&U&`"怒2m3퇖%f⋺pj9\?2S.N4a%Wp:H|3\׆ymMػ6#qNo¯P]gkK夀.zɓ'iUV/ICvw^噾[/'I6b:4-rr$d́'S#r0%:~ⷰ#1lUFo腖?eϼ=՛ l Gdu\\Ai6H{VDܛeј[" s#3 ٲ''us z} tUU* @w>"P|S!\yx]M5 { \S"#twXcu h'qBJ棳pnZ{8ꆅ.߷*yꎀr UMlVX63R FXf '.?UA~pqm%7hdʬ;`9#/USVm5q.&w[y;:"4,n QE+0Xnh?8Au@eK%J5fjIb&c<9Gdd[^n9s" عGA} _yk4W?{? VC!Aܽ0^|4' -)mAfI7ɮ9 (0u|y  o_vikjQB5g}IȦwO1\`Ր? 5gϞ=iV.ۙA9F"2K3Ze6nf [s3Pڸ=a1IH ~Ȟ&& tlXe˯R) } ƚ'tt5Eݸ+Vᨵ.Ya]=a ,Fx|t c E碞}%98ёUA0`k"h86hh[cns}̽GSZî$ܧXx3s_$=p(?ƌ֞vYss GO-`|:Ql4* ,\ ]/|.Lר&,CKt߳CMQy7ʢrٖs5aQL[v? nx k@Wv]=v7prYn{wR1\`ՐMbk_p G-ܡCMh7YWTx=$:mY@>!&ҌFš pk8 *p÷`B)'V#Jx’î |l<噩xJrF7t&!K [~1.S0\D(౯m(nܹ+"13"&Ʈ L` #^_ԃg-Ȓ́lSa^b:|}w%݋Y2zQ0]m<4 BEQxy^Daeߴ*bf.FOX6\➇XkgC*zVsCl YnyyHP em7+Z^ _7d^|xWvA ۢ>@6ZGg}dT@ X=dnNNNʕ+u-O65*?퉋4v1rXfȔaKɶ$(rb3sϣ`#=lJĩjny,^!5oc T xJqsF b:u jJnp4Ǹ8w|-6 / ^7tVi:r/900jf }X?G9T(EG("/bp͋o0/qe] n`C8ű!~@=)psAg84UqNmN>EYcP]8?qZ:ԥԈ.J~SZTE1yki(@ĔvppmEpB$"y0sm!~Ha祸 XҔ97K:Nʼn3gpYwe:$N "&dQqN]sؽn~o_ Oȯx]}D%2ŸgC]㳺 D~pUBvnBP\\ j T^֧M\um~X@4YgeJcp4׮u/ba/1mQ Vn:b< 3~-뫭BDs's즦8lܜQU1I:~Ȧ'DptFA 0tm]O~Ծeȏ.ʒՙ6.$]V||\]{ d݅:VZ|>_NO;6`X׏*h aMljD. _}O8Ztǟ< g_6<:|z¹]=ѠMOL HECѪ5ubY~FeZ-:ÿ'8_bg'%u`DŽhU O>;kb_!qg# fĽTQܸ*ZkGqoykiԱDX4mzw@#?a|͜ & n  K_[.# ߵ? 3 jk=Y&v mʚ;Q[c4o_WP*}j$Ezh5 ⮗_NV 씔:c͚5/;6jԈ,ם;w#u2d 3gâl~#&@Ă5K [7]0vLEqhԱB<; =]ӌQ*ϔ츽kf٠F@HnL*g&Bgg8KhF{xJl+xYLg2@2d{)`-g)zMQt9@Q(P ?ݻwiE}* 4hn[.XTٚYSqhVsco8}Nhq(6d>LM6(OMeyܞL&?Uhvsoc.4[P+3 G [H(@ .*x͛1׵kF=lC}b[$$l!evQ(@ ؆b [.@˖-_###6'VU LBRl' Dm( mCfb廠$ l!evQ(@ ؆b@m-әL$- N DP@ ȶ%VVb:!ۉDQ(` ٶѶ V@Lg2@2d;Q(@ lC1\@ 6ږXY LB&Rl' Dm( dF++X1IDBʐDQ(@ plh[be+ 3 H [H`,S|kwRAe*Җm/SDQW@ ۍX` LBlE2BCY \}7x!)٘Kg}긡xG/S~!vptA.b8_4oQkR*pp;R֖qh_vUV)o;%m`gw3u) O0'Z͈Pl;-lmb ̵v#&FoL:CQ3m![&ڂs.0#+ڣv7Qw @>6OPjGόk.ھ e0 >{_8ۢW8p1=7>D@9) cB u@j9l$RvWE%PE,t&!K [d5Z8{"2ԹG=׾L:} Ri1 Qezd| 3]7Խ tAQ(6H`HqiprkkXXr-hڽKqbe(yEGq\95Y(DGGвT #kFϹ*PRG-,B*R 5 160\ [z4ppcΨt Gp![UثXֿ|H6 CvechSS@|4pp?uOV%5~Xxu6:zaL g$l>Ulv$TY>.45ca4;U63!LA)dsm1W^3S:qvWtĵGu˒Rsmo \d2)&\syŔK^nu:1TOL6x42\ɵPHsw/ 3 t%tmI-*3y^%T\˦kSL K釥 %&6XGyȾoGӱ]]aֺ/lC=Q(P wg]-4cb*^Vf?]_bhR.0\D G} \;`L荹Wә lmxVO5pt[?3KeRg3D@sQI$dSy &h ;t[t%V߂Q1Тj@672Chvlsm lsyZ.q}ܽ= F<޶Pv %j47&{ܼ*j=雍7}1b O"vрI Ncp g4'.rk9`I̹Ry ,oHHݍl& A1\NC6 E_|Ps(S1̿3 nk]*-0淃8{ \Y]sùV7,>ctתozdjqˢhִ~T2D@+ qw'F`ZF4cyG`FU~|_ᄋ2ۥyG$ˇE1#$G әlznp4Ǹ8w|-6 / ^7tVig$ۭ,|χ٩.a,hw &Uy/oᰓ8r `ޗrtvt%63; ݐ娀.x!SoTT*vGu)_*>u#:kdz`jͯ *[AM{e6&dU:t&!d;Mm-@ҵ @w \J*`? ロB߰ܶpp@&m]kT+x:ÇNņvH&Mܿko\.p >v5 AQC GӟοMPs HN @ b ,SQNJF!pu%>.vV*SMOz(砞~uO>e#)G]^W!rT9( 3 +BDQ(@ Xb a4(ORLCrJPOokK霿0ǿ:=i8/5.8ȁl*C?lQL ʬ$T(l!evQ(@ ؆b Çg]]] :5Mo@+Wtͱ;x$~iqgpr q>t9paӓ!ߎS#qnqĤ9 LBERl' Dm( dF \,6EŸR nf1GpFt[:6'ppD]1+}D. _}O8ZtL$#k2&Ք$* 3 D [H(@ .zȎ4ljGx LBRl' Dm( #uhb-* 3 Փ@2d;Q(@ lC1\`Րm+Ft&!d )CDQ6mm$d"l!evQ(@ ؆b@m-әL$- N DP@ ȶ%VVb:!ۉDQ(` ٶѶ V@Lg2@2d;Q(@ lC1\@ 6ږXY LB&R+w[NeVT6os*JCn9L\<Q |,: *"\-72bХUB1]_ϤZ-]wR޻. z It& d )SN0f_l*nhL I|jd(WC>.VLvWe}18[ LSXdKqcaW8…X|(@ 4dG]/ aܙ`q@) 3 .يd B8oB\Mc]̥3>؊^uwcQ2Wa1c#x8ū wYL=^Pߐ)~eT Nǚ>e2MjTu2I u U ڧ" =jm![u} uZ˚~nq4TwEu2j5vnAsO v){{ek-RQ 7S'rr̜s# ә sЯCukSZsDnr-h5(chV>t0eO4WSk04vGa(Wĝ%]R<"j'VD QN& gcF򬛰֕br|D>ە俶\LgW0ZN90vj_nҡ}Q[|$ Ǔ/ȅ2Q.csa `; O؂^ 7^/?GC = L)}Ъ$f]1*Uj>| *{_pqV~\P& =Ѣܜa䎺ky)lV wIJ(. x9u+u':1NRSZ{׀vNUѤ0_7B5'Hj|!K/ۂ <&l`nzZn#]_wlbUZ3o*q{a'T77 6ݢ3z> W]n7+Ͻbx{4v=$qhToЮIMH#Ӵ[KM}{8 (pE7\5?]\s x;ϗn´h)a+Y& ظb j]',[~c~2o1v|wa=M.˫'qq"l:ET1|dCW@LgIM=YΞ>KRA\R34T v9|c.7<0͕g.?eVu ﯷ# tWbPݽDz@:"4,n QE+0Qܴp \k'0o#T3V _-F8sl+~Zą\R\n0|><нq#Œp 4jq*F8m­J]̼dqQvmBu~6[@wSMs8JjadžlH7 .a{ot|݋B~ i: Dx!,. l).Nn1z\8KnjoztD!:: F-jq0]sgʩ{>q-V*-0淃8{ \ ;ց"~a{Ĝzu|,Qc/7 -G+.Spw\ݔd(6) kaڜV7zkb? ń0l4dO2 m%n菰<^J0##aA o7mEMw3HX!- LlәJ+45ca4U3U63!L@vi+]~2<2=2KQ[!/'}]LnZChF-هCv%nt [A0M W#z _r؀6VEjnz+%jA=hԀ*(U״smoM1ZLyr2KWt_Ti'7 - s> 9M0>9-<\OY/d+^ C6l_1\`U=`P-h~Dr4OiȦ^|4yB[07t,cm^mw5ICs;Z#qU{*@26]Lg ?dOSm:tP ]*{|'!Syeح5<9(qgqWMf13 !rDͧL XDE=Jsp#J;`VfcnV _K0p>niĩ[;Ib1'|]6hήASW߯07wfկe@x݌*hI1rA:3LCZ]8?5N}7Q棿3Ԭj>u-G&!uZlLmY g^3 z+׆27U+\޳ú47z„q2 +>XK}QmR@ Xd0hFzc1rtcAӰ{Rç5FB8tll C}X6@.Q;g0qQ$ي t÷V"\U>{LU"\ -r_)7U先lZǀb,0?k#4[vȎ3+lcDC;L؝7n(=+~$!FCgnч߀&#>]EFjmi9RB399:}p|N3ePCm(1(Qm`d!oc`n0rn3Ǽ1o__U%0W , *} TdkɮG]{з%\. E;C~01le * LvYv9:'ٓG^]ܥKwC0|bs< .?dO<˖/}.,J¤fh:]83c.Cs?'%X~u%В'+iiHKnPo{<ѡ;W>WBYIxOFv43cQ-;]Ag0q5Xd+nll3#S*ȹ_bUl׫B}ٱ^p2o_eGh52sORj*Rvwn$;jk"c"ʤ0 |҈MMђUrنbTNɳ@X{HU %75>Rv[KLjg˖Q<_adGd U㷱hC1--/5OQm,Qǃl$(0"WLç æd3(y)}_*ٲMe~JoelcswRVU}hl@G "%;A]pq1>hL)8s[sK哆5Q >>^c]^Za|'ªvK7Z oCg,\i?v,K1s:|Caq!bf8S1e= mCjz Kt0GXNkqT>MYJ_DE>$Owk33l3Ԫ({Q,&▘1QkC3 %*_V=A+.zׇGS.KpII/_Ǔ #"VqV*ǔүt37i_BdoljDNJ/óK(@."~~17"I &ͫǖd 4c C[ OW\CG4._9=CA$5,ORݼ)}?Cc/)k#MMmH-[J"cms+-Vc tsFGY0ӃU#tdnQ!Ǖũ.ƦL6{N#_<-ag-0q]< E>`NxFږ; j@l_t 5B|xc`ASyu"o#}GJnGkdKn G[1? Cs;92dEvSA8B_bΘlj^M~kGxp]൨-~q̹_b>L,RlD]L[aؙa[;Lyae J5lջUyY~`%9|eǨW*y 71Oׂ|6S@@QU9IHŊ +QWý~ܭY^Ӵ!˱d?Вg1T 6Y IDAT]*O K" tNX .ǼeqY脤+ap_]l&bbs\7g"%$?9w$ZR+:M "<[V>ٍ&WtITL5?#.guyRc*TyA|2ުT~|+balXuUmB qo- fEWK>z{ F!E"t"O:& Cx3+ֶƣ#lMkdCq2|f>%ji*.*2ΉF3(_=@k4C芵Q ށD s75dW-O{!zV_b㈋ GoPGn|T|z$ j?"U9Fڪ<-)VJ˩ tp_܎uoh^SbI4Ason68+USiRw{cIٍtQ G o">y7wX]e;́ش;O>*ߕ@ק~mm=t)05qGr|kcobLgGEz0na.>NݔZlZ&mSi06i!$9i83 ZufD(g0)ML D/)^S]ZZ"{os^@-("I6*'D" |$[}KQV3>+Dl.2TN D@xI0f|W$\d"@0la-EY &I|G88kܷ^|j"@/ :D`I6X^--anX6fZ >"n"o' tJcOkNcq;[}}oCj3B FbڡÐE8mGoa]q}d!z;zjbf 7jSa|'2m}YJͫ,X-͂,]m#5j0鉽O**M:Bg';`hf=cى{+x4[r~`dke- NxNI[;G dxOci Fa1X4~4OeFc݌>&lM`"Yqay6x#8yhkbkWmk=M P3~qb,t±w>71DGf%YAp3uQu'BblLYB.H:$[ZqALL$V n}!8uZVEoB$/0͝0{X1 ?M&F$[9샷E"@>>^S=cdv_bqX>|~}۹ukbgק^-w`Z'y?3phxs4C1("5ł(5ha8ք`XE%Bc% LY_Ic}z oT=oO{ְ!~ͫp[PfzmݍpÌ@iMuAv03҇9읇aCsfhbC}۠y1IY}Plmה"ydttHfua[)Z)ňz-S/|ߩ)L -;WS(y`pvDf1j' X"@I߻az~Ҳ;v-{,36mb7oشi#6ߧ`j8>S|F?\4wΆ!K ޹6:Q4Ivu?2{؛Ӥ[K-YHdXv"g!`/s/"~_&Sj oOaX=4ķ #S%%8$g}Y~iWlHΡْ zcQ\H<vQV\Uji|*)) 8|Ya,.E?Z3d#\?_%MdB=ϱ(EvheJrVEZߓ#aRSԨiwaFV"m!k}uB al51dRpa>7nb$-;D{y13agJf@8VdZFA9 dwuLctxwнQb5keSSr#09x%/%F(t[= SsUڢ4$^6h+W l>Uw$(ޜ埆[1-hBq"@xnIБ9s&&OHBVi0q wȸx+ D!RuvUۦYk.g0q,ٿR-ٕ/KUS_뇸cfk=S4uۏ, H"n2"[,5M%c,(FĔVoҘt ױcL؅8rI+$;}-u wu98;;Qyўl.dC[\f`%1=K Z`hh(֧I%EUh Ppaeb}8yDpH6p W| Y8?)ZbQjm"@4:%كbҤI*{u&&(?LryX&7 7(ŅE_$$p燲`R'y@>VY,|+'g0q$ي t÷V"\U zZ{~7tA=E<$/Xv`g|v9KdK_B?sS>-٧~m/fjlmdSޯL,{qbe8ghFnc 'p@7 )2زe hBs| $@^ 0aw*nܸHW*oP]= :*R0Est,Ms24%~a\k5."@xNIw} 63.ʳL3<5=${V..aW(aV))-+/ ]y~ wOi7l i}Sv#P\G`\IKCZڟx䩢Xy[|-M͌زONp#~UA^]O6 .."SLMx}iF@1_ޒ]r :vnYJ|27hr3YHP-(dKsphlsԲ7-WT,)>6LUtN6;d{0WcxK \bL,GZ YOB[Yieaf<^5FrZcQzF@dӫ_d+ ,6# fn!9ٵ+VlmyJG+3|, Ff qKLl~~WɮU^/`/DL);z44EY6 !"* a1k}|j;2B^ Cd1}'NB#;" 3V0k> #"Vqa~,XUwt&ʯYI8oԓkUNϨL6GAX :0~f ɐX?' syKLZ{ 1C;FvsylC/2OBddE585- !\oprڗ4q":g|dkkkKޥhflS"2RSB$kXd"IdNy f${wq!e)HI=.\{(_dk,ϓɍ69Q-)͌TvdkE-О?`¢YK@hV&h?x!D~9${Dc%cߣҬhL'yg3*~)z04F.B%l.2<5yD"@>^S=zƏ_a=l Bg0v؈*AR9]g+yV=ϳI¸n}ؚ?e_:]ylll+G!2x*bN6IkA VES2f$pZ DxNI]lld+`3b={lǑÆcUUvJbl.2TN D@xNI/a| ozE07ا?\.1e F/&bx0zI=ponwWQO|W$\d"@0lTׯ_ɓ'DVVrrr++5W.` 1H &.$\d"@0la`Q_܎uoh^S,LU| |ץIP9 Da$[ L\!dsr"@  Hѷe53B$"CD"@Ad o)j&g0qHEʉ D/ FRL` $ "@>^@-("I6*'D" |$[}KQV3>+Dl.2TN D@xNI?!lBpQ8ĤDܽ{_A`j8I6*'D" |@$v- Ip |ItF/">b-l.2TN D@xNI6#ЊGii)& ߸<hci&7!$~g0q5$ "@>^S sAa!R.X1"A. W0D_{ c*edyL & dsr"@  tZ_zS? z"IIϞy6bDm#M0csf&$[g0T$[ m"@ >^۷Y|&)ɓGxx8ϟWwIjg(Nv٘&j|Hއ?`⊂$ "@>^s5#x)մ8C#P ^'l <=\}S]ʱ.F3SXV&FÖƫDRmMm Ӈض9\#D"y`pvDf1j=HX6A5dO 1+Y\O|4:[6]^S]PPJK&O"y18t8\y^*Fh=hȄ.a?hc /u4gQ:kcv[N;S HIIC cer:а|GLD0.3=q%- iik ?;͚c̊F8Y絤9qoV"#c뼡hlbH-E>HEʉ D/)x{vc}8p`ٙ쌌L(Ov=sl߱G/he-qݶѷ_x(춳(-;sj3 )? bt[rE %rA+XI-ꔇ͑."}błI-y vͼLq{at&0e@!yY?6DzK9:}p|[fgϰ]bst؂HK „VUlBlY^+/Lgĝ$[g0T$[ m"@ >^S} ,_QQ8p6nڄ%[#&<\߁$=|$c8 9E.+L1^t+Kx#v.JG3 "y+"|%>6&2?E6ΐ#VyL*(=!VAD"@Hdgee_>WE1{!~13k~yXŋٔwspص>>wyIvhflS"2RSBRdKm=DXLdnE3DNp['O NBbCg}Ѝ#"CD"@AdŃqe$KDğM@\B#>! glR"'ǵ?!33{Iv}`g8ڊYmۡU1!!1~ IDATG[&Sck֮ #3Rd3T+c:;B,҃uS pYv3e#_<-ag-0q]HZ(BtiRFz!2c1;a? &.L$\d"@0l nhׯ9~Ǽ< (JA3FEʉ D/)f@`BEʉ D/ FRL` $ "@>^@-("I6*'D" |$[}KQV3>+Dl.2TN D@xI0f|W$\d"@0la-EY &IP9 Da$[ L\!dsr"@  Hѷe53B$"CD"@Ad3~'qLU~+px0.^ī/!H3L\$"CD"@Adg>de:48N<g" q :7 JC;LρoC3HEʉ D/)fRD8wD ][;a!X|9;{ws<&nKR6fZ > i. CPySXmg0qCEʉ D/)ff>Xdz7bʔ) O" >SNe83())y^*ogk؏ A%HZTȨSa|'2m}YJ_bV͚-2Uz|R3D]nN05kքoH~Iœ+)Q쒛&:uWa<ڛ몳qw3'"CD"@Ad"$8QQQ8y$VX1jEô)Ñx\_p;v/^x^dKjg(Nv٘&UYwYw/$&!>&SsqbsԒ7Nj©7F/{SDJʜpۥ.U?\ͿF8` $ "@>^S}{?'N`ڵXiWgO?#@ط'Of cSF>zTE~{ {lkȥ*iGİ%V^aU\/#~E][L;%/-zcC:01c7$(Oȿ=T"c+4-,EJcv{kv_CTbCc{hm*} &q +&Q!Fx_igi&N,qC`z(Kmag`Tjl3#}y6$>/og0qCEʉ D/)^ǎCPPV^K<1LR;q̱۶m{/UQgZO<f%[RbkSo4dKvA4abbaܠ7?)j7W@600oIk!28&ܒ͊XFEگc"c e% =q%- iik.@ܮ02kGq::?ui+,=+;\h ]ӆpL\dsr"@  tJߏ'bz #}}b9B/_=ʎaݺ5^dk2ZY`\-xE峫rɮYj׮e335MaF#\e&Jl)J ^^j(fAXV)$/%F(NɃ-,{HD_KSQ^zls91mג @[SXzE6#L+]DZcLtxwнa޻Rpa>7nb8>EPnL\'"CD"@Ad`gw1}tDX=jVl\7kW`֭~;v`]zTeɖU-{0Gl4Ie4[YlT$E²!6iDI.e)LEM=4.e%yAD"mᓔ_nZI>p0vwѭdTG1=A4\.jl}e.#n -o3 "CD"@Ad'7 L:Ȇ 9}2"a$%[۵dlM惗ūRvɖ:~}$[D"@@d3_o׃y,]_q[ѯ_?,^-_ݻ$]c?U-m,a=`3^p\\EV4WqXFcUSIaBEkX&:h^~O}lLe~Jy<%[뵞DsHQ ac)[#?{a DDE!,p=&vO-z`ⲙl *.gqdJzFM1tj'Wlfw}y'789KL`8{3ozuہ :d5rj#eQiV."^@-("I6*'D" |$[}KQV3>+Dl.2TN D@xI0f|W$\d"@0la-EY &IP9 Da:)ո[f5<|׮a׬FP"g0q5$ "@>^sI?#`k-Yq&oVbIhVŒfaE)(|WIP9 Da:% 6#aaa~iҐW.#5$=Igq1Ğa$]/^F/Q g0q5$ "@>^Sͤ03،`? ϞeL<|ݽk!B2d _8.fF޽+^(A`jI6*'D" |@$Y0W^AV2x!M\ d_1K(,,|^*}_ۢF&=/# ܜ`)j֬ =CS6nzACL4^K+zCd CM`jI6*'D" |@$qHLJģǏl<~w7D󈊎ĒeKۏXr9>}TqALL$V n}!8uZVYw,J8po:1٥$[|RqHGiF AS,8.>5$\d"@0lf&G&IaD<ӧOY6})bΜƢ%0{.VŽޣ5-Zx⊲$KemojG?P0fu0&0];B9T m2>=$\d"@0lf |y5ܹs.$][|9qgf3nBqbd^]FF"wjqX٥b[{s x38dsr"@  tJueBBt.ͿNO3ٷ•+d-ߣHw["o_?EØٳ>l j)a w0l9W\ƌ &Vdsr"@  tJ/aGd)˗<[qmܼybt,[^S翚@l}"$$fr"1,& ::]'KU덏*/.Jɷ9 v>Dzr!A Pg~B*'G`⊎$ "@>^SOu /oJʅ Oí}!Ƚ -ęg BsVR =5|WHP9 Da:%YYY;q]ga#=bv#GI-[cG/db)\DZZ._n_zWӮx{-(I6*'D" |@$[)> I6*'D" |$[}KQV3>+Dl.2TN D@xI0f|W$\d"@0la-EY &IP9 Da$[ L\!dsr"@  Hѷe53B$"CD"@Ad o)j&g0qHEʉ D/doڴ :066f{x0z>$\d"@0HLMMQvmԨQ666_UCQ @l.2TN D@xKvxx8Xfm۶իYvuu#^(A`jI6*'D" |@%j{Tz͍'!g0q]$ "@>^w^V4BVE͚5g(m4u=˪fߥ,:BRD`hS|/HHޤchtq>D6hӶq &֐dsr"@  tZg.pqqa%ʊrV : ">&6F/{SDJSf-4r݌4He;ۢޣphF,ᆟΣla|WsHP9 Da:-,Ǣ"йsgvzРAWZ(hK*3/#~E][L;L@g[stC QK+_)$g0q5$ "@>^Ӓ` dg*Bnn.4hC"QNبp.H^"3L zS&ٶnT8=lu~{ ᧣*ҮT &dsr"@  t^۵k###cժUe=q}VGLLLپ*oH%p/5 ,L ۏ"!aaOǁ5.퍍0pW9:PL\m "CD"@Ad>>>#;;`VaVa~bvy>sL~RRtUKo]FY}5j^u)[&cd Vi^:\Ǣhgl-8JpnI_wxYVmT>+6l.2TN D@xNIvff&|czovlڴ)KݻaÆ={l#hx2.ow78K6o]K,\Aʫ Š:prGJT%ZݑRHFGL\M!"CD"@AdwTԩSl텀8;;ۊ2#xf&bZv"~$15| ?k F|H`b`jI6*'D" |@$GPݤI\xQ= L 杰l+>FWGrP4CKL\Q}f*I^@-("I6*'D" |$[}KQV3>+Dl.2TN D@xI0f|W$\d"@0la-EY &IP9 Da$[ L\!dsr"@  #ٛ6mB^^l^ Ӄ)j׮~+ _FP"g0q5$ "@>^0Vn۶-zkkkV]]]~(JA3FEʉ D/yfF`fݻnnn>H}:(mʹAIsqlT9 I^bO_>,S?W=_ IDAT6|l.2TN D@xK޽{Y ZO]nN05kք)lw~Hx\ī '܄07vVR ͂K=Xt䗥D`hS|/HTp?+X{3\y] #v3 X ΝKDهa]bekkL\'"CD"@Ad>c\]J!AuN%E|L8mKSs7l5Vf-4r݌JR#/E]mjw{S^L\M&"CD"@Ad3؊4,wܙ4hл\d-]o|(JG45HObpf`Tjl3#}y6$*Jcv{k5J"bht l`yBb<66=|␧8Bdf7kh; ƪ({doHbWz|s+}Ug-~*zcC:01c7$H"{(AlhY}yOu]g0q5$ "@>^Ӓ` dϯB/Ah߾=$*'N!مWW~@S>^> 0PL(j?p B#q*(?wOגc"c e),>ylnT8=$sȬ%&`&\"=q%- ii⑦i6v.]g(V)`yluG򐰰'L#j->,.NwG ݋fҝ3'[m]C{%[0[[b KlAg5CThz&l.2TN D@xNJv޽>f&́)fΜׯ_C*UYjEM`R(GM|E\;5G=+sX5XN-y{p󁃱#˒?4G"nae;Un|\g1=A4\.~PK~-D044? PSA`oCo]K,\AJ0[N iuwK򂃡&Nō7xf^$j@) DdwؑBSX EAl退8;;eI.7>*ח} z}p<V|]dKEIpwHؘT_T$(0"W ç #K)JG3 >\~itg|8%C !3EEʉ D/Щ=zCv&MpE ]pi)[ԭLԔP-@."EBbq@}ŁQyYE6QGf@Egg" elY &پTgttN%ӧSs+ߺT[LfmFv@=X*dGFFSN%J7Ĕrl)][?G U7njaZE9/꯶-*N]5W>H[ܙlcyY `{R+ꯇ zݵ.ypo: [}T{ݷڕZkzu(;~<+ dIJv& @fl:\oݺuk͛K.6+ݭ  &M6#C; @j5aÆzSO9^|Aj XLf=$d `VrA L/k+Ɍ !ی  { -bVY fdh ؃\@ȶGme2̺H6#C; @B=jK/dEB!@ `%Q[zY &.@+m*&`e0umFv@=Xl{Ԗ^V1+ɬl32CAJ.Ue<$`e0mFv@=X ΀=e糱*dB!@ `%&d'$$h޼ys>|AXLf}!d `Vr-Bɓ'e`gdd8+a,ӱcǪ2mvyy tx ィR}ͿU)+ɬl32CAJ.!p?ԉ'JUԩS6mK{fŶ/Y"ҟ~cU9ٛ4EEO)ޛ~@԰j:jrW;ŌRg(>0H]R|IsNkަ M:Yҵ; n9VʬXoSK뒋^ݷl얞.RzMwƖjQːr%N_;KJJ&EN3 VAw䪒?,9l7.k2ӁmT/~^vA`*Lv'6 @^j+}רeAjmhܣu+JCvvl ݢRn_X'EvZ.ڣt[vhZVɓZr&O*Y_ _7]g]?ţV_ij%)./ZLfmFv@=X.dgry\{((3n!#~\̘ hSqKiZpVWxsk=/}2M\Q|l8EG huZ Kp>RxszMn mAEWhhMwhgK%3D75naWeGi̼sT3ޯ:"4cLzWq`2{l32CAJ.!]{toFOLי+7){Hvv#B nJ!uzL۩|OԊf-BtG^zw[^,]#۫U7*z rmvcRgr888B6!+W;@1P"U$88RSSm޼  V5 d-^8PU.))IǎP^^/ĉںumjPq*ZB6!88>u>z(`bw^C *alN^ܪ 08tX"bL :sw_րMzz;ߞ$uꀱ=m=<5ۓVj@&d{=p/j]k@&dqppk@+UkzSoOk@&dqppk@ ژlB6!pp|@ xZzaw0xz{Zck{Q)ڝ}9z=p_f7,@g4tn9@d%kEyA A~W)C لlr.5ESx":GQ4o*t`y-dCn]zZ^e'd{?!xfrppp:P!;N=]a]iϵ fz>?ڧ쨑+]c˪""dsb'V7>dg()n:ӂTK^W':+",\z u֑[VmΑa 좘iѮ Wzy%Kb)2=Bv>Ϝa}Wǎ]{D-{4dw\.\܃pYϽKsݝOvIQ TGO}`E>w(E88`D#Bl\]< I;S_"kR \zM[)q27ŸmFwP5i%ئkilkCKnޤY}嵵5dYr8 999E4nbӀ=4vz޿_;*p˱;KӴzSm^} W-*:!M;됝DFh̒SWf SME(\0oF>iӕ{i}' {La(~_-Ɏ5C;\wT4aE{+y&x"N!O ٜ$xx88`Łl}1lb0NW5eAewwE<;vVɅZ`̜f֦954[ѭ/~Mim3Qb+2,L*E`l5ڲwm ٗw-Ш{(2w M}G%{nX:zKGԀO|d KLpppTV.o iY xԠow!j ٜXVf5x a88P{ػw=JFJJvjj໐mlB_%8V8{o m aۘ==v옌iu!v?qpv9ӧO˘M5B>w nSN9x;w=Y لl,Kty;w n?xjP1*ZB6'V˃x\f7 d 8888>vc\ٕ}e#tلl\qppp|!@kU(B6!+W;@1PʾpppjlB6W8888>vc5!͕+ d(Wve_F8885`p 888ځ @ @ @ @ @ @( PIDAT @ @ @ @ @ @ Z@WIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/icue.png0000644000175000017500000134070714062723141020467 0ustar00jonasjonasPNG  IHDRe IDATx}TP`)gvfgvD&w5cMQTǒc4Fӌib5fs;3; " y~=ss}`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`F`FaF`F`F`  F`F`F`F`0ee`F`F`F`EI>{iyR`F`F`FI/^F`F`F`}&F`F`F`&LzF`F`F`YgtF`F`F`2eF`F`Fg`һ^ZaF`F`F Io4[,-V0#0#0#o ّ35CC_k~Zhݾ[v靐cϛwOLL;^bl;NK=| 98u=&hVW/On'׭=nQ,[s+.bC(7Vʵ-r,Y+~.;eгGN?r~e,Ϻ`8ӋN_uK4.:_|ޙ+FzfSk/8; QtS@k~\30ǖw6Hǯm䢣iӸc;/Z# m1ew?-~V-I>LuSn@0-G}7 F\ouRۋhӥ=L1ZlV}Ɵ޺.>eY+^ϵ|~zT5t,IR"qI 6BU涹Xsڋ=o{FΚ #0G`w+wWCc+^tّ:-Ƒ+^Y H;z{aG|&&&>__}bb…椷pݛN6YApɕ]B/y{.Jmݗ/^3r{nYvҕ{]7nf9*}>t/9xAnA~؏U-8p+N-qހMe7n5ؾ]z#_%?~vyS<ĒCWv`S)Qzh:jAK?+7=amԧg]=6X g[_v'6^PvX Z(n+.teKj<5 IthGtGp̺Gpط?wyIAiPLm95eQv{ͥi@;G]qa!^SѾKirO?:gz}`=g\q;OͷPHokz$m4*ۡ\"^:MW/iJgޕuWϗˌ#02 S!^Vx(/߹. ꁝ.m&&&>k0ZLf 'j9JRj|oz.w Uc+!ZX}纵~IGMC+~xBzU@왂FK8ҍ[Є[|IWuҘIQU@TA? {^Z;(j9der :s {hBA o邋oe> HJk@ wԀk\O];O[ln{c* L:_eYM8jsj z_y:m^ MF.^XbWox+ i˽Hz|>l{]\×{i^Q9kzˇfF`J:}Xuw3^ѲIʕNLL|G^_w2NzO-6f2٢]V</\ϻӆ-wwo~Ï&*R { hqT.yvN!0zOoVEc~sJYաlG9&\`&=w\et(ԭLz#g~ΧмFagy 7>x⸘"-mHs]K_ZozvWmwNSh14oQmWvW NgWއx=nٙN,mR5~5o<6^r,O5M:yy##0#@gZxq]@zcܥx晟 E5;ݺZ8kӿx~(pˆ3RVud-nӚŽrTwt.-Ν4飮7I O' qa`"=tâ>Xœ6uN^/ Fě6?柜7t`-3O==Ϸ|ԥ9h)-{{}Sν-\u`&|HTOa3q%vzFvK|l?.omiK^E[6=Ug^}ӖeDlpsTë?q;hv]ݰ4_zw⯝x=^kgg;ճ@ȷ.rm՝tmo\g!{)3#0tښh!޻蕻W%_fbb^l{&?;>*l7CzϾ{Ai;maWCu s6UNᤜnxš}iqb;JofsEmdO `ZߺsB1y_im7^/ݰ~Ft &gKo{ˏᾲpi Z[ڭW{`OuhyN?[m+s}?Na^I{Lνsŋ1:$ێ˧D^]_r9y%2|+[{=NP\o@t=xphݥyx'"n qնK{"{]1bx=sݳ@ȷ.r?r큁v'|vF`F δ_~=˖cǥ˖Ze7bwC&&&>|vfײkÏX611mۿETg(h&DV7=tr%=G^,%8zL>_ NZ,CVI/3՟-M[nVl1~Gϛ7>okEKl̓]x;㔇An|ʠH-6OaO«J#Ga_<+_M$:R8G<6"VHyuqi:p{LJyi蚶tLw~#UA4]j ]` q뿕o'8bzG^y:mt7l`ojTaۻTnO؎0r#0δ_[ϱߣ1o,?5Uk/e^/OLL ,XmTo_Ys#s&k\011qFtHz &ρuuɸ,`rfkk(oCt92k4I/\&ԐUOMVWZzɽo[r%->X*?׽c^~8I2G\tÏ?-[y,LHCxy]c/ UU=kÓ?Z ~zm3;^Di>}ɟ=zﺵ*~1OuYʲ&aWs݊|b{|<喓FCpC<''z7?,w1Z8U}/bSm=[B&k7>!")[WNDٛ;BJq<_Q).Sx晧?Oli$?֘,믋ĐI/]V.0#0mLk;UmPx]{w=5-hĿM6 ӹ36_oMLL;7nT{/5Z-vYh&E|@]ʌgc_:Oo!b :t;I&LHfqhQ87OWt 9alh<M6j(04Fr8F5nCG@:4og $lEi? }*'Un8  E[v[ݥiMWIqWѮK{"\  {iwb waF`F`F`CI/^F`F`F`}&ǦgtF`F`F 2eF`F`Fg`һ^ڝ]F`F`F`ˤ`F`F`F`EI>{i>F`F`F`v&LzF`F`F`Yv'@xF`F`F`1wF͘6}ƌ3fNBk]O1}FWO2*SyS#T>kFfM.1 Go8:5;:W!M1ߴJ]]8AeQqt =TꚥD-P١qdFL*7tt3d ܶ T|hGMzHW *6O>m+ބz\)F9wҬ9rvl5Yٳfw~3g-7kv7wu3gwu͚=GlG#(9s֜9=ٳtqg=̜5ʳfwwPgNwO<`0͞Ӎ ==YES=Eʢh4wf1flGm &ԙ=hf`MSwh9Fj h1[lo2k{ fa2 `Xo0Xlv'&p̶lNپlsXNc6dMVÅ:Vl5tmf;.lu.j7ve{%ʨ׏VgNY޾~c;^t{^Twy|vbw.r_owm}}^kw.@Dxaz!Wr{!?׏r/t{\^o =>(}tP?u?u}Tvp0}@  _HTt{H"IA/@8±?EH<FQ9Q?&ñ/x&Mz@(OЁP4KdCѤ/ F؎d%(BD*Kdx(ʡX$Nex:TX4Mh"M$2d%Cd<-ʁH<Krx:"L*WLdH<̦X*&|:_$2x:+P,R*W ŒD&S(@LP,O糥jNQG~\GŧߋO0> >ͮ~/.x!Tc Yǎ1qy|ۇ}^_@<>08}Xu#i}N.ou0r@*Q+Gsbd96aP؝,Ghf{/Fh,6p-vYQ`Z.Q6z1v]p-v%bGbWG0Qi!@^P& 6AT"8" F^B`^'hPC0֞uД_}*|T7}F(\[|tXѕRT]#tuh;YPOH⣻gݘPE5r6AT{eTг\90k`]]ff͜5X._Pbԙ5[PbbXu;ʴlvAh{z x_bьZ`j0^(m,Q_p㵂&,^(5Zx{ fjFlz&^^(0X*ٯ xmvMk:M2^v񊤲j:\xm[o`^l=Q_pF0a iZ.Me O O CC6~0^CeTG_Sb@L5 7t$6GDZL Pz8w5^`b4?Kdx/p|"4&/I#t0&h eL[~T*ڗӾnbz+q4,>_Xo 8>͘D|9T}A{!ZY \`}^.Bw<;͹k 09I @3V c![z_,B{#1ChWʬ'$&bJƋ!]F J"[$bXp{ fB4cQix2uP HmCQ(d)RtttԞL[WDmPeDHYU"ڌD;+]SiG:V|gz:fUUJ;R}:w8J4 @_jQp80aTƋ{Anuޫg]r~5 AxlU2ʠ``{AhQ& f2X.>麂7^*C[&&,b0{xA\v*`2,{,FQHĤ,_@r-Vx[YlBi(x[Xv -v'-M bfc&pi`ЊUƫx~8A]^ۣ2^LbUx/6Ct{Hv|n(-'6J">)&>``u1e.d7l & h1Cc\ #Hׅ+dd9+:$1^\TFP,+h\hj9a $_TNf|8D4s))Rzz/4^HuC4^ UKz/YR9S(c Ƌ"`| orCŧ-B`h;~/>|@+&W!(d_~_0z!MLUcR[v<>|H=^\UGh4] ˕3b`p S&1l3*c)Yj:pRaEqpAZEBІv,:I`$XMbfBvEn4YJo+_MRYdB&Ej2Ou;mQ!ۥxa%w?MCjed^2Q"lD8N Wڠz:T֝eN-$q9u@N^kb)>s:`Et tp8QQ]3gS#jb.uQ 7ʢK '3*ñ 4Γt݆Ù4^B%0)7]] l\\l'WYs`Z&W2Ud&Ƌd Kem0Q jz$Z多 ڲ"YAf*-MBE,M&([m +XfN8j7-'#t] 6x5K4* %sNfpLsxSc 4>*&J){p8p>3}̤ e0pr+ cGp/;%gḖQI+& `@@/d4_Rp;q bĎRH"MFi?LgPH<@L8.}kR"<=30A&9D2,RVR":&x /_2[FP3՜H.W fKH"y$[Ug((ng38"ǚTUxlId`T}1+uPi0*d.[LX*lQاSb:H#r}8+3gdtOHL( 828:?+ur}bN}x4O XHB89pC_fcTYŴ Ѱ a??_k9 G9Pdpgܨj|pƛ-V 1D lztb Y27WoT pֽr(Lf ń/+r 4r5R9L BQMHs<&y1;'҂CX S(B<,^Yj, C&ԁP J2Yx|IX Q9h,88 (t] qn)!s FOp( x)  W LXz<.0>Akpᣏ)˜.wy|`nogQv{񢲎T1Y~],TK fTjHLP`h!_81tmlB+ ("g4^!Kgb B܉X "cD\ULd ;]$")CۄG|8'WzE !wJ=!eUGPAAp pJVU|[ EЩi[]T$FiC,Ww:`.aH  ۱Q8]z{23Ԉz:tpQQʰQa~qhHe21me؎8^Y!hA`(Ϝ5YxQ_er!B*FDfaEFcU&Kl <,cwvpV\"XH ]2BNf3 1l+gfv6pvUFe-vj 8U>l5KMXF{4ǫjhf4^AbW~mzW32 g ,W S"W:/%>}^DqrXبre-][@Mf ~" >Ob 4%b D(+cz1^## dr991h{U '3 rn'yBI I(ĘOx{`uKqIA/9 1pT5^h){7CxZ,qFD58DžY ^4PRmwPG()# ٢r&PȮYHH"dq*O88`Zm ]/2O Bx!`qė vG<:8FdUI`-/5+= fBmRi5$@xR!0P) e84Ɯ;1B eZ ~KڞbX_hNcpS!X/qj0B`-EKb2ZRu1d5LJm rcFaazp"跍VT1SBP#;AD**JGG]etDΉgNHĥӢ`DĽIS{T;Gڠ+TPUV̈́Wb8(]):eb8LeŠNGL;CuDݑTh[ 1[:IxdǂYW&W3\h8&JIYV%E`PEj)"wNwd[Uc4^0^6Auc0g2m8 Hy:5̂Jk0Xz,bwf+g˚ Wp6X"/1^Pb;M`wA^avKcj1^5H/}݂!dGcN X:13r'2bw)=Җ(H)0x+ej`h14^7KYL٪&5V7%2Z!t&L3 (sT0DN)\0^YG$lUȧ781@Keh,Up8^m GxBt\Kf B͕rrQǙBUW CrV8t*_OL:Ռ1"4:*+UСvqP!R/EUZnLf5*Q+١WytHDV1D-j&aS'(4"T"8:=5-K302f qo N Y f")BjuZ<qos&6G00])(8eXF7/7ϥ 9/4"qāT G4d>#ڌT9W Pp8" h*MfKx-AXg8Ռ~L2Lť)iELȐ q +~\oB"ބx+b|Q7H70BɃHbjV< 4s|AfLLbR,X6rQ\ͤL.B2b4/%.eH b UaZ~iqLCXƜ+[c0ATx%I711,v !c`S:~i$z+-{Yd ; C*1 =lEܐAL0^lu^DBT2T B\ 1j/Be=:VLJ3U6H=!k4*#q뎺SDV@ C:J 8zEX+Vn{=C=UzL_+-?eb6*b_QYx@F p :x1jBE;Ͳ)Őm W #)vEjFer1ǫ\繻 RYj5qNj)4RL/O6-WRpz/b6 #%22\a*IUF*!PcSeYSx%p5c;L(SMW|df 5F~tq~>Pጯ#+UNnO#/[x '*cAfPfr nLhYݕXc '3b,HI7{J8褞3T4W'R"sFB\)Ƌqh01P,h"*01W媌{j*xù!ԐޫȼͰBԯ8(\`\,&C"+HeBZC/f U  * 2WՀj^PRTC)1^5Tqh 2#|PAb! Q|a"Ĕ2i8^RR.! `!BÃ(JԆuD.4^DB4F39A BAAhȕ` \FyUj\fx)<]""PYe)|0݀)O1^1@QǸ (%eq82H#-nBیCYs.(YiH*DPI)ӡ9܊qH z 9uq*&jzA&zipc$լQ X=2]bl5t] & 1n9d8qAu퓓Y++m` ZY!fV$8^r5=U" 4/&1HLe0X V=K&vxlU` '32 -KpQbu8b%!0k^)+W*:jfN.ΘDbg8:tW]JET^L=NfSdMNjC͊oICItcu"qj\O W3vȟtk\Ze9!uj+̮~?vT7u`U -ɳt9Mbud Ne{H<^_ f .M,7 N`E RX#Ŵ8^/(bDb(J"v;)3x:+ "ْЭHO= Ux%[d e*C*peV%# )(3+ѽ'^R*ӪEjd z K IdƉEX7 NyMr!XU IDATPMɡ V'BL/ <`)-Ʋ4Q\o2[\''*5\?|EBIMD#HqDa\:8602< qqB:erաyg8^4e^Zd a[)J$L4ch$)p\‡F=02O+ 5]5WI,re,TG#X Tn<| _C*ՆiEBEEs7TT9[EYMPRXT9-ADԀŊ>b+Y ?E28\/! qX >MGp2cG躹r ٧0A&$V9x7Ƌ+ $ Q%/!?CoEC xŒ|RjČ$_JC\ͩldv厐u/d|Hp"}fccB9O0 iV'Gp\sHɥudH QpQ_0By4^CK &(y:Q1 1[Rg,g8)cHCUxld5WԢ񏜵W` f{ģ4^Sr8SrrJFt5LJq2F8\H:(O"ב⁲N!D#Dt6WuUI쮕j! N2ZHPݑ„XNYeq*YQLnŇۉ jtqi 5H=$UmPLDA7]yGQƩ|`HدE 2Y5vWW1ަ+SI:u:rm^.^Mmj &29/x"1XӋ͈8^Tbz\rE"muKk6\b"$YӋ]b` 4iZ eUcS#|&0˝KaNFөBh R Ma$ OjAw,5?X 7!|h$-QL/Uabʴ/n0ZW qnj?S*,J,FYEJV"SD!H5GfZbz"l1AF -A%,zɂPaZb%&2ի&tx%2$"V* Ǔ|*SL\Wc2Ǵ\m"%E1g2TA3b;\ƋU cEYNa>tpc|IABUn}5/Xn۱x0`0+2TYqifiC= 4ՌpnE+0XE(%Ci*Ӌ24^, b,eDZ;+Mxl#W{Q`"NDzKdץK ]:prH ! 0bw*!헸%Hhp17B""Z*Yձ81A$#:B@hĞ|+>R8L7ݩѥ4AG'|Sj|FTƫ) pArd4Jk-5Bh_\nE"Bc2W]M{ek iHQu@b܌$.e)YJEM+W-IlYf JL)vok*Z(^W,EX#WNduscu"-p٤L+Q^[oy E`chl%*bGk7ZoJŧF`_m=^9oN/,>fjLz/\Pg7X^Gbm^a@SL/keRxGM/x+V!"njbHFD{)W[HNk_]pN|h(Ve)KJD. W8@PLH[AhR# 06Eo:_Ujd]İUa[CV͚aBF:fРLH[B,h"S}%XZX70+PҩbuŪjY$v&S^ #8eKUQdp'H+/Q.b4 4j cHh 9HU1Ӗ𑋸*"A2$7ɑ F q4HaFϤ``"65d7Wq(Ԇqjq9Z*,.U+L1h\S5(X2{4~Am~S-O540Rm.xasP]h nQ_4. $W_FBˋ.b 0 ] pK#J(g 2+xR+Ft՜C+LQ?Z x+brap<&|2#r5 b1; g2 )*Ӿ`4S9|tb ئ躜r%p (c#DjJW32OAQFh!s|4o)\͚a! 6 Pp d, ʼsY'Eb7So:})%/0@*fD⟤*b6_F./0T ٖ^CbzQ4^Z|Z轒͢(˼8^n,#0#0#7#@R Fv&^0[(TVi(%&,0xo>a# x%8kv7͋29EeRx՘^ZDfhδN~'ks{'#0#0N dXXhI/K %33Um;ޅD1X|`{{zKqL열\6EL~RCUE*$ Fs{&uSfF`FؗҘXKXmzK̖"EQ[@BelltY`\keq(Ƌx]рڼjF2gQϠMWb%W3RW*B.V8_F`F`rDܯ\e61HeSsxi3t<66xdn$~H41<2wll}"@yr. LȰFXU`ΚMec8a^mfdaˏ`F`F`/Gbz~fV䮙ed՝"/x#Ătb f Ƌ2/xUKeZebDL٪l4Y ,0 caˏ`F`F`/Gbuk6Vߡn~u𠃿wA_)$]W1vX췡ޫr#3\,Dd04^ǫÖg +#0#0^x^dN=%W3Uau"]56]ooQ^~p: 1HLE`ڊD2.lVs2ScDS`"#m &ܚȊ=uqF`F`vŎ8^$pKz/eRxg4;AzߙĄNһsW  2ZEZflG*, 2# .4^~DgbFpLX\%``J`:ܘ_F`F`rlv'fp-6KI)kZ>κsQ2^>Ysh"5sBΝΞӭr^\ngMnZzV@SdJר| ,a˿2#0#|yV0^نLoW,u=U3fRy.]G;x~/^Aȣ~|_L^p8K<@]s|%]RȗWs'X+7\ݹ~ú]sΉGpmh@۱z7Mp돯c6̗0#0#g0ㅫ`X1{NKIudݝ%{/ /W|b+O_^/` ~GcGʐ4 ~~mۿm|sتh_w7ry{./xw}Kڒ~m^|V.aȿN}nMyʔYu84^bΥ؎e2WQ+r/#M F$A&:/}v {饗@GzK.qm~=~IEzle_K/w|݊<7|YS<+_l^^^ך*/^}ow[>O{ҥ9Y6dVڧ_='k}-'V/ %eF`F`=zjFMfb*E{ⷎN5N&wI17_q睷{?o|߷^,Ǿc=.<wWt3W/-zc?m9=/ɿ\ a_o"Mz5WfWuZ-tc'=cuYkn|;ײt_)R*}\ͳtƫs5t5K-Wyf0aj2WՉ,V=&c0n8ws[JIϨ4[]zW>NО]?}[꼃j) p+W:қ=a_>39T:s׫|AšKˌ#0#0_ S{;\kح^8fU/Kv<U5^퍒Kټ ;?KZ4x̺ʵˇʡw!/z_h 5w%ùo;aTݝ|!e:5S(X1T U/T rcrq~e/дR齰Cnh2Lf[wǤ;AqAn_={sΣ-K-*rوݝݣb"QFvex{z(S*8ۚhMc0lN\%|gIo);|}q)l~yk4ȉwo9, XvWgˇ_»?Ci\+#0#0GhjzJ3@h^Zw7:;2&Uw K[HoZPˍ~o^uP%;QVvA \1P=e?y[[9F7#]DˍsS:. x)sլZ*frقb;q(񒫙-i2\c1if;]˷v*Qw}Ϯ??^{зn×.?lP3ߝE*wԇ}Uhj“NwG:}#0#y^-ѺiUoW,~Q=0J&WMz={ _Ww&‡Kr뭷ޱi/;n8B_^Vqί(>KG|cxI???ϧeJ]m۟|ߟs@x0TQ )_%2*~  Z,]0Q1xz<)st8/rǙb<4K5Uwқ_!AM%wofx-ȶr_Yȗrc>滏^|X6Ion̟.?ȗv탿~ֳ=g&M_F`Flk6_DzPas tnueF`F ֘^ա'1`j& zCP%&ۤQ;^|M+GZ]q+?eQ*,o~݉6+uYwrySkoQaGӮC`9=r U=A;Q %,] YDSOcu"6,cz5kvZ[N%Ju=,>|Hվ=e7__yE- k5$b,YT[\p>랻%d5O+دÐeF`F`!`4Ymvk;۬K/4^5g_e(Nlo%qo$>Gʇ޸rLWOzw^N1u0 /Dz:.t9]3g '3- ]W-#OЪng5)bz F6`1m`le] TYe;CzkG|maF,HG~qOMg,TNGQz}y 7mͧx;Lz 0#0ނX0m6k^LyMAi(WVTG_Hja{dahT:u`ַDlmnϮ;yRP]r>KY q!=s! p ~FZ۞zؼgd6|78P(VyGG񘹍u_wΜ5r.:9Hs72\ۚJu5x%C6JkmŒxpo٨Ǚ^RnGW'_ IDAT8klsXoz٭O=őhu=pE726gեW򍿾-M*-eK>>x|R]xķ:fzaVA߾>Ƴ4xwܱkĿ2#0# _wvb7mVjMV=:.&\A^%`R~tUT(YRӛ^~+T:G/87yG\hv- .?O٫.ƿCǷ??Ê~t!#+]kz?}WOڤdSˆPe=?}o+͉mQYzoAu aČ#0#0_9FhVKeF+=EL..9:Ӧ%a+֊Mgkr-ߌ+C ޺=W+OQ橶g y862Th9r993gFe +X.4Y 2C6o,*H܌5PFݴ:dx K@v)usXowRzZ}8*rM*BUryK+Vg\;BP,qh|l|@'b}lD1FKIot+#0#0AhYN kگK3C٭LLQ2wiZ:m %8 ލaZ&ƋX\ȶ]]+c5s2LcDѬͲeB$]`1[Z|Q$s}BF~uoK`F`F`#2^Q ^U/-9ռp8蚩e[Fje%_2WAm\#΅ۙXMxIlxۈ½&XaW :xq¿2#0#ku^j̖2WQg5ӫ/TfһRܽG>dÙBs[cz* Zދ^j1HJ,2Wu W3xl4uBOn85F`F`vp8LVf"5k6.wi3ӻm;ޑ^:pV(>HJgUV X*`UU ۳bp۳ yЋwל\%'" |ok7WOUī َ̳଺ٗ6fs}j(j&?uV X*`UU\>?S^׾+݉틤g\drmNxiklUV X*`UUKL۳̓^xi_ su]~ʓVKvA,T*@!;ZZ62Pv z vIZ*`UUV XD+rxmvǻܾy WP.HXcǙ[kU#W'O!?fυۍ`\l׼B؆NޮœQ/$Zl.AV X*`UUV. :]^Hˢ9D'N9ϗ(mUuQ)3l+m* $L3@p]^+kwZlNkxmCkwKn3ecUUV X*`Ubͱ|wS{zݞyWP.}zt*2#'DYo봭 <@, ;x*5kV5CW/\%*TD3p {UE/n7>^W BӰ*`UUV Xx*`wyϊ]=+/RgAfH|ǎ^Ѫɫ7\$bUV X*`U/{]ڜnoͷX/cͦcO,$?qI,(kUS lוNE$Zجj66UЯjzKru5ELZяwj O/Vg\Z0}gp{ozѽKe 'X\Q<x'?>I O[omUUV \XY3C>Я˻p/jYdNKrd8K#߅#7,𢬍Vg₏;xu" vwW)mNE3$(ڴU3^zgj^A@b+烦+7TOWVp4/Uo^n_ghߪUV XTj@ɛ=>ϊ[DV.+|Kn3HXv{e-p|_0~3wWjomӾËZ.LO] 2vOqV1 X*`UUpz W+u +{\S4{O^v|%%𢬍VgB: ʝS5+Wsfxw%fw--;қԒp^ӋfNZou]kW_[,WG;} z#W(}]sZ Ooײ{}ɯwye$>di/<'er9?u <'oUV X*pUWd8σޣqSo^/4]l**x[;7JGT7w.jc0:r6`$].d/"WSU⒑%_y SKW<Yg)V(i33tCxu,36txNfk.!5:gſx~o||ӛ߲_g;3{݃WGZƏpnOWO?yOzkV X*`U56{V P/?q ^_8 bfw/vv6"s~‹W蟗ϻnF.䅳}{oƷ&^+ԯ_aWi=lp>%9By-wiٱ͇<N_u*2۳?lw>v ̻{\+s罻{Fzv ?fÙoW=#zo;b|>-Û>28 /\ѪUV X*$WѩȷpyW.< U$W!o6K*TO:}_9惸έ9W{ yy77 yc/ <}泟̃~7?3 鯼c߷K@0 2{ן…\/sEWZB꼼l.xA [8^q/]y7Ҳ U3V5AF^fHAE>{ͫ_}=w-o95|G?D*J>Yx|*7 G h"/<)uzO)Y*`UUV W[S gw@N<#xwA'N44K#_߅z 3q3q}6W;µ;y}sng6W8PD6>‹>߳;2W~/yCw|҉_{= 5~ei_ſO옏v֯.%Yf+cvkѩ9l\ǫ/;S5 gH彨>$fv]w;L g~ƿ{=h?eyU^q^=$cpVO,Y~"z蛟,ɭQA>YzOzkV X*`U+?zrsE a.wess[^l^[/9?ѩ~6Wͩǻ2wG‹G@O 9B(?]/.6_u'}뎛w P{-o{}:eϐ+oDHG77=P[f7y=|睿)/OmlO,(h+Ьx Z|D[ J+#/-HNE6 Bլ:F>[} ^ēizT>vwO d9XH_D*}ͯ}Q'&.Dټ)ot[ǽG-?7>Q/[UV X*`UQ+㵻w<0P|lz9r}pl|s}C;杭UV X*`UpWxElFO\i.d2>wwg/_ps[#v 7+xm/Z_Ӝ3?'yy9߆#;;{ysɴ]a.`y@oO}W¹|>Jqw_sS+W7W7fg4~({aW;oͲ: T^x9hYdwլvqfUg/F_õ85Я-)V\n5>uO_R?Pxhlwݕrҝwe~EuX*`UUV < }kJ=N](Y>+"ʌ~cxx_9tUP,?Gw|˗j:a;{nD^U(g*(߅uzkwޛ6?CrV&d l?>V̿J^}ǹWcP4$ӭ/z/ TvU/A< IDATTUw"~Yx]ޫN- grQ;n!;\ߥe gݝY+tyݞտW藽> /UV X*`UXRP8&U#{zAd6#^/,ģ[[۪x]knyvy/wvy0}w[?nx /6F'w߰ 4^O~wgC_|(qƺ//;ǯڷ\-i@!E3[w >^I: =;۫9F뮻>kB`1荎?@O;}N}=Jo~V*ś7r>7q}_s^uLK|Q ^zfu0jƚ+ <6vV2ۖjЧWPxg^9&W{/>{ x{{—[WsUV X*`Uҭ^ּp{O\)|/c"c!^sѦDvk{kwBwB^8CߵUny< v2pwڎ7*:‹@o|?|R]__뙇&ۤ*O5}|'3x?p#M`O>no֬E:_ɇʢj>y9}z9lueQ5/~vp5Q8fWe|؛W;bO˟zSV X*`UUK.uyW=+~w;z]؛IZXHR6E/^?F]q}> }-oﵯ5dz]+Հg| z͆_^,\8XxQgʕR?w|/vUQit?x<Mf;{+iPj'ǷL"zy>o?C޼Or E"m~ELoy?|/iH0cuYG~ %j+P5֪ff~͞^~ 5{ɛUV X*`UUZCwx=+?qQkV;_qL) Ob!V-zgrEÝJk1]Bgr]&e}+o., ӟ?xM1R׿~g?ޗ}UezOқg~ϿG7>7?6E[ӷ~یEy׾/7=O_ Go_l6dhi6ϲ\ZIo^#Ԓ{\|~.6:VFn |=n51V X*`UUV. }kҵP8{xz~]]v;~b!X$en}?_  kk"z#gzS/N;. xE]P$P?ȺtZ( R>4Tdldݴ JzP$xJ9JB,B3KL| әX? H>3^|f]P5.U2ĻxiknAd5W02v*>o۳!{@hy}k Yxwto@7#)wvw]] xWk0q|+u}kׂaƞ}k(V_[#:;̍}kj`$E}kh(+Fx#Cx$}Fr5h"MQ0Hz(7P$FT4cml$2lETq8H xz=l$|d# bl2]GH'Ӆd92EGtO"X2ɕb,G}cD:)7h"ʕ2r$|9+q.[$h" SRXId L:_ΕTn#+b%R\-V|:o4ڟlnvGh}z0kd};ޮuxLkY A3޸ Ӎΰa{993UZp:x{}3ڪuA?utUaոs&~3 vMvۧ)`sw}/mslt}zQi';gZNgiFۧq=ݑۃ>O [1z{mQjm L͞,MvJ'>]>dˇS}z';P} O2<lFg؟zc6_`7lR4&|/o]iz-*FgNnlT-[?RO*^Ҭ4zvT6UVҬ(WˍFgXms4:R)TKN=Vwk~TFP7. Śl)_VBF^56bQ53r2[Tg}O˥z+_%EƹR5)dR){T-[l1sOd O\"S(T<'XJ?A*u7\GϬlyإh,rX2ʥs%t>5tD*SLeXG0Oh,L|(gñDF=c@h#]d6M#[<p4$!ڈl$7$D6xd$`Df V@hX x0) o$CyB8S?aX jf%*(dPԳg̍׮.ھZ񯻼w5o5dʷ__}kü[[cuzWVAҪ BL2n{z{ű]q r!p nwʁGD6xG>я|oz[\6ڽkMtN^9yn𢬍VHj*IΩUhBh *jٍ1tiف9/HB1262A l=E#RVir=+~nsdɍIӻ uָɝN0Kе_ϊ__}#YA5TAbZd`[W< áh?GE8Pϛp0"T(_[ z@19|<3@˘'_c)v"X2˘h\8Hd@82d\A ٙ)L[Ksb(`(s_V 6~ոnƝf-~A~M5# 1 xjqc 9818ltS8 p hvG]Doz 6wqUCtUۃt7٩w{hmxyt74 9`?lVgu4쌶ۧXΙ sΤ?il6 f`3QSct@h [p7p7N ۧI7Lލx|;ji Ac !gOsitG`h$UW;i_}%VǠ;~';';=vLJ3쏷𕔵/|5*7#>_@Y 6 rYQb݇/,Qq+`Xk0mkmX0|=ȗ q)׻\e WڌY?rPi\'71ƅJZM߬s'ěWr~a+ r v˵rXkt^-&~Y,֚Hrcf &l+rg ͕<5xtHfn$3 [A*-GHf2ZMd6ϯ칦oKT%`l:[HdxzHTKձT*S% TȂu4fg  @RƑz΄!M$Ļ1yl$5=\[7!^E7L]XgĂZ0[ 0HXog=*?'(4LȖDCgůf\0.tT ;ܯVVs꾧 M <7"p#i;]O/;cݩ ╱pQtyxu<g/ ]a P-F5Ż?Uqe;"A ^]벲X/GwM+)VA(܀^1U62X ϛH(5p4mQa㡵9^Y EjXcAp̌xpxZFјBT4HS9An$2ZfTVѪ⍥rRD)f <3HApTF9/c< 6W2L@L3+*`!ٕ{ATLT\4!XQmaTkz,ZwM Bdz$\i([o>{aY4Pa8{3i6r TkÅB%16{06; [w/)*C1yfwag@)Uo%-o6`,Td,o x 6wxD.c@iT`W v| q@;MvjaK ­ar OjD/QPUE Sog h ;px#U*fEƙxfz18C²*Ļu~&ԙhFqp @m0p16Q3n~uMTz.qCcrk?o߫ƚ U~w8UEoK}1Zg}Y)NZz7١Pܼ#z>? +i]|>ɽuVR'LWAh+._LV>O3%*cK/WBZuf 7EDm* -TZ[Vٸ@j+Wsr wHAB(_*Jn/*xr|P-VXkx6-VZ#ךh[d/Z y<@plBϖ=ajZiz͝3kQvG[Pd9^trj{p%nD\kG[J;gg;M%zZ5;8;;mV'J2% F_ UdWzԻp3Mv4^&jW+tR8[Kg}p҉IZ}%2j7:LPxsF/PYHx_m%CDȤP^o3+™%p.b"pNK˘//O ^qֿbbC 6"O@!tHfl(TsT=k|OŚ(Aզڙ?K 7.  cpFCaNጨ'˵B+Oڀ/kXJ)QZ6P8" 5\S(ǒX2͗߫_JP8get7 ZՌ(O"J pG X{k2I`bߵH4u [[DJѹZ'P4S9s(Fbk?/ g(`x=po-Ԭ,fۉS;4OlL燌}k3I&zWvz|L+BlN;}Ϳ1'^r2~_hy춻{`>T!-DWW-h͈U3]vMMǒteٝ:JX ⅿU|/^ Krrx׵阫9/LJ%#Z!^׷]%.呰xYTW߀+N KӫUܤP5Za!-"TNjj%\lŎBQǫMvH(ӂLHA3d^ G ^tpU3*w7xú/ςr# "l$E*o"w! IDATeXZ,xzYKrKsB9HcMxScɕXƔ#L5DլQnv2ѫeG>^x14j 3Bqw7-f ӋaU>lʕ6V[4~2xBtUw&_nmtrC|Rqr=P>: \Q8ørp&2a;n;ʗH B04|Z&jl}C}@eDwN ӟt$ AT`e |Wmͬ<폷G[J{hxVn/Aj==\to-V_{Ȁ;g3o9= 鎷'ʎzsLST[u[bmH[;g%]k8FۧE-x x[ǻ}u}zu-Kmr9VEGۧ &ؑF/4-荶6w΀@ΐE 1h0sg9\DbF}%ޫ nwF[jCE<b2[mjGdOpz^ f`R.xyGDC"5:hKx]a&"FՌB?* 6:ÅRZ(mҨ]j~KܸC2.9dq~,Zql\oʕRb94ea[kyQK C2  ABb-(EUOsFj-U=״$@T<'2F")(Yۅ(S"rh֋S"pHdXEg8^Pn8`;cA~m%CPSjfm ;DolRz_q~AFvlqjbĘ@/Ĝ)< ieH`eӯ>aL@ wOǎ#u 3\G[1^X0'6WXyN@B'o yց{,'`<^xQFO=yjɌrǫj.W|j diV^*x gUUW:]^<@bu}N{YJf^iE en$bh$ym=q;T2kU ~W벲2' !7kxgc1X Ux ,͊ A|]tȆ\JK@fpxzQ`+S9DxxQA❩HsSda6SdfEFitZddH89/ 7fZ|Y'W)+fx](\j𺕆 UdJrSc>^c%DR+ڕ*y6̶C,~PCΐ7;y-x 2;]H7Q,OPC,x@05 ب<:K/SXp{.HGU o@i|NJx-г-m IT]G MO#(O*"ƜmڥJbmDžDl ^6)dQ5E,8l- I McӒ+ &[4T3 =eXցlk3NqbWZ}AOU)4U),W}tpWw]n*]2xɔ"،O8l2-ag2 k;@5V@b8Az)VD^) Vp퐼:r;6BZ xYhȗ@L\.fBD+XJuԜnݏt|T/&VS V<朅b*$U)Vc0+A:A%)Ξ*jaFؚΗCQ\G$^bB$Jd xt͈.E, 35xE{t<h"aD̕L EDiPxzY jںib;xŜC8dkG8^Ds̬P8֔zC(!W0>LL؁U)V\Uh[Qb*G\E?^9 c~3<: 1]~Gؼg?(O~u]g?y8mvuy]'6+UKvûkjk$]A@e2A6r%Ŋe35RdVhT %x+cw*O]Sx`^6#^sJ$. ^^+W L*q fTy ePj EH#c~Ӫq]Rd퓛;U͹'H8jxz5pM*xxxAXwrw+/OJ)Vx)VJG/o, QM>^ۜʪD+Ua*2*/^LnaP>!<bGKA&Ȱe, gι;27{we g T^h$Wt irꍡ@ՌPB9ܠ_޴`LUPmU[M6"$[oAUvdu̮ftJVG:lʎBU]ơΈj'*tJ+x!Up7mQ8cm 6ywW<팶=ex5Eӻ_*.DhO/1gG&=YAϪjjmVu)V?ZE.W3*:SyZAo pi9fR=ԓ YLF ͵VO/k:@k l'&J8^P."Vl^'/58d xqE۞M&/ goFc¢hLƓlF2Ӌv(oPT -V[5fBU#V\l􋍅'*u2 6?)Ka7̠_f jP(@.K=7YhtA/cTh6+U &y(\mf}"Sbݻ}xIjf3V&3 *f")7)VkJֲ5[fċTZVve6̑C߹/}2=+~A c:Dhs}z>^TǎK"k_m{Gѵ_s޽򫅠旿Bvdgu[ԖCyO?Xeɫ4;+$W:Yʹ5:~NƤX]pp((cY=%3T 9/u{YxNEt'⾃™e6 78^Kq̺ܯJrH_$FΠ_gĂ~p <)VQV*8 Q8( ga"-B2E"hT$@e9kZ4j$WR I5%yN#em$2(k_ӘAk-DeVxA9Ēb.n,Ό*uR7fx5f17 (e]KΪZ93=Co$HhpzP㝳MddZDI7dOPIB 0ݵ@e$0cW/%Z߫M0+`79Cc*T6>ppT~cf]Q]CtU!d9@ے2 1S_g^Wh݉lem $d s:P89BeG43NEŘw+6 փŻ[weZO, <+1ee )r%g{ToK kHJt!b)Sl, :^V35+^vƗ1;99<Oߛ LQ=t+Tɕxpӵ*>^!ĒYiV#%TJ7,-( /COvQ; ?K@hc5V3XGHL,l%<b% gxhP+0Ь?ʬ-Ǻ9%\. gƒ"./>P.K_C]q'N +˯8^^X0zyo'1wCpl$_-ۻ7Lv]{TpsBݸEl9‹6Zx" j䪥eە'5pZZF-xQ2ZZ> xzu4ns,Uf~YQN~۳N/WrgX SNQO/@nBX|"AܚZ$/ZYA+A8A3h/.kܩh\52cA#&@('؈BwăÈ!*[ktBUt2EeEj+FR:Jyf h(EŃ6-I@",!-B;UL;Xh7SP rGZTċn-Wtk*p񢸦E:7\?jS82H@ ^b O/ljMhTxhwPT$0DU̕HjvG [ix3FW=9ы2WFiNYP.rMʬ8鍶pI e 6.JCu-& i7,tIs[@-" i+`8Mv VW*  lm\eU=*!Yҕ>(;-jՇm 6`MD[]/g+l;‚;>X% L8+ T=>:`sٟ6z*(W_\jluǻݫ^G2f`rGۧU?M}$ًk7/ Fٙ` ƊeZR7(6yO9>?UIr$jDiˉ300_  ?ƚ+fm(7Zˈ:y+c" 8!Yj$q +%GU5<fCL>sxvnO/ gA\yf sZ I L,j̴/c&_m} JO;0\E %Ng8f72 Alug{끬#%bŢ3l%H\|G- !)(\LdU±Xjm=P x3SîE3WV e.$!)p O5 ,V!$y E,+1z <5&:K SS,׌l%JЯkB`Gf̙v+wм%86?l8/xL@?;7\5kuo^ND+vHX㵹Ujk_m FxQD> ݂~+U [Vx1X$0;|/|!j<1D ^HWt/a]7/e y [/|m{q` 2.zd}FaOKS_^^RҝUi_)>^dXmhq aC?9pzHS"W8^jN\k#4zLC3kbO1ՕLybGuat̘d5#ċ~[ɡuBGtsfXt E܇2vG @3[xҤzG8G~H|t6-iAtXpx5؄aݩhRٟ9 \YjLB9ٟ&;]L\ݫ[FoS@hMɪf/GxwdҸaTvc;Bbg|g`%+,Br9гfq!qY#i⯰p??dh7pZXT-f.WȄۜ4ea)n|Z:k\C[1`ր𦰲J@3-TrJo:#`V8gxCZ5 [%ٕ͞F/SPVբ%{⾝+UqdI ^nJ0pOe){ӛ˕xP#%3rhC8t[df֢Oyzѩ(_@p邱y`zx4 9̣ '/HX|A=9xw' _A``uQp6uGFc^ O6j^̅eE[#a̘gưf΃т)>^I,h\ѳPd 8+A^gޛX5=ڀ׼<'@Sw9_mfU l]C_G ֚z.̖鼚 i@5 g&=jFUm LޢFVm3vPNJE f8^e5frq2T Dj^؏AocEXNDG'{Hvs:@tNEmgy¥q,݉Ӡ\=ڝhM&Jj 5tn3\ 0'Ot_4 OJUB9Џǫ%[0=4:ZyS5UbvUHaV.AS{gQ87wռwX8=;6|{g.itj`7ݿ=&JlNOv r&j{Tv`{0.LMTl]mVrqy}{hi +p_i|NcBeJtQVLjO쌶͈i=>^ ^zP5˴Be|?3O3/ݟ@ݟBs;1قx:o=P{B0ε6J*XsbY8M5UX£x=ЛHҤS_wYʕFToq_:g48tO/˗b]K3T:UћLd+\$Ke1`>7),NgU>ƙ\ kt@HdHβL$iy^eUZ-ϩpf[r1a 9 KuUrh87LXp {DO eg&NH̷BEAp2 Q -a/9(Q>C2c)藶xE<4+W8qRr%j J޸+w_K7.NJp(?̲}r˭?2[ RƧs)V*YWx]ZZ6杍6'H +cir./;zz=+nߚYTFfU 6 Q7/hܧ$zb(هf ]E"2& RAުYfP(B6 gaA}kX[VU3=7 ޼ CY|b(W7r s=Dt)q,A iLT*A>j Wy:vrzs:"I2 Łk4&Bht^V[rR#0.Z*<SaHS}.]}~ghk0#I O/``do5Zq*׻g>^iqķ[{[mA#Xi52E4Xk*IRw,RI# LMA=/Jf^Q5HAXIv BeAU՝H^xW_FY e& bJ:@B+>^0B&u AJլCd<+~W|O\y1# {ĕ 1Qi2zJ_b_&hG?>[ew_q+n;;{s߰@?t!;KG"Q5Ko^%QN-ͽy[* f[3ӦncS9纕s(Q$Y A1i1a& j֛s[-Ć~^^㮺UsٟIUoCc PncS+ Z15I},1lc !^[:Z;c>T͍YAݼ~1cR!TF\IP8E443bMbJ" KJfnJMLz,Q) @ 5vmT4z#SYIR(8[Be [F"N"|/\ ^tRwz([~Q3RlK0⸘ l fxt"]aC+ T͔A ]P2ckTmvw:=P b=U3dxX#:BHurǚ:T1ajF;+Vhzziə@$ 978P\(O}pV%E @;b^&QwQd.);x R!m\d 3H%Sy]IXsf !%Yyz~dFy/~DFxwYQY<|x=L e74r erSSZQ>{D]CֶN456n^酆RLf~mtcn^` BBK2mgQ% @$3nUME/9_Vi!aYj=CJ~ Ok0=rA 3PH^Kx!gHj x~KQ Hiy|X rR< @Bzf()Tk AFd:ɕIw{RY@-(|B*҅tɕ AqK}b_&8=˔ o4]b|/o$ F\reő<*d;-V,e=qmU%K^t&/Oh ԮX*4b/%x/N97`lxw!` DB=HrbO< CZIgwrh‚.8^9yTZRī70/\K|G[eW:&K gpT^(e55g[Np>Ez(^556Em E Q}SKS+k-{[OK~րn@^xɧK. ˀl4vRVs7MڞDr^B%T &Ri 2 cLL ˁ*A7aP˷l!FLD@s x!S[ /P+tݖ<˹/W533Ve9^yP1Vg?Kg 7$B/b:OZHK\6,(T`RAr_3_*]rU3x8]dYKq,d3Q NFR.ejؗk:Ŋ<!ج8)IdӜuGLw"gbӹ(2x] į* Ʋ=>fZz/}ұԺ_z*֭oiZG]&E-?Ǩt53p6gOuTͨ5 U3ø/Ә~뚐 x3TPJWuMS56o` vJjnac)mih756tKyV3Njfjq|qFōe ~tŻ˳ .|Tr%r_nE9g25ׄX)TLkD 5VH}~0x]<)׋'%:`Cq 6u&jfN<]l.kuS -,<4vr\]!ms 06Z]kA7rR_$0, A;gQUj"Fh J( :ZgL1_.Z;(.9R7Cxԗwnxw)Z!.HNHS͊l%Ru4z˓w X*NzXW9!,.`{%&sn\bg>+nF;gQJdDR_4 $#^eFgw$5b/30P wlWMeXY/Y -r fRClCl8U64< 48[gtċKC瑓P8`# XdX]p$lUK>^ BN9{PiZHSuRoݼ2 6ug{J&CWA< y.X;B^1*[k0Ε"?s޹睏3[4@Z̒DWRLn,]$2,8{|l+D' 'xfxZr6̃EpǙ 6 zh|h, z|ڹӍffd g~ڜ\ލ|$ӋZlM°b$T_l>s2-x? SUF6eֈe&[i:_F3vc8^xn/\Q;Lm9U0[XT^4dqܜP\Qqrj  ]4/a!Xv> `+1CV3VՐ% k?Esbr/ijʼ.d81`^uhE>ށ;3^:sL$9Y1Lsnѯ6sI_5oƁsZ "^aɺ:fv.E98ߢgITRU35>^b5 o]]S83ﮈxYUc1MQn]Se!v]GЂ˭ohv6034r!nkJJo(H7 R2~@C Air{: X@1ݸw|򱐧X,+GAT3OFJYc/#`)@qBjFt$&h {ru#cʀhv8 "W타-En3◫xyr:9`6Ϭ^ .v$yi/Ό^KZh7Bn ?`+ T<菦R>S,-9,,t* sDSX&ibO)h~G \,1St ftP0cc< bͳO_a AɌ;)Ⅺ # |/?̈Bw00uNjrk-:py/J}xj^^ ^ 3Zߢq xK`ڠ`wE **1c)*Ie,/\$ ^]vf0y^XPgq%i 5й"jkB[gFBXdbE)Lp5^4ALU@ךH5fs_;'}SCk]cb^rAixMB۞mpŪ&[mѣTk/5tp >MEu,JD|̣oxWLWgT8>^e$0eqcv2/|EosRKZRn3 ܌@ W<rHB3+$)N7AWkLլ3YQdq5x`[n^^rTOW@R)@s.JݖzzQ[Ea`dZ:FbHe?LeV{xE5a@lqtPdZa~pUO绯HitvLYz;X#sG;b&=2p\K :gsV^4aÇ X*khljc` 4KfxwQ)bWr2r2.A, 2o`[۫= ͭ؄kliz4UЖ[U<ã>^ܰP0 R/n"Q}pv}JUL=N@x7q]B( ܻћ5:"ʔأ;^Pfvdjz|H)Vեz3tVZ?7u>^< 8n_$IB7hAo*S@ ^4Fdzd9yՠۓr"ow5:rx> FS6e.#3kuz8ʵiO7g`qtF ^dM5Yj:Zk dPkf ՛lx|[cla`Qrr@hU:-bՁ2E8ZB7"J"kcO|bC g 㜰x5 T.VeC fj<,ƈE3,|#joYn^AH W {"ο& #tzreRY՜#cV9 uez3yEd Mz"\W8^7vsߛftB`4'xzA7$qC4 .o\.zzfAQ5q.\6CLUgfsbd x5Xb=8^bف̎;/sU8^AUF犫 > Z:*0aaC^Z'xt c 6k!УDZAyx9 kbC!H{z!$v HR`ex7*%^8^85a@Yu9/^`apf m hX'~wN|ͅBMz<ŋ+/9@ U9KY_[_Wȹ&in3khhhF_x[q v674q r (+L@7#RފoK{'4,xwH@}e'>pD1CcC7kdKUؿJCN@ Ж V3*T:U+Sj`ƳG6Try&a+W3{K}>T@H.Ep&s&WeE}VgJpTp$2m ƀx 'bDjY4v's=XI2Av¾P<K /j |@$I^ rL6+hujn P8csjCf(\_1Jo6Ck Z Bլљ7(h}rh/7YvR8j/8^WgtvFF=t=%F,xz% B㕪1{ 4.W/~TUݼuxWx,ȊЦVg4irԭ頱yiX&&o#>:7xt[54j0EUدHїJݓ&M UO׳6C:5ulv"JU☣_m 5U<|)+ gIUr/P.2iL^v6"W/mdF@Rŝ|,uB?C\%B6aQK^U+2 F@SbƀxW/_htȽֈV.HD*RA'6˦#ݣL0o82cݼ1捦Kޱtij=1CP".2Ůp"fx18^WT LdK\9g͑DY,:A7/Of(a[ l. ] gRxV`y[4K^ ċ@5:"O2H"36b&j^vc ^aͅEfQY։)5rA%D![؉;j,cdJ S> :ةdVbkс4zˑ-΢PN!uєA g)42AM/XEj}w[SkևMRD\Us O U .y_Tt$T`rf7gt:#ڍc1XKoc?:79lŭ'ʅ:&Ɵt:3g'-I RCٜry> x2s wzlhdA_8^x/64"[9^MmhlaJf ^~hlikd>޶6޿yz[Z AihnxQ8ŋ>޲™eoxG^].Ƃ4F=!^k +yu\ފCX qcwy ^p={ű\E+"%K1xI0uriGFoG,6]EL672]n??ʅCc &y$H}[^9Ǜv@,ϖEU2Ú?1NRҨp Uo(Mr=cx;Q:fٞlH8Ao"SLBW(x|o$T/`{% '<>-9p<$uzA/`wdVg xݾ腏Znݵ8,⵹bī| |L&`xM6Յe Bhdq.6|@P2Cυ". -}M6j t*@jC&`<|wNWrX9$/qFAoK ݐ@ CyzbkË+X+F,|mTOQT2łvY5 /yw,Y29[R+B䪦VP$;'!ށq 7KB+R~/M`gT͈e╎I o}C@x>sՌJf[ځ`I Xmlihn%.)9ֶN //r~[ۉ++.F|/(̢EzMAQTAɌ zv"*OF~fQñD3xG q8^TwQryȪv*g6mXyیBs[9k* 倃 Af]#ljׄ;.S,9=MvkڴzOTŒp>^_( 1h*-ų%/V&X*K~ZGݼqݕU<#͛aF!MX}ϵcŀKygVN3<~ŵ3ʩ_[I)s[)U1oLUK_ /J&([gKXyw r.g}\ɜlu'sh*MN횺|y蚺~UټEB6>~߂`r([R+E=<+IF=~UsڟLmj;sC0Us;\ƍ5r\g:8t %{Eh^.2B2nFط5jneHT 4-R!vYjp?eW*Fu%Y;4o$*4wg?˓:Vx LDLl A&TAQWg/1WFz"V|J=V>zdq:L+={n(+Qcρ篚5g̝֜qqR N~%l䷻N7+̘u kۡ'G*okA։x_|ԏc2^-B\Sgח${" bqxr) Ht}k7_\v{͚}i)?m;e^(yPs%˟7/.L6{"+Kv/ 2T.{íH,X%Sk!(>^~\D98qZ4vI~iC?~,vKndTPGΝ:}˯~_r)3g/_7/U5{/{nURT`Į\rܗ߼'>sڭם&ܰO\*騬ʠ/UěM>^\, 7񆓅P"37z/ؑgnviIZna^}]GD.ϙ>{+_9cLcoP _\7nˮ U7\>я^yΛ?w,~'|:"3O/|n_6MIt¡XEn_Nx0.+Hp<91lRgN1k}acΕ3gYtHZg5o|`M:넵;7+6dUO9-{#X"NNdӛvw<@N5WgֳUzU3P+T@h*xzy3rɻKwj*< YZfXH@5F.-Q0aVYΫ%v*Ԫr;#vuR%hH]U@Xb1^pxJ+ ަ΁CHiaI0Xd8A^ zsϗx/ORUs;_Fyڴ|Ajw7+?}^$л &/L6d3j_83pEØTr~1FS5W"ʭo`-xOocSk}Ct,U8&dfYu<SQ'Mmm2\dnlnmlkpfSRkD,ݥ g!Sx#lefn xnV)2AMhe B T,nOm'$H4z@岒Y^ZL,ΨTdʥ6^2jg>?r Be.HLvniRym~]zmkMwצz8 c҅' 0X BbN[V`҃Ђ|^V=xw[j#к_5!Zט{kqژKq`ݺ9ӧ}{=~Վ'tFu?J޹{dM*@?љ(4Us2fblh\ope|ț9RzL4Aux2Kkox7_[vM+mxyIsM{s=vcvw-%2Ͽqx&r۶mW&zx7ڽ~:KBIfJ,eK4另TNj|Sd;[vSeJs7k^ tLsN|$&B]vݚ嗏 q NnlbzҘ?ػmͭ-[21^R)fu#nF>I&^_`Si p"Ȳ䪮 aN`!v73[ ߿>p@[9(t-xǫ'ۢ78~'̾{ٲU-~zȮs%SCwyK/}ǡ7ܾ얛d,h0X{ny{u܊/Lt8ߚPӒ;gV IDATGkN g|h*ܯA̼zض/ߪؘluvDlj=E^4xzیlzSr@5 }.tF+TXH@z$WA%r<N\Fo1k-/)1l=*B(8^X1%4cj*QX.YyLzU8syOo=gJ_͝HH:uXmx[%9H͛;~?C KW% uȐmq *U35A 5T*pMmLjgx;|nneME@ڀd@y% X`AfۙTɤZXT5+UI xߠ%cxb1." s" ^ɲU:aN()5 5RDJB/^\lDZEO5BJ% xBOG)Bj !5hl?@˖wyqof5܊ٯ}ū]=ck<ɆK 5ɏwls˖.?cꜛ7آyҊkӄvwc{w#2$D@&U3҆`g ^R5*K.=JT&fG"ԁ&/Rߜ[|cSiv=rY zWo^ws?k֫N3a#<})uT/;z}{<~{^5UJ1J]=OOdN cYxjn4bɜ' ';R8VL3O{t{H~s7{{Ѣ/uw,1gw8B/z7֓.n߳[̻|H:lx/N~}]r_{^,sJ>8CV%K2텒9Uዤr^_xݣcnV% p(?3л~OUNJ?@ˮLUf_(cZFkۺKg_cGL-;>xfM\6uk% ͝vD{8Wwqv.H6Cw޿kThۖۉdI7K">-Ϥ8'Yf#AR*夀?݋_RZ:GƉ=wvu7^>[:wovɬi3\s+\v3zxNe`G{_>'':붽e_'DTIHxCt n_c'/|@4MyQ ,_r2 6^Ǻ9?s]G+&W}fν~;_gQ\s^7\513orΟ;gcokڏ;_ڿwW=}?wMN[Z3'XUv.d =uNi fauz5M6j4݄~KuFP.|xL .&c ) NzT"ޒ XO/gkzLW.hxU#]*֠ai=ƨ52W0j: UXDI20&V\)f,K;慻 @0"4&URrVՠ%u\E/~~AuW,B@h B[[zGj~Cglyz))VNjb7o]#)I bU$0^㭫o&d[^Ȼ[Q27pqCs+-&yxA]h,Kb !^R5#&ǫnJxfD㾌{.u wO+ݼ<h.PY;v.U~YiNjx@0ǫtJ&1&` 05)Vzzkʛ??o,ek~/;B_?ZbyrZqS=_2gԕnDe]38=&q$3^};ʛՉw~[sGfӔ*FЋ*u"f ^o-ǾP+˞ROA}=`iGޖ(&xʼ=\?n=^5Iɭ iBpwr\y׽#eYե|s n)D:OÝH2. *K2ExIL2Tˠ?~f?>swpl > =ϸjGҘ&G+ w}}GZo13>嚁fcܬzlݸa?^mf/S} #R ċdo4*SI9w]r߼{7c2לmY*2}lQ]q3wύzqO;4Ň~vsJ/t*7nqmҤ/ݽVѼj޷1ZЍ}ӂlX(7[TZth2+5kKډL"wJ??|`[~P?~ JUn_S+36?+>N>gzZRW zvxmSAKǎ8^=rr yDߊ{ˇ^2.o|xR5=sSyW.jmX?'5ܱU֡C.B|NfAfeww,p6Wgi2j1ؙfpP2`gjZ-xLvSH 办^_ ,uI͌!D1j /5/k-R%8KHͺO5_XՐ w=aC()r@()8g(hxz% @$0[;5@/X{s; aKxSD.f]:pl5rqA=wkaxEOoR@Hux^8^Ҙ)MEC|666" 6ʈ[@%Wh* aйU<08^iP.r. =leBC)Vv&ډl)ۖ@Rr9x[og22Za+ݕ*Z/낇Y敶< %O/r)JI\UF2x#:CרWkoo~ {ܾZ~$*d́ׯNGBvN{^ z䘉LX:iu)GiA+llExj&a沧ޅI5ҫTj^WZ.,^Wm9v۶qSRNBL9&3#o,ujr{WIwnℋ=뵥S.tΧo^bL|o$ULGwԀZ#^r L\pa^lݱXyذU7=Ůپ\iܦC_[ݓƏw6_1Yw}Փ'OJW~(.^{[y1ʟӛ*94BPbi\ll)KGT'Eg^Gs爲=_ٽ}7?rYPu}|d]6nIt _q) l'L\qsWMOd ՟|Nyz^h?7K7Coewwʿ\2ecώzοl^c&/yjE)Ԯ9۾`U◭z-ё_r˖3} &M}_: l4Kk?s-_K @լ3$)V6{~b^~kBz2gJ   ,Nb !^l*U:ADzFo ^_J}6"&Z8^PB|Gn񪰈djyzAb=(*mX41+ cHs-΁0hs@W^xpyRV3<^p9?[Ι3f!o|0sLY%gs2ס3pg@DVxJU<@P;Ә/|@@̗9^[3%3H;,qkxKkpnlƭ;R، s3RO/lpv`Sk:2e W2w7TN ?;'2dy`r/PiUZ1ډ@/CՃ )Y < L>46Q6M%/Z4*CԩPSL!RB` *x]@xBMm4;,6Jcʄ-o}es[R $e {߼ ԙ^1n ӫM\u[kC|d׭\SXN8Ax8>^*ޮWpM{?J|d ?=XOYXW1лZPW<ݗW\7^y,k/sW/zs{%d틠0|/G$9ɕ`,.d<_Y zuUxn>e]=h^>i1O䦬1hލs%z5Xxt+7ޖ+W޻d,'W腪93&Cowl"ng ||`<J3QgzՑ[pyDJyN[?rO M;<6Co;)8_|~_r+<%z7?w_~ȮSU4w>d'S$i<ߛ.a{%UGxh2-`g魊ZmÓ% ˔e^?գ;[`ń_{W|_Z/+WyAo_ {HcgJG.B|f~VeO/ݗd m]XJۚp"o᜾ \>xcל_F5@ڲWlOX?ںaoј=Se<4EN9nOTMfBS{u&8Xyh:f VenAv702~>ޖv(+DM M[ o-ZCsJ&V5!s/ b[!yz!=T4(X0*[aDrMEC {*-)h j4C/*݁~Wk O//!a%t&<{HkBITKC$P5+U, LMEo'nh%ɨZ4EeQjGY8 bzJƙOw+(o Oyo9ͽ??yCSz볕2Y~O/\ViR7x] 5-'᷃^7h{\0Y;U3]E&OsWJ~5BlX5ZbBmNY7'6_^%-CznSz%y+S~eVoagT<۫Y|J |R*/h7xzAN|H*vyioK]3K|P%Wjޛ?pD4GU[7Gy8oPW^G}[W)DnEaqă.w0`7}ʚO=9X>Ւ+Ao/x Oo2כ)BU82]f8-utdfGp"u B]GN9h:aOs/፳BSs$'3myM Yvn4&ٽFFIK^K7g U @Jd; '.`,̥L+OI|%GL\КB[5@B[ZׄpBjMK2vW1剝k&U fo%kK^=sYoW|9ɛCzUS2 (C4ξPRow:*Ǯw# i9{WMT+u7;q!?@B{qtI%~t\*koREϿ|CAD/_J*ۤg~psΞGo}|'^?LJfͥ5ZvEn3::M7Y3kMܦ5X1j7/6fmpŖ>)hvW#AmEs,QWT逦"}?O/4jb-KcG!+/zzq|b,¹yM!/q;>! h1 lJ5˒ch$:v 9< xιKo4$G 7^/x/dD MbL<^B\,uj^׈fEo=@ M͍--'WUW/= ^k$37W~vk{<(Vg7E ւ:B Ži=W|?6`G't-}u_~~;O hѨz1's諯?t_Tcze ut'j߾K_248lvL}__zoN~ͷߝx~a-N|,]e`,>L+D;.xX<ɜQ&z^5_?}5_g\o?0 3 .;y/_+Ǯz&8_;c'qC7ChUWfNܳsD8j; ^+믏}oOyn9V.^Md{\0>``"}W!Go{wβ *+S芷_y:۷=5/*3ey3ɸw邬TM IDAT_7)>7ŋ|T$OY/|%/YKav/7z]j^AO-}n>\*Ы8w;{֝4۶> V)]Ww;x}k#>fw7):xwh"^kr?|)_r?b ,Z~v=uyYQ j5$ c3O^]r޾/^Q1ׇ/X(a1[5\dks3Y-RMz-Τ.@P528&`^sm0ۑ>H`^+>߹WjwHfxzL`c?HےXRPC7:BeJ+ۣ8x$B ˕u9b 'I "OUXVXvB+R,8Vi%^a34:~*2[*&[W'^d8ճfR5w(H. Q40Δb*[`-mj`hjg@M ,cuIb+i|2Ǖ@~íQ#|P # \.l'Rcx D͞[I7M%O.hр9p`L⻩ #OXc?B]j,]H5ĤP<8p<#CB +SR%.G%ӱ97&iq٩pH6h%:[ Fk^VM<֘.ݤyy Eݗ]7eB27, ' |>Wn&A:b|R/9Q՟ʖC6T>*ˎJZ3ʫfn MJ~ oq ӛvbD'UJPلt8KpqbܯxzwU ZLe`WlKjvYUkU6V]rrR{VVx>ՠ-*s7# ݼwc@ ċr a/|ЫPKΠW*d JǟvɩU:(9iKYYje5,49j5k%OI{Bvw$+L6V{֛& PYom. ^&z8&HJ<߁`4_pZtlSbd` ,M#4nʔQ\]Nw0@r,6Ur$ g [h\UT c^d)E4H!E&DU NȡA/8^ [e/ES9-_~Gn͏ZK :gs %3ypݼÆ7^4~x]#TÆ7@ $LUFtHv"te^6t˭|os['6Z:¶ys[g(bX/!Ք\^jTDd] r/v;JvTl)q%/|(ك@2%WuUI 5RYOLţ."ЂV!檦"#O50!ȡ1Ʀ2X ͥh'K g$WY]v?IRn3Vg9߃??#Y{<ډT'wc&s5J{4rwxz}91h&t%k'Kd!] Oo8YvA/țd7]e1}9ߛ"H**%0z^3gD7p L0rx (yp/_cRU 8^NΨ3,T  !5[]f+k-msbGD`! .m#ޒlPmclS'"Vl]g/(u,;FީWtJ [V8/u*:x ,@LPcVIX5uTTkH gxzQ'-2:U^ jVR$cI\׵&9㽐y+r^X_D17_4@ 2 ( ^x1\\9mhliljmC+p$ 6B$8A2_ ^ϣn~VɸNj$mfrh^gMmK?]]{9{iy>{3ݡ vCn q-KAHCJ^$B"NB,!EM/ ID~gYwsϽ{RkS| Մ7KX3 Pp5 SHBQ.c13\6Wˏ'h=ALrbUb;Zz O7$~֢jOW(ۃ`F<~1/7wW}鿅7K=j.ڠ\OV 2 LT5q:_ԦLARFŪA|ުMPnX.qL* !L_?3{)q RQ.)V`T FCS\m*M83ME@ɝxU8SYY\98^bA, VDag_uc̞OdYh9STL֌"^[D.Wѯ*kѭ#|{WG} |@lWNj4x]SP5S;$cF"x`_1.`|1U'߱[ c=3ҪD= ~s.-jsݼQ>E . |*wac鋌3dsMdL}- vƤsf?RbISVTN}j7%3Z#FfL1^/' d5ڞ 6x$nNp[TpVӟg8Sb%=X-ՙxzt5YJr7od9;NWyo7o62 du<{:^j8Qk=wK< ZmeNbd:OW^t;ZlT[쮆 zzG zATrQ5wSrFsi*2ލh$ZKV3f{U(Y$6JZt+VK*-lOWmNɗ1L3]>XoՌyJOCRe:Ȱ)cIvahR' t\8x_e:3o`qE7/^3౒>$sjF#y65E!+J}\uλzQn?R3ё$Wīc;ټn򜥩ѱڞ^S wmt=~Ż f:.j^ƚ OD q0'j4W/1Woa5;#讀Q.jV5fm[FѸ3j ǓwS~ñd,MfL;QLRP,#DA|(i7V<@Ȕ㬅 [cՊBX}><Av#*+kf.)V6Z Njepл\xjX+Y[{ޟ,[r7^'p\ѠxzIZ_|41MEpn}|ȡpw&&ۛdzew4㝮Q8/vw6ٛ7$̝g+j3|ɲUZ+xzw9Vo,[b\o%9/~jb'ZUNj`:et8C'f/ gdsHp\U6-x ߛHe¯Z:TP4Wbӹ"h@; 3>^>sהOU[;xU $@ȉtfIJfLffBNU3l(5H:UNj+ "E]106:XC gcCwޥHS}ރķ|sU`7ge}kZ>|>>.D/=6T_:6]ǫl Tt$Arգ#S܂~i*2>^ZlݑPj 7Y6ǻ</Z|^xݩ.Ϟ-WV`\~=M9|og(U,~W7򹉧wۛiux8-vOfV2Zlg]S.|<w2oonK:l\/1WΰReT jpzN?VA\iJmUf %Ʃe+MPΪTmRTiP`ŲxAr-Ǵcɗx+U-rA0E;/K^/\ ^64ŊwNīJf ӻ\!/U'P) c݊׉"f8^8|f40ܩ=3DN ~Lz龦XgVr\vƓYI(m=P2lՖ6Zf,!7ӻ4ʝ. sh$K8^;^LVg77wg5:C_mk5ŪNj`zTrMU֪lFPRn\mȅ陋 IT]1JfBT;yߋK9d:/ #Vϔs2MT4yTbE6cpx +f2Zp<Oev=K,eY9Mj~j6z@[g~XB~hs,2_ =A_;6x{·3x@zH*=Hri3e[lLE $v}*HA~pvWmk^UF v#8&{5 C k;MU39 ۈ7* am' FbܸQ5G''Q(I`dn3@ yZ7Q5'\Evd:fBB\mήm?F jzn9'PzYxpmƨ+tn~'_7Ϧs|Cd.m6Q2OMgmRfx/7/`1 Co¼-vIR?ND$W)w{; M4.#NbE Xρxh!J|öls9[>l g%3>^gp/@A3ZP&Ͻ ltVZrUi5TkjV3|T*kJ.㛹ʹH_\((spn2A@t>mY(\E?>^S:q;*#9Gi.b6̓xcI:/rExYLUI$rHWAZ"HI9EYMHm,S8|ãJf'di$_?WtYp^1Nw~|r&Ws/zrSUbqxM.ns{OoMyk'2PWNשdv-v"-I޿V$P4ƙ\Ƙx2 ,OP4j_<7\bJ]}{ 9:s)㬒mY7a5s7.g>^'ٖjm΀n^Ii{熱y7䕉mM`SѼ;MVg9F|>sjsٟntռHWz5U˳]껹Zwg{BpaPۃhq<{2^ڃ)>b̦V<=SN?yܑ\VEUo4LjvLF9uE%Ū\oirUrrVYb\o+xhǛLQ2l#ެYXhFɬ/^8^<:wUͮ}̕X9U><ۉ ʵ}\uVﮪa=ހ^+9@^/g/1-Dtds}@# xm^W.[npRx6xi|ʅu;r(N&Ph" jVWKrlL2yxHbFia)3 IDAT^XJ3$phjb:WKѸ\Mº @NOXjUEd꼞Y4֛XLUÑX-zլfE ^|VZ`: y||lZUo8pnƒVM6#3vRԧvwޜ?q_{x"x+؋)Ufs `.W,vO(w}y:^vh,a|.zs]ϟ6FJs:GP:2Zl[̔p4 r,?͞1~݉%N{#ϻ)x1^ %zV߄y;:vbjQV)Ǜ))1Z\ެU4ڹb5-FU,=kƂ*rNěɗvʤݼ| O"d:O`Ϊ7TGӂoa#.TM 'h,Qia4KӹTV*X"U1y]/rhtypfA2SxeV_xw1K2P+ U1pT9^Ɯ幦U^>@xֈ._ΐ`[>*ÌaXfeEwYdOx>E>xި?S96M_4o6'_q0?r0s뜐od1??ħ>!s}{0>?x)NȒyJ' @WwǻOrձXxk{w uul+P.,xؙ|x|>c<ހojvQ&sm6ƚլircM|=^)}9@@b'B 5ݼjn3׉ұvR I˨ɮpj, g`lɗ^<[Z!yLzh<{C43Ñj9D@ͮ$W)Nj/n[2s!|G`:^&xg B 'F7x!ӥ?3Oc/>Z=j^}2>w`o΅œz7d~ZOW|Οj:b?ݰk29}CKf:3ٟjtX5#,6:þmtͮv|wv Go`<ήjn!Uě) :o:_d,1WmTi7 e_m6A 7gU@ XyHo@9kM;t d߱Gb`gV 9>Ȅc):^ Ǔ65ݼZxP\dƊxhyxldguXP׊@[|CЫ`Lѝr  :MoTbSbYs򉊣n"a''hVZ65FE~80Wy>h?P5pѣG|>ztrs$ KkGGB"Gj={FТR>HxeDxzE{2^">^Iy<^s^rQU*uyO/Q7cQ7UՌM VGzYo֊xeIePH\D'3yxDv37!W~lwbT*U2)t [IzC2Ο] n6:CT͊rmUvoO@ *Ve`{9,Op罽ww:N4__`/[VԹfy7aD&z~5ZT5/vO*2\~ 4M<{ ❮.(4MS{gvls5[_V[ԝ|@l<ߴ{cErAmvG4jl/ބ:wDV^ר(K&(7/KRIs [,y&9Sf e͠\$Eu91.,}V8uENj7\*O~Y9kP,^}"]ήV+pǻODb~OW|}LĴd+O^rRpbM*߫X"86Q[z *) 筪S)CG/Jc;,c)IHE&F FA!|؉'BPD((Ήx δM'D$H\)[)8WS%z`zЬ $v=omN;1= U<,瘟ç;*P'\yo>gu,k@wO`h(l$F̏ku\p"^ư Uj~`/8#4J?9^Q5|/)V\ 1c5裣cn^dƚz>:r=:rxDδ*E+ߋ5e>WTAi:vI?E <6j}]MEgEڱ zڱ]&{uSP}xwKzx9ۖHhbɌrTʁ%=-D:ejR"xXJu;)#RaF/M#Q<u4 @93!^A˃ agVxw;`=.?x}x}bb'ᮟi`c.9/G닏@۫OVt Hm@r{* 'ܦ%'`.ݙ\r>^azg\.x`>\!?/۫| ,iu@8![lur{}` %{~ݩnwShnjpٚq;°PkŨ?FbK. "U&D>yCrɹvt6ڽ\Zmv|/^T͵VPg %ҀJjCЅr:W,-j 4DjU"L^*5~.l*kwBX,LD.恧7)"{Ls((nzجWěغ6vK3|*U!>^m 0E9^M".AcO0*N35 gHP4N9|fNrp! PuPPѝFE}B9ͮrN|݉xobNɬiM`U~In~"'q~ ?V}b^A>(rBe 'MQ'OH+8-/2$7~x=E>?"`dV/+{zǫf25^򢪚mZPGxMn=G`B}YpAyI;ۉKrQASkjv}&s|}S_T ^>Y`8sQɇ1PJ_XO* ܲXq:}\ōX^ E-ĒQu%.3."UԚZT'97uD`UxUFbI~k>^lrC%ME3޸7v{l$o\Ԛj#oCqY'@0Jv#FZ҃mWɲc"sgKjͧťjK}ɛO\պ\rEY,f_,ܾx"c\r6}҉:8@̀hqFV|% ލ ^ i}P&g*ח\ y%\TrSb%W'2f0̈T*Vͅ[`8zl93^1L dsw0wOb^?>t/b~dwug^A/HZ8^'BY=bE|3r qT F ʥg]yh76)Ve/q"coՈQS-3kp,5zd8Ls_%X8~Y!ZS"*S ^,_fc ^}(Y)>dHp"^d)3Wn27aa9EȂ\[}NīLOWU Xr"^ ފ?yoSqܛ>f??'9 /P qo'o|՟ e*wW/Q=7-? )ŚY/X1JfYNgEK/dxQ;bj^^Gys5H3 [P4i{9q$B)=^^(xwxc,"rO֭J4(wRیOn*jV+(L'|g^KTșLTߟ ּvzU5Zj3ι/4%Jhh<9똠M΃<Oo,ņ>MNe IA(W/-*CX)ڈװαXQ23,tƣ:{zo >؛90`w}KYQJ:MzU~+EwrOP 6J~}OtU9m??ߊox0?TIzB9~b=30ɺduam+c}&"^uy:Q.99LV3gcWun_&(CR~Ɉ?ҫxYΜv"6렝hDpp'жs2xMyv>RE ;$tg{M> լf,F9 5jg+T ƄfۻktȪd掆%na#F3Fip0>^FB?$q\T26'ɉ7~g4 rmIrIƳX,TLx{4ʩ≅$FrĽ0Q%xtҘ׻+j]_ o qt?G_g՟V6g}Pk;sݼdZr:Rw@)$k??V֛d֬o4dT >.W>ZFT_J;_Zh]P5ZNNjBty:71.7Ε*՟~ube+i/ z?5dUkt&o1_JZ*hy;ZNWǟjS̷ٓDxy~^?\(wg_{βV5ca'ȔO}sׂxQ8OV닧֒b5ﺣ%>tZ=w~oAV.O[Ob^0a?'Z+}dNWSp` %Oow0?Բ4?矧~SUjpu\'D*[&eʿ-S$[TŪzeme %cVm@, g;Ū*7kS9TkR/ՒقT"TrFld R$i">^R*A@e[&rXND2~CѤ+jI4.^pqΑMSR;xnYDN`A"dxG8Kh K;^P.KWNKh/ٜ ^ DLdʛKq* QcBAE`}]PȤ`UռzRqī't[R󄰔:U>hJVڌΏFEῂs~4I^W>@V* $Nkr?HӪ:y]oo"^W/`V,m~꣣m>Hs5 ءdv19^K7"YjN\1.pv{65F*{bw `T b  IDATkTܞ\rr(AuˍA#T&ruV- Zh" g7o7""@l2fSm}Ŭ>^/fҪaF|/>l,dVNKjJ޴6?]/}hO0}K]{%7%-1_:Ur5zUsѥW]b:z#CR֘ϾPkS܋e,W_+軺!-ӻנ Ҫ"tu&Fl}OfǗes㻿'/?OFZo/TR^7_tu|}zjoםmguwfd:d̶'oGcr}{>?yA/k3ԝ%Ppc{z/$;Z6W=ηW'Mփv{o^a?2i!~ e..K_`kųezz۽q3NWɲu* l ˘N2t{FI+3O Z̊{drrגw7K[(k./|[mvtLQP&*/2g R <sUc y]4*ݼumk(mjNl;Q.}1cDQd 8LXxċ3Fe#T6W5 UEA#bX2|/|-xA>^8H|v*u g  #uv"BuU =qNؤXD|fYs+rAŶ4Px 6F꼷Zɋ\{^w;xbuyq@ Uċīk@`;ۉT ?]1di a2Ӌ~55Y͊xmI]xzS`T 0 ^Z"Q/c9V+vgTx~("i-kV^kt)`r??o2Rk/>**ߖ/+XezoY D2şHU)vo\mFnWw29c}VCZx_qIMALWR;镺xIZn$>oڬbOg_ F x-jZ)3?ͯ˻2yˀg`N^_ULrD;6WNBL;^OV닏bښ֑ GSwQ["|Ͼ_E\Piܱ*u@b# ^VO`&wwu<?WOo~ߖ?qd/hT[+gX/f>59t[`?;N5Zׯg6;}M*fǪHAjV[4o !ϖŬU)VR-/UE@P  5WywV,k7*BWPڍD׉x cozzNċ=D 6vxs1vA2{n'd~tbא+Gi@XޟDBnZtiVk$i7lKۣcI4ht[_F\j#Oh%ty^E@/ kjJ+w8zܟ2j=,~?r-ezۭh>)/~ne9k77xjnJB6; gxyzO\p$fD{3[NBc\rBh_F-qEcS;mA˳\o'ǫU6|zLV~J.ޕ\rbcw8/F{]7vv:SlOBj}xyNr`ǫ/VZK}ﯧ%ĝ[o/'sW%;"IYZs^B擧ħʽ͛srH3A"nޫ juUBMqa67+w1bI..!ɸ2I.x30@o7VO/|fTzˤXw-j=+BnߊA9f bM/( /mKҲ~3ރC|؉x?|xLYў׵δOj6϶W/.1WZ$gvݾ >^26d`za~}*=xMEli/s R O=x2CMrxaA`DSጽĉxi`'Ŋף ۜ# { C4eH<}$ -()Hf*(dRo*k 5 QZ*'ODR\mR+/VH lb"az:#kiozAլ>\x{9F7#zO$`Kz*ͥr-H/ c O2d>[J(zGn7wGs-H$ͥ=s{;mn& rL/;dX%[;#%,Z2͂F"|'fR'^ym|V30gr38 }xHT4[E@,Sm'wVO*h"mQk g-eq籡MEA'xqNjG/3v))klf%}:cPُUcMB.Gd:%nc=ζ s^t!2P$~G$ w rWHTZ#0DMUw0-t8k_{|"Rj "D*HfkAǻ7.?IT.O!tK#:vl:W/Bٚ>`jDWx/Ak^sIm_ cT*&?o G_@t_E=^ΆËm^_|xj|۫ONV_B܉K8'ݗ܉F\R QR߲kOyuz7gOLnz{I~:^Ο1̶FGS y^k^vwEj* G>AsbbƘLsזy=}ROo?!j8]NGtE%@k_rф@&ҁ>Es?6OU\kjpƢgQyyMO (&Rx2*̘L@8C.B8mƻX<B~Ej:/w+Jw:[5F*m%K"фZeI%r )gx˫\r.7` "1ϝG*o^ >l<{w0jrEˏ)=uA%r1!%'WO  #~qz<ߣѬGx"^O/^gO-Ɩ^q/ϗgOg e^݌¼]7/^rA+ -|5`HD2 G>n7l%|x*,}/uOCRx^W}PD&O;{pă^HW$z|};?3@5=:1ci-">x>^TͶb ^gD+pDXΦtJVͱ3:xU>^3 (7ޤ T{(l #ѸEV2tl6P:AY\=B$QJfgjk(RϹe &R9-O>A:Ǫjf90&B,KnR=21'ҩt>/rAB)g'W(sD* G'IBւQ 8Wy@zYjm8jTSm arA'.O$!*UD_5@l:WJ[*y+ U2SWT&L<^Jl*^(WsQNd:xۃX9xT4o:l}=~ +ݍY*=ߵ5{+\rL.wxɼ \%&\r/0G"KX3L^MohǙb+U?JIO_Kr{rY*7V9}aɮV&g ?Uz/LJۚDH?a3"ŢY룅cwvmORWF346tc8ާ˳mN+tu1}^ZN; `ˤ3aNnq7/b^_I}ԉhKr(|]NA/DxzV5@~Q3x-齹%Ȏ . , KT:Kr2w91={љѸ/KzP2GnxAZ/f8^>LA/|Zel@u2؀f*<3hAx5xsV%+j&4KάXP8Sޛ^$ ޔIb4T6ͳF"J}6uA*@¡XRhIf J EX>n^GE<H" n*iДzz Ntp-sUҹ1;j,{mщd8}&WdV ⅳ[..BF"{yHTdby2>6F_++Zྏ׀Xx]@ewz4EHUd4}=?>^5q m6I6^}*#)~lP,ASZҩL9UaQS iVt:&"&R9TlZ%y|t*/$@H9SHeג7˚&*WerqYNj#ʹn,kd zpX)+yt(>Ql*Xbf0 1Z,VK^InWzgNjTӟ@Jyk1~(_β6~ X2 ƓQ\*bQ'!!= EXO IDATUE %\oU%>=[ZYTZ:>^6{AT* +b U3ed59k1YmP&S/U3lЃx*U52IrH<̀x|_ftTVΉ/:2d>GqɅ`9RS{8T&4wl3,bR)VH* u9KtCOoxwRqŷ3c7p|(G.rYr=|dЯQ8?fWk\/XsI˗v"yleW[K w 5Hn"^j庼~5ݼ\N#kV!X"x ^w[y8^MRUs,!1<@4Zz^x')؛ʙ<Yy}UԚ_/_H ѓߑUlR7ds}6cBQrJV9UWBo$7=LNzY/7ݟ~#*jnA^%ѭ41ASAuqKU^?⫟Jg% HF de*Vѐ|/'ABoYeKh,ykvZ,*j?}aUVe[=M닏P8&6gˏm/_De)Mws5d&w{|xRz6or&R&I_|Rmԯk?h[Ŋٶ'L5>Ŷ7ӻ6)$ptTkkxR\ڸrZKG](_I~ ɚwJ$%CDb/e>翓.XCA9 ~pZj9cɬ2 hq1B$qa [ ;ӲKp "Ub y!J9^P.ziU;+"W;lqG=\F_پ7y޷3Ug>^i*zĬLk5ΦS^?:2FF,=Hm6'Ȟu!qܾxAnD,A#u(W/m3;p(ќrO) ߋE-> GUNK}(GNU3bi<\$ګ{)KrQ/ROo,a3KT7Ox A0x Kq#y)^XI=?$?*|/:NdZ_bZk~_?/Ѧչ?qyPJ2novpf %L2KsL.bI]Aٝą5?7ǺI}ʐ[X{cErLt/alwgW ]ٻ=?/D'S9Xp4tꄈCXXJt뭟>sXaSr{C#D2%'`D"dy%xBSv:aEXN$>srMϾ_ ̞LΛʛgKtv?.H]h<\~h.-f˳'y-7iZG?),34QJR:wmRpH\ROU.\< 5KN, "Mʹ|p'Srϥ~_x"sL7d蝯/\że@1׌қH\5Z0?vB.o8璋_Z+\n#x2OUf/HvKGIJ<|@l5e `_Uތw{Ww0/jnƨWx1,0t6_5X_Zy$/ O:WK<%!iIS>^h FRrU9KtnЛ)뿕|U[8+c+vwHr>ZW7STk:=R 4׌˲a/>^U5xzA)Fլ/T>A)>A#Ͳ- ɎVl8bɝ+&5!)Ip98 1^3\MWahHkL$¦0Fz>ă\|һyzUwx ^૾׷3rHR󣽪!KT&cj;U*ۉ\h]6]Yv"Q5{Rprg9~ٸ&ؙ=9e a\U+,tzGDzDD2NڧX$|/P+g:xBK El^G?͋[F j).)V4kw ߋ5@WWr{ټb V5"ĤD^(Jr8:zrH)%_\&/z0KfL G#=q{S\R?_bHϣc7t86OQ8wǵ4?nMoq;!y[vVKݷbqV"6dxK#YF")8 a H`@>dC{O.=" O×l[{b_ oU* wUm@yM5m6Y.5uKɜDTA? 23-UXGPXa~~[*,數Í92a{UP5o '0K+~[_"qrifA\nhM/jצ\H~ .bWfeݴP Ogh7T3y9P5#i.8]xgw~{|LJ&_S5?l `uDwF H3׊,Ų玳6dmMO9 x+5ͰL xNPQ޳O{?$M#ڍ*6 ^GGWP5h|rӪpJ}i&^Xq j]+ݿ=ZݦnRD:ZAM$77mh:rhIr:9 D13/YL':ޗvv٦+3dg8ApBME"o?o[@n2]Նᇝ_j˛J3m0X豽Ob0B1zrlؚ(uʹ!Hn(E!.[)׫TyM 놔bU5˅|oJExv {yM{JSŌ4_ɜ) g a"<™dXb]mX#^a5]5զYBtFy%j&h[պVV1Z gV2Yc㥺]!μ2H8x&kb({KZ^.NP&=omr7/+N͡0X,?yӧ~?&Ylލߝ?+g([+@\֭7xQދtM(Ezk%'i{>e/ 1!^sv"X $lAx Tb XmRo %Bs.E'xA!#q4FǠՌØ]"jfpmE.lAA/vk <-x[MfU36GV147hT 8^ܟ{(3@nyDrR.L'[e/+ҭTTnnj5*y˞lP7{'}aв5#mMv5hzm5V,V]/l{K vW7,hl#Wmo5'W5׋vwn~E^iTZ|;kۼG^.Ⅺx|SOoA*=Ke{")Kq#k/9v H/h B,%XPLۅ7n-޺tֻeyQ"&cUͫ[{@p y{ڝx=ټD aء)wl^vRCxT5EVQbdy`/Y*)'uvjc$wۺ luʙ=Cn3@w0:NNF'x A$~A޼waG_1 uDŮAHN| ~'0!\%3 ]_۽ruxw>oN/ {pt;N]w1n'gӳ(촷rt :=rG46#_QDREb]qkA*xTUWVw{r`Ӌv` .THN{kwOo3$'t铧v/~A_ݝݫHc"IELW:]"ƔdL^.m{BI IjzYN*+u$W I3m>:B7*#M'9 Ke^iꔀ{E(%I/n6! MEI/Mu9Ǡ]'NguA/.uc`#UՠNϲm?6-B}Zӈ2+˹mR:^hNTM!yOO:VQ" $ ڿZEUwmg<=~{yJ&_,VDA;lumZ.*d R0m 3p+kZލ )/G 3|H:hrzVdCbMo©+e+俥3|lè uR1BF$YϫR%l)4i<91U=mM goNS*Cyⶨ6 ף)'\a-U6Lڜ}mO)AWA>^C2vEf8\SiS\n4g-͘^y<B3ME1TͭM߷xrE!OEDSN۠b"ݠZSYo_o+j3l5|驳ݠ\ D=FT]Wr3|X'&3mNَOB"vV7꬛ׇ"\tmň"( E)K4%v͋kۍpO6v-7$x-?Z#b 2vL.8^A>;P4x90g`T5WڡpnP]qrUQ5P635~UT rh_p *X^k芪q(iUmb"B@E3"_0(ry\nR gCy`,xXWW'|2d =zϙz6~3w2 :[3U3/|+>ޤ92|X`n^Eyz939 %˹B r.I˪ffV5eX!./3* c /xW@bN=<\p KnͿ4+|$J4Ǹ@(]T UCk&[zB&N,w) M2{zNQS)\P7uc !SSm7xn1ܑFAK|YAZkE(GS3ߚd9, P-ߗ^?l 5-+vRTk޵=uFuxk |b%<7(5rYԜJVW˦Z_*+%,/=u XA-<%%75y 9L!:᷑< Jpz%tbk8ܛ 'gxso|#O/^;˕Rƭ$"UFA$g󚖾* x W ,Gؾӕܪ$uQCsJr\0b"zh2y zsQjNN?9:=8d^`DV(WCxg|9]xvqRi.+VoZ]Ն* [Q_"q+Sg=zYl F' ɡd❜aP߿%-/vxKbW9$>vڔDYfJBWM9PP-bȴh&'jv|ͰՂ@"]<.W_:`O/x!2Nk;5J+u۫MAo6GXTj4[,UȜb{";7)0.HiM`r!njU)WS5I}|xmǧv.oUbJ %E\=ߥ' wu(Y͆tC UN[]aG\#^ wÎ+ 1ݎEV38Xt1` pn]l [ ^T¦m,bkX$ 2c!t5Ei5 1JpT㭊1* #jVT Y͌r>^j ѯxZ/X 㙧_ h!B)RDž.MEi@~Y{3ߛO)cю@;^''JMMd2K*ԳA8tKW2d5؞\>x?|~ǬeI*xp~%Sg;~>o\ܽ{^jxuz|Wg[;>X{g;sWijgl ITb @DbgtbT/~EP8ltU[ IDATqx .ưc!Z9b%U3z4$Z|r'WW1p{1ѯd]6x $5)V}Qm V趢^x^p`fX_$XՌ{c6)V@)rCxNf i: zUT(dLSFD_m7W\Xؒ@hz5oO{ۼ'ދͽt8!A/pzNu%K eQPA&­\u2YЄDE3qYEiLa69չ- ouq<7ۉd-7>=ݝャK$Zӻ-ǴbJ$6 ?*~TR tY(*gM9C) @7r z'w8 ҪsOcD&y|6Ro:n /%QUn:I1OYd dDohQ*]HZl/)NqɷtLp v ՍA;o覠wm^i0$ӔK+5_ a0kPU|r qTB~1OtoB췺ag5~M7]nYuՌ+ ^ 5uÎQHOo (R/$МՌtX3ɀ~!3E.&,r.#T'MI jh1-B )`e"U{(Vx%P 5Rt9|/ g,2er4o| ^63^ {?5S0ٳgOaْi6.{^{hn^N@88=rK>{<u>yjwUkVW'Ϟt X>|? Ξb囖vFN4wF=л,N>G)=ЋA/=A'j{;|[%5xfU O 9Uҭ[!V87xN|/D36`"cR;KM%WQUڌ2y Y͔L 2f UB SͰ"Hob+ ċe 8rV8CPK$@Z;KYDbViq"|M;*45~na86uh6.U2)XArk7Y͌xh'B&j"{{QҩP(o]ZREqYu$ ,W*L8HJcvZ`}zÌ5["#\^ Pk]onr;Q=DU n$쬭n 6P;mm&7 V`:܎f&[ O5@N3mV2yր6u$Updj~54 ݃x8=pz齙dH0 kT 0^h[M?hW:wI-{$nC&u3[l3}?g >޽1e;|;>R>xbxqrpt|s@rޏra:TVV2ټ$Dq.U2^A]Ֆg3CHK- 뵀)ݲ?z{'˛b/<5NN'gøwmgt#y;! FkdD%]|oI*9nЕOٔ#]cj\DREE寐f,.,#IO/+HB#pj*jyow7,J.XUZ dyHz"lQH4%wCiZ^97, 3_`nEuQo-É}pfOEh*bXL.|)*^~ DR<\^u9F/X<!=P;CMED+Aӎn閃5e̒PX5*|JCrP5'l\WyEoFa]הO2sW6~tg:Ǵ_,{4$Ae*;/~+ΏwXz+Л5(/7*jg n}[(#֭J˪neE/HZ1๏[rжE= j &Ҫ7xUj-'Bd/$н&ǀČ~j9^;G"U(b4AHujmebc%kaI\oakÊfD#" 7!2&zeՓ`׏kf7+xzgדxK0+IE3Ceh7X@taDJTz *6%m+(Xtf&>A@=gu@oĸV5*1Wlk ]ph4 gG[FU[K"{4բfW-fz ^TKnN'8 }5h0&)6!7o7_K:BsnEؕȩ"wh)-:)ޜu,o`t>^~tx~c܃˳;Y|=&WBAf2 rrp48źQG"ٹlNכ]<qc{udſ1aл58@V3:xӃx8=Gh{:B_BB Y#D~&?ذ\U+:<-|S TAiyS9=^$TMj0:98+3\n,eds;+ˍ5^JWjrEGKK:#0xr"vxw8sx9y{h8=G%W?B5~>{zuW2yYL)զGq,л6LrKy Zݺ>PQSY)xUhO{;;R:xcgʠ7zzaEbr@)é6L!K](uzQbR%?׏z6Q] ^BLݨۇP قㅒtCp~ /|Ř0:,/ fu|t*nU  `WDnsM%KcJ\]jiXMê,Qfքf4eŽW_ _CbS, %։[9/@@bݨ2eJiԤA/{zI3T0SSjd8*ݻ^]p`)q^5g_S9 _nZ:4B|IT(^Dɪ=db" ڶir)!JNf}Wj@ *Vr >7ψ+;O$[6꠿5[rIDJ\{b^~mw}{ȈHUFN;co Dq͎a9yIF@(1/1^AE :Dž?3EêSܛ^db'AVff dgU79K)Ęr.,|yߎ' QҌ\t߾OL`=dƠo_"/RAuJ$Mݰ9:~lʉZ28V"AoEas/0I뷺&ƔP{ȗƀ:^wy`p­9F"oy1 (ڮn;^ rh-r"1"wؔ(rn3G3C-))nNk8ŪŦ_ ! Y՛|-@QnM}=ź`"(%$,bǤA/+~}P8R˫__li_|TW~|,a9wp|t$~F} e{g|y>: 7OB&_>yR"m\OٲqO?n.[}M|E1i鼶w>;p?3 R>UJ yXpP;9^ ]W++yѯ Zċ"bL+ZDnXoW?~ka[=c?^:?Of?mzNO~+~fyI`$k8ǼqnǿC7_ƚ|o~ TP1K.a+yu˵ ~Olk{ze8 LK-aKiIB7B,<[jMb EI.QsaI0KTա"J7iA/#P+!^[VeU38v 7zz_ue1A`ׅvl +Q8Pi2HR1_Z(%lIf-%'JcO`t4>Nvrcw7f|; )'%oDV PdrcxJ:ٔ+@swӕnQm\PP-O@/!7xɦ{q>HN/Ed)ӋI ͽk͋SeXv;n"I"2‘K\-٪^W9L9Y.)GADةOa$ŗI]( ϫ ږomSXA>^ ^R8 zz_=nqq%8X~t)G{4T,+ԭ7țg~ʁ߲7y&zM Z5 ^4~[">+">^  gl{V[I AԅrC?Z#"w,hV),7}V874 {@ss2ʅ 63[4&Z&KO*5r5 cP@b\V@" $| -d5tF8j*'W0IDjH֖^V/sn3#^9# W vl~{{G2x@o}ñ? փ{5^E!l9BAJh>ĐAV7Wn&Wn;_|HH`>Kǯz_ɓ=:U\-y3?oxrZ@ܕLNǛxWVr Z1Mf5\^fc@b 4jFrUXMw?_ /i!7 G<6a x%lNx>^max]ZA} &jMͪ87֋jtm EEKBZٲ\\*s" 7L5'0Oڮ[H55 0y\ۇ~RM3"KBɌ*$vר.|k[{{x(\͐wPKh,+(x bK#|?lE|r9_m!Ƴަ"G9>xv\P(O[ M$. E;.QZ9KJi{7>98r\xt|7&Bx{vA`juQ}Q5 GI&8!jwM\[튦+\^Me ܼl-hdtK7mÌN;)N&a_/RWMK|bY̪擫{z(ËO{zpz>T~WKJ,a ȇtTjV,-VDbI!ͅ)!7N]jDqTrQݨK_U2-RE6ҳl@D(aə:Y.% _^R2OΎ/@95w{hc&"ӳK蚓Z*e&_,&4h.R4cH"d 6,TQbM,B8|UIX# \|ٌS?MYfXJDRuLoo}gg8ASQwiwm]Ս"^JrYQMR:GydIḡB}څb.K;S:n!hD?v|fXrBC\UTςt-ݠ\}V@MSX!aعo,+լVkڄ=z[^䆝Vo-&}0 IDATjuvi:@noðqk+k#^k!t\.șE!n 3CjF!cD`UFX13W(6_8f6㘫z4f(Ի 94vf¦j&P.,+ψnar kv"S^Jp rZCBIc$W7i}czsra/b9{8[}xϙ7 _{y3 zzÏ(7NDfae%3@Ѭ0!N>ND,q%r6G^JnJ]X3q3,IރW3â3eO/rmo㖉f 3Gb#TN[#K=BjlJN.JZBQKgSi^jjF~^֊}T π-jhjCc\ 0V,a;Mzs 9K4|RͣjřX0㩪n<"{X(7C ߂jrPͰ3!Fh:AөϑޏYWUH0OR(^HLxAgzGxk; dfU3%Wm ' pq;cJJ^I*fߠ)5æfr-f$|,=׋4-ۏг Zh]7RĖoh,$9W >L^Rh 7|1\ Ftn9NӋ r蓫^rŰĔ 8uD VzEBk4PM|FӨɲٸXMnU\HDS~?Go ôren+tJmEr 4 Ȟ޽ ]]b5<_A"TShӪ$(uiʹ^KQL˰L͌xgY͔Yn8#J)J w%/@j˛Ѭp݈Rw aqVxu)( #jxgD"̚AzÀ1Aqd`6M:6Y4 XbwCz!Sfdtݼ@)/})+x9e4{zI`^$Νno%dU&^q|^'%c^O_+]e{'vƕEo|N9u[FEĨX$zdM|tE[rD _ |/ߕL/#^`"1TsfQN kj'"J|36MTւZmOT$ۉ0^+x{>A x)N#h:H~Qq~vH-X H\4-_U8!@ :eK9PPvĆ}@I. rɴ\+nXIIW]`g 1x>Qgu7 692/w@wnLܖ C*%Z,cEHpBMbbe) ڼw 75lu7h~r?ubW !Ò\2_z Qv-cj>_l#(c!:=?<+™FMM^DeF-maRҨfIKHPcij@l㕅99'nF\7-xesڌ]3d.Z6#"{&o>N\%y./Onj?]D wG%?Uϋ50BGPi}a֑^Ӧbm:`\4&IrSw̼\kmBq͍\CJB8ۜBn '!>zU1.'EjCLJE=Ϻŭ`H^ԭ`/" )jE.VY`܊r]Q\ N}tl3MDxp;EEEB]#nw] >7/ v?ɼE%067XK{tHRҸ=NO>lj8~Նz!uϮ]|;Ie O:=ðy{A/\BxhuP5'/H)VrEȳo8"tCD^[?nͤp®x]d8#bo*Ź͌AREV32+FmYՌ5fH8{z!mVUm2K1T=ZS gQp@@Qa❣Y$*[ oXAZ×Ovڨ+R?V27v.񽃶RgOvJ}c=UpFsKFYn3$Ќxg>^R;'Q\/_mfc ތ ӛ-RWCz~g@@HEn\|=7#@ՁfB[ᐹ[nnpHg(sm}3ttQ7|Gw}0-DX0m@ ЛL/^ЪR'|-TV['?\HmI.}@jmZw˃|ht[ݫ_v7i+섟5uvx氳UskuU(ٟLzA/ "Vm~}mRѫQ$vP4lzW?HlX\s㡎FOgݻ֥^km"Kiu?hWg_>Qa8n.>Mo]vVo h=x=?>MEGk;[Cj*98SaEg6/.׏pCcPK*Gj-5[ryI[; OsސL62 x"hgd`&?}oE^͙sx=z>^ۏ㝞%;='Y˨B__+爊^U(q˚PD_7o$7iYP_pS4tڔS$)/Wko:mq *g<}o|K͟nuVè#$Ǵ+{|vOnCNtrzg|r{soJG Lў}|׵hyEXU'HKAoCWrNK./̝SZIEn":yiϜv$̭HOJBST {z׶xYJf԰7wywXFfޗצAvI b`$Q4vYg5xF@EB_Ѿ̼^]\Q]N~r+!uNyQM%Dӧ{fR5E#vFYA"rV3^ ^mFxڠՃ#J9^#wJ1ba-'W7 N  Z `H[ZXF֨5t٢U@[ol[kP44R F)EfMC.+xRoĪfpURxⅯ8 z&WqVs(5^SMO>9<Νz=;`,OubD>L0Y[O?O/?O7jy' kAV͍M_`?D%)'/?~Dk"[쳗?6b5{ş@g{񻅅wHKՓG|ɋ?'nfj M™c:tO/JmܶSa:"~ګ=HR(3wD1": {56-ATMH]QTK'EG9,ܿ\K\n 체F V5oM㴪DPwǧk;d}0^dzZ/?>KЪTl4*d:K|aY#"sbݪX"aMڏ6/Mn t^PCSd*qlab loOYl,oFpz~z6pF'1;"9zik[} |_{J{J^j8 ϥ<M s޿c+gu <FSNOfTSn ՚]g'mjjt(JfHġdܛb%9Ew'_‹S*?21Éf\m@6\{E{{Z\4êUSN+WW5+F.,7zx//=muVuQL/訷׷ċRtmk/]8w#$Fcv%C]ܢڄcS}-QqO_2pPZloUmJQ,VY&OPK£NnM}Ezv@"dE{n_$W1WNd+oyy ǷV!`iyfكuIf^;ںA[(5lZBd ` 3զf8mޤ79EWޚjTjMz-FraފB\U@l/x]T(0P1zzla/anY}D7oU@@od <}PYϟ?W/@o$N6e0YY*VE%`&^Ńװacf6*~poc~`rTQE9BvŠgYIR*]c{, g@XV6NZ񂃽cwEXz\xwc$,cOo2 ٞPmw%[% ^ޕKbAz~,vg)>^oHqʝ{zE.P)74 XլƦ6 8ިc8~Ӥ+.t5ӛM3jv?^^?ڼ.z^TP3('ieVIEAV,Rt#?$%Ą/ 5r^@tF2:\FH&"*h["ߵVo7CY_ǂ\A.UoUk$V,U=JyFz~\;dFs)I%L>.y4 qRe{#Dt!no{.2lW21]ƈ?Xܛ 8XRƧ2:ʱ? p>M+Z^VZ:!ٌɡVY4`5)B$?K>^3>{!!Us__wdF.'UJ|B*=x|$pfXa>8o?ml>xŔmx2-kH\7L:ЕhO{gҗS)yĞ^p"j|rmrzgcw(0ا3-y8?KRzٰZ@:"ڀq)UdmKK~ؿ*J:8mㅒy?ǻ=N98NPk侮(Qۏ6~S_=OA7/*{pp^:.j󩠫:՟;NɫOW9ru='R vNUQq}]E7/#^$T/)a{P,U–m|.6YͦxzA yev@xS /VMW̉,bWhv҇\/j>0|{"zw:s̞h(AGM! #C~&c81l&[e gx}BpuZ xK+~w籿6%/i9X!aS^ٜUN"w4˶Mo/*Nڽѿ;n{+'>oTWym!2D}%e׉+*GxOӧQB#mwESdso:A}Y 脐lmL㫃Y dXиqFS.Ћ@ܣꪞK\LzIX"qb$zMW!l^t{K%š= ֗:R0ʐ]J"/Yqb~CN@Ey]VMEH ;{E7oYagU;@"T,0hmJ椧7yhp^D+cp{1hcD1cQ 4(\ 8pNUa ᙧf IDAT0jUBBՌ^xV&Y)V1xપ}B,Ӌ1<@B\)V4en 7ŹxzSmYzw̟33Yuxp*xz&S<{!,ysyWH WoecR8 UˆW.+_]>xjVZ\ŝ\CA^>(>^8]+\]nXLˋk(hq-靕 EQ[DݰAPWjr9oY83`ꦝLM~R@<ce- /5FW^>. FӀ1zZ PV7X>x[Qw]p0[nڂ) Jo&1(Y7 quQy 4FX 0@N5yu#򒢐WZ]H#$$ډ]]6GË"Ƨ(wg8]T7=Pv4edmQ88lѾLr/&y0+EmbLNR ^%SNt#{v@A #L9W zٝLj=\C=|@E"yH L>Jv3I=LT4f*jC-r |@o@Ńl o0\|ټTW5?KnaAaT*5{E;D酎`rz 絝598NjcwBG$.H^k JZ-\,Uׁ.1(} zeq0/z"ܶ?ށO4ʩ ҉De^Kz@-⇝fbOAƀAm펎x׶Ǜ퍏ɾX*2bS=BDp4(:  q0uw״]p9Ue,*/x5m]tl.ljkp'89~"nxQݧ7%.*xzWP5GݾR7/P5;A0PnuI,\.^j"d]lfI͋@PۍEJTh@zպ2EXt9{1߫[*ExI -8^D^X35FrUC[ 3P1\U7b%:lAIw^_[RmoN$T<)V^jgxz '@/r6yzE̕P#>CNk)O/&j6/RhYl)F&KN)'EɄpN R %\cڎ]Z snH՗X?!&-!{n8,W4Ն^Ԅ}$mm  N{cgmk/O' +7D J=LrL_R uMK̬sIn{ c-z<`ۦHX:jH|KH/B¤|z;TSUN)U;-X-!\TUbzG O.hw89cĻ+TcaLUч&ZTa<&A#5q:XJM\^+p27?;dt9G7/ҪFW r7/to|JPy!j%CT,>r`T2mvq)u &ZXM҄_/P,arCS4A2b@d:mi39=RdXEn}3bwR ZUe;C̞^cbo 1ǻ;>%O!1)+D2M9QrRAnS.6qjMڝ%\F7Y?Hњڤ"nKSNbٯHri̞j:MCrZ9r͔PupMECxz~!rNjfȡ^њ+%T]XtC.iP?,5,7hʉ7!PM%ޤyݠ"ZSMƍؽZUi"9x,L׵\AM?lz<\nr*uK4mmpvɭޚߢ򢰳+aG\'hU49j\) d> fxz^mk>nqpqrGS1ǫr,^{jDhBT+STǀ1B碢64\'W1 @[Uj`O/}Enj,:ސ x`Zⵠv P1FirURoGRك>[ꭾ3n{[+9mFao&bs:ED H?~|͋J^*3'erẪϴ/EZ d{_~osoR7wTt_sW FjL@;dۃ #yt[ Kwfju^P +^T̍aAMո1x\1iJti NA.ʊ6R-' U /kD&4&$^Yx>eO//36Zku(7%`QO74ZgXN./Q홖鵴>FzAA75aE1\^.)A |RRkRϊ6U 5`A̞4Z.*U C`gawB#ND跿}K*/\NFW;GCzc'5fi%vSly׏ |eS2P2L`TRDRDd\VU0M`7-ۥCUWnq#Xs"2r|KWVRbT@EEu9P+n"sύ_ތ|^Awq2vqg; yA#_rcszY 粄 L GWvC彛;GWvl2<4?Q2mOՈ('IխxQ:SheZRU9ݢ0LW;GɔT(KeVLrZW-RT6oRwnoJxrŅ*pClP`9ubRsT:jŊLXqP,t&[TUݢfGHy+(媦kB}T,~~BMb'M}zӻ>GVxxJUFq;r$*:Q5{½~sRMj(U*rMES.KS8R(_ AU6 @{ev<+xzKlM7GWXA ^^\ P8cHc0!l'iTrq\ K  U3t K yj0,Ndz'~M*M@N% @wr|\P1]ShU-6Q"gAR? 36|XʫH9Kr|e 3 dfzlhNxxDV30TӱWo鶏1\q5*Ac\뱣 TEUՀbY,DnsϮY"9j*[Z(1|HB/x]d5ëEﰑH n\@E&EG q|aBbY(E;T#jY [x [ϼΞߑɤ$<#DjXG<g@xzЂV@x~xV~$@ Y$#!IT$Y6$f_;{?Uw Wn[>W>xь'{kKW_ap~ǃ-ݧZ_{+_],^s^x$%j h*ˋjb>xP4!<3$5uz=!/ nJKfz&-ĭ;7X zA"(;6-t020!)vhiU,Q/|?r P+|P2F跽06xlkօޑf<5.( lݰ@n:C(-oVr` var`EXK F7Z/&6 J:Cd&_!e{-]]u9^\ʭE[ݣ-n7zI&`K|F{)W/5LϤ`deFA?j7(G"r\F{%:j')眲f2TjWx؉z.\ܻ(P2C{ nowU9 vvzg";,59 ]r*#86gVt3 0eYo.g)?l.^Y M9o D8-b;r-- rn]X-m /.ma`i}>ލmoBqiLx @V9=,V0nl!j~yEj: +jbO%Hgu9OϤfXs7/;_2w1?GPӱ18o6G/HlNM{(e5jx #r/X$P8o8A젏jgj!;،6k jHLrh>ުjB0 x#3GOv[D{ʏCh,3"DSj8CHP# gb2e~U -WUyHxzI=B 8 >^B  >^@W|ocp; h-Bn3@O`zϞ#dF7ǣ0"Q4L=Nabizg L'g@Fx#9'GE3VlxJmG1씄n/dʌ"=3KofҟǏֻowx {+;? ^Ϝ/7;3?Rk-H p?V3ϝ.9 `p.:ÈEt gYbVŅM\Q4 TW1(zH-|-yȷIIʧ B4JDMDE֛W ^zFN#Za^~ W~l`Z㑱0=6GJ|r(gx);cN9kw 1pAǒvU%ǯ'SUy~^c XhV!?OSa  QF.eX\Ut?Kc6!^6vίDI |kȼ:L:>_ v]?UuD RRIXlez (g<5(2W#x֫{1ȲLۅL8{~B1i\!лK@h>Z>޵JoUͫn^^o\owdOʍӦ} 3jMlo;|yEj8:.^ؼ}.M*R1r)P=˦0P(Ln ַx7v^X\v/)nu#L/YhCă33QQ)toW^~־ fHE;i)/y}{Ǖp'/IylU G~开ܔ v; +Vz ݍFg 2"Wz oh&A¥)GXIl:0[o>ih?a]S R^dk'~{[7u?l8G^q++2@6]2Y*ə%4;a{o{/ƚn(/PWo)g 书[N@cX 3P`jfpQh*2ݼHRvUsHPErU\5_.^@!Kaxq\ӡ|cXQ ZRyrmK7[(ȝc(UdP5ʲ(򅪹PI\1ƣo&W$c/URRlr[GAP5 O/_q0o[ng` N™[v^btI! huM>s9-}Kmⷙ/ ~ˋQһTϿ]ǟM5֗~>ƟY=7?~|7=E)@P^QP7 Q_ܮ[W_N"O~O ziaMՔz3Q 7 zYXޯ>5+oga幀be&b3 P E*Y7l67|(n-7^Λ.Җ9Q%9 ď>~#@o+|+x7Hu!Ļ>x:p ֑3@F=/4)!:-'nRAt}q bʕZX)x!r$4M0P]9*NDo"y(#c Qn brM۳>/]9^})Z+Jgd< gW\>G Wrl~MR^XnY^^xz7./-o6.03] /w9o zmt ;t IDATERA`Q4>2,MUS U/oNѦ-g,bY.dr m5bz鲬,8o˹Znl/C ^Y+< ^%>XEǏ- )BL"_6w]7Y(1U U)WS ZofM-*'-wa 84;E/P.Erfѥ 墩 (HŁN@j:N@AYBL ~@V\ǓAP5Ɋn ֫/V m>ޒlcċ1Rh̽Gx jPrxBY 2/ UKLȖc"_ a(\2ʅL(婾F_ ^xNkXA|3?/]YeFДr:eFn&Uӻ^\>DGku7D߽,olv7Y,JBSKWx7^'7P2ʋ2o@'bg-thMs[1^T} wQh*Ze7BP;9;%0KL"mX"~Lz!SoVc Zel> ?Kꇼ 2M9^8% U3t2;$Ҥph*Jf1ty[j·~SQ&\J8?~Á^3Tt-\Qb&.f\4J'ˉ*Pp;@N&rLsOT2dA_\`oUKL&hFYV`\IAVy o ^`.-o]aJ uj:{msA~ݠW K/*=: H*uׯwL.S7NmOK9t/yA)WzsKkk o-7:K~oЦ\QU7~i#Xb# vڛ11{[|hQ]JHyE7˕rI lq;%<>%̓݅jYZnw]\w]^ۄ9q[{1@r,q"oS!&풐&S#Z5Kbh]Pԇ)RJ8-lѰȓeZt%Reh/7: ufޙ7(7ljg8AؚEO/n >^ݰ 946z"J_V5{tivCly{A Bm!|kiucEV5‚AmY졋1@x+U 4,WȌډcJBΪjK\ibd[ erU5U^ ^iiݼ gHjNjvcyP8,Njn^zK_+\鍺yގ+.~nngg`%NF/ӹWFD3(7^I)7H'xx\:[>Xz?~<}~g3𵙿#@˒_}Wo|Xz O/oϩ5N*5dDDJP4Ǡ~`Ey/xLcC, ө&ޙmd2ݥ0$gPb9RbY3$ s Жtòr|Tw$c?܅ o1$UQ"Kkq^7X$Iz .NV"UBՌ1|e1;$HOD.z^rg;-ڊd.1Ӛ&y. 74fvm7M"di1fe!S%!@2 Pd vzRAL|N`ۇ۝]mpwycR@uI~:{s댜0HIӞzKm<5)n^F/RvCU({.\߽{ͽ8KBhH5ƌ.#%ӑ?g&ur_7nUiyIK)5mhs\hgrp#6 SLSNYm O/qj׶-Y<ͿrxR"WU*e6YNrqS.K$Ӆbٰژ}YV/3rMϐ״]/h8n&[|ie ib+LJ.ݏ{zWz5֢|fN7gxWzRgae"ӋJSvaE7}mOe\:6}ja\5ll':Z~*}g8M4G$$y]_@ ]xGEs+ eX\۔84;**^efC8X}QދcPkؾ7;/} JXx ċCM f+(L(K1ՌqE+TEP.Uc8}RM+5 V\8@d8EKi O//^exz1WObz#wz$Z={@g.~07gAKs`wm_oR730=H$SS3SS3{i^rZR#Je5C 4_ ^xhq(wcěfrE a$C^Óo?~S݋9qi6/gK 1hAH!LJfuO˭JH:eyucgA/s5E3l'pP3Xב-5K0>rMl?՘HlAi +Nߴ\DlXS M._kzGn{N4mo_୿{aU  '^ݴ]r="e8ȩ2m ٶԛ*޸%-euWWxjV77ᇔ0N18 Hț4 ӻGno'pm'޵CL6Jܟ_L=+ieyi(O6|Lfk;Y\&yR N=lƬr0{%\"~O~r'%E5L۳l_ )[?[-/t7b*_6/mnl'Ve{4,Za̦ 0ölozΖ=a&[Qq72ghSɴ=r@_TTv.3Gĩ*d4rUs=j:5RlbJG_Uz۳K@a{rŬ>8^->T5/]jq0\D^a Oj:^%6Ea@ny g ^, [¸ H )T$t㝈~E҅r f_+U$F$ㅪ9BC34"5EsTkB @+Lg@E%WpZ P8O(wD $,#?kk:-X~ 鮬G77I .Wb]t"jz(W4vȞ17E<0KMprˑ=KER.+Τ2(}>=7> m~܎]r&rjiH$Z !=`l" v6u渧ݺК$NƢDPg&SZFXi&I 좌(`EIR"a⾐h,tXfK܈n:.a՚h'B o{>ޠ9-aQ9zg8ޑSWhRTL`dfto2Nxsx\m2ŭ#O$ y -8 I#~,ˑ 0~0̒`ՁO]a#ra9OZẴD>vwĻۻpݼB^xOo"as_h醓HxD_Ok2GI˨Ϲ49.SmVw{yAij}1x#6dJ)jX7S^Mv{ѕrq ޵2X^$Rl1>_ r"ID5J4E|6t/2vhI K> zhB<)Gsv$Rbw1HM]Jk0{CSQga)ESQw}xYXr VZ]ꍣD~C-ilB$I6eۊ/'~hV3W g:۰O!~u8֢sK r e4M$3EDrHz>@/ޘycn`au0ؿ3uyz}i!7:xpH%bD7! n*-U*2n[링\Bh9x_gQ5C(ũI:-)L{X8zk(Ws{u(>N(ws"g]~I`K= z {+|z/W|5J%lŲx! sXh>a9=l IDAT¡>˿[1.NdF%^!Ϫ8XPYboU^koKxzW{/ =H=Ogs7'F'{D-{:9rL7"lR rHݢ)Do"~Hyoܟ% S#7igiHW W# `Wc>b!ŕHZtV͕vsv=6jnl<zٌMޮ\EaHPwvJzb;CsͰ&w' +xFy?mM9ҵQ`ULJT9?8+ҕΥ IZ.鈧l{.ffTC\ ^7l .>^tF/%W ^ k:5 B te q Ln#Pf2b1D[a¦cLSqU1P34H+͗*F(JURUt/`e/Up;yv&ZB JR/#ޡ\q:[VR5/"J|u19kl0FY :^0h9(IyC~ȣw~9 Kj%O0Yx@.xݰ5+B7omysU>ȗ^$BhZWS D|Aq rU90 K&-}O{?Q45Ktzbw8ST\1eqLY&z>*:H!ӻws"h*ZZFZd@vϯlv{{c^RETfgIu?IUGQ@EG_"gCR>BԱ0Apbֹ~ @8,eEviV[nPo?ѶlJ*jw:xx7.\E9^EehNhwӻ}xfd5o"Yp(]ﭟe ꭷ*߅|M90H{OLQ,fR((*SuvT ogw{FZY`r], ]iNLu0򟅧/2q[.)gW6]\r56Տo{3Z[>g'Sl.cX"%nҔS9m2B<~ߠ<$xe hPȯ^! _ZM-d*Cͷ*/~4Gj#ޒ[\A}mrըyNjn^7l` ^cw(m*0ǫKtP *khG۬p'\b#O=F@,h*5@En3W\(8h db\j Hy+7 ĸ(׀NYP5͢c@1/<ǹ͕j&W;О9{wH3gB}ۙRM;rBny LO'F8rrU"3): R/^b_}$ <0UI;xMIDZJF;^\ī[C"J@/A U3N@xmGf7TMtqO/[}執x TXLP / ]Ҏ2n[|igN*mƏ]0s(]F}5r~ڎ$ׁZxpseBؚ# UsK='͠<јmOxҽt$BUs^OXLP2LGBol۰rM'Ԓ2Uo(V]1VEVAwjJf ^pq xַַg(֠:hʽgE1EfsE"xw -Ynj%v"tF31~>0Y#0AM vTyw bw9KwӋh:"}~1ʦ"Y~I!kM7$ڰR*6.qNfHP.p m(1>9%)}71!Q_SSh/A9߁T9DfD=]R5眠>6N@VP5{^$WA {`p<2]Brl9ytCŰUö:ʇ")!auí@bj8Xx ])Jx gEppY&+( *8^\2P+4@ӋDH\xxq1GKg byw"⍷r, zGN?o!%UH8|™Sx2 gZ P5#ME Sm&; gBNHfrg|Ÿ^ygl!!VZ*w»J=}-[Htǹ:]q;e0 [Wq{=?"TJ.=Ų;\Q[:eo]#C"t}TY}Jg`zmoev{ӧw](A~0lJyl,ٴD.| ';J7Ռn^7v:K;ؿ} dBc=SU!  o H/1<ݲ+xFi)I$p͕9KP2\X^ɼ")gXGiCx"24rfX'9j c~"O{uxm}} o]D\ӈ^pFaejq{{+ٟ_#3M7쾻MUGobXS@/;`C/;Rw{T}_^yf3X W^zL۫{ziRE75슳4>IwW70=S=?2"2e,Ot&jln-&$TF̰xH9۞n^xTԞwCOBwc酈hKGj+=V]ڟc}=XgLܒJSx%b_{%Eh|v~]杣)W*˦h9 -$!dEdtKo\}ϟ/Z Oo3/<΂hn Ѳ>ް= /6{n@ՌMg\)-)mmtɶ}*"D ^dֹ% FP/q_T0Whg^b3n{;hf0qH5Y2cpȖ\. rX,TJTsoځ_ozf{u숼 HZBӰ-ǏBA4iÞ!M3RNiēfYTK"JjM7L\!Hm uĢ@A#Oa"5=@/8ph$q/8zgQpb}Bތ- 81aE븙 B׫#Qx nMZӲ"SLT1LJ,˴'FZjTI HfXbMZ(8ͽk[+P50ӥm.V{{{za&%@!@C \ƱCu)}7V>|Uur%1&wK@˦S:3|tTff$/_ zz4@Y {p龸{a7ؿtt{;GW.5^P4SLrn]'X^2êZ͓3T2)M9n~ 6*er5im9X9[}S/4> ƽCDsteycgycgi:/m="oƇE)6 1_,[/G|MrY)[6a>BIŒ>TT0"mr|aEYY$1,=f}b̧ncy`wymsms7BcVwcY]\]m/Exq*â; 0S)1DS!4H9R&Rh.kn-Wh_4^dr0Zam)W,WlTiaϩtVc\o 6Y5]&y3ډr V{E/\@i7NPMpx>-?k:z(V j4iR0TS؝N[&L;f!^ Mh glbc+,ΗdJFM3VUU YݕP2dܬh** In$+NjqXF/TѸ,?#jjXFg%ḭ o0COt因w4_xID1˗NjH 2'Za!jĮj2C(|oRA^xd|/︧EU3L5mWU͊@ $UMUr 3eæ)9oUh,+-9eTr0MZ܌e&Y:YVEލݹnĻ:X:Do*[  mlͯlitL/7(rf0FdZB;1_3eYN9<ȅh,b?1mO7mȼr#r-A^6W/XM7ݰL y]mlAռ>ރKw/0ݻptc U4mϰqb\rjM O9-rA>|q4\w|. KM$Y"kC= ˲v=W)Åw yǻCV3L@ {ڟi-b4ոbVnOW OrV+]}9_hRPɐnfY ˪OP>0Lg$rh"7T587w7"wc5L\.+]s ^|M9RW!©<dB{)WjMtbre0FO9~F2XSjgIхekIRLU[we@yzB1ܰj⵼![ĺjċ^pEfy-㥱w*FL/.Xɀr*x1^}#I?r8ޘXQ*U PeF&ur]f@[(x y\\k@ U3xr x| P+8L8Az͏>_?=ۤac?'|S>?xy- O~_5%y'~ǎf˻\Ϳ>C/~}o{5_m|+qwg>f:䓟d'?K>4o[z'>{N:w}gqL"VIH"+/^^Q!as;Q3_2)K1W o"MF_1Mf2:{zSO9wr~擭w45 !!!tO/bQ6Byꖇ+D1W>_ ^_2 +˫{zp/#eәl*Ma3qPJݬe"zpQv#L녪fKr6WLKT))G91G TiZe<7{)dS4V5 Ix]'3R.I!os% ;,5:KP8ț3p F^VX:Z ׾࠘'˗$'BbBNgkIY|"J7uzz*GHF$x#now/T@/oo]kqO8uT0:Aݪ;N4ߢ)Pq؀U3 DBZ-/)4^1\QTA96=`h$Ig?7tjg IDATeez[oEZ8y0PwʟK_<M_Oқcz~?Z{_A_I4x{|c}=7k?m^K??wt_OxI7/wp|;){_n(4,1Bጴ*й"9x`H7 фpIIxO6U&3Yt%mgo|t/MeSvj:K'~M;0=jgP& T"9j,E j8塩[Tnجic6K5YXO;.IUͬFM5 ۰ǥ ʁ)iniֱ|): Jҵ"OYşq'%f aSZ*/o ]q⃠NEk=PޱZiZ32\4HUs.'S~5iS*'9wws0p4-olla`K}ݕ݂9rKP@-i.#xʙa0f7La|)Icfs'| R4~˱ykvYX,YXYKll;ߙ_^gzO1IJ՜ƗHәjXaYp9irU5{YS[{rs(*EB!"^6: wEnsȹ@Hf5Fh»[ [L/͚ /ig(E@H+txDze?KX<~\ H|fհW+5-T)Ҹ,û)ǠpX6n\LAY*x U3 @B<{>޳q3ƷvfZ4spm,> {]!u+yuۉ+19w>wcLƒ?Ov2/ȇ?هC _XI]_w>s??zOwx_u^l4Y5!;7.DSLE`Hq;#%t U@3Q;4iˤM>R5f3=氩h:Y͈Be<0g)_Jfy;yn;ϝ;M95}b|nwu~wkjqw:75975#wQoܹĹMMɡI55}ǹg)u+ۧ<<ǟIxD"&}f xTOKhK&#ESIJ1k,%d1N2hb)+D֠j}0Ɉf$F6#P.8[\}VY G12uM<!#TtYu{zM\A69q8x!)6I#"_a|pXe歐XWB4Uͭ5D7/BVsKkbsvyk{?^YtO pR(]^!qHڏ6u՚4Dƽ`, 0*B2PBY^z;^>Eg}j^\ۂzmpu8{g?,W)mbVvpY*Y(9~upwoDOoO: sPO- VImس@N%_wdߨTMgw 9.DUw}n^ [xz墏wni@rJo{vqY6FqK"I$%/N-Str-N3&n=.^Q]ֈ@+@^UæK,T^H rExla>yz-@77n^r8^Ű p Et%/_Lv ǚ Ҁ cI q 4mY5l.Q4XQF}EVU O/TE&R"U3񖫪P5 Oe`/<%Zd."(>L@+,- g116}e%<3sKדmҟ v+'- T3# Ͼ'Y<1}CSz|]ϾG~yc}kup:bxz禦fי@8^xRvT $+pT:"HgR.=HJIy8)<p@c31AYR?@P;UpQ/!2O,>1x/UDiШ c'\U1E8'JUfg\öhIVO䯩&|,@%YYE.N#=3 7q- 7I:VujZTJU%=w4mC4#"8":D(-*}?9T*IWtyoIsnx`Vs $6}ѯfq'/jόd0GIrѣSi_*ײR6Dg:%P{$3aē Zy :jFSx U+|sիTYt}傴'(.xKf02M='D}aU^? Թ 8uXF[ǞzW7[» :]67gpO 9 #"$;#*uԮ#;Axs[v',BvFnb^ N8,ލǒĮ:edċn^Bb{Eo^񛊀x~ˈ㠧Ӌ),dfU38^F#>^{ P.dόxYв™xB ${ Л}Ob}o{X2>ŧ~#''ȿolv{|mt_+$o@O~s|w>_h àqp,__O;zz_+>'wy^O\BC|X97pt}Un~ڍqNޏ%Q"N&g8 p{"YBT8G p@0qN P4KFb0㥦p4OEb)aMdBP+tX$?Gq@<f#^ a?D^T8b;0iY w1?`;]*+1M IJ'#` @Y8#|"uYKd&ċ>:F0wyD>uH x\ _fW5pOEccX%<VxE jk+E⑈- ܿQ)8g4 AA*PDՎ)D4H؞TUH!Qʪԇ Բ }%[Jgaym'秢[u`Mgb2liւ+>}\SȍjLP PR4^q$޹vCYFc!#g%tvE@)'BH$& B}y`z\< WA{.@XKpJ|/^/U[f c Z#F@gt"Y_leEdJ)pfrʌ~9=@bF3]LdˈKE3oVRq|Kݼ_rsŕ~ Uz! *ZV2B# VE eOo4+ ZGn^P>'3Q{Ͻ1{Ͻy]V5ssU@ }l~_7Y}'\~Wg0 +\~?K?+*?U=8;l_x#Yg8?o ̟/1? ;\O?O?~-ך!@uFA/oJYt"dP ޫS!jF0Rep>^TD0eD CXp)x"CB\A(m@(OC1R2#u``NsH<c1/a X]穬M@xsЂB7k)lu3D:Geqf$d<*m.B>T8E@Ƕ [P $Wv8JgV;CjgqQy1EW^ cY`X.Jec6W@mX3,7C@yY[;:GoTryŴYG$UW.>WeL8ɱ wvqsgW[b:ݼl׫/|#L/'O+ "Diltgp(F9AaatͰu~,rg{\|2"˛]Xv:[8^p<^ [MJ!Io>g} Y;$s10t=1>'R0R.~pi Bx"sq1zq(oBVҺGDMW:Hἱۥȫgɯc> It:6v>K1"WŶpRCOvO8sNZGYZv nwsj_Å.ڞztݞӋ GR^;XF SKBTlLbͰHC> # <߼JP¢-,(:gU:X\[h5zfu~l.vr7bk}9ԮΒob>$b=JbmR#J%HiS@:+6HsdP|9| BxJcMEJX ^Ϊ͖ $MEh'228Y5Hi]>G/|¹8O Q7,fۺU ɜe 6|/|{syΠ NTV_YOe eB@AᜑdI@~` G.d7W2| m 3\fP)HOxq(w[s=^o=rzz?{`&wg (}/;?_?z}3jmZW_4 U/??C 1-wwüv/~C}߻wOg\ǠwyAF(WK˞M{%H^F^R/OGjXTY#@^zz1{zTSaxz bRnEc)NBn3xʰxً4y 3o(8jf3/9H OA ! *݀~HWd B@F=#)fYNjD`* aY5+-s2Tb'3y gxzQ $̈6{zU^QkR.\%y1V GL^eT͕RAB}~xz#.E,r&F'ZSc5nd,zA^7XI~Z⠔rA]+I ^Sxzz7k:ډ7~U h3'hK^jq QL+n Hp='0"4H&R[(tȈ* |FLoGKM9 "^咀y.8ޝ0nf}Nc4ru'Dx˜@&aVg^K0> KMwy uX Rc~o7 x#TƢ|AJW㱀@b*c)q0x֑(Br|Ӣ;o4=ǧ/<bySd六bSY /)L66xq+.% "(luS.Jg4]uPAxJVE $DS.++F6'r-Jjol76{;"no$^Tb`p$ |;Js40f tV)Wyes9%h, n)ՠͽvsW[h5FY$VEB|9R,v,Ja˖h'&-鐫*ߣP4S.K4P4Sd\Y5Xyj_-V5C7m!)L(Le %& W@oc 3T-U@]BrnV1L Bfj\T̈BNjg:tȚI(WmV2 2{zLpXuca1xBF?BTA6xQtob B3hR5g @@A!-O/Tx *x9Nb{z}B+ODi*1J{"_B#oL7>W&g3Ȫot0[Og#*=|_3-!zyŸѡ/~'^X'y;O?/{9#y_Ksuؙ^qU﮸_Sn3w8j*Uǹ͂ㅏ1WHbU3B )x# 'p=W*W*`,2YkŔ3bs3x?}oxF+1 am^SN%Dc Yщϫ lM‘\l#7/)JgisJ̐pĹ%\D лMzc{y}{OȾ\ gݝKyzIWL:Rrbzq" | שiˆE*T:" ~QoN(uLg`x+]==fOocXj:K2Nj[Jglu_$Z0ٮ{($vBQ)1^Ȏli#qG]'| L(*\E3i8媶S?x"O zzrc/-P1h[ ^UXKP>}.XQrWt~[NyB^^ AՌ(C᜗uT{S<@@X gArn3P1v@ŠpOކCBbL: 3MEw1~/dJ}_W^[ w_/zo]o𵲴0\^~Oas~R͵߽L/K~/?R?4N`w|˟yp0;_]Z?psA4ʠg{0ml}o~~i{7}v{}um_yxz-N^cߔ@WKK!>^p6 ċb@^#BiM8D>^ R;<%,E̕^<(n*8x*Kfp0X;pP5ts3`)~Voi. :{Qh{\,cO-I\e/8* WY j8bT3]"Z#Rmw>?I-w|/=kH_[^|}KoTHS> rw&E/ҧNy-/׍SS3!3Y.12B7/H`\?JRH]3AC+‘[E:!xzƇ"_QUFq|A LDY2rLhZn:KLe"1*$̠ `L(BvD|_l[j2Ѥ!P+Y^m>x[jȷޘ]\j+rU~w'^xs-j$}!ul4똖kci7h,+ɦU֎ I3_U*IR{G~׫.ZV+QƩzH\Dlsvwډ6A͈up)Oo4 xGJ!$8TlDɚn⺉)g˪Dʧ"wL2nَGJ")ܙPD!rCΧ}4REw3*$WwڃmFh9޽e{zEdWg#OO~\Ѿ)kaIYv#2 qJPۊ)Ma ĔH`lE."֎p4Prkj)_UMRQeO/|͵ML6$WǸs ){7:[s˝}$ccyAOo,r{GOO>>KJeJ4/,!.)g)Gx|Rfضɪ EpKW^_H~.wj=+Vlox8rc(wvqX$Z6׬5/ʑo{}7˥r-2KeZ ^deYU3tl0E7*S%b%?%Q>d(m))ߋ)'U4AO/^u4g5c r4Ft"|/H4y\tB1ډB B˚{+E`Z*ЍXp͞cfIB̥v6G1>rX_+;Ê1 1xJExO@d^h_?Xu VfKh\T#}ZXћ cEE32<>aj%2Ef8_1!x5oJ1,.:rdTfQ+% [T::On O/#^xBՌq}~yiXhyR:N<>`: u+_ǩX|Acd 2TFViYWi! :tN\71FQOX^ ޅ>q{z$4 2_1j`r\ZΓ2$R[M(+z&'2х,Ԕ#52d2EurDӔ1MRO @o ?EC+6n^{}b}m/\3@nOOVN9D*qU3mb 9IM. r!5GF"J O9).V~!/w-iEY0MMY޼wDHFNహIJ].Io'Ҷ]*kV^SuA;fT,*>k2*\]N\Ca+JyU7.ʪ._NxŴ[~1ϛ HP3ұyyT! d cԶ-tr}~yy{)[5TyzSn}Dә?t+'hs) <"1LH4Hf29I .b!x+!+B"?I~qۘ;BLnm*VL̞^{E"*jh*r*+29^W˺ wfVtR);"u5 / V5-TͪံؑD5P88ID9|PwVPeY3Xp,(X٠c!BՌ({a-^{S(:tr3Y 5-@ y?3G#qw+WCJ=Vr_xdF0f{kŒ"h9KNSK :b!,\f͋C8lXbxEy/jP W0Bft1IgU3x]Nyx)\IxWH,n^蔰&,UxEA6_g~a)HHaJ2bkBQYRuA*=Gd5tP8H礂nچҰeEL&T%b$dr9I˚BI$/(z*CQXc7ȑ\WX}y+DW#gErռZgĻZgwi3r)Oo=R1_3L6<KJ>,5YI2b$HD*+F+!ta:1MQnvm>CNA1Ww;`ċvwgn E5n:K^)5^u1嵗@5l<^f#T#I O!tW2)!עrt\Ad=~KQ \~gk|ۦ)ga+^{4xau>|`6vnnyppk:TeQ"ͽss-Ҕy>rإJerjZ 5Ec rT2ys5knfrm1?ڳ ݴ;?'OwA~ yNRsxgnrl݀w3\ku{Qܻ[ZxH/V}LG7?#tphUw-b t#frQD2$I\%˙N^R,;xG~P SkG_T^gKe;*ϼt zUXhAɼZ/}lk(z3t:z/8}kᥥrvJDu2*f:HTS.FS.\!_rDL#ayp}/)Wds¯6:TV18^KՆnxPR@kI,t]S3a"9Jb]|/6x;[H"Oř^Cӂ쭕)gI5Hgr2$s:% % &}GXBNL9XJ8J* _p#>^l"]_DՏ+7HQR] CO$JC'xӊ*Om)' MKr9m=yZ8SD-<`O/zw{.HZmx}skeқgBD2c9%jS׭葕O:h,yL9\7l]VQJS2jXsrpVϧT;Ŋ>G`i1XX^c;\m,n}~w^ի3mCP27Jj)R aP$I.j6h'J7O]!/W8HcU@c>^BCo$7P,m6E -ލ/Ɂx "\xxqĽ1W~EA^ * IuA9d&2`U3xD FmRIѴ=_Sӈ>^|//x](hSVﺥ*?YxUW8⅌R^1Kh"lv ɖw!Uۇ:D^Q&=O,g$JVg®vj~ ?x<4].r㺦s\8E1ĹeU367ui{}y} UHܻ18om;čhww.e<[$PT#:?䔣㤘r|.Gy"MKLpZ wlCH<ez9*s9 ]{A<.vzu*xɶG橐p ŦݼIBe!A@/b/A O/^t 󱧗  gVYC̬je 4xq8^\V }\{uAޑ=J\:,u͈v_62;V`3>eU|k$8^/{66xC^K/J.]3tX";Ŧ$^7*z7wJfAɼ}+}owustbzOO9x&|4ASN EiL 3a%`aU`Ńc!jMiMۇvrkAsms߽U3E\wqy3c~4(ϜrG5OB:Kgp&XFt>i!2'R"SK ͠pFBAk?fЂ^{/#obuO n4F =r7A55mpL?9p\͎{5+u.+/`2Tm3"2_cHrQk3/'W)eؔRR#^Eу۶K@P5uE>)L+ pؔ%i<0 `KKRݼ#OjEsX8}Ғ@l*e#^b BS9/v&!a.KzEds<5FA/r>3sw/{ʾM;\X7n~ݽ\ZL+G}bL "A'1}^V8OSgh'b}2EUP @ f$Wy~% 0f<0u\[t88{Fqߛ+@==7/Pb5C#9FA3H`Fxq;|GXs/2QryE3/U4 V=ME-#?LuUR5;w㟼XB%HTU3'˧GI\>xj: >rc픈0Hxvq>^6?\R^YB؜Ihf<|AI ]ԉ N>3C|}wK $ /![|l <L@kM$3h:`RչyhV4Wܟx&N/].Nn^:`swR^&̹h Q8_ \:?DEM;dq␈Daq˱Y0ق=s1B O/Bլ%\6 aY1l-ݐBWT"b*~9\.:@+|P%4"|Xe=+@(B%+u-_CASQ*[f%5!ߚ!/|顩(hAl\X+H$=b0@ϹFxa=nw[^ݴ[vsW{J}K/2˞^X)Ϙ/H3|LJ%D3"!kzw99靉Nx/Ʊ$UXqA.pv8>ňo@${2󽜋+T8MK.LY3 *4BlD}C sFR`Vt xQkDk^Tn,./ѓ%*՗XKZA'(3EU+k?k~gLe'@g2v󭍕Π{9@yj5ry}&iM E2<^z7vR&%b U7ReNkmVuZ*W5:"{}z]9"knܿM]}ӳGc ͠ ʭɯ~ ZqoN*vkW~CU^ +d5/{-46ݻ x7nK-Yw~ \n䗭;:Yu)*^f4P./RQg=B= c,*TÁ , 1P.|`un5Njb!I=v\%k@?hj (7Q;-@ŹҪ,+$L`kB\Œ_L)A xzwO\)UEO/{z\|/<3{j;@ VH%B8 E@Xr\&bOjgNMPB0j^t~fel΅" [V=0xȡN`qTBV8 ! ޴T6Tжx $LHbO/D>k3v[f*xlEMW a'H0@#bP8טٹŋ.x$beGL45/>X~wm SLXD}s_"jay /MB lEK+g&o>I[[)?G'^xg[^#Iۏ[/'^OVjJ\$e,} +ݵ8jo|/@pi G<^MM9u~Uj̘m<2bɫ>rSe&_𔂇1BZMEk=2ǻ{Ap5=O<'("oxS5߱gY3hozr'r7R 腧PCɌ" vwǏL۹qST# eo/zqJoDʂow}r,W|o1L~= c*k=kcUZγ++TƶK׌w<>:b@V3BMuUņ`=κړ/Y6Ȟ^XKrc")$WAl*P5#q bn8ވ-ˬ%6ü.6Au,gzӫ[jLf X칟^i`?bI8^Y5a `<6{([rݐX\!u 3D G Q𽾧WDb݈B]ezO{zTect}W=p]9\ċw vU*|YB5{i߷X ^{r2Dq+~/PѨzo7\*gaOou9\Ei0 m~曫Fuy@o,4l{_7:O StkB/UTcoyfg^\KggLoo;Hwహ ċ!o ڍwe{n"J ɓgj#S¥]m*RgG^9:ǻ}ǻ\ۻPo~o3y?C~\U}-UX.a|˧ͷ7R6 xϽ] Nw}s<ލ.&ލ^힔lh.y^/?V*4Ϣ.czKJgD˂E)E7/,~[f{΁d*kۥ}\u޷gJBVU8^QY1|xurY0^}Ϋ[[i, -sv_.jl9 W5].΄xjf r=fݲ]( z.+DfTEw/#% 369>^h{x"OZnUx>ߛEDRYJbY͌rNjD+h:p$%ʀ> ST bHqV%O7tYH"Ϲ>Z+Ͻu ܽ\a7/֢Fܫ@>aX^Ä]AO/4\\o|3-N&|pD dUT{.ǒ-'39B%7#D:%ㅚژu\>,V8g$Ƃ[ƽ8?-PWḦd3ټ# IDATy U3 (_H>^B EئEL !d*(UӅ LaNDh/)oGKi^v&5bNڏ[w T9a\\|̞ޠf仠wvq(/Ϻ\EսJ}z.֬,Fv\9|OW7 rכ8]Z=[{jnolv56YDjsmcpQ3! tӹ_w/Ke&옄BQE#yG|!&wA/yzx?v >:d5cݦͽ7_pHl^&ş2#Kjޜ[d4YꬼG~hoyySEzP/~݂^$~aWest}\@ ^T=aLywLL&+_SwJݰ ϡH^?~֝բWɟ}V751n,݅G¾1nV:wm oG )o,#\v}{~S}K՛؟LLz ʍyJ焊u5nPMW5u4iVjg\nnΦ[;4*V1jvȚ喪™/~:'@MO/ցr@藓`g.#^mX:j&TFP!)YI)Y霜LKBݦ*Q XZY`d8s&[p8G8`~ݯWxWQ}3eNM*^x&DŧyF "QDAsˈ7$Z=Gch|H%H{ZA (Wch'EV3_cIs:bP&; iK1G Y EA- ag1ɡE7/BHJ:'cM'V9xEC0 $+MSEmHblP]X\g*( шKɏXjfiYs3(inAVe~UWHe]BTCrUJ6DZX,7זWRu)DD-XP $Wav@a E3h|:saX&NXG i{xOt٨Y~F:[FBjE$xb#yzGbd&_4ÖJ*;^ Dʫn)D23Xx֪AՖs&ll^m=͑waymiO/wN<5M6FT5#+Xr2;C*U4C Y3Yʢs;m" ^}Hl@ ~3nonf7SDd*5MBC/|%N;DSuK X@ #/#xQ<{ux}D|֝bEcT.WP5S_!KeC,_LNYj=gStͽcC6cW5 H;wL,NtlA1D왈/]<*.]N*SXgwW5vpFZ<_p[j;w„r閬Y) ;ꖢӔKer(>+W߄P2:COxxV$WYEx !P2c9jvGT͐@YþY59ҲnLۃ@P8א9^,sS^Ǟ;A zblcTаqMX 1\!WPx"k( IP5 rberNFhCJ?)V8+ Q XT=+WwߕB xyzݯW8 ̄"_;=y@h*˜Jx OСQn3xb5%p$!3 (UFlo4 j;Ž}xF_p^V U3k!i{"BE[Aճy_ 4 r0A,kESZՀ)\u~A#[0^{PU$ l!%3bG؁&xkbzyt$ Gb_̄w=w [_S ¹0^l# (wMw3 E‘(-.GX<$fe.bTEAY-{!]z%i((H4~)&L$վcy3xj^m ND]hcp?8u<[P _EmVrS.;mME-~)Vt,- AVД#}/ ķД =|VCyU>^kR{ ;\m[wm6FG(NL9B+Fh[$W!j+uM ;h*l>B7/x`;~|w˰Mnx ZAuDXi@Y@ˈB6XX0ʅ ؅?.+xR5UtD)x9 M2@2/%uR3 ݡ UF~ݽwWSϿ ]_ WgT454FuYAǠjP8&H J)YX2Db@dT͢~]SAɌi/(08^; 2!{Ioɞ^GVgx[|\kKt}Wot1퍖G]A;>dy(z8O7&o~}f@h1b[*lb{e~xogw-gw)J0o~Ѧ?^n og0F ^Vo;^g0mo=^{Lby3¨L 򪝈<^(ã ^HbċGiWd@kIɬŌ21/(elX%msy+ dpEV5c!gdb+^tr߬GBYͪfxXsZ4AYUGJ:BU'+TDc&Us*>p,/Ӱ\tBIY, tppxqQ.Xd |AO/ۯs˝\/wkޝ+ps^z޻܍x^m‡/Z8C%o0b_zT# cF c&/albfPV4"J gxqR3NjT+F? :7Jf.d:$Z 3'$9Uq™Z Ni )˪f¬pF; Iк=bRrzaEdU*V]Hx H@=x=${oyCb^Iqn30|l™zzBĔ?*" ˼n0bJ~@8R5 t=}v%@D8e3[Փr@iYfxݞ^$%}Ȑ4Vtu~{݇^TYa U3$bͯk z_Xilnw>'Wmwǻ0mvYjtvzz__z_ ?NH[`v8ׅԁEOozz__hOd5CLݼt )VaĻwphq; W@Q?;et HB Xo.-) z+Usw8nv)jw\jcf}-ͽ˛rO]lnC\(U;y-V^McD1kVi~)w7Yzxd!4JX)zJNJy[ vՆ ^0&īHdě˛ؔqlsI*k51fB4I:wO$_X Hfm Iv :wN ^@ gF_y֝;+ps.;^V2q\bň>^ rRx~d̀rWf ^lۉ[(}{z̞^(a&RWHSNjPe-ty?w:TxF_n/<9"Sh!~',3ߋsO+2UEBk= !Bl `St#riW!“r)yLu^>ㅥTj V6[M*hΗ3粏wgw >^X+қ3ﲼyy`w?^NC_t.;<^xzܟ`K^  Ow7?yO[?,o-Dᬱc[ov0F yNj/9[.כ`eq+xz/LB,3E#ģ<uX]/Q7 }ׅ)iHrCr{ϝ YԺ968?B&4tġ{ t\:{FeO/xpc2/Bx}pX c}c Bxu9>ޯso|ݵwssnKR0~7oqx 9ǻA-@P5;c:>^_#8`$= /  CQ֭jH< EX E@c @bp7$*#ܡ+8S!DYS5a1|V8JX*9xOwO)z<%#~]d»ˆ0e9);V5C/ MJt2PQziU~(YfLct2l!eW`:g ޜf T(ײyyz_aBywǥ]B;]kխ`;DQ7jh2]ʢ7_h*/O&{voBTԏ:=hv!Ѳfo{?:p/R?fxQ{||OEONqʞ)|wۉH,8_1RLx gI>h;Z;Zw"ŪXilw+I4,^w`z;]Pr\o"~ٮnrSQR+iW7B¦KvuaTFTͨZJU]Vr!NsZ#͂ /瘤e+Oq^(QXֈ\*ܠ4Ϊ!;f4! is,XOapcε`P9=X y۷ /^Hz|+ps^_#1U ~s>^(Hx9ʭp# g!?+`W91p@酪>^W7o [hxUxRhq#^Xx W0=p&9B("9Y$YLiQ8K3 rf;>Tn/\! R ÊW<-Ō1\,΁M7`~YՌ",W+eh[shְX/>)9^2VhTyO/p@!+#$Ɯ\g" u+T͎w+fp" ξ@ \^lex} //{zENU@^_1_ ^P8X! 6]'*Ü}ƌr'wn^qɞWU4fU_Q2uxw1ha?dDC(|IOerH 7-+7eUT+ Q|srxqGyP+!U 찢#3$"S6P5;3q2`>y7PK_=V)y[4;=(NjF"酏w3Ng{|0;Y$# IDATU۽ `v;fG7c)L1rGK\Sl˓G#y0;/O>OPr*6pFLjL`&Y$9ׄ8HLJEΝb*"K=5d/@0|ssnUW D;^x}~=@ \!TvB 3b- ^z}"NĞ^xrS:Wxz1Mc} /#:ME֢XR™n@+7K0^*%%:.ad X"\_xAҪ? s@0rݚ%)Ҳ 3{zaap@*{\l E B1W#$_OrBǚUB6 VdTjKd5vzFT߆ |vg@jݼ5jJ/9;Yyzۃ9d['WAh ݩb5ZRьJShO//Ԁl\'^?}O# G/7;)VW#8y0;ޛ|dmkbZpBrZ@ˆ3F\߱JIS@HBS\X8dM-D鲪Qj `IxI7NO(q(*TLE@0r;95)… 7bI119X/ux/#ޤs_Ac]EUiWe^^,êNrh^s^htϧX]?D@n; En~n D޻snlxޝ O}P8BU 0T@<:q c' /W͔&B"jc d_"fq( U32#[xzcI /-ӸnρEl=-Ǜ$Y P8# 9 `X21|TM2'WΎ)boZ\ĭcê^ȪAn3`mZV XѡpoU(RuNrUNÆ(xS\RXpƞ(4Eb^yӲ6浫8* Ud e>/O/m0JgUMQu) SARSQ%rYo7}\d5Vkszp8UU3&|ۼt Nj7#%xo3O9z4BS.%ɁPvC;cqvRp~ԟ@ܟP8S@Ҕ3YU1۹hxxzgcwӮ͋)Rg0OJ)UT-G@0s{zVFs|2hp6Zݟlnw+4@rVUFFV81t@U}]*շR˵]kj Ǧ]QiWxs)aE.7Y*2_FFLIKԠp .>0`fy w ^,Ejsٙfp֬Ǜɝ+dNjM*Me(젶XYV5*d:+(c&X 0"XL" RC3NJ>^EeP5Αh \^׿E؆^c7V"eȞ0x]qڗL"L{ .0*xN}_1$ C1\hV8stx}kØQ1~] ^p|+D44À1AK 8^ה<]r`gwD@k.DG8b$Hyxz[8"Q7'C񚪙+KEruss7t&gvKUXVz(Vq/@/hۭn+m8 T ;ϩ7'iR`WLNJ8;/Bռ;Mv'(ٝ^^ Y=rrNՂGx׍={'Ӄ񃽓S5$W fgPtx`vHqMs dBo_ R^7bXnImN9}3}QGp㳃a{0'/ FjZw|mAvVfw|h*"+ocWjz z}DRtKLjjz!Hy)7¹7m\)#Si*zYF 3z0Fn{zp̼i# vFٖV]nR=!XBxq`oA#%`hNjxN2 v*j.?1y;ka(xa%繡)CՌ@,/lXC\ sIz=SihPYẍ7K J8K^Pn/s8U3}#ħSZ7?7W t:Z#O}%6< K^e2/Y^x2NjPT± !|bDJ͂"<>`U cNh w~ F2ZS8:ˈ9^(Ϋ[8`//ܒU3w&)GAVqIFs&-ɴ]1{z)0H>xFEZh[Ÿǝg@x>]hP1R/pmxYK GbX @5lAs)I6R/FT)yd-wˈ^6t~[y,_P{Z\5,U;pxyڟpdD񊦢$t&gJb=Xʲ$"vxqtPx{'18^ѽ{'vtznJ[/_ӊn` q~Et >՟(w<|қ3Y,;seMΉ۸qOo|@v͝^u |.Lo83bl:~1X"܋7SgW Z$yhY%nFWuA^bU3G^.U1 \^3ɵ$ЯF" X $Ria3G >׸s8@cɌj#*&%^3pk|/ u $2>LևP2E&"Guǀx7v;P.28,q 5Ă$j9 ME"ќ$gUN@ 8[YɃdEVnO/J(Y޴$cM,1P)8\.a.aMxzYՌ|fxz{;ފ%RgA@0`(1!p/+3u=W2 Us0g^cD1Xfb; am`U3x]Y[TcO/ F)p@f"!|/6 J^ ,9PӋ1(HɖۉfbWc [49… 4dhQ :xE~#u6^d5CDpv l"U={ח7{h,L9CH/9?#,c}Z}hZ<+J4F[jF</KY/|y)Vu;p~ 6^{1E7Oo0IgASN#ѣ /Varu4?MYBڅ)#R:-&v1Qj_:|=dDxe;aNx.˓zXlvrMΪ O9%oWOz#pu1[zFsR57;|omOZk7CьutKܡ4YP?r$t@v8CbLC'K+O]%.WX U.շʒK+1x&a\("*UEV5M*UΛ6@5{ӑLix"*8^d5j MtNjrUd8efݐL5U ) i[*FOVa/qcw|B U373m4P5:ǻ9XpJLtgwoFZ8^ [1 z'%n7W Lz76_{ϳqxhf-|@X0%]! }pf3:{A(]pz}w7K| gN=0vB#^l-BGHg`x#Ӫpn"zρx@TƹYK ^eRʽ<ȊsH(^M*+!LE7/P1yPB)a*!s6cSXQ(Jg:vD.Ӕb ETLvBn; cfJy\ݥwe½]xzwCR2ך;zaj[Tx}ʲEZ xiԍҲY(u+j9E5 %׭hWK+oo|jFot<-ۃ9QYu]١L/Ӳ4^" 3 IDڮTZjOLB6t.g8/{ zS`Wʲ^{ j8?Z-;gwGg +u^?Ec ,Jl׋[R0x^J٬WtZp3?KmS2NE+ IDATvƘ:D4vfrNSx^yMeӻbMu )g5_hA/~*eU?)Vn5>twnۿ$/Ow==X&$/.`yH$JЌB^3t EU(V*'wجmXNDxyus<v-hD5@HnQs +Z+BX95^)Z4rR{}\~}W_@S!J+ir7>^H#{J+Y\*QlZGҗ*T@0I rf$yE^<a{}$&?rb|?K2ޛC/(^?-[+wvS1_f|ܯ=T v5ŗ@S.*yq oYTk+ފ]FVoTlz#6;5};Y٩4WNL/㉔aڧOr72M9!6xBPJUf$Pd\|7ޜf5TU""kUkD(Fi~A 4?IEEhBE-e՝\%4BE.{zfU#)0zfU( Q"7 |x NZj\,)dO rW@ MEx9 x̠3O^V23ʅo|]n*bﻷnu w?+'?oWݟ 'Ļpf |/Tx} g?uB;+Fr9y*2F"DLtَRP8븻yr1θqn3@ۆcDcs(GOR3ě8*\MF >^B"IxqG"] ī6?{pr@+[ tz\Nj gUznҨ7;P5խfTXgNg.`읶s[ν!]@0 {?pЮ6<~ZLT/^v#FB쫧F"]ړyedOûO>ҪSFVszwaoͮӛHM4'CmdktrH4A֘9^$3>N.OgmOVY͌x{HV6;& #^տԿ ߗa5wY$6H<E/e, 6bϕĻq;o/^xӻ#o{w;Z4f3B\mY?7;f;!^KMri.עK\( G>ՔK҆i+wC_,U)*蕦@oms'oڥ:B]\7&zYㅁvRB]kFA Jf(o@W9vy!6ֱ0M7KPzh$ ̙רE"h0RqZ'ʇxE/qu,U9 y&c e2DW(6ݘX1NjW2iYIV֊.Ścg$ c'ež^u/ڠ7#}TV5:W{^xk9xs x}O~;ZMT~WO>7ME>+=Nn Ѳ{b롨U?\8d<)"U@@1V58bm(HVp$N" .N"tIJP2E3D)]pP2UͱELEIV5Th flAՌF8'2\e΄~srdOe.T*@]FSSv"3^R,?T~h} dߔ>]NLlP+~h},ؕˠ,onnCɌiU O/vB/)lbfUz=ǒ~99QЬex !$#%HH)Ҩd!38^Z:֙^skM λ50ϞF|x…LJ(}z-vلٟb'<{$y|roYAƆ;(^ˈhf'Ļ @jg!B o0a FӞ7 %32|0l8ǿB ˈ}XUEN%!JRō^ [+%3MH2_Ԕ`#,3l 9^hcܭE~UOUr nYe T=bTc^愔&+T7 RՌv"ln*ƒ 1^MToNCz>^F؄vBK{5>*51 r2YMݮ,>'ӒMgr?R:2xBEAT[[g5Y}5}vV2NK;Wm/ofk'W4`Q(DXh,d)KKIr<c>(}s+Z>Qx {z ^YxNGcV5G%naz> Z4Eue.Ԁ;BO$˘Le"ep,4-T ~F-Ҩ[]j^dЋ|/wV㽓Є{ʈL4Ӌ5~kh5 m=p,J]4$)OJC/jbb%_[+jMzJp1Bu p}lpt5^/H ve`~.rhkESĔ9TZJĔґX [ iog|ӻ*RSw0tlY? jew9)#reS.إ "gd_ ( ִ+mV"h*JU~w\dFy Ѳ /ܼiSZU r'Y8ʬj2RG8X0"HC8=f!ĩYyvHLB>^ ^cI8Yͩ _K.ndr<%CLЈ]T9c}E)mXL‹L qMJP ^zp^k'b/|L/G]hC^Rt n! .?_U@yA&~r:+TU~*ןCr-Wsތ_zG?ǿ;WW ^?+Y|g÷T߯h^1WHAP8# >; gn* ` E6|+P1R"jFLt4Un3P1-0x]pŬp&@P;r3n^&>9.epG E͆8&$٭p{ 3^E%uЯ@Z^/*%3Eo7ߛ˛N>q5۬ł*EsX ~$C$Wy|eME@fr`MC{R9B@uˈfn'w޽Z@o+w8zӏ>3-hݳgnԄ g͝g}xQ `'N&&Jӧl;z{N}`Zhu"Zه<{O ٧ɮ?`Ѯƹxqqܝ{?裇̈́$7Os~? _O/?+l8o@\ n'X^;P}EѴ2eW|,^SpG㓑RMAl%$pOiYMs9Nj-I~+>n @,T=@LTY`f\43E\7Y;hW4ZDb׋f ċxwʪ@aSfHbys}M+i?4Ha2DC*tF4Q:K#1cH"O&RݗU3tѧS_򑧳R:Hejpo/ڠ"xՌ䪭.#Π;{w,gœλДK;-Am U36 n{wFsDmv{NwĻ;"=WSz8jJ]j#$g&VNĔ&i%yE ˑ>HT]`!io#ܙé ha*&:R?Lous[/RHcd5[*ƈ(շB9oJݮ5+uF8GͼiK4KJ9~sZxk \d dl#0OwgdbuNb I+Xۀ1N)`7 t>HDX'ĀEѓ#9 8NF@< 6aS{ț\P<3Gj+^7eKO>踕{ha^#}(R{zoG!5vQz> 'vAsv|[9 gjߝ?_^q!^cn* LK.eO/ nT>҃9E? 4K gȅB{"nGYR5GP$fWPH<q,)|pD&vnmhb0ↅ;ᗔpNjxZ" N* S|28E^{Y@W쨚*#Y͸3E""=у*Uy' Es~#B Xx:UͫHz|Zjv @YYE3L*Hi QA}|uBD]8&(MH`],}[vga\o.շ;(AV~T/* &QhvbǓ?RI`>?A1ƣ?假cYU7De^7㉔H'[)( d ayx8?: SӃûO]?lw kk좔έ͌7h4 |p"D^(eE_hNr<`v/%WsA7'qPjEy͌œ'7 IDAT^n)G[xB!cL]Uo~O9fysJu3V5S@[p f޵O+v b](p{^bk1~hbdEU -w5FNTkhV ͠mjƦiW]uE/]!^=߻[UB0x]n*RIZUgHh$YE~=:)0FB3yR%,YU\ "EdImIVX k?gpLK: ^h9 rL(X@rZ'ZWv~-'6'[5wzIa.$9xzVK ӻjIγ O>8No8V|x}zvdA>:JX?9mŝݦҹ׋>@/=/ߨofBf8~4$.opf}# >+޹ccÏ"7!GBF"PC~qPo=O.Yo @s`*zb pxA##rdyY|-Gw.AᮍP8<\{7frH[+  =NpP2IF4eytq1/_x}ݞ^E6sN3~HG)ɪQ}[ EEPSwXl l.:䍬Al/Z90I2,W{sz:1n׶j k}$?OHff1Ǔ}mC>$F"%399ESBӼ$ee!\]$ev6ϥkY=o?w=3pgwJڞ?p(-u**e纑xlFVry#׳ MH)MS$38M<\"1]uhq>y ;?w)Š_KOU5[Qsj\x&1<)L6!oXWhOPW)C-( ko ɞdqtU18^L0vx=kpWaYr~ˑLO^dJnsbʅäOok DM I)G[EU3c u0m'ѢV}f>^67}$WahvKW , ?%'S4岹SlNŎ].Nn%-+WZ:8^[ģ/P.R /s\E!%^ h(»rF 9r轢d/ @زEn !jƂ/r4Y_)`NJ PaJ2VY@/Hr#^p~2e Ě@@baa jgX|?(|߽ux-f2(1f]t;NWtZfJE'M 덗]Z[[#|f@?ZA}[-6"++7O,s`v'Z=ܯQ} ɑʬmf.op"~3 /^^(YA;P{w2uz|*n3RpO G.ҭE7o0 1 ^wjf}5ē@xvdf_(r<G~7q0!Ʉx&!ɫJ7})pkpG>^QvBv*)^x-⭑ό`o8X 4X(˪&9J~,T&nn__0R5f9L.I0mP&*R V#{բWӗfrk[mjeך^{w\l;mlu@0LF4p$ SdZq W0-EWu_w0^6S,Gct'[ռZٟ/1Nj"xq r5]lsmNj\jD(eq@P²ױ彐550ӋB mؼ yho5ST [= 0i 3 )_²Z%:&|Wxپ/ěʞd}|馭TK'<:I| ;ji3۝agV%$[^yl*oaӻs?9{x6pWOjrd) "fw˯ܿ%_z)?=+__6Rԯ~__O|ⓟ䧾/>>+]XUfUb O/erV/M)*8ihqI Vxj㭦\%+U]1ơ<;!xTSO/\jyb;n(j._`^h阛x2RUJǪ.j@tEZBՃv__ xޠ(rC_g$ ċ>D^(7cAHq穞5Q$CIOr%Ú\(J֭{q5N`C ^SDݶ51w L۲=Fk ;!@/|xxj^»Y+(#F[қImJM6R9DsJU\HVT߫.7艈h͟Ր`W7r3cŒ=kZ.-X*hS7ROhr9 P=N0sl}B$Hfp>/9:3!|zt=tC+̆KR#mtuj%0Sz\]5_(Ғ0Ǖ, ՚l]psofxOxɆ:>Er," F㫳<'*y?͋^a]VonsEEmq.UϴrevɱZ+^@0?Ht"<$Y*8䩥Jv`zQ.|o`dU'ƇtXp$*rô]ۡ8]:H`%U7|ҒöLW^/!3U4[u\TXr\7 y8޵% ^x[íSӛ+SKLPԕǶ'oe *M7[].g`GMZ9Xsae%$%Mɛ-P"0(D>^ 4 "4JR4n#EP2gbQ[Q[bu4Ӷ$z F<? hyzvꫯ]4lo֏}-YAK/-"&x^ӛJO"ht>^Ȟ\(rU,4E*U J"EDf3wzz!/)HU87+ur! /^Xn:U5EŭߍN )8M淙{+>^-e +T8AXe=TË#~w|H3:=S[lFbd8bWAD Jʹ)SܗΆ=8j2~Vl^*P+joe5.gH`_Gy0W7o]iS~жx/K%P,d*%rDVoU1f˰)8:ݰ)7_,JeIR,p2bY+ehP7,zGgM.n3)*%:BBnFI״\"Aԩ` K uMKi %RYyf ]?Kv) fP D1B*.$лH "@XQ5;_*< jRϖ}Cdö²L5ŲBbn 7 {iLX[]L65 mEnP;qK;am+MV{jv9os`ec7Kӛ-uz˽>T9nhZNR/TYrEb9Â(]ǚ-͢ki$X Y,u}%&Dr&8ffXKTE3P.0*ƂE>3dƛ^f3'W{N@crhö$=![."/"p\.ٯUfLxK)EIz(w|/"Emu6i+j,mm=&KR»M6O(\EE ęYy3D" jgیQk+} 7rME 㚎IGJ:)~(w1){+_#8dЛ+DgOcoݷNڴŒ[juv}&u-JUuYo1|3X)"GPd-K'{.Dm%"eI2b=Ϗ?jy`+;1e {08|D44m{GG'6xpxvouk{DJRu>}8nw]/ƞJe$oe]r\654BQ2-g(#JR_m`].H[ k 7lo1j~] (޵R?8<POowuO~ݣa{i|~gycwK[`7 vo %M¸s:o>wLjұ)BR:7JuCeeY{>%]RXBU(Q"7TdJˤ3 yKO[Zwe=vYn~$}NjCa~]w,/ek.\†m*#"6cpQGd5SHVQ:^LhvoB IDATND\ 3<>]H"^ UKkJ Ҫ)-Te 8^YU3vY2Cx5"EUh <΂i>7Ǔ>?ןƋ%5<}r}Z$QLfYA/xIHfP2œ" h &%Us"<2% ^penˢ(W|/@@RSr $0Ь@"Jpeԛxb.ċsSO/݉CA(E[1i"3N.qƃcz`L+"[.WSSGB/\7@"zpD8^({5q΀xH{_?zuo:|ۉ9 oId8J(ZWx=Se{mRARF42qDz=Y mqÝo?Kզ!< v7:KP8'5(6]ܻWг_H_"caĩn*LeMiנtTtGFy%ͰT(&ϋ[ HZDDje21q7/=}$O׶ǨN]lǻ=(e^$Jt+7(؂"2t+1M$ J5%-EqhE* b8v̄ ,]9*25tpNw W.<=XO(j;>sX",;/|9xj߅9E+5 QB.ʖn x6Q\(h!=aI5ҹ|jb %bZ!"K|H1蝜ށ>r{GF;ӝw 3Oeɕ}rο>nޗry:sj$ e!-9ϏE.zQ^s1%WS,_BwtBPHSy"fCn\/&ퟛ߇4Nw^{z@;hYhi(-Dd9{p%?}"JLl{^d0H. 0E$-Z (逕/s&AmJGa^ [lewܸ{gc0uݥIvM+P8%t O@"YtzQYqUu,hm ^l$p,2:C@cEiR8^pOB/ d+zzf Dk?5G q ߿$oMiQH kJípa e2N U3m@ӛ|~c(8^*F*7fAxEZBM{9Ň+ŢbE348ŷe=v4uӻX"^fy)U531Tdx U3^/˄I Rbc9N 5d5fUx5{+nx!&A&n:@CuD/SoF $0ެ)VlMҝs}&l"oZ=WXqd (P.Ԗ)M\%!)k#xSa zb4'"?xCGS=՚I{ﶱ~eB ZbʹR%W ! U- q)QRa!۽(FIqCJcprJUvp5?]yՏA?"o lv[]Al)KWja we}ݿ;v&ߗbW~9Gcw,1`vrb3L3 1xKBވ$S勦a? &HSț^ǻwx{?Y9Hǣc'101YQQͫh:^H5[DoyA,P<2~r-U0SjT'B߁t^tmW F62 GiUwρxFˋ{u4vЋ}cCL,sjż!c EW v#?AR?c:y|d9.77UU'<[=L0@P_m')}2 C's;~>nu+2Jǿ8#^Z/A4lIz>!a닁"/GIg;qCqD+R|c}JMA5',+l{m1Wk#@EOt'q%) z ` j]qP ga#QE;(IF@'_qNqt-P8yAB`7,oX%_t6|w0{)-\e^ BiC,j+dpfˋZ&5qL(Ax]{x1Mo!B,< 5lQ>^dhYm6uD'~n=jˬ)- dcXHuTXllF|rCE%}+ť apUQgAP2C  $ 󟃧w* gb#PK9N$Hʢ_zU@B˗~p3(R.zzR;TB $n^xhY^ډn^{0qq6}n2+nY+"_[/55%kxY_c\~7/2$@M;^LO;O,bY^[=B,/RkX#?nv}K!ْT]J)Wj)FBtGi j._|RFJ}O@#p/+&a+K0JGsB0$! jx4Лzz͢>^tb NuJ vwq7qN>Oۆa\a S]e̅~6L4FO/k4YƑ=8ξ۟o}3g$3"'DnK(k?M( k.]ɑTveȍfܷXr䆠Jf)P6L^1Fk}t9ZEdLԓ=zDz1ޭDXa|t~5Zãޭ^qګ?wi_]/),i$]/M%Jyrf5hɩtrKKYGT{`%ZѾO7 ;bE xֶGeR5o]XmֶnƝcw'O{lځ0=5qΧDCe s~lE4FAso3K''Mt6 lظlv$Vm%'  o_Aq%'Hfۏ17f ~ ^=)Nj"\~L bxn(_mFhax>ެ&5lg@yESQɅrY ^M6/N&yӄ |H>^ a_*rMB}T\΂ޛn+8^;Fy~gPhL2^FHMElӅ=CY+<@P8yVEї\}E7oTIy].8ϙzQnޢT]-*>\QǛ*YU3|P;JNj,ߋ#7X,UT˸A 4+ڲfdD  H~V5jg8F0JAiS /0T=W$W"h*F̍t'NLU[$tӛÌ|~m"{Q*(J+/Ʀ^Yѵ*䵌6IЩ\Ⱦ9?[JfҶ&/qqj`On55%~ ev+33杊Il|#aP>9 |d|mifRO.@/<+;&54ŽUtxllwuVֶ6G_Lʴ?yw9ZS70L( _3^5Th#Kjl͖<;GyȔXS*' SgAM K=C6OlG 'WrW6GBռ<Rݽ<|IV ?6uYq(=«F>_ˍmmjkdc(m]?0.9NQ$fi ;(rҰ=/b>eS ) ^d5_>.٫o xIg@H:W(Oo]Kg+q8P2 c{n%l3Kvr5\kl 7sK%] %*"AMZ(1KZ {KL% 6 z1|P zxt1x0:Zn Q'&! .B\,Iӫ֩pXF4ɛI"&ldӒkj&r!ŤaDmfВ3ma4wK|k&A*vW76vGq_ym{^"5<].mfݭBy rsXy (IUrg~fya%)b.g4K7l!LȮۙ?ѻV#N6\OfwLc OŬpfjKI :>%UsE;Q[Fn3ۜAgDWP8墛W ^pF" [ x^H,[tLonKFvbhU#E7/it,YƲ \, xEpL]m /_ 9EE;M7m-<+W*΀ho "|2 U@SĜec [*A(e(J ޴ "UTYP= &⎣T}Kz3!\jrYjb{|Hf:f{1F*^ wCQj" 0P` , .*A=-dtRU&x-R>C)Mbgpi&S;GM+0 g0/"L/3UxoL~EVV|*p4^Y(bza1xy6U8(w)2P<\^I^,r-*: %(R|uC]\.zzex/rLofa姗"#_&+%\t[T-~F%Gx5Ygmh븡Wn4F5B1mՒ$A E7:xfwΐ\=:|.6bw*90IE/ᎁVbI2m]|AjjDL+_3o".7ZS7P? cw9~^ZW T <2ӧ? h1,ZAmKwieqݛUx g!ʅ.AEIq= .͖yQ^{ ,C7m*YP.l+zЊ]]=tMpܩnC/ .1"C1$Ѝ2,R5@vxifr~&W".tߋTi>^k "O//ެw&Y=!<:x E)PHBfGXx~WxS[(˵bFJfbĈRDz2c\I\*͠|DWpxZ Q<1 4oSi+N?#KOGF lcT Nf+} /7*4}z΀_79^pf/j@=`nsE{1B(IU%{%JʳjT߿~V33'ކhR8Y@,<8.M#~4l!/ЌOG(uۛXʀʷWF $KpZ # ZW$&Ȅw iu9RKpw&էߊͫ&GRn7}W$WA w}k׺ۣFT:쎟Pp pW .1!D'S6ªn~@T…mͤ*ct=S;sI%frD;:"^ A,`Ļ=:^_={ǁ]NQ6S4 u[Ue <9 +( cYQkU#⓹hd\KNxz'(j{ Uhry-z;/ ?靹`)~ؖMZBf.EJڶeR(e%-5jf/9QK:} نvI67ͰmAӋv"\$WG;Xl@~ ӛYr)=-7A/ܞG :`*qM<ޞ8^չq`]ݰ-zn_H* =8<Hͪ~TLc{+ͽͽVge5|YZSFPt@3aY.]JR|/7Vza`B5m[e2Y%uH ;/l?+mv$ cx-Wǝi;yw޲E\!\.|-/ D)/40Q̮?j% DfE ti-aKCqCy|-Lm ] ΢^#^)V(*i4ZfFoM9Sz@~YWKT",:+ꬼY$T O/nr(ꁄ |=MUs$0!XF‚/œb!_* SdBK:% ӱIHfrdVRW}w|p5hcw3>ASַ;K{o 1\.,RR|y9 7 qZ_|W5V0eoGZC k}g\(HŤObH)iߜLJ~1נ.zw{qo 7li̕ 4-8^_xv8K}h.Rղ|Oc?)Vt  "5Á 2m7BЯfunօڹCO4gDݣP8㈿P$/ɀP8X5@Z"Dj+*0pa ^0Ƴ‹ㅏ}/(B` |f`a!7پO#^{zȃ˗ KV(*EFiyVrجH:ҰJZ*go^|eWH;3(pc=nډcU3=Il8Emxzq8TK zu#+ `lw֛IN񿄬t0^[]?RiaҦAԖEʤ-FarM\m6'=b^\^wim D#W7Ofs˥(W!EQ8$Jrrzq{?A3ԦX`y|R]}r ˥@X+iMئ6zSVSX&Em=Fɝ:1xmM?;s]5' gђ Jݼv}t]}跨NRd s>/mh JR0mhx!+NT@:/ZoliI'Gg ;\ }Ǘ#jxt2%|ZKĠ\2n*4SoQg4JqG^WL7l(yv|ݰKNas6Votsn;<8HDw4+Tk|ut~4OO\!SAbP(y,o9l^u~J4zk^;PfQzG qp+],9RFMVi[T u׷qGKk[Kk[`w[l쐧wgrnASNfnbv+g7 Kϋԯ&hzzEKǞvS3Fi%bAV('{\ͼлYZwöuj[u=j&omD !H .@nFtCr '9uRp~܁"ijD<(qDn~j6]l@BA+5@xE7/v i>^tUH $ BcpdUzCx!sCQeB\kPOdS#2$8缦PP5DzjzzxӄF +Jz =7ѝ9f\$ W$kHy ¶$$d9)bBDKD'@e2o 'rOV6GmNV'wH'w{$>8;f3yzM71zqי%K-/@IY<8Q0guz,L$&"=]rl jSh4wċd{^MEN,fd8!ѳzze *.QRyKSgNaY|_o%bϗ-D|T t+$c<5mOqqysǻ50}H>^N\ʔl ;M6xulyzWO ,UU'eL5/ t@$=_X;K(;8>}燿Ew->gIß Et|^r٧CxfXPI2 N\A{+I͋ǛWN__ ی*b{P8x4fLm~]/ 1/* qrU ^x_B tj:P8 ^I6̐@i|/(\ aj*RN*Ѭj&yjK!x-c+T5E>ޛ [R5|,EVsNj֢o}|^@y_x <]Wor"^1Wh*"O"2= gn'ڙn0wb/H%*/(Y-xEkT2ffAx8A .:y~/F ĵWbSEZΌ~>& f`zVOb$[lxJXЋ.n!e!A8_p73lAIVkTE^L9Agi]dr@>3td2m%d3w[ y٦JЃ1g75Sf˗uR+7hoE ]]$h-,x;;^;$ѼQmלz9b4ZT0gzh$Bp=f>ó{#K֢g7EϏզ1 Bel7 Su;sC%+Tvw39u9DXy?ՊIKN7mH*ǀ錘' (Wx/>\Oz~U(>|t%}` W/ꊚ "2UOWC֛F%mjfr;;oeSv#%",9zRpv@ț/nEn3E;hryrhxtA>ӻǗ7v)0{z#7D˫Xj%*iɑ j֛>< +ˤg:knA8?[(n m|X'ӗ@ ѶܻY쎷GG@{[p GGqu\A3,w| UOƝ6>Ij>?7+q$bX+<0' :qt\?B5- ӻaq*o_{.Wqw"j}@5ś rNhkˋN  Ųc1vZ\T |/6@HZ*pPY ^X Usbe@˪JYQS$|"k ˹ ŹcPP;͊g(-RW r+yEP5WFAVJUn̂^‹oz?^^ʢx rP2W) u_Z sH9b\C#QP(7_-. RrxGnTS3]\d5#JtNjی7m@".T01B:gRe*ikgb̰)ii䁝2fYL._4܄e׻;øc:L3mE3w(7.n׍{H5Vz-[p=" %C~Q!Z] =:\\/"8*Ku^2zGLW6G@B `.R{GGS%{dzDVԛe6RjC5j'0@_X͘ JAN׬]tզ. ar ӡs|&3Šf?3KNxz#yj4[?@GAJl> $>bR xp$Lf_X,JˮFϋP+nOZr~HBIQ R{rv@G'(5?z3>꽃^Q5ubN"ELl\A_l^blZS%݆B8/}cmӣyn>sܰ[,zʒ2du_&ͽQusimkg4]^ߦ37%v91@N~溣$M  RKrQ/ J]bʡT| G-{|ͅYL#@owi-Sݰ sgi e? v 1Ic ظd8 ݼx+'H`8, ) )ʵ=TM(wdF7P8c*xz]CFKYYf8A1|5EIpV ;>^ _F~ RkX5EnY7Z R>^ph*U5RW0>R^`g&ς^fU͢Hki<352 )ߛEC,\Ne/1 4%ߤ g"ʇXH\/{NBBy\7_*5b7;KRJo\_7 ߛun$ 0z8d݈B)Y"cZHǰ$Kyfʗ$USY]V%$HZEsbR7 JRuq#FS(knQ/*ʕ\hXd,21Am(* MRZ_:@/V7՜U5o {\Ҹ] Y ʛNY*zA>_JL/s5tUݖf%aV#j BVow#ϋ .\"e?$tvĸ 2$ RQ^a$˅@Ph'1C Ļ=:~BeQl8^>+$s1IR48 3i:~w=?.S-]䫗+Z]MBϋL˭O$(q׋7$d;_c1iʼ6<2Vs+WH]BV3|@wëG\<M.'}`ӋW*S1X br=.nF}Trl|ף%lRIu IDAT*bɛN]iVjqdW0dcܠXi>f%T$~T<>;3H=8b!{͘7)%12͆ jMB=FZ]nHUmEg< :yjZJE.i^%vf1W4m@aeMK.{'<ۣ6P6[&ƃqwycecgkx9\ [*Wsr`u['H/KRevy`ǂ\*4n<4[FU3JӲ=%g8iKNT*>v#[wj- U=NK.zW6&^ԝAO"9vPk.I-/n^( .8^E 4 ӜN2^hӌLEԌ rCZx(ƹ</ha{Ls~a OoS`74BN.BrЩ@)eˮ>ě JHi*#Z{zErډnESLyx~gk|}i!DU3/L ]X,hBfFӋ g@e\/J|(UJeEV3a /"߭~_| w$ݼ8@ͺk,7L ^( B&B˦kJx}JmTO\+?,ěǀ&Y*:m2tb#%[Hx|)nP$UMSJZ(ɍn:[SeZ5\ 5UÔä#&uְjkމ7gf4, G-YE ՀI]xC[t76f@/u,3-WmꢡT*sQuEv,EH+k'ݭa[URy/x MqY9?9<U36,ѕɦrcp ޣ;MhlO3,:T8^XʅbI3l8K7?i#!% K%Tk^r XмUk%' je4gOfave^ ދ{~UO\>xY/^C"Ʌ,uE-8^>^l~m(ESjFPSROoɑWˀ@ 1m[JSWz&JFScN"ċ" 2RO/{wNj1#|Ff`a1xEgHxgr9񢳗Ce8WNj"< r<酏u Ԝ|.oV_/uwZ}OHf˿wEoNR~g_;_*QCP&Bw$ y!XH%Zn(ډ/h/Q< +ġrec-* {nXBk1զ.łfJA+M*}Ix4B"^ . JZMn>;/4cU8L.(T._YVjF#Qoe="ow)VA_CSQoexL=˛a1`ү^4ZcAtȸvD<0@NXO_>ĩ v6vO.ll'G{zQ_rm!;i/JF_D 1 9 ˍbIj<~Ė/L0.9VR]퓎W([Bh[l R; o:BYQnNwussxHݼㅪwgDdz49H{0?[Ve d2DQ[+uˡoܑ(yjqIt/U?{o-KU }e/ϨāsNU5U=yp/P@PpDDYD@4 pQ(yq)_>u{7rmtWﳻjo]T&2길_S{y!_<- ҷ4fj!49Aq 2Έ򫍨ٷjf@Qp F/m_x|,.˂e"xZ#}m gYౢY U3zv gpX~8Eȳ(ȑ)VHso WѬbRHLv7/yzŸHh +r38a$WeY W2D1/r"AO#@nx{£  /P.-Yҩ4)|qi&]TTL ]KDoTx`\랒$ŧjWf^@bBbCn=ɰ}TVzn5/L@N!ꭁЋnCw>B]Z}QQ !(y>i4WX֘6%fyӡ܅G_I1WďI{(t6tznb5U7%\֠pi&e!  ʞ5&µCǎv|(z[CcS@H4FLk`;46* ar-}RV̓\L0ԣ V%\=)˯wnu/S5ÕP(Xy ;ӻ#+VN/LQk3/ZYԶPS,@rz~w!xsC9d,yvyk^[:|;|>|~uӋz7~H{}QByfK<" %#5${Ց#ξܭ?sZ3QTkhm4Kru`}j<-w[4Xlo&g` ނ.6MߗGZ+ ۞s..,oBɼ9Mp !"X>w|velziam饱C'VHv͵e}ְqF ˁVP(c)֩Y1@2t)vFktP:W9m?_o_{uO(:{`)Yg(l^P]foTxU{MeVR/O/.O4ƦgZC8^h OL,ᢇLP\:^\vvc1Ke|{h|jši<"B(coP7 @3͑fc^4'pؠ@Da5s,y@}Ȗ%DZ2#9|DWhGߎ{Rٶw;R~QmME~Q Zk k@>^A O/8^trO/zjoFC-/6b:A"-ATG6(l列+:[\P8Ðe:x]\q6X,Qy  x. |$5Nbގ>^Tق+{nYVsU,KbEd{wK- #ME8wyz\y] y)4N8EF9q d rɫ3œ9 . [r \᜞Lpf`ND>I$WeLuxsT6-_nyA]<_l<31-Da=2UoYP,nj]e*v.KL0rmLs])B}^xo$j ULwL;%ATFc$Y&;e{i6c1%0^F cJӚ;>[)>q# wO^^@9A|c$C7ԖfMۓ+ŠZS40 ƟCerEdwi}e5O.0]Pp;>^~'gW*AowOr?KLW>2jff "4k9^YdsE ~XĹgT\|7"\Ӿ{݊j';p EBtsR׀_(PSv:wwPP&wjaDI/CǨZvqce Vg6W>|a,Q"K#hz >@ %&$k*ϓ&vCڊ#G`4l7ȟ٫9Wj^"\\wȜvZ: tɆl2}GLݹaxo߶Z>@ Ӌ3s++[ESxg_+s7Bjd'qW92m }GTk[})fJE zYVRd wf9872925?25_G`Tt@Ig>ujѡ 3Zv15%D>_t`^o4'h:Z}6~*Z,f24\&c<G=#7<`.{pnZpgj"zXq׼Nه~]~Hݼl}Zg ~H g܎vrRrj-;y@6Ga'+ 6]eԁr5,@n^M<#^bE%ZI#^U-e% H xYg/jUePqI6]ㅏWh%_x9Nj'< HOdz<짝ID{;xS7"iS|Wx;oJrwRp18Mg vbě"==2L…'K Av$WOJ;OzG|v]7\U5,03ǮSfǮ#;lfZ>=WB™Rpv#zA+nɼ-pr8!dz)^ =,V*^0=m ނ{aߕ <۴V43!\ PT?xG|[FKw88> $6M(ϧT: o(r>^t׭R/$sKʢ a.z9IU"S$hkCvd]! l7Z**xEj_}mW$rvT.%3] +Ǡ7G~sz78SϳjSL{zgpZLXM/!ō񹉹} kIy3-Ia XnX&ע\&$-V(,Vh1q( "܏> zW@ۮ?M8.8ptyjۓ8nT {?ke0"%|:G\ \?2Lcmu]c];2v}׋Q3WjF-r02Zt&VXmެ^M}Yxˍ3ms256^zz!rr~X{Y>ez _AD<3tb|C,7e׋xxؿ;73Nc|;u7lTY;:r~Ҡ֬yrUsYx]xZw[ z#^~uH k8^~XSX8n Cay<Ùsu{n-yfrM'&Y3=}PV#}F,0pf m(e` jfe% /x]cbAVVEaxU{ +<*N=}O=L謳Lgu#ӧwzN!{ɳe!`Ie鸝b t;cv1IID}ӛT89pd vQ:W$.!d8{xz'}?NM=w?p藚G М:gK3?s| .,OѼ:@DMș@#8^_\go`~X"%?:^cH@8^'Q/ېN"^yQC 8rw#;%2*afE3M$oNȸ K[pJW\|}gÅW>RG^kr}Jg>r=aϜ0( f:p")¨԰2nsZs*sEmXNebO/ƈ\5ㅼ ME(9˲X9pŠg)U-ǓXWKȤ~XdeQ!_ p2LkHU1,fs:gB4Z_9SJ1")gL/%2oμ^jzlaT)މ0]ޚ[\wp|әM_^Q}"܏7;1=TgŢ}tEnOƍeZ}-4f_F/d^kǸu\ ,=d IDATmKө!)JA/k ܾ`m#璢eŕG7~֡cLί-9x':tJ=Ӌ,`ziXؐ/g(~7`ʹ~6z=|qbw WQ}~sǃ$M`Ȱt&rML/\LL/ `_W]*29} RUVg5,ḆSǸEfI͋Zae_uNO!ַ?Z`ɛՒ EyI]7 BD5{UoS_ ~P~60:T57,@MFkNS7퐸@~XHG׫#9;eJinRxc$okcTV4sM;`W9L$oF9<aG_2(UՖ7a;5Zk Ag jG3~lv5~˅, _mpS/^r/nX-]E#ZNW7xch9E.%\,+n*ߋIY֤ 5! $ErUE AR2+V8[Պf"iu 7_fیTI*3RIsqR*s\ /$)(N8G=Ho7uƀE1uXr+*_d @1Y#QW*CevW"x$r&_2IOa>G6_S?{/v_|6{;Hy^d5cY͸XNUtm7B<"|FP/ dUk3UI/FbZ\/"Rz3=B .B_%~:KT!TD8'å4/@Oa؃<Չ$BbN2EgANjU bK=1`9W餏΍Iq/rG&fGu`Z|Q KٕEPmWw 2N z>/dX)hR:I9ȹ9RHj&K ♥Yz>^tRm;AVdP.*)vdzV M,m橅ETNί MO̭"S Ht\'Ig)$b &gֻB@?mxfZK*fϜFGQVT^X6%~%I Um T&בlZnsO/Ҫx;:%Wv"x_#jfuv^d%wHӛ1\TFӏ+%惆M\@ig9SzgA:(dSʥR@eII:"rUgPz oQNL)Y'Ù^jZX~`tzibnuy ʢgW(y~P/. ]dttRLYF/`O丑ڟzwr%AJfe|ªcܰg%Y)\USh1ẌpA輪)DnLMWsKBD>X/!ۥ:fwhb5868:bgw{zӴ?R3DEci=i*{^sb"dÕg:Am5,Qts,տϧjq`-*_e-C @o,CQHJ 7jsOoXoE^ 8lNP}`@r*# ^7^Y 'pSJf$C17EJqx$ c(™/Ă_MEW˲謊j}HB3ҪTÂUg5Xh@nc8+j|/G\ vxQe z)V<\ B̳>ő;=tzzf|/S5wu)VNj8͒ۓef^e- $1s\oO&wޟ~ݫ\{Z=qQ}os_^|Kc“[ϺfeS,=wO5Ћ()&r(Wee-ounsa )=`! v”HtK3,IRoKVCeQ*jc!Zf8/ Fb,)ezM2q(( rTJHeR ĥ$PodD1X E,xIYlTT [Ua}Z׍@>-A7oRLƦGw`dUF^lNԷVE5 2C( «EYWQ]+L6͋a:D,)6/I"оP*e ?eO%>(y VlBFuP(U*4`"U2EbTcлHhU3KcP5O.Ḽ.ZXӋ#KR2(UPRE?8u-Ez4"a{tX? $RE5t&/%rXѶ'[RdR4pEzZS|t&'am7$ό湕 x_k=5lTPCEq*v"" %j2,ƪi'rHiNe(QduA=rR**%*,byr*^d-ʅbϛl^M ,>T]/BY7$+1}$=/.mEnΞL7k%EmM7QIEug.1"w'۪N:C_4,MXa ؞re  a l~oS涤F/ acQ3,:ƦA>(5RfN/&wr7<|Ccӵ|nO/ޫ؟I/r|E3{[{^I.+Pۓi-W4,UNR0ۗiA)(±9cGrnڶ)ɕ)+drx3bŒHzl+ .[an^!*e\ˋF/RX/6z!^\}t n\`탾Jג醊acw[=հ-'ڪfP5nd;=LՌ!VīEЦf*(eiUU4dƊ|f̪aQ>3;hqo\żH+V4_ݼ8."R }!{z3AVuۜD=ԏ&?yvgCW>2Ht“'oO3Τ޹-| +ZnNo+|/o*x݈Y%抜>8w/Oow:ɗr)V\7 z{} EۺsJ[GnO^Zyw8_==הyqJY͟J<|Yg]ׅ7meݼ|5NIR0[O2u2~:~Ռ#ҩݞ^,{ȧ3؜\hr'lr`^!9}(WtX3pI5, <_5.1~~PʖY:-. BJdީ9XXgI˃8`z4 @=):׫ю%{$'MiB?wcSδ\׋5if,l =?rHխ|8v>ټ NV{4,scP* BKE*3xeG´~`.o<+[;W5^[I+%ޓd~%Q-68UϏnd6%rW2%& `ԑzcM~E)W,QV926= ҌkWu/m+(}P5_Glq^$Yd;/ l["%굃(MݑªP4 !f>h}X*aʅF}|>3A*H6 Aʥ(ϰ=M s=i*@b54>6XLN/ y`drtj"ݞ^vLvMsO%oKlP$͠;ʅaeݙl;^@_dQn_"*NĨgWX;i},z'/<@ VOWۈmK͂v":Hb01nfMyzqw ~/k?B0,s=jIIe|4->0(A-"b̌F*eNKqZ2-J/vYڑ4@oX }mݼ3L̎L̢wdbv6& zͅ4CVtߦ()$:8 =T ]VD+u~ %/iDwP.|#S[[33˔V506 )VӋ(_wja7壇ĦT ;Er4ZV m`~wwGH Ur+x$V (Lᣊ)1Aric|_\|!vՌjٍG[X?>KO:tlvykzqCޜ%l6A;*= u iLArǎ949Ń!t{AT˗L0F)ɔhoO'1dz% ?{z;>KexgȗZ&gGj驹 +BLq},~qce¦3̂۞rGkɹ=) EY̧sp;YqcO$"g)[gfpXom yQowQf_CB{pf@a>aT_8vxȋ\xUEW,.,'RJfۍxnyet~[G Jf5W8s/\:ÂX5I{-˪8+=%WE>3bY*%IŊ.j.IxeAx*{x<$=ANotuNOJr.'\.!~xybY5'x l$#ү67H*h "yk~yEV&iޜ~V{ǖ>!~eڬ^m\gPnP;΍׋Vɓ$*1@o|\5Gbc6C_="`YP-[IϖEq b™CڍYXX ) 6wFgEکc1S$a(`l3tIDZL}R#CS CNί&.nvT^~K]lBQPylY>׏hOqE¢?'yl˛_xmyHY${l( >>*⻏Ay~ZӤvYrȟ̶"Ef" h^! jXIgrO>YљzM9QuO29J;ҙBû?@\ ʟWC we08^McP_UX:w)76|-ih)x=:^{zP0DI. Bq$ /91[Ww˛>(8ɹh?xg#33Sёž^~JɃlP}%s}øQ]*|2u]hl^\\Qda}ߵrUdd3d a=xDZˋBߢ\n&#ȲpfєD0*ܰ]lfNP3W"6((rUVWH+ %3x]tsUsE5>^NYeYcUH"3KȖ#^ "(a-2P5wX2|eAD^r9 $̻y>'}'G/uN渏 ]ipc$Wq/jMgrɗYxwq 8jN犜o ~H71o|vz1ytryP@XQ 0g>n,rY,q<%a-+[gDa,)lʥESz8>$mл2>qNj晥}RkJ8$H̡_ͦ"7vL9#J/ևMt#}5z ǍVg~#Ϲ޿B_dtOQ 4ޑ jJԓ|4v$W-mcUL-Nί-"L3KH!7Yi{`#ә|@׏*${3/oP/E ;aeyU7~sXꥄ !)U$r8i`c* FM|w$" z`iUw`lzlzqp|;<<1nz;{B;]d!.+b9 O| OP&>oa{W9PWg.};aoGY2)+4L9wϜ8z1='V5ˬ7F B}Q75qm7y~ԚxqhڠhNc 'rXn5 "^7QSnY^51<*o (g5+sUQMls.W<媆#HY,TIu<(n)Up M-52,YbQsU0Rzj>^xze$ʒpT$AVv|LA-b`T1W{{zw{w<̳>Qi+ӿ7@] rh IDAT+HO:x^J-0+f@,z21nX߸7T ^37YNm7Br^d_ʏ]?>F~/u=EnHa pVpGz+䃌bȟE [^* */Mgr(~OMǥbQvkWܨ(PrP6-tN934i+Wty= }Cռ8:5^ixzdl L=QlɾۼӅBI)O9fWP)S[0836&PYqgO?n]rfXR\ј&*Z4Y,/=ǪUׯ68kU\B^p@~K.^ }~*7!uFe TPn吶Ld//RN7W1k8vpf-cW3]YhR>^xz|/ᲬhqkQE5╹MEHBn3LjU63o( UsXDHxy3g|/񴳞zƙgtD^ N~-W[x_}?y# ^pgY?BO >CF?9k|]oO?M/:T7(5|o箛_/T+:Gw]3yr_i]pq+P韏ǟy9nKiZF~˽fI5sIoP/i+Aёҷ3_,#ʗհ\S?+@owR<n55u6[mȌ$ot.ЯWfj1>^Hx6*߁r*0m7"/FP;DB;G^p 8j:q3*@fA 7 *zmb#H*C|heډ" \. ow}?~'wx 2z=y˨F-/߲YũT]7e- fu6yBGOhPKeb{xwf~ulzie0&V9xd>}.Jg7=a½y[)DcnFA_ATw=>fs@ Y)5eԗQ7iʝS0gɹfLZf㝜C7/8&ƦfTSėB {M@Qf;Z6WąA_~XC l *GiLpd|kK %:Hbks|sNl gj@QEu(qmXQ2c&sډpk6ME ԣ|d8>O gQ - `T,$Jr^iU0dɊO/W;WTP.? /qx[:VE5,+[$˪^UdeG,U@ո.T sB-wwZœgIYp,B<􃏽L} _zw|qŏ'zo{ۼ|T$YXSkU s+"Rboz$J"I+Y7B{0Qk=W~F4@*ډV뢏wbn)V㝡Z#RǦ@+@oWwP(^DEb0\X⚙dڞT/d59ʗX bFAI~B0ѴX|<ũiɨt1gz^ ^>?Qy3TUP1?% ΔKȳrƊ|A%P(v10ػ) $$YʹE"eu[Dp*MUg2'0wz@k8^$W!$!s+ۋ&Vw|N]iUtN9.$զ)͛/ aQqbP*M7#>v劖&;BLP/rb%igSP" "[,J 1QBxJ;422*l՘mBQ5@* .oPkUq/Rx9ߋ+'  F\ APV*#Ze^$ .\ ;<@PD" 6]xe 8Jfʊ/S5Wd 4х.ƪ̒M/I W ^~ 1dxUx 3v3:g o2s0r=o}Qw~㻷=g9ke%j ǟs>u5KҘ~ݻnT*ukGꮇq1uglU%77Oy- ?.ygl[?P+?z+WBW+KxN>2a\m{zJŪv6f:Z%g$1L^o2|/qs2o2U ' Ҳb |{^pb-[e1HxA 0qwD^C궇07 =O/Ede:(:F;E +%gW݅h|Q(V[dr~1Aoiئm nh0'âacMr)熢fr J}bҔZgܲ jໟϗѝcYO[P5#d8/GAhA\098>7O}Ѡ)q8jxT5|tlL#EUep)|hZζH} JJ.4766 (|P(||K. <w~awXLQV R}Ä~b?297:5?<Lysr1Y˒ˍ@f3,֞M`M 2|'Ž7$Ŵ%Lm;ۊTP*3]˱04ze{q=aƺL7<&X7~~^hw , P+/׋m@/ډ@:>iT,a3~D(]^Ď/GP/sU30 g˲3bq 3/ډ@e\@ %sYVj<9qċ's3ֺ'>̳xU"+06uLJ?G_zb(rp~kn?Df]QJ]zׯV͛m_tua{]}_TFMC߳O_~⚅{Wb{&Ko'^7Vb/⁷ozFVw^~酪9>n#^{Rv;|&6\8&x/ہrjP>sEs[{}>я׿^zw6W: 9m `v (zQwtx9 3Ky5!A\Fp qݐnCB5CVmg=%]5/ÜcEa)*Y0Və\:CxZ%,u5f^A/%9"%iSZ`!0utt@7,*flۏ֨?02jJ$Ws"88=RYc I M)Ӌ :K.HMۥødvͰ%Y-\!Υ2l6HB/QUۺ'j:&%0SbITu MEk{zV7Gg&VL-ϭlO̭">p Z;I'ϗ<q(SOqS|r84׏H(%Nm"Y4U"0x1Y tH`H]U7lnڞAem[|kq>*VjFS ޵'7we3?4,W($n9nq8ՒƒuwvF_-QV E6h>C`N>2S+h+9P9U)Fhje;>t]L>9nN@M @Ͳ hjY~5r5jdJ&%3/1.7ez.PΊf *ÎMXP2# gHR*F,ͲiS5WT+9߫(XR 6ãL\Lx%E'O/{ .{˲ZbŲ^ *%\Ay/ ˛͋v"~bBx%ƯGnG|:z~}衇~ =~.w=ѢKo?~#kw?y+"S׮wzKo_~;?([O>ŷϿf<`z_AGcIޝN{՝z9]lur|SQNO/C1E;|E[O)1-8z#^(sER/CՌ>^1+lA("J=3~>G)O\ !L=Rx+{z+T IDATۼZ pK VTMk cҭT}ޓț{RIV]/b-=cVL!;d{=n=1ӮW5)Vl+NuOU \atδ *6zt6mR53jwx|][m ZC@1e/lX5xRy3}%m=+>uHI\Ѵ]2Ac&@]ҾsmicY7m.%wjduʪM8EW-ei P5Oί MC;><8>7m`lvneÓ +[']O $U'b}ŽKe(Ew89V$Q6rd=hؔ4>2Ko&Vv)'UB6-0wLh*!;]QCiѭCf&Vxg67_;oyn 9=,ǡ'@bY._9y^QZj9hm'Y3lrL'r`L{QDi13S"+e'MN~7LǣܵGfFYrP5,mn;9><;3T9*GS'w:\ by|1 rW6f+ܢc˦,\!ǮZrt(UMzzVF&65?2Y]h N͏1 tk`tt  +}p)g>h8mx U[X쫶$_^z Gk /nͦXY^T-L7$ϚXPm6d]a}:~ 2eӋBID,be:Uxc9UEA*:3k?;gx<ᙋ/|=?}c,oHcL#sY $ˎcO/C$WsVx1,-/>^<,;%Tœ\QH-cǹP$=P*1[ (80sEql`[(QZQZ`cA&b=.X%JխBoPdjuЍwKerqRt9@Y > Ma9^]yA#´TX53<5XNҟhEHj0[m^A8nV6ԓdP8gj^Kͷw8k7:1"/蝙[][]ZXGrG#S364oiMOTYm7j+/* d+`tr:Y_H%9xO/eȐq¨crxq´GI~rˍ?M\BS-Os7c~û;1y|XAy$fJ9{/Y>@~Gr_"2"c2"Z*kʪګ{^Ѐ@UFؚ xgƬl0fɃvhΜD~+^VVׯ~:vʨs?NM絏|I;EM/+* EaCe"U *T&շZO9L!h)mSoŪjN@E_=?Y=pnDk4r` ?؟RY$Ri}\m텰:^3Y ] Y|.ۦwer$ID"!Fݢffll;\?}uQ#|ӻ듏w!/v)jY^k/Y\EtOo:-[߮}{ o)nZCV4 ΗDs9YO9Uh+> K0m#x4^1OU?fNH=uxP8y 3!ϐ0|5jAՌ*pFfȬ@35cxm?O&rLFx붏AxE™4AEx2 l>Rj?ÁukJ4 cpr+):"H|fB0g{G/oQb}HkE!+jI;xLfzZt= gvq>{=a_1U'~?7o~_oE@oZ&޲\]/|zQԬo}~hN/ ?Ge˰W~^SWw×sh {~?O_Svdu3_8{stn~q~ain~3F9Oa%W dˉVH*#/|&@›qU:|/X1&3lP{%R>p*W_Va3ㆂ,hy"ផ+J^nR@?ow.Y~\D_9MA4))DYy%bYfJT@YcL1 u -!^ǩTM!Bz<bix/^$#C c\xAbK)VN`8x9yp&O/2 Ƿ0W4dWaΒ$L# ތ(|8?#Btʄ.YEMÈ $mYjf>_FȞ zRko.~7 EjćlzNs&]/]yӛ/%Tt:( ;|]UduGarQJ*CSr/Itmڤ~35ߌ F8w )RÓnޤ[^!{pk,WPl׼1۲bKNG(T&)G͗Nޘ%}֢n8|SU*JŔ+4 Яj1OB%ɭzpr6ȹHB8ٽc{uOo&W5}kh4Aau'O9ڿ""-SYO'{E_N>|tE6d4B!erbIQfAJao =m׋ =H7޽[bE[Njoktxwdes{o9FW9tzL 3vRr~@: i=~A$řv4 &NT&G~$Rb< 3Kn?c=@Qd/=ح[׷k[͹nw}pnlwbBB|ʢ\xt[8Ծ&;k" MULQ,Ϧ3\AijZԬ39GhҔRЧA$S>Ib^+%[q0nE6m BrvWZ3j/GD"/s5j'=Ua gpP5#j~]lB>ꖇ*x@|~E%3!1@҅DB*4A+ Vیs]^ P;Ǫf VKKS+(%P%E++\1_pgz7Ř[̳#=? .|?7~SÞ7ʯS7*?_~'>> {_'/ivʑwO>shOnĿ/T/W?5'n7<}W@lQRR$Ci"dF)<>#ѡ&͙4^rn39Do]:7&q ຽKQ$rhg*:'Ɣ/ƕ ZؾE+uYL7`cJX4ӶOhf6Cxb{Xz~#^xwV$̯N.mo>QSm(Q7v[(J~P_?st_M}YVMgRZzDzOXLi,yNsI@ kBIaXעUxS鬬R7bp,;nm_i+dNN97#ycԹ'/R1x]Xa /ޣ)^w仵ł߈o\"wTQMXF\|0 / EɛiOޢ2rEreW!D'P6~< ǻwzpv?¨Y,ISIboNI!WT!0吷,tz'dvg9% uEs9d(q.Wַae8{z;kݵrvz+; ^,-zi^]#?h_O8u+<Vnm$챉$8^ʦe&IS2KQ%EҁiMщiRdv'_SgDeۚ[weʍnyxBd/s8_ۏEAwdDޢ]E<9K$Z%+^PO!38^_NjaU"8{D,n*9<ЬP؁.J*^x{(aB},J*|ЅrES5kLw|$Ur*^ 'B&{z5lT";UFO/xQ6ȍ,іGSg`?(Ka#m\y~j65&WJ?eYb-W*c<ËP\KL^z#^tB O/^ ^UTM"2*NMkQS7t/K:J5ɥ9RϠz-y&<#1#*/PCk C5/i:q#%/~,%HU`T k)L*S1;?9 U3 Y2O ʲ LŧJe/2LJ۲nPnD"A`6A+ƃު(ZҌĶMQ9uogf3"(<񔓕j~ŭ{rj>T٥ #27,B.I3$#Ssj(WBNz ]rI7mr+;g 2+NJ@$Q|<@`>8d- Oǻ}ͩ=/vȇQBaKM"Ft<*ʲI6*"Afiʉ4&qA>ty\8^l%7P 8nMP_1 %f$W Bwݼ5ZSO1fn99L$ L*7s IDATT*T&͋H3J^eO/8{A^äΚဿe\ж\3s++;IxzaZcY1V |j|fDLݼ#F68 -ƜV/P5'9^ (iF8#UAoү1g8sr>^+O//^*TueY>]Q3\\彄TsV5sU&_KEx)JF10*{zq!,z)Vm}VeIP.#q*6Pu iF5x>:хG\xz'\Mjk:0E P.?@~T\/DW\Zcw6 hڐ5/_bX=?L5QQ._",LE:nb"fG7ynźR_.UTRtz%XB4wnmVpwv G3|\Q4E534w?+G%EdSl6[/erTl&'o'ҼQm/G"mJ4+UGK2^,S #я"7n T-P/w7ve3h^dsrYQTC|E?N&Ε=?dl.OSNпb JNȹLlb] B9/IE%Y-$ U3[{[{cw齳{;Eތ "X)< IQ ꖃf&)WD׮czb>>~j&> EmwPՉg‚||zn'h'Z%W1?s=ȫ^oxp:%ȊNLŲ*QIF|p*iQ)G-b:ns28<_^j.,żry]Ŕdi)`!*;YP|Tjt`uC2ܒlpbV JfN兆;A]”Mz. 3L1e˪fhBnro@>^q00#^[QrErHVB bsƒh4AR`T(r>^,E7/oD2@ϼYNJ*0[?2/W >s7SxBV3_QD{3raJcc ږ[:Ly] ^R,3e,S+aA#0hh`Ř9^4⦆c8  //K_':xqd]MYr*UBC1M6V=N|PDsrH`Nj`~2*V5^ Nj[ aG6OD"c ^Ȩy\k3XP+U -sŒlRgcky"dlT*W肗2<@|G1a!2K/Q/W\$B%bĠ.rۋ1KXFr^Xa1- xwԪ5)v3vR:7!$H9CacU ) \1cD|&%]"pd>^ [~w[{+DW^s(/dfҙ\Id ¥0.m.6Y H虇KD y%=j=ρb%O1 }g_ܺpDߺ}a~*™n?G;^fYL9">ĀrQe{otS?IU(ar/7%U3u 7YF'2LwgлF" Opی68=ȫtЋ)GARgĿ8&'2rLQy˷[hR,o+*.M9 KQujNV:)Ǡwny D+*;vR1WpmMazGAH6MYq?hL+?Ԁecx6'rq:|ABӲnXm$#asHcwK>5Ԝ[ex~\,톈ME㵼5ԁ[1x/s5ǫױEݼX~xArk="vC.,X!4E^1Mߪnc0y)Vx],{**,)"J(J^^q7//Zی"/'WA\VȦa/+B=z~]twwSG[M1OAwys9R87Gy":V>JJ!0Eyo*ʽ@frf% pa˂l<Ɂ<#NX9ĒS1WL"^cʌANxKNi (bgU'G2(U;nRVURpZVF[F"tř?Sh]% "! /?WdvCbGc&/`b6PO>^uQ cg H=/lY4+tB')+Uڏ * 0xQ^4d9FUSDql}Sr@of Q_Z %DN9mwț|U%5Mhˀd=r|(f-jF$6ʔ[dfjȎ :)wUp[n.\xw@@Gw6w7ump0;s'S*JMEz$F<͕GDjSKє3mJKO3ONhvz%092.|:tOcCl UȘ qLZLVB SY DxR\ElRHط:o$ӪgGg}{o 0odR Dʹ',Bqt _E&fW5&Max[{zW7wַN$UP2oᦷbk,m$ێl7P5W/~z J^ adܥ8L6Wݴ\1HpI]1Owai>^?j6疢x5x` ݵ]NTx=m?J"ޤW'%P.-pAq-l b`gU8 {P&7!"]~x9DvQRB/Ў2PdE՗&X%v$:-Du@b,`% guXP q0ee!aS;<}|'rl+nY뗄y9 ⽩L&'%/i;! (!U@\7$c p vp[(1⭨8[t~$7&pœhZ#%FH$Sywe$FݼE|fmH5lx+$9Nd8>^1?p3yDvGwWY>^/ӓtm/rŘ᠎)"l]  [^Ȟ^'8dG2DHd%YuZPk\e,\nm:蒬 jd9,(n;燰N8c^xm/DㅒY_^[Y]xk[;"o~sH#(_J!OULc RUtK/Uj\n@Bڕnb}aK~z7;bnYwcwes|!"}{tg{4}SJgr:^-\-|ә|YVdǔs-+FIt.+Uv})w|s$=w^8`c}hݳ{;Y񧼥tC.rq/~8[im5ar ur\/2l+xKgzwD&*{#M>Rf" aZK3L6/jUL[L9,)T-R[KjW Gx'7s{quN\w}+냷.U[5roMSPfXZ5J56 %Y1iEԊ\fG_'Vco5'hxa.%3uㅧ(Ӫl?"wN0"MJfetƵh:_7.5D xo]Β4 #U3~^$Ti'w&njP8ci4 cBXq@(907n'KG,^̎4E^uPAJOnw?{nxG,~'Ø+WݼIoUGIW,T:1H`x T*h"sEF @񘚊(.+8WB՜2-)̂FʨIb/㎆"_S|m!r˨TiVaY3 >^r ꝋ8CP/)VDHB?x\n` Hx%=~Y兔,9ق/)i0;xy %B,6 a.J/ә?` I?) d1(fثa-.VX4y@o^lwŊ0j/vWV7\3V7V7ӻ>=$Y$J/"#Ƭ(%9}q29h9>Tuv|z>B)8qcV=;G[{=lZv7vgP8#X]HO{zxzɖ)U D.MlBIVr!)WUMry';Û3iG/Ҫvo} ]$WE/2{zĠ$,d.ɹ qa]L.uiѕur*#c$ޓ!WRڡI-P愦"<<>kwOF79r A4ݒ*dO]9am0rϫUܻ^zBOvzwvDSQlPV{7]HOBI HWTE3n⡻\^%|ae)7vZ!)V"# a/lݼ9\d83ߋc@ׅ,+j6+8^B#=W㪦c5.w@Z])V[Tg W06 '610xdU3lbPeTTcqX!0͋5!jkBc9( q+Ǚ^N8^V5}{pwO|zW9^ [` 9^23W< @Lē-|벒Y$P` |f%HY q- 7 V> ݼ^rrUҢa'NUqm,W l6\ȱ7ɨ= SzzMdE 5vWEAEYRuRĈan+!j\)RY.JET4LDz]ZnaŒi ST./_gU ۚ2j-wrowmkiu9l w6-$,)4Ml@0ae{enŒT*RnXZg>q#%IJHvJ2TL"'tCd5wy}0!{[{e"Hv0S ˁRI"ɪfئn`i$%)TEIL9hU-Dz=(uqX$QⲹBE8Ѻ<_zcD`ǻs4<>u{{tgk^p;wrNQ`xP?R拊~yԔ&ZaIFIS ˭VE`3r%r,KSN(2& _uJ|}5U;l wmn.tos;]_NOo~)VtL9Ӱy%c9.g{SXrEVj)TP`YL9F,WIhU#OrsrsKF(>^HsK^H+Md6;C5zΈ(AGRi8>gs$wHr}Ƌ* !Lp 4,+QqX8f (a`-W4P vp$'^V;C94[(±P#pfKݙhxc^/|BN1OW}s xfbEE3,fYefE%c"270\QB`*xy=+L\$p${rETʈzBY!\T8s;Q&_ނhOF}$Тy" MA"!(pTT ǎ#ƴ[[UYMcӍs`0xAx Ǯ꘣y Y,\f}5 &bh lPsoy!LDxbQkڠ7C]:=QD "th$&57"ov}N)Hղ4DT 'צb'W>^v׶˽~nV^z}O(Qm7 '+įL/% K)m׷ [8"fEFR{ꊪ_ΧE@ytֶrֶǻ񹛻T48LKL;%jCKg(MHbʥ84Į\U ǦCA54Gig ^3 VGjh'B&m7vH|ezp+Ixr|ӻ/]_,TP$ l^( ~ed#2%QAmC)W< VTi@0KSNXp'/z7c>^(UogMNRTY^4,KqCP,ؼeְ󂻜ri{UMs.G%@úˉ{={zW6wַzj^6Yɼ՜.nwV7^q}y,눑}qkŲRYvݠNKSvK0XqVC`/+Uۡ~ZEMϏŸ G.vףS~2e/Hר57e[kFӲ׏ҚQYͺ$МӡYZBUte8E!!ӫK9j+BMyVl31WVu]‰ixzFR5WbByG> E*UhN0-JCc%P.LeO/uBBP;xeM"w3L= C(\ZvL#.Z ^!#*#Xu21¥VΈ*pi /{z%V(Eo*K)Vqxӹ"h[Naċf98[ MEN-:˜$U$W!E9` IDAT^%+E.+9Ŋ3 QLt.\BrDUNj$*P@xuxN4~uxE0vjM'm0n @疯 zNkCG]l h˅T($_r6WdZ7ef6-4Ԩm.zr_vWj[,A,Yn/,n.RRJmҭM\PD#Lϗu+H#98rԢQmNOD*y$@)b߃y7X>;=wweaml|xͿurʑ W6D/ YNŊyal}'j1˥P *qby.e CL^'/?wrѝya1<5A/p޺e?s߂rL/iA*|<{T\ylLj녅H?={duF;;G-\(W61Φ2jը51 rj"1Ȃ˗z.WrhIħE 0>`BŸfXOe0bz{agqu^𽫛;%W]xV{}B+͹7?q wˎ[ skc-wQ^bw8bP)7H'*>7OZt/=v>?tzj|/xzS^_ ہzgaݼx.@,Ζ֠81Ę}h'Rf1x- ͈,I1Fy/xTX! 1+\rT*/2sE<go}xϳ7|/;<5^k^q&}ҝ}\m w)! P.8^|6'4{lF@S L2ݥq=\1_yR8#fmcb ^/|ؐ1G2P4>^!8I6s6XՌy+1m)v|fB4ǫSywi\%ƕ4$Мb7q"*~O/-c^#%/am F0}lْ\G_޸5@C'%ζAu}ЛJt˩ko.۞M n7d2'ypdƂ&"Tu9_'󯽹x}ujQ3/%|1Pkжfkۜr-d ΂ڛ\եnf~@K׍ 03`KP7hF|7_a֛'{q z{[{=/zz(M;G39jQKko.~;׉m DCӉ.)wOTUk[<@Sί5"^~הW^(woyE;{'wO_=ݗ}Myl,W^C GNGvCgI`UpȠ1x,P^>zNc5\(yǶ{ Du=LִZ|zZh{~}4?!ڀؾXeYul0h?S>4z u$T+wۜvW{FgLo,{AՄctTQj\*~+͙t(y^t jwgH;H9 x.u}pimVյ ]Mg }ԝ zD1mDQM6g*M;,xŗ~)Zx-л;<<<x|7|ۇpkg_OyuM{s&]U?>F:Fv\a]īrȞx X*˞7Snðޖk1&_7V8 |wpchε=\q-S6_oq5 ŲLw9Q;.l:y|O] V^[¯(wc^}koqe1k[ù53LEZ[7}.)BlPvγk@/>j6hbKws&nk~h7F9xlj gDKܚ椪 \c!"+|R2 d gik/[-c?=ىlYR5tVT/aB+lr!֢֫ۚ4bX [,Ւ(![xE*N%!,B<zN޸w=C~;<5^k^n:%;B2 U3rEUM"^8w&^ ^^Oxbs:_r`n'~؋Z lFԡPtDԮWo^'t}綣֐*Չ4M{zG;?[ E;K࠻\ӛJUEot>szc#nXo}&%bXfl}ix|?~=2"N_$O#@W r ~R7׿KoFۯipԩ92fOK/׺?}>.A,)cG>齵=Bn=<>ǘBNMgrf`wϛa]լ^۬ RlϦ2U—/[;> mr'f&]C({!K[{(!_ZDr<Z{a庠w"<~nG_ :Vj," jf}: P,;^06~\_>&6^*G޵SlB ^YU\Zs&/xBUA!k8HN5ҧ|QErT5'yP)[n&1r.>^qat|jP7/neoUJZ J12}8ׇ-5$ uQKHX ^peZP>㠗ډy|}@#H۟^w%VB̦_WH 6 8ɋsTgٙ4X! Lt Yp&d+rErc224F@uK2<Ƕ 1݌:%&b@q$W-cs$l~U%,[MQe:XJ2Rn!)fRBB1MvCNjW RuvC ^lb[ډx); wjFbe8bxa|7uQ{5ڮG)PlRFEfRA7-B^}n@DYy@/yzHjtjws켴 +˽<ݵ3hl7EͪfMTXSN 뭰NYDOBLY")MzcUC)Ww]xz9jmp%xzgۇ],J=MzLaD҄^X,-;OT4ZDBeIVrX5ZͨşR\;<>?})}ϐ|ty^ΦjԢf+{ ժ:t&GaCSδ]5pΦ"[jzރwx|ݼ@wuwJjCW3]n;~o>\!.+oYeY' ޣT*_M3ٜ0]ӧtkݰrܠX,vͫ!@t׶V6ll_ΦUͪEh!5Ci^qrLfYEè٨;j'ߜI-MEr!M"^mD9xz!^ж~uRɈ~4QrY8 8ɔ`syQ\%`-\@0ƪ.lNx)]tU@Gt.NtN/wC\F `NjY'xIE(8Ta\$/)UTXderr-zz ]<A,OͧWW[¬'W! ^d8S;Q6/0s Q f1T4o&_闳"7DH XF)v`H@7ʤw7(X gFHiE3qn"f]mYlUb @ %sRՌ'x]pYg h[A_F3I_r5_I:+FPT&WSδ+LW}4лwxks"@{'wcOý(|mO/r7DiڞiS{tL*yy#RBۚiWn1f2UF>XMJ`Pc+嵵͝Ro^4u׶gEYI+8L LlE'^9@ע} [~P1 ){iJOzcO([6)ъS6+-Pe3\8SfBN4+r~rxm?rk FO/U6ݤlYBA! ^b;bIu.@Cc5_tBOZP @[Vًډ**/uI*Udӭ?b%bYb䀧PY ~և @GE{DYNj8+x k"oz!WLUcY͌x3yFBՌ|4~3 㸏7_$0Ѷ&=^r%Bl4q@@8ɦ"Q(A̎qP8**qGÝB͑yZS=K_*jH$#iTDJ.jqتDSsB JNC&.1/J`ţsSfI [5)fFʆC3ގ"BDeE`H`ԘIet8%pT(N#hFtRxl(@d\=}ŕ pCZ^;KgzqVtGqSV`~t|+\З[ ZԪ(P"P5Zy*"˛Alϖֶ,s|/ud%~),oC6/Ⱦ BS.3yJ,o>8{zᅩR5Nӻ/{(fee qYTu̳pf;<5^k^}3Z X,y 3oLCgQpO/_-+%ڷNg񅿙ʐMWgSٴP51#/P1|\a>"~!^61*rr cXT͜FxŶAiCS*E/nP/1=((Zې:Krn $ą].Ҫ$E e'F@ 4=IPjjxz?٭QrMd@BOkAYG\x+b?#\6[ȟ!yn$wdZ .PnZh/n,ƅ{zيew:+ qko.\XBE`y}?<#yv}OӛaRzcʾe?hʋB{v;7v(*=qxrrۇ^;|z lʲi}B^ϼ (+b^Ukd}{x\Tݼ@>탧(^jd[KJt(Vaׯ .AH/lj!@Ŗ^;e0 E)?_{ޫ$ӭ( N3i|Uㅒ?P5-}t65m7j5~j~hSjq>tu\:B!D- rfcMO\3bYex+STx 샮z{zh[mf%3$Ш>m^unS^B ߤӕSES~@ Ɯa{Y͛Ǩ2BV6o VO[0J6>hE?ȡ׷?͝bֶ ++xH3]o.t7ʨL6a__KQbh1 U3x]t֜ԁrau:|! P53f^ 0t[x}ݢ4$Wْp`}=x+ lck:ARɬ[/ʐ`bO/d!LWRt ^,s~!Xb 'bUY`h~=NADO//s0XO5}9[ sb5CIU3#^$--'S0Wŋ\ V0R#@8U\MExq+Ɣ\ @!3Ŧ C 7&| Q*@y`W{=0> &zwNj"gSYiLߜā7IrLQ/^G(u1($Уxx"Uqs-+fﮬc|s%UT ^4j@jgj* \`eVRUFdx*wR0y|o2(0B /?WO_uBz*MBF@/ҕ EIR-urn2ʅw,# cZnl/1Y\mu??_TMTQdC# nO(lTͻGwnvM[ҹBIdUVe2=3-9xz;GHA|r"N_DV/#xc=xǮvݟa{fw;UuUwueIJL$R9T03k=n߅16] 6["5y=F?;ܵCWx&>'a/ډ"^ܮ>+Vyr!Ձ71 dzv^Q)1WN_,gNoAvt|;3?uaWxAt S8뭦Ē|TɈ!z0JeLpt\ff]6NDeۜ-2 y/3 9;fB&'ѸGyf^JF#9٥u'xZwKnR2xz?\\fÂSB#]/Yl8)U^j6(wv@O +g7o_|uMy98JךX(Ż™Q8~=<>޷G>ݩ+1M9^ v+A6:cZ B"^-'`.5+xͬK9iUK_3gzJ}јq0]3wƓW&XvfzzS5Sjg@tU>=^*v󪧗%nfxg(yzZm[[kud}?~l$7:=7Z}וLFg`8AC#g;iW"~ZŻK7o4Y8A0 U<]?Lx!)Vn6_wOs3Or~q%6JV}+9t3YFZN{鯛3<;iᯜӓfDBU5?6n4Ad9h GXZv{NgKs^]:Y ?NʍWOx;h;~0i\rNozZld|~eUVEV3UNU-WLJTUL֢Af-T c6@amuxq{BE:&/se'>]f9_1 NLAQ̤Xa3 M&`Y?`E'ɹ_v _xE˗_֢deנ߃cx jޑR ^@}-wGT߽"_Af5Ҭf ^+==9akIMvݼ@ȌRd 7^d0L g﫚ZUr׹s ]U>}U,_+жp"E#͚P,mօRc&eīVڭܨ[R5[O/\V?3 [q2VJ?OZ'71uzvڳz̛ w ptx%٬ADlvz>>>Kx:ohupIwxFS҆IJwN"d^,Uc!R'óKCr&7ONv3xq}es2"_{ޱvOwj26M6rU3= ^NwzO!RSg}aA<]3O%a<*k{`gD|ksiRVdYm ^mt{ftlMV3UiUKVVj6>^B%F+pJà\)T.-1ܚv"66!; |H ir! ╬>Nj֨Aר>^E)ߛ+]e xOή]H;ўW:#zzy~% WR`TmR @x$x߾}jGsлVeH+{Oŷ۾ j}u=īWjN}~%u gmfm#޿>^x u$Ե݁%vn8fm?~k'M0N/>h&Ū?>;Y㱄Eqvdu_;]K,/Hk Q仸xp{eTͩYU(_Q+}U;-藠<2f^+KӋF+>Ky)mreM#ѳT@|e)]k(_(\speĦtz!f3MU٢܄ 6 ^Q0Et-@J5 D[1.w(WR=V$BQov`2]|#8(ٶmߨYKeۖ`8QRov{`-hP?f[n4%ќ+$Py}~8MfsIrK& 0ǿd>95^'gOM! ߿1^d(rSG˱!crQ51]?P &vrhwF'aݫ4^`{QeW[J[ooՖݴܞ?Qz{Frv-w[ 0Z797δё'qøiN{K@FG ZkmRxYQJwxwZR>mu&*Fm/QҶUt#oarڎqObN mZV[E^Pit9-M7-rMk[ߋwzAi ٲr+߫~(?zڴjm6SV~_մbŏv J/7n-lT[ݶBYoۍn/__z4:NT+TSk[5:NrAW針~gxq| 9^Nj bɗJUKlNTKz+ܧ-*FPn\eh䷘/+VPqd%\4e \Z*BXi\犬Kepq+RRk}jPu{ŕܳ eK.V[-߲J#MpMG.)VZ۴LEj+\7_FL uT6\K/ > 0)|Af %)N"_ RE2@1UW|Rd{&i{B`Sa8^@ + { :xj6)V''^Ҫ ߋ^g3𭝁H|{_xhpi=U5s0^8^jV$H<Ի_v"݇TZI`{fio5k~ZӦ]rs2>^#8ARŤXq2 fx؆ A. 2f@t"9&߈px/r9~\YCsqT\~={u2~Q5K54҄R\Q+4KvtJBP&fU3CeArd4pͫ*[dū.BIl)x:W-]fu]yr(k /q7΃B!`5#V(7XTʺ)Ыd xsshw1"I_q֪ۚU-Z&*MG nqQ0m_j˪|A qza] [oVToUmtRY[Vޖrݲz)Zfe{]e{FXk5t]nˍvqPVt;z配NҰ]kmڴ[`Nێ_mM˳h_[/,7 go8 Uh,b88ް y?8o;`NQKНדQlz&$m/IۑD#87Hx })8t IDATȗ2ATlѴ ;IJ3G?_pZAgݾ>f XѤeM\Nф̿N'HȿZyrY[^Do}Od5\5lH3Juu2".VK ʜ:8]MSL.u 8ް.2]WΤ~c[xCey:2W2ZH~kcjˑ'wTkʐHx)_w٭4:n_p]itZײeeqڶd!tjvf^lwxQUF~A\4zUmtUi օru)H8_ך]Ŋ\! 22+Z[(7DTuTlϥ]Fwb)N]ОN.bEۥd íu&_Z/UlXmZns/Gu px>^m'BPOo\7U@y*Y5+T* RL3ctx]rsvxQ5ct$cxIʕ>Bʱ:!vb+T΄<ɹlM;$H$pF3[S#$Ca@?Ï?Op6}k~~ gUW^xw>^ˤXlMoxMѻWo_'Wl1w/=s7;/}\bLLѰRP BѲ  04ַp~~)j -ixk/4:091*e\Ĺ<` [7$&Lq`)~Hޒ\E]r$W@(M<HXe2\LĝIҹv;] r-SC$|QPX*/kJ=/^قZhޅ}ǔwAlԖ\kS)H\S>PR*}]-TFS.Pi?k-[|^4k-T! +xz*Nj[7=a\usfjmYnm6:WkZ޶۲-`r۶WmvˍNr[NeuzAr+nXnk-i ,䬯Sk-Zۮwn?~:!EufxÖЏnBu+Ϯ[v(<PLIf[pFY͓=(7U"JV퓣éPN#?˫d3%m`4sAt8`.tEI0`&"nnɶ~p*;H‹"$0 -ㅼ7tQ/&IpZ0^ 'kwXn⥝>. U3ZqTny_H` ^^0>a%(Y;H5؏&N ڋd@`ޕX]yEKΛMyR?+ \G< r5Hi kᔓ?o4Y35o*oɑr{d~/ i>_^0 ;dڋnO/Y=3Ã~(~d˗2E [@lN;p>:>1藞^j@xãwd/_]wK#ys0ˇ'TBpx[Fz6<`y-kpm\%+rۧ`3|6C3`%+Gm'AY/:l k>O)ۭlo8de{C^1mo F3p<ۀv'lϤ0Vp I7xc87 h/nLa2^xѤ5l}_G-f_pm/g~I&[Wd4]Nj~x6`̫1xޕ ōDЋ&N0'oxIh2mhxϷL2oY{2IFS9p x[dw缢x4];-7b8!]Nj`Njw_Km^?,n>ʨE20%OYhGK|LL,WЯ >dJ"kO'&m'h{-<^4H[n$gZ BAם^شa>X;ME ئ#h7f/wavBP94ebUo[F܎B <ΗvL,w3rf:|/\B@x9l3P.{3!%qxE~r1<:=GGى"𷄏Rkr^D|v)1ѸmtlQ ֢L/>xA&o?;~7G~<88GjMoIB5Dgl)1 !|z!) K.q2Ue:U(ڕ+qi3>UK897\u(ݼf)י|9/3~EMe3MRO/y ^"o :k {[KUנ\8^$J\qIH*[ѳu6 ^1T!V?TTmu2/t.^WJV\ĪM ĕFGZU&Wmf'$fWݣS,ֲ:7ȶ$&+喪-^rfY Wm7Px(zz ѴW?J᤬ٞV_Xߒ8 m{V?0An"xO:B STUղE?(Wmڶ['vpϼH|#I8ZAuC4G98؅é?lenNX0 m}poUt1#YQ;&wϷ|d/LVwVp8GSGdmyA"0v<Nי?%[ YI?duNxt m &Ј_&{`ќntS0^ Ed)yP0 ) n4Hpvitz!_TǗ!Q/ƶ@rE!fxllD.muۢ?t$-ϳ0eC/[`\m ǎ?my% /xAa,jv/kҪda|h*k q^c{7"ֱ= ,8ޮ, z)nt}=BZ."8rMn^*f嚈ʥ@h|ΊrC/ݾ薁к^TkfJT>]-U[%BϗVmΖjpRNL,\w@cx ͗(.*#LEsA8z8=Xڌs* ^m(\ -đ3-~%VC(YPFo1^i:hn޳ M'ӻdFLتdf{rv.DgUUF|g[;Z~R_I^2Rw/ j`vޥXf7Gos M:8zI7"g>^ ra%3xpԬ̡jHU3 \%'ހ,hbdxva/o sK$|~i8^Q&.В>zz"NǫX}UU`\/j'n^l90g0Uj ^^j0Z Е|-R(VWj-Z܏sj!k9Ҩ4x’v*$Q'ЕJѪ41PWmfޖdj^uZ_ێxT 8A4ZHbmL5tťx54Af6C+L8f#H7} ~ 뎄*EI~١-Bt8sÑ- pliDI[-Op8{ʞcnE"ȺXLx m'腱Wް-NQi/nfug^cN&dD%`2n@ ^Qx>Y#A>h"1t4LWhof`4dq;YÙx?LV#l3<EN_^|ή|~0RO\jz0J= _/\~]M l/#<\聄 Z 3>G k_r(IDm[^LJMIKcWe֬e~* A!E%{w5NhJC2(>(%݊p{_,FJ𽬙;F̚ +I`ѵ=%&S@+v'!%3ͿJPHSu͜f3AncTL/rWLdTL# Fɕ.bQ2_\ Qp6u iC.sF ǾjRҔ%ܽ?=zM?L֠ 3!UTډnޗ)VdP=C>^LٴWS@_xq9lb#g32ْb EJ]K(-zfƥZK̕r]n6c7iֺ:nMZ٢$p/q b;NJC q]`Z4̃rJCK*uv%ճڴ$W׵˵vYSZD:`0_,L\k7-?&'hPyզyjH\Sْmӷ݁T XL8m۶}# &eؓd&_$ ^8vQKߧp,h\5?+*Dۋڎ/ p⸃{kDbo8if^9hXsz\JeG>^r+j`QO`!(ǽ 8 JImB/Z#䩽 V7ɫ]S0$`u(өq$pz; 挒Mg ,GV'[TWO{oe=&hGp~O']P\S*Hx)'&KƋPB9Nwp[%&;xGډjnާ_F<*O2Y/Ofui}t7Ov >+=NT;-ah"t4 v<.o>NMϷͣzN'EIH^?(+Zbn[I'O>Yz5YXAۭ̿ Y{sǛfݡ\JV2<}M$-Z3Cd/!c~@Bb W͕k0a*kڞtBC[Q:QW%`-9p7.]dZ7&c~_kz%HUvUE⏦a<'WVP~0>!s@0ܞ<iAp7Grk:~ǥ[˵KYsM&(LnD V.26յF$\ESﰖݚi.ZK$[o7JZ~wt*%jk$YPQnmѝ۴zpwůKKAb!Vi"^A;N26iMɋ6rEp0jT%3p4ExMF&svOk;Ef X?1B'%{q;5)V*2YxMr_yтkt&0"&%A9+O?B>o KʇP2c5f\E/1WÆ IDATNDoIz;EAoj/Bޚ+BQ5 g/K2;=QApG UNѯBS_y iąra!X)ǻxd2q㚋mX:4XUui2>^/\j.\AHTT(MOZ#lALa)T,7(!@,giPG {W9 yVz/a.2f2rۭ~ZiT<)Ue 3p!ՅAjS<obg2z[lk\biQMv❪ͮ\RD(BY ފ*rk~[Eu-mjlY]獸gYbCC>Z\`81 gO3cE=%U+~4T*lV^%J+j(w4Uc[ `8=QGI+7xSgjFS^]vVu,dx&8ERֽ?d*=%^@0q +jF \C lsi;]?B/g *悗 (QX7e 2{LduLz~Ϸ0"=IGR?3w`w;W)Y8n4V < wBb;;<>;P*uyv"Tw&\fy1G/nA5x [NVw{ޭ>?&3Yn?T}]<BwHׇǯԊP?zVo}[c{S\֚bu|r 5&٤Xpf/ jD)i#^op{zOk/H*vF?C* iS/F P$5/gX< ?Jֳ,]\r*hb7 ]Px%iP/i΀bY3Ksm0}rkՆ( \5;Z(~]i԰J]j3W|,i )iPKKl[u8f'_rZQ5;J!2/I۲JS pmJ2ZHeݒ le|i|@AMy]M|iy\ŮQx*uZT%a?/$լ!M6~FQ O$Ӓ"fNQ:o(sՐ-BV ծѴNVn$ָQBOp*;L낂#pm EGE1PR`ھOPHvtbz*XMpUM-! |z9HInXڀ[1 vdu? պ&.n&;zv#m!1ݧTPl󰾗)`ōh=V$f}Y^Hp/ӵЕ ?n~.n?K&tsCS <ǿ,:ηO7OB/o?<:yc*4$H-n7_m^܈wy<|m$iqy)MϣV_CqӘ?7_V̷/Vۃ?N8'aɚWNqbޯ?G(6<>Al# ?w-o?f[(_/'GS1WK/FhgUK13ێr7@1}~dͷ87&v</$5]A}X 2GN*'a F? c!pڽ3 48:O\ #n'pds|uQ5䪎ڳ^vSFBMc9UlU IM"iJcbniHcYaV;^ԲZ2Y#`K5Z8>w s` pU]%b'FWTB:jmY]LuIB,ȶ - QKw)V:bb͗\36>^ ͛-THB\ 3$\<4Lru$TaW5d6&ğ(P2EsӋ JfPA`TS6i/=/f'dH),k(ܿXI.4.˛O ??xHA ᯈWa{MZ ;wovݼ ^<ćG_l(rSOclFጒ`gրX/.kx9ʳ<.Liy^:\h ,GMJKtm8^b~ss#\z=t=x@M7?T5i:Fd(ɐk~sbaR(Gt5ݼulT[ʗ$69_K:5֮Qtq+s0f2MQYQ.J+[=7r]Eש-umRV]d-+U<%#T5-IL2N~hrpZ;'ԙ6qT jNjخi- <qSmi)߫. 'ACq|IMAp<rmk(&˞n g.rfsˋu"o8&+HԿAs ZvQͶ3 V:ZA,Qu[t@8 ,M `5D1f`ҡy!a@pY|XUě8'ػDh]}-$L/Pp}(XUY}f/$Fx~<n~> W o?Q2jFLr`_6?϶A}6';o~@>{'_0 BϷO_Q L4iҜ+Ni 5^_ۉ4kDDx^ x4AF iRRRUOyQ:L6"pjsxzuMg/( ?o|8^+Ax׆\%!Ϛ͡"^o!9"oB"Z~N}Ŝ_A-Fї\y)o/g^D-Mҫ$rӫOIJ^hk+볋 Ԏ]F 4JӪ+U\{ϙB?~9pt;BܓK4^b~,qV/w]rU^K jeh͚pHVHY8\9p"rs/a.ZݶrZnt#V-U\kIJXp՚ !^v7Vriб4s %"\}ӛ|ApW| t +p`iQ> Ǡ_:tHRd O7GZ! 3 FȞd#/+@egZXJ7 %Q"0F9nzaN<.aHEqϪ8]0{ץJ#Ii=藃SzVЬIxS`'t' $B^h~X}t}RqPOQ3)WRflpMwBBrn~5OO//o~'_M{/SR }%^'%Ck}s8%Ǎ ^TQ/ZWD>^.wT,rI7/2.I`<:G.2kͅ6 g1X] {~ed8ǫǫ(lRW{zQuf8^6.藀U5E/$0|?5?}%OӏcOo ˗/_dW9^΄T|yME÷XQՁTrd5}Ga냷WowW̯u[yݵp~~8轂 )U,m|xsp*Çk+DౠYxd-B^o8^4g\"cT4AY#99B|+CHZ1^e [bM? U/qY" R",U3^jX7_դVx]Fb*Wdj[mv8-Vکqwn|Vi6NJ5 IPu /]=4oYΒ̎#Lf*U.Id/7-Tk[ԫzX#e\//8jF V V:{XpIK&j0cHbV0DQhYM!d_@΍n ڊE,0Er/DJfjHb<ߠ|NגM;h8݌7 /50Q”> q<lښCN8߀?jU%u6.IJ:A `K%w`'=ּ 核Lܕ@A''P -oEW D7;<-!E?_p>r+d8>^ıpջOwNĿE/׻OU翭k*'"m^}<,g !o3F*CR>~g]&;Ik _w*Pit#G C4C$-55 [ݓJ0 ]|Z^dyp*1.(Yu`~xgkd#h7BϪ8]wa|4Y1&t wwY`1X+!`8iYM?JPq1_ov̠ڴ0eV%Ӑü7F 8\mwFI).;\ù[W+f]itKafڶ˽h36} qSK[bUx]%QE=cBǤ\A%ΗUXit }Vj+FmWT6:\Zmt؄~A- UU2' ^x en:_](XL^ډ@B#?{bP2oyzI'gW*+|r~IkLU6m%U~c%N7O4lId9Ttrv9kҪ~\ { ~d gŋW*El8^*+>^㯕=zC2 g@QOHBZڠ7xQ/5 UDXmӻ }R@S"cr#"gȕ2uoΧ-e [Dˈv.\9 v*H.rR +&Oh- e͌Osנ_QCi(!*Ijx+v%ʬLd6. U57_-Uku[V(Fn.SWeP%Cr+.}w5{&) g!08hDAkގtXĢd6v\bm A!iUÉ˝𺣩Q5C 'P5*s7( ^^0MQ`"X0w<ݧ|W'Md. ɲ 2 g!PZBbM@df+e0Ü,¸BpwW4YUdu?29H?IH?cD,iUGסj&kqP5/Χ*h&OFἾ j"a`J< XEW ی(yy3ӯ@hjV nl 1O2b،̩V[1Ck!H pt(VFJ-F ,1(INVϤȡL'b" Q`_J 80옮RA{Z|^6ߟōQ8OVf3-}D.̞x-: /"ς8^{XU\H8tCOi^^~07mE(i* 9-Dr%Wl(5S@=\Ђ$Ј#/ ˕4}¹#K+ g. ^.>r۶pSīf.A!w6H5 P"BmK7/˨X{r(Pix9;)aT(i'BL +D/hdsB NjqQF(Xj6k2<2Eh[kk/6]q#utq A{zpqu+[`dډ(d 3WygNt~Ц^ePP>E&ӬaPh}zP8K>λÏ?5?_cOo ~#O7o_<>}]bTt^R8\ x"_sF;xX IDAT^U~<0W13 U. P5>^Cs ,uU ^N/2p_xY_k(wxQ2I2koP1V6[4i/Lޏ7Ai"<'D*-f`E(k@k Pmu{Rd1sM7<< *3KRƍk[dFK-*ɒ,6 a i0LC`4ݢA,$ ݦ=3@C7 Mss[Q,U'ϫOF{w(NS@Xrh ]ܻ Sa% ,tIHؼͦ"i4gp̂~{N -x;GO%FX.`* y;g,-o24,h00cM XlS[۰NHǁG7͓ Dd)>^wf󥒚?%60H54KJcx]~annQQ3ius6`"^ʎU{XA"!;`mkazc\ e*`' gN\xA P@̮"vb|P͒\%>^29'/ԉď;^|$ɝ5@o+'0'|Nxjlp!P"3f`0hm n/T_!%jpHxwXԙ>f7v GXNzhKp"瞾![KFVgMC)`*I㲆=0BX3MEr+=P(kFSQPCI]p Un^sɦl  XڛL~jmYjJGǣw\l-G}betʞA Mg|i*r\_/׏ZϴO9&⥏% pPp)5A-0o 4 ŧ$f|q񲳈~4K~-,(\0rE,>{ J*¹oTCF ~'"$ -wq K cWπxIB;0NwA<61m+\hJEb%#ǠYHKLemf635}@xz'gXŭhmxthȉ'gMqb|2H& )ʅnlbV̵ Q́xTZkH K/7h'Bˇ hڴ**.ƍ >^B\&yrvJƁ5|7.57v;SF}P=^_wvyS|i'v5wesO/L\l Vŭڴ8}fPB:NOIB$)&S'F ޿zP [(QXT8J*V7!C'ͮ<OW '#/Jbi'wkߘZ~p0Wo=g,d)O)1d"c0`uql}ttd5C_evvIi|`rMvdɯOh2B손Vc'~u^ vnֈxsk[wwu5h~(ٻɢK8%6aKyxa} x 7έ}#ʘ[nXln0uF}psHvֶdm}k;n.%x:h)NŬQ889\^t*RdAhpp\OmMjk" {fVAȋ$Bvxjne`Vرm ְl~k3#6=GwlbWR:x(ـdn3Ffc:ŵ vWDa{ (Dz ri':_!SXBxݼ ^uIWKFX)uReQ$Nx\U8P8c^{|o\i Abj 7 zQ5_6S{1ւx9 ^-c)E|beo uP{9hK>3kzzA~={m.UDOgQ0G 4n H]&)2Ѩf/̱&fA܅Id(ϠV/Ȗ27$W 3Ɍc6_ 6CN;쵁Z#.6<^P\BD&e'i텧 ޞQh}xz^+#ytR_2a4Қ-5y[]=Ļsg\\7cLBxWs_Y MTz _ހ:dbvi}=B 0 n"ƦA`j}HfI7J`2Us2m¢zY7>7xW% @Y ehl2F #l51@"ۻpKElrcf47w|o)蹤Us\Xl}Ơi2B0xzK>Q3=rѯayHyx"p\lb\eRn^At\Aҕ%*[(AxA^t|xtJ$Wqˍe$Rd投V+fAI VWXݿ~NNn^|xzS+f XlCy/#Z(1jjg,70'k7Re:6bxf-1AXkuyP)N*ųBƈŦ YV3 !™sn_"AbYUEȞAжQ58 ǹ<5W)+|\$upw`pb֤, q*#Q:qE4 T\bu qp2!B}Ccn^R%Vfq"SXGkxz9*.xf3j r2'VdFFZ>F+S F]*Jxz-NC|xwU&|& AZ -t o\oS_k.ey YBS6ln-hHmLY.|hk>wۤxyċz9x[TX@ykZI*zk JT|umv4<.ZY%ړ8j~ܞV|#>U"ʂM1R ܗ,.b@:Nj; AP߰͐ưr2hȓ_xi0UGxŸael1_-yciXsd}O7)eh|#kq)5=[;j{MI/Y8^,WO6Gkقxpx: +C# ^$ã;rv"ֈ&(fOo(Q̂rFƁJwkDuށxI܉85QBIGP&< &8f59Eȏu/^(ܻ@jD/u18NVAi+|/ Xe\SekډHU3mi-aج(n^L v",,^*@FS=Qt.fٖ*K&>|FKS6]}v1_A4B -J<йp7W6MSæ)2$DgHX$, Kk A3pg{FMnw$k"4h5I{)o Ŋ d5f 𠩈*ckS@˓Kxw[bM`' ^ ^jŇm]MU3an8f]_Xv}Lin>^RI [;jb#yz޴+r#S֫vvaus֍7Lz,X-.w15 m;G3kMK@ۛ{'ǻoJPf6pT7g7$ aXArs$ˑx]5}kC |^o+b0L%)V58kO=$U=D^ sCm_ä_<$I"PdJ\%|q*_U@k| TpW4FKet}zV A#&fx]ЀU)ai4]\io+Ix򫔒!?ir-`3|OEџ[# /e;& y=a<Ӌwes+Ҫm V8-Ox6v 3O3Thm&l3D[ k 0LZ`nNV3 klS9aRfnRK&cp\܅W8^1y~TKy/S4 aiȖ%WIh7md[HBQ5ŝ[Z'%3$WզP2̬Sx`k/b%wbvAP.MĔrxl2+!+_/)V65<6788RNT ~A=}Cö86(FP5ߑbtWnQ!NKnsWX4Ҡ\r m[u@WOUkXU]= ʍJFMSQlb%UB m`KUxm؏7jn"(̵ʛ\ <.ݼwǓfvA):ׅ6h6W3q^xMs{<Uiœ׬>^87V57Ӫ AA˳qpE,^6R{i*9aknlZ MЂ]JOPu&l:tl3pN77 <&C?4tYy=d6ra5\Fj㓦ʡG& ŪohU32*h[x]7Ѧn/le[b*3 >( yr<$"3$dH-x QڌMbBw|rP?&gAYr!Zl b?^ Uha3$U3 gJEbP5/7.L2E.YTFhzfaN^d}hnw}898JDr}qp^Ҟ\ߛ["iici45%΍#0:]XFK+,.2 ya{'OI)>O'{0p^4wxq%,Q8K;h&Rڿ8= IlݖBI#dҤw{ւ9c0HX~? @W],%-@e怓9oJnVwO m(z+DG͓#fM~UNn3p)\5eT_=XQ84ux)R}|~U#ȡ@wO0 䈓Y\!닦"ֶQ4llv" Yew浍D o,$ièi.7ͯ9]P ǻǩF4@S߁=fUxn 兕)uÐ1)aƚ:94RXWڤwni]*L#31$O}+>fVQ8so<8:A]uj m+^s/v"b w<61-wlbZ/({ IDATi6}q2Hmp-g.YqXyAFYf[7gS 4hhLTt󲳏w3DHS_ %i|(]}B\""T͂r!Q8 B^W|61Wx]΂xw%N"^έW}R{1kN)^HUp\E/^_(\_|x)= lSQ*cRP/h99 rv s6µJ_zasH=Hj-z@q7 'cߎ @PDKV3zrwg=A9hs/&F&Oo.*rǧ,MB۲&#2r}nk3rg̚~ ^lZyfJ' [^ YzH_bNUj3$Z6! +urk#ܲ3K[{u:}{ sR>sD+(O}hBZikmw(Q=kjm<ݓM0 s+[bSAP$W"F;G/]"6Ǝ \o|b{1#D$њFC?gO4-k_cvYYI[ϮkBqes_xWjiE K"4Z񍰎pO'-[ta%}ן|^TabTAN{;GOP n`*a'7+Ҥ? Άkt 7iZM@gq}G 1D^\rpF澠q=ӴňC n.oBŮ[צ7+9_wkdq0e{캵>}2͝#I "hlL/DF@)Cl| AIֶV6 ':~WK7/$ rzk8^{ \/ %$9.7FR$)6[mrAjmfkׯISe498Z33;=?< IB\`\1{G@}CcunQKUW24rmA^~a٤xڼ)k ۤWR-5*"UH -U\5[ sM:c+Bii-ȶ%x}/]nK_ \ Dj9^Q5C8xzAo7~v7s d Tk3dX9p_n- LƠE2JEQj ^d[Z&͓ ޙUJ%- *%eTSƶQ2 kU͆GolPi5U)1;G6ץL T6kխ=Y!}G!u$+Xug+Dۇ7j>Ah55dܰ4 B_ *WBRk95Z\cɷOAbFcj: *%>1ildOO{0k]0%Z%ygQ2`LJ,bzyBBYKOhDU/ 1O¶]ִ=d>`QH\R/%Yqo_'r-#aH~~CcҚKv狏wesi݅(_\/SPБpϭl_ڛu k ; ]X/nַrKyb8-s9{J aՌF%T*1T%Oȭ+DHQ/$S}$>^S(ﰷxyf2J{b!YHuKy_ZIppVxz<ޤ'H?8^ȬxzGj\ctlщ~6/7 -nZ!lJ rrǭ2vAǞ^[}Qw`+a(rCTxrC kvHA᲎}6ȫXl.yaMX ,7ܔǜmk KmIqܦr1!{@qOSw7mel#npMEb[\Ϗ ߋZr0Y5="gDL/Zdkzq'f$P6ȭ[xM./op׈YTV"fJ7be>Wyfi]W)4) rk _o,ml6o.wм ] K\äses D2΅mcYݰiORL4 IBwr %z1?%RLA+ x艘4gA\WX7IXVP |$KHmm4Z)8\PJAS Ki=tnEU21;Tp⮕}惊_|d3:=)8#i!*T$y%D$kèpRw$Ūj.\%I!Wg$"J8J@ 63W΅8jzkvRy\^r +DxSd8O-YM\H]]@9fzN/*5WLNmAF5Rc)_㞁aŵ4f rl"J6AZT%I[f=ky{pZѯqq" $Vߒ~1(v&x ߛ+(/_n\[^oٶ;:+W_ tҎe 帏w[xRqdQ8Tl{zy\/^Q;bY Ok饩H80[4ra.^~[8^a\K`慿ֈVx`[h޼Xbr?OTIċ$&@kok1&x8@ё$J/].\3xk3 [ALlhlf9Aӻ< >^k.<)Vr]"c0YۗA*)+pp-tll( M! gsjcӋkS [{y[zCzJ4pic{r-ssDNxOn*3K[)en!DMÌpBbD {| HT^|T[.spFV+Jf8^/-\؅nq@>n@L{'QȤ! H\,9FKTzKqMd~NjLl 1=kMQ,3pXhq1A0.Y`ؒ% ̹}$}xzn_|מDSM,-MvxzqwU*6v"٧TPV e;}/<2ҹEurTT5f7D6o< kCy~[Qtwk.mȁq/>>+|/wxM Q}/^jx94)a <|TPp~жG Rylt"kv6-%K3F::Q$b&fGk3uI|™y6冠D hܕ|ksD KZ#Oྡ1q;hlLWj8F'8;5xQ͚QnE ~ci+%|/utyO.mD &*F7*ohX9^^vq[!jgb JTjD\+)V>r`+nk︰F߸N&8 Rxw{Ae (1:MU3ܯdA>^;u "+r9*G\1= "{ebT͞EmME~U3u،a6-w6ol JAd+oFKS2>|][%/1Wd]tЉd 1 [h[6M1+.19xhj%Ŋ )ٲ:bmNq;82RNZ&rp30Ah`*I9 74FSQx88ʪHawu9=\I3pJ"^թ4wlbvlbvvqMP 74 +un_]rGbQ5#{J6]+۫{=߂U$bN[ZZCG>drgMr&,qYG6vwi}chcpvycam ^wnB4>M;Ռ-XYk 3& wV%xzLKr}O=b3эwAB&7O縝h@^f |{cQ;`K ^!BPuR_d5K^T2tjmp&S9%tjsꍧ+1D^x%ykmb81\6QXן| T;udG2d./`/c#o ;DzO8dO5klpP ]%^v$û+URkE <Bљ WA VVX͢Q2/5@u"whlxkqi!jxǒ3Gfͦ35 ʥhhtA\58\S*M8Nj H|a[e +q$lCۢ-`RŴX5k$Ѕm'_N"^ڏE/w~R@I ^nl+=-UL+=pI/!2I sI2o[{\}Hg<lj/-įpv/ݔ&~q8^Ϗθxz)"9^)fO}~Q"~ D"Q5ToȶhV9)8H:8͡NnQ5Zd/{{}e{OָG@Tr5oeF\8뢑™k8mۢjElުQ/ja󉳚 AY&6yYhOΐ]In3IT8n^[0ux9TWEʚo')b8eɛ{W) }]oAqj%%эwFPqy v2=O<+1I/^dD=riU3`b!JxBHc*gЬ S X%\ Z^ d6)$H%8mNPZop -vA?x awo|~{'Bݑ XA3I"Y>-m;6'&wxM/WxzW6wEɼ\ߙf]kϭW7Afs!}l;Ay1sk;=bPn}xyey%rπl儠Z0!;8PH^A )-w%k8vSR)x[]6-l\\A]i$WbU1pD_1coɷW |x9+U 9 *U,} P.nQ5|sc$E v)Vݠ_g/s<,mxxdzZ݄d5/ր 8&5$m$vTs̭me7ne IDAT&#`cS`gLr%`mkoyA8kT͜"nI4;;9x)ƽBNy xJ [d쓉p7e+_.۔(j/8ZACuHA|SsKqrUsV_5}֦ ryQ:h't plFߍrFww| _Q5ObxK䆁+T͢,3VeXx]cv$WOrt@8!Ϡ\ /emhVIj@@(z6*F $0/VKT#BgZAoK#MEp-`@gqҦp5E a A¢jza@$Z™u kT`WQ8Q>ǛÌFY^ ػDlo "TW,GጪI v`e?X%*m.x)X1Jfj*6%|*q-)ME a>k0xzxM w`4Kghb;hc 3 ^ i@*?Q MpF͚ IMѩsXpQ5{lRm ^6!]:5?0RtN?132>97R&3ylbv6CrӠK'6ȍBNj$&n>BD b%- ycfE֞Y>$wjnemko 5/ fEn3N5dv^NnI ۀ̱fD} 5]A(WpKvOD;v#`aԗaqn ik.F֣9zYtPNj X5;Mi4sR XEaKmʓ kmH!Iƻ!ƈA% 7='7aCd Gjm;/TOkwr_1z/B¢wnf:!!AB`F|lۧZֶֶ.u Q5[[ܝYZӻ ڽ{Lr(;uS W6w'uv>^r\kNb6P3or3$Vpr0j´'9^dujIUxC썲 &bN#$ƫM.4\,ߋ50R]+v{% VP2#úwpBY*/Ks`=}C&{.҉HV쭳ώw5.qU5Ua9x-{zݼrCO$q6_NrW$W:[(=U5ۤ+1C^-TE,)V'1[B-ZA(Ż{BX= 8iTTdZ'?ޙ 1W͐g`Ӧȳc'mbAށx-c:% U3k q5Ȗ(~t:N@'  2.8^^:xGڥ^oxŜ@:L+ gluP=:,xzy*?ȚߴOgϏJُnQu*xpA}xnu:N@' tə7W,^P2'9ޤW^L>&JMk5Z^_:MWprfG ,]l^{ /WO:N@' t:zxzUG@MJv+wTDݿ~NN33^\β& ċU3 v"Y'ή5/|/H8sQtJS7iU6*zOpu:N@' t7gxz"`& ċr[~/_@oY'pf.txz񊧗fy%9WR@aNP.oڦNQ>*–I%}Cx- T|xt:N@' < Qn [0Yѣů+DԖD;fNߚN-v;S>$LnޤcbN0g:{6H=9'pT  t:N@'&E V(E-} ߫w؝:SNgɸ&x60gA:F:6 mI/uߨ /H۵s6Q녹s2_3?Γ  t:N@'N _͗*~? 3Η.3|~_'`&͸{Si li-5=wX9GQZd#=m*9U8[^7{a䪸(e cLAI!3fJS +}6N@' t7nn k=$Z2uj~#U#LUGǣ.ޤ+hzzd\Iܙļ{i'2Wy-'pMEy % siۊEsf7̞*gU+:V|˒~N@' t:vx{\1_˭7jnk+\%([av^ɤ37,\ ⅿq8^֨^Ϗ KJW/"^IrϏ񆹢f9|zO}K;}SzN@' t:6~fET2'yC}/^'&ЙjrNƬӮEWp%Ŋt8r3nYdtBٿٿٿ~iq2Q-}?cٿ{}~a~zpp泽幍ҷ=[>^6riy+w(_זOU_ȾzzWO.V V);7x6#tJTvu1{cR.lo_7+#\G*cKgOW(bO Oo,*žo^??{ϫǣ݅RX駆>~}Q/ 7xw˯Lo-o~{Gzzx_zeW?1;ʇoj_) _fdм/q V'/쎗Q?>+ N@' tLd QH[Ao{ǕwWTID[%ozt L]AMpRijbsǻ3H>3%ъ 9i?~U8clڍ=n=W\+-1cF`yi]Y-o.|U?sT*Cx'4.'k[1,VHK?5^tzz~?7ş4nz z- /~ h2֋cv^z^.,|s{ʕO;9'N@' tlb9mJ;WxO/f5ROo?}P'p-J++`v^Q8^`g0/ȖZt<~xmh֡"[#`Gy7lTu߇76=囇|~},̪dͯYH˓hA_:7ɩVJ]#/{&N~BW/lWc)7>@ѠA-7)=|şV ٟ^{^_+ꫯp1~ox{`W2A:N@' Ι@RG(Ln2*x&G ΙQ&T5p\Ǟ^xM̕(q&U4 q5Sk- <dl "j\9oMic?ׯMMwwM}UެY[AtI+禊??퉿'ok[Ü{NY߯TP^;?{ j<c?:?==;}/}vֈg TӃ޻ O ʅX~xtT"o~#wt;_hw^}oO/ck~'_~^{[u$N@' t:6|+[(R_tWΘ{{;ӛӵN+Nഛӛh+k"^fl Uʶ^ k3>kB/̹a6 BqyDX#~Ao__\wuo|~艃Bu\ OL}key~˳2sO 5BW'4wʀR_ɗWfz^\otW߮̍ߖ9k{37ӓݼIb#k?r t:N@'prŲFٞmCroGǣmrǣ N `%Iʹ 6]PnrDr {~$^zzìAAd xڦX93ߚIomқ/>1 7M['W?1z?gM__~v0-#[/~n9#N}Sp7ό~S[(W&; 7zˣޯTpihԼ^z+Oڳ>wO/N@' t:6k^/e 3zzmkh+H \ $y=֩T:V/_@v2rGq $TeMnsd ?f0&{zW?9Ã宮tQzz7~~3F1ͭWުO3dCuuUWT"禾{&>R*ʻMXwOپ74VJ?H' t:9KnsP "|Ret]}a'7ٞ^޻=} A>^&c7n09fE|ߞaKV_[M#Co|+?5zO_^?3&~;+ES5t_7?_:^ߙ쇇MP~˯_0귎,}oo?[Wɿ}mX\8T)V𽅿ɿ_XER|;Ao3J><J|r8)2{6ߞƩyp+0o9ڣ t:NM _D6B-޻x"_,zo ߩ Nb ߛDkMxE %ؙD+I2|Qӛ|Ən9?S0D~|C}koXLTKO_+~˟H߽>~=2tEX8]+ 7WJӯ. vbIKNО Ũ]֊աSyRwȧ_ݽ1rOn4jMF;x_p{ίys=O:N@' ⦢Ry=wI0gz 4=&*;S;PYAd8dyU0gb ([dF$X驓›rifX)ܗk6:9<+ N@' tL W,GyR]=x$q/:HI' Rip6庂x[T͐VoOr{zA錟Dqa +TzasCy/ L t:N@'Nd b9W,^*onk+hn$r 9zzA)%l*1tqrM= {/:SN7hfx][k%&=ei*ܚu ^?ʧ ii/p7Ft:N@' @,Tu zELos/jg(N=M@zzNj9FV,|xz@6*ZK8ݪiof{M7c-5ޯ)IS' t:N@'NO/fl>rN@' t:Nk1 [ȗ*(YVЛu" vF߸N&{zm,3 Uekǣn6帐1J;xz a*+z Xa7%Mq7mc t:N@'xx'd ޳=xw/]n|f|-`@gI vvh+ӬX5^P.|祐beZ\ ~sxFDAQ {St:N@' t7W,g %nZdOorARqa1q=MOo69ޔ k͞^y;ήו>^''7H9.^вF ʅ㕘+-F_9~ج\n^7ק,? t:N@' < 녹 [hTT8KU{Ǖ+4^Xo^'J38m-2ױ(W|Iےh%|o:Knn89c@]/ o̢p+o5W t:N@'x(&q [𣼤XvnI^*-~U|GHg<^8^VK&s{ǕG/]nt=jnkȗ dvSt*Fܙr:iߖzxxm>s kIOG$Wb%f/e0)%2U79 {vW t:N@'J^㥰7[(=~{~zwzNO@|pL9J;Lem>3ۙHUqd9&mA,/ݼx}|F XF0e t:N@' <OfέL/D"H`)_p$o_'&'@rUSRi:Y;֦KU:8u11uT3UB xz0Gbt'{vW t:N@'0ME~xz0 z;x&06sMsQ;*#'m򜝴Q8'=+Wy sqD5Ψ]¬o=NڨA&*a.?#}:N@' tD՜+0gZ9wݽ!|6w\nk؇_'`&8\e8^S_v ]~[s1ܒzAi+msxf3~h80녹9Ϗ2ߌQBN@' t:Nk4PXMzwIBmȥkէ &}3%(Z>ޙ)VV,)Vxzƞ^ǰ\ns L*?ʧ-˖;lvt:N@' .RXs&Ù[xzEM^! { d22!2U)EL/cq8^ѴB8#o/AV|AHޚ~N@' t:Nm2|A+/:+}w+EDo/C'6)TͶȦX9c|ox-?nepx{?s@ k̂(u t:N@'x& >^Q$״^$mAWDq塛`ejvME錗JeLC}tLeHrfMOA 6] 6 7pv8 O/ME M2D~_ t:N@' U\T1=At襬HA&}"uxzq<:w슏"tg$W` {`gAए^5)V?47̾t:N@' |-d %IX2p,vx/G7NOcw:N&Tzz r\Z}{"dݼjFܙʐh=f Q;{~EՌ+Ϗ2n`LQtz.St:{FU?_YK4{Qsܱe[.Ʀ'0[ƄB365b-wgsk{4m*k=f[&  'gBe])zIRՌg0o;CZսb3g Rm+Vnni    MIEUV{zVJoL$Աw4=&И"MV*mI )V+x{rrUbem^5tDh~mni߽k|ɴ܊WPꚷp~b~,y|   e7W,AzOoysֳyq˞Q|vzz{U8GszSi ״T3(^=>k(^ л ;viETg?/*k0U]c-ї6~igN6}b{kX0۶|]8B?c}j^ϣ p%f s0W,oF/=RկӋ} @Sb6,tl3-0^4ƫ{'ZA+j'YO[Q _?މ&X:_(A=gy睿q͛7[wsb{дb{s-n}_/Vu 먶ϴxU{kT_ڭ=wEp=}   M Ȫ*XYx0W)z%J*+*G񲧗 16MW-G\;mfkXcӴQ6~1Xcbm;>/^Qќ^Ϊwzzg̜-wÆ blذAt7{s-n뽛/ m=\?: D6Rc„bky„MۋR[f}-P۪˥-=/+X!z+->]sS{DŽ]y    NUnUνWEƷXd}#"H嚶5ڄ5-O+^GZ(^Ӆw\W_[1$ڸ/6׾ow5?~ձ>O?o>zXñgHHH \m ʛ+*}u/;9I`'(W+^5gmA{z=x-5Me'=q.<^O9 kT8c>^$fx U._27o-z7n܈w |~hCn8tFS}Cnͧ6{/hm?{Rk~ Wn-򭋯{oO*rڼ´[?OR6MNw-Rm~w!mRqqO~EK&E7:X,&Nhk】-_>TsS JN2HHHHN97ӛH֋u_JX~H҆(TZuVe]lXaVFV骵e{k\/>ezT8c`l?׿Ң7 Yqޢweћ١W_s9ؓOwxzSvF:_]_wEE{_'6}5s"M7wto_#_,{K_;7V#统qaYK//?zjK:9ilɓ @Ux =HB>3N$뫞I*F7RuP 5J[iٮ*VxQ 5_#U!C,l yzQ5Uyss;dy[[w!-R޼Ŗl*/ s'F?[Y<1UR9q揞hn[^ʟ:󸞏?^_Hޢwc*#׻[>UiG?ownWMMH$@$@$@$CfE7Vry 1~1W/E vоcEr*mmn01v"JnOʅ+=ZFO/kyt<dO;~K^`wӟ-zxˇ޵| 5/k_'O`f[~7ߺG>#֞O】ݸw.<z-s8ݷ掅G   f'ȸA6/V+ͨp[[bxO=I`,P6Vsz-xka]{\b,5 + V)x{ԯ&Jl^۵ͺe wȾy 0a뚷yEjۖsVwʛfuf3oo8g^}ܴïݪXs$_,5MYv|uy/\}ؽ[~nYj*cWSoUjf w]/:b>{N'HHHH}:J%ZUӛL6`:J%3xﭩU/˛T 4j 5 PX+I\Vzz"U&Yl1WqTɬ%ۜ\x7{ݚ[w/0_(VBSЮ IDAT׼70HHHH;X9~X˒,*Wo._y o~o0rO/~՜^CU8W/*6C:n`~E X^TTO/+WϘ9TjdK3f^ruWW7X{{$   KUxQ\y\MuI(^v_޻{/G#J@5 a=^'`]=JxE'I/"xnFcҪ`"y+6- 8R9pƠ ;iUpʅ߫\]Ռ^_Ύ"Yx$@$@$@$@#/Ld Uʛz.L&$9Y߈u"YL6XOCC ШRz|]VӉ4#p^Fe{jU:RQFjr=?_uPmO0YHHHHL>l)PHnZb~D/ S=޴a"/k;_0L*J-Re6-b%nڰ7"l^(?x7O Pxn^HTHʘ"Q g=^ gkJfQj5-'m(#]l2r-~\h+@9xV F)yd-7HHHHF/̅B-HO ?jrx칶.1(I`X[x˳y][aNViEu!hxM7m^]-~߫l7mOM- ǞG"HHHHbg|䫈^HBn|-). Hns*m`6/|ݸNjIE5 vJR-ʳRfxFYvVti9UUE|]mᢡ.x\/=zd1 ~nSHHHHNj *No^T8'q g=]#zz+iK_\Quxlӆ-/2f1Ho<*+ َ?IHHHH N}H sjHo[墏pM$ٴӻ]X#W=dtO+I\ӋFߨY®*%wY(^xn'    A沅&/UO'X}j[H;E'ADtboEo U5hvU-kb%V*% :Y'Wye{(x$@$@$@$@$+m/zz9ǛWxW.%C`91 e[W\S\X/~B6UoE"9u-/>Whߋ;_HHHH`T@OxɇBfN4YQs"))V2ֈ=AM $W76T,sEUsY cdrUWi<^{UOT57^U~UIW: Aqs]]݋|W-ZoWWI%    2"x SE_ud6!U\LH$PA@Qጞ FʍRzbQpFU녎`f"*ZTVg¹-]]+V1sv~)f̜bꮮAHHHH`6A* 纲+P:=5M1;5M[<^J3|`\9J*-/V,b`AVoƫ}~y &r+^Bk݋{F    8A&\ 2XWO+Ӊ%еudN$'5+sz{}'vOL>dcI;KV    My?POo9Y߈^Xd @c*x+4҆jgIBsڰSi4{^vUUahbzw "gT;KO/g % gb 7tuuϘ9Э8c-whQlsI-MeQ:~bs9 [nФ,Llk.NHHHH1U,EU3xQ w\MT5K˛w]cocJܲE,3/$ʲݴaA񚖗6촡mǃUaΖ+h+{^bY YO%JmK틖RzI'ڧyy٧l}o+!L TTVm6k 9m0~4xiCEOaXm;PMZ {(T\=(+UӋ }[~_N<'7jkz3Gm{=(>r]DߘHHHF0 [X9¹BGErd8%26!SdF:*>޴ao9ʅEVm*q}rSi%_0Wyټ2wȜ^U^UVl,/on>-l밟>v S"yw?x-e׻7=zavM$@$@$@#x d W-*xK`gt%2a+ WxMULˁmL-:m(E`;`RΨpJ>ky븁d j[r_섅MhnYxM-ld,wzz'Nսhɾ˗Z|բ%vuuO49$@$@$@$@$0@PNNjENo"Y/dY2(๹ qN?6p7eƔ)ټxRկ^tugI]VXi˅ ;ƿ#> H'zzL9 J(ު=u*ù.Px$04 WuF*W%ZYRlUqوrB6Mpr]/_skB`=l$@$@$@$@\Yfrl^n|:_cx8F7a" >-_[Ab;/z Rj:5 m%~Nڮ9rl~fltд[!HHHF=0Wl^/JыJxV3ĭXw?4E}aEUݣxkbgUɬxU*c6lbe: Kche:+/|]HeYCUJHK$@$@$@$0 >^/@Odl7JZTN޸eOCۣ~ZrFsZ)aUm;~ڰY)Y<^t"]X"x$@$@$@$@@|REV3TnQv/x iӴ jhT*w=^8Jf`=H\d5Cb6iyһ*trjgcOy4    =Bդ cL-,o<^HTWN+Cz%y OJ#rєZF~@C /Bs(9k5f(aT8o[vF뫵Vf8`    sJE㇕7>}^E2'H/TeRkxh(^=7mzjpN-X ;\e;>|`t(^HHHH`@dPWzz ԢDR5qdKqn'A+=WJȳSuӯR㵝HGӆݻ0MK{~f5H$@$@$@$@CIY+^?3zz/PvX#xCy< \g8ʕf4BFV~uӯ6;P(Z+}5 ql+'    |T녹*͉d(ފf9|H`2W\Y49mB:Y<^MKU>Gytakٞ< ' Jjv0g~ /}b%7 wTMn' xU3ҪPuuu^ lj+ae;jR ŋIEPj[V3x+!P–IHHHH`( H;6p"j\M]"YodOP~<׈&fL*RQ̦Z$&0)VR o*m+SxuP酲5LB },&̋'    lA [+7޻[u]S +Y ! W|h6iK=Sޯxڌ&JՓlPxڎϟ$@$@$@$@$0 @:~(ζT7E.zw%$%#/@Cc'ʴ1~/J15޲lӆeٮac/T.5ݸz,_ 3 |W~&o{2[E"[[K$zܟ&0CMTJfð5\ӋOj_6UcrzR;6L5 &^_7V:urefݚĸ:)Mx/$0x0jfㅲMWW/7Ҩj* xguq(J_˴ JF^OM'mPߋfszIHHHT5[/~oiU*e$=C,zzQJ :2*<^E3n6Wl-UJ-E`g=UEtzGc[    1N 9 [*=^T;W:q} 6C"kXEˢx _CMElW+^4׵?v< ",5hHHHHH`xu p2jNo&jU\UMy*n`ۢY^(^T;cc>^+#R^n*e JZ gs7xbC$@$@$@$@߉ gsWmmb;b 3 YlejQc* sv(^xRՌyEri9!iuSi.ҭ$0T8k6;nzvx$@$@$@$@d񆙼;J&S0~@û &Jzm1Wzz~MS-Gk>j^ 5 Έ>^=(0Lm׍^xrmk;> ^:ZWV([+)V+I`@/T+{1µGjye% +ײh"-JoEJ C IDATȫI Twi Q3<^PJ/FUT5Cp4Y=U+kQ({v>;q_ Ӌ+/{U㕐p&oV"pD+Q _(xO}|bfb"&Yi+pvq F$@$@$@$@;$f7Ț2Ü녕NL'B2Is<ع.ór |ԘJ7 C х⍯Eh΢x`gI CUnTzR6lX֢x As|6&p6]~S$@$@$@$@$0 'WRRx3L6h"x2R,b=U:ozY`Y+MVˆR~o*mA"9 vJX^D^IՐHHHHH`0lި^Qx¹Y|PF gBBE3\x5mL0!6KOiyJZR'~/e чwD$@$@$@$0`R(^7ȺG~%=c:ð$Z*U5<^ñ>^U %G26/*mǗfG     k:d0g{Af([$W+kj=u+^ =p4xU3R0(ǛJhg~g C6EOa:=t =(d}˞"IHHH'x0WWTn㸾|]5U͆2]2MqUO/]/} f:n`ٞdƺD.Nwm.H7Y%6J UPU0^\m64#=C^xJrmO.#?> M: \ yWg}ҌW/45)Y   aB*v AR % ŋ`gɽ2H`⅚+*FUۨtդ"KƔ AkkXz{}U(^aێV7 \Pl翝½9|InBek/GB@[&/HHb])zQ\HʕD+xԉ#x$zB{E C6ґՕe+ʹ\Se5es֣\sz>yFaD={]/{\i7^G拽_mmO+לW(}ƒ{$  aBvUrxAV2. +*W%y/9a&Ѧ*{UPP b,\CUp_jVM?!Uk:ʴrkMcO0~AJnM]r\kr}󊳔 HH/yaC *szE*[[%U3{9w,<7B@&F1xab/*|*7x\dVeEO2UU5.ӻ[zl Jnm9$@$@$0v suLeU*_nK$xg%귷;vNI`W4xZxP0fle|][g[EZNφaxkyY5u֋_}>ym~m_PY* b%2厉@ӆ#S989yX=޻-WdCnW;.[|+XQZlE׼wZs-n7nzkjin|c2}qWfrsnwgఋzxeVs/={y'N:Wy%߿hÝw)q-Wh%ڽG#  'zzU~sxWRzT5Njf$ @@1_0,4mLreN/Qxa)-G^)==WW8ZF~)G~%k=u񋛯8R~Y n%~kRܹnw/?e˖?7 |0 6c;'{Vݣ37A4y Piw^9Q_Cg^{Mz䝿?{鏼εOk/lS/cSϺןQӾPqO  2Al0AV1+Eom]BfM$xD~Ȯ'"MEGnoŋF5H^=nWU2KUs0!F%кw76TNRrFY(^\xnw/߹- εO^~3O?-󍋗NWU[ vz sw{o/gy;:ד&OK/ n҅:Ҫ#״bSe+w*ײ?n׿//,Wn>g>۲ڷ޻gҧϟ0m|ۺ|zzw$z~OVjS?MWٺoh0{ 7gd])zQ,j~kw<2 &BR镵E 4RdѬg gQa;eJuhq W ޖ]Gm{PTٹ_s[|#'VSA*|;lm۶/֭[7o޼kxqtd 3c_|۰|%zSi,zNJٻ_;Ohgo^j꼳";M_O?hɄ'nf4y[>oN[C".>+kIJ5^d8kŋ὾i*?*;q_C9럿Φ|ڭph:ܳ~%Tw (o fK/?S7n;&n-zO<õe2¹ԼͷKhs >{ 7u57}pzۮ6AƗJ>̉g_}3;ngQ% L3^yU^+ۺD28xy5tQ^ C[ߐJ7J%3l[T,{z^F)V5D,B㾮FƯTbxi~ szs~-LkUߝծE?j`Ra?dP )ko>2B t{.ʫޫsxcA%,]lXztgY\mzB}w7łŎ׿~=ڦUU˛HH`t_ rZ{~ƓJn' r,a c1U%Ūw!nQ+*7^N9}"W^Y븁iZ7l7/>?>{ߛ](kz[ۇ7>s}K ܹ?n/zӝ~sz|]|Z;'8a&8>}R{k[z΋|Ҥ)36)4~ښKŶ|uUln4c挩Rmz}Bݖ= $@$@$0b ([TJ:nP׭MúB2ȊR$0  t2ـA )^6{w{f$@$@$@I1vӪ67_K3{zMyYT.zzӆYߐBO/T.,\4Ub CU(FV3֘T$=(hrl>J΢Щ&FaHފs$@U 441(.CPE9uxm+jū!*mXv\0W΍cT8a٫ZFs1uo_ŋ@׷Ils {j)z'  A%~]߲+̵9G3{zGoH!*b ۘiڍ4>T5# ~oOU gYy9ڎW(zZx$>qW .+F H=lHHF s sE׫^ 5H&zV/Yx$ YBb6o*m$ʅjmhLK(Tڈ WW5/\bٲUUiC%WA굋}xÜ@W9^{Qo@s=ڱT oO7/ma]HHH`P}^]?tV%TAxu%Zvg?dP |fQq%x{Xl^T,xQ\dfq+=@ r`2<"7/f*J^?]U#!=&Z7Zi#)v= lX~y$@$@$0 U~%SU5 L'BrUŤںhHk&'PߐJ+VzzNej*9^ՌQFiK̕aX: .&!1K/RlRUsen*m =QsFQlDN H:Fڮe娗BzA&a&ɩIEmň(G_g &95ъ7Ȣȱ<^x+U"QaR?Op>]]0yy$@$@$0ad=?cX+R,.`h􎦇2bhF%3:uxa*T8JXbD m\pfdƆ<FE-]}¤]˻E&-]BIHH`@rg ghUdD."Hy^' YjdŋټP>^`_ʭx+SirfShaWxӻCKдr5\/>c܁HHH` ؎k֎T^عxټXHծ_@cxO!b yATXACJOT8cV(^w,|;wqW&" ME=~ v|$Z9:̹R%u l[% ZO"Y?sO$C+aU.BQߋ5ֺJ~6-Ϩ|Ƥ"cz,A߻^R/.@uo%  uѤ"[X^he+^IJ7b(ZɆ],X54!$WA7 hg= IDAT1WiÔ^_8a+eԯV-W; Y͐RlX"v+o&WP7SB)-A8`\-# Q"ON< SLs{魩IZ_Tp' e0r\+P(uJf_E<3p G-<^U5뤫";㳈_P?.r%/bJy5WPse^su\K?L>l'b?p]\Տ4%zb[$@$@${ WyVO* gދ+ y޽ȣh%<^] ۗ J CzxQɌhNj7,}8&ԯiCeul޴yMp{O3u篤PvP8N NDo tYsǞ}gΠi>P荿g| GN/7r= SV=Ksު?.zE]~ y   ~S\/[xwWEs?[$@B@yzNo gT,…* gЮuԯxBJfHeYߛJrȵ,щǮ=>⍻BpR^^'WCE{ۼl~k0){fwޭ_mtMGp̢I Zó.sN>O:mGo9l>Gn5)4O]~SN<ሮ|'廻@&[ؼyo~{ϭ8`}ӦMO?ֽ;xٿA~HH zOoOo!;Ud}|;%j oDUeBAD夫>^=W*E++!sz!z_=%zJ3O}?yb0s.`ͬB3/:~eg\{ϯ9nZSse3;>cdq[:=`G^5W]q+¡YZ:K:kuqןb6Zofl//76ݪv Kw/]m_[T:;t%߻.=r %m =p?==~3Ƈ=9d崖\'廻@殹_~^3K:gٶm۝wݕ/d{_ Sq  =B=b fb)+{cȝ$0@BĢ2XT. I˜^j䪊TTP{zײ݊f8Ʀ J[,_}Gw¬|/uªy/xʉgݼ7yBG/={y%o:qwɖ޸O9ʇ*9wԆӏ?y/Ւ\oל{_uUoWms>$zJ^{)'|溟0i;1ߺͷ01A;jj/eDK.~/ϝ;m^~m۶ua0jћdvqVU4n$  -\fmǯ*9PH e][K+AH`@O/Jb]\՘R;/-{EƇJ0GUbePC-Ź ucwן{\R˲kz'禹'jk]w?|`MGza(o.JJ{Ϯۯйv^w^{齧ߧE7oQ+__nvm3N9總ZG76{RSۜnyçw[Wmtpo#? guζm^|Ń>~m۶o=s<Les'IHHv be;iW*oWR8h|1;~x_]_ۯ&zJ_nfΏtƂV9lPlļzΟ?{ >Zw3_ZyEYt|ww\{uJ諗RVō$@$@$0H0U͖S魭KM&jjIEj\tzaGMf3^,|~ӆ)C}%Ū!^aaH{z!2W ^m/}J͢Tk|܉";>v 5T*M;OpD^GfG?숖ε[>up?zp ZnM>Uuhћs/熃75YGk=@ޜ>riEnnE?inN;K#ڶmۅ]]ޡ³ @iP㸁녕NU_7ZԲ$0 ŕIE~ƒE T>#J5[mv%-ǔbڞޛVhjij;7:]*۳0g?5qS-mS:Kg7X٩>Xڜo?w| ݝ[>TaՋܬIk8j:N@9rJonm/{ⱫԢwB-<쯟zjgycdBe#MJ`u3[7?t5 ;A`y;)%z: ek  kZe{W&Ub;xe]>1E@T.<[%hSiQRɌ努x3c굅GP+3mRfh5r7mCZ׿?׿~٫fDuJB-~pO?>Æ}ZŶx|_~-ߛ/N:loceb[fy_~;|翽莦.P^ v l1 ʻy0L{ ̳yԮ)^7,dB5\q3梻㓖O=:C#g!  8 v|Uly{zW/"'H/d X_jl^_ZW颧WEb/^ ok9>[e.~woo)O0cdeضX*Mm'E|PiҔ{OԳ:eڬS&E:e&P掽fΞ2!Vo9~ubkKL:vj=*q]x?W:=Pvq;asp]~vhћcOvcHHJd-[)*=jJ"T5>q<& >dSAmoD5zwEJU3^$Wau=WW5lwa;ћus4v]ye:\t'n-u &8v]~vpdѮHHH0-B(ūUzzE"JeJwOcYT߈Be(^[\QXJ3)zz=VcT5ze-4xj^f+_dOHkТg^Q6_#wIHHvzzӋںDEU/wY;PՌcp+6"J{w?vPR}PɌ>^6mRޱu NRr^" T^yqO   J3RZՂWre$o"YoI`l/WM6l^"HC!n~}5ߨWUZ٪ g]Ռkl״("T(65 EԼl!+gG T5ێX9nPތD.[xwY;P\QܣuY{ )M&jE" ~o*\7Jvh4BEܹ /XtE /Xm}C*iq+W^Pһ+zzzu%s~szu:ӝ:Y5\f /=?tU|N|]^AA 3=c(z5L$@$@N@<^ 09J6%E/],dR{F:oE$ZApvx769Z끽z <Ǿto&W7T7W_-%kZ7@Qb3uHHH`W>^=W_݉VszEƫe=jrA, 0ie69C& JP0iacP޹hEޛIVFefvDX9DFNYE9 "Ph"" "(4T C1v9ݧO}}s}Y79TT>,v]k{Gﰸo@r\ 8]n$sRjhel 0Hx)..@׽ppw`]_*MC#kc') R(y}z3>^NjV aF8s^AA02>^KoT"ċʖlzɬ\/"QҕJ M/8帲.k^c{M|<yᧀCz]n̋5{:Q H) ظITsx}QX*؏F2ns.s[R`3+*VΗүk# 1+Zm˽ym+-PcI|73M/ܿ[w+Qez7w' (w#/R@ H)pPNE==qާr_odD  >^TD/NE"G<^Dfdڜ^yTjMn݁,}_׿@˽ uᬠ_? fP@wEs' R@ H) 6j+ZoO@ET3N<ގ^=fV /JXE~o^xE+^0*2Wno(_*hRV|gRNπ{?7+uR@ H) \xѮuЋjU][Hf0~Ief2"{Xd\D;Ko:[x^{a[!($ >^B+0oI}jrg_R@ H) AQw^lқeswxIU.Ɉ8rzL.cls~`bGEmo>b;Ux#I;:x#n3ٔR@ H) 6@OTk帖ٕ!EKV7)֑mT6M(1yE$3#Ap{Fflk/8B°vvy@blq1"ǛofTk?LI) EJZ녏U2›K/F(LFa@l6vߛ/n3ܶO%< XsP3`R9yo ZR@ H) ᭀ%^VkaN/+TيVL-V'^n@Su2WL⅗D2NG4-QRQ酿`-VvR@ H) @oZw6wW*=q;z;Y}U.W9oʅԠ@PQT`tN/`T~Ebkxa?v.vVsR@ H) TK9[:2E/}QTEk(] -j5[^+ŖIr#_D8~dz_"e772+?I) R@ HC_[VNoP9[:љ;JCAGޯ le/Y΅ۃ\ %&eo1\ml[@n#_­9H) R@ f+ݵ>rzO/63ȶ}C'n6kxK2N`Aؼ~]_>^,:8|,s^[!ї9{VNf!^) R@  po^TrVzV 7jF(*JiQR+^^_ъ5AI-TpY&{W-R@ H) R@[Njzy%sK IoT "w~WnKo@v"IAN/{I xRC3W^\ H) R@ ]q QTEyN޼pFήՍ%6YJ`TW<^m3օIZrAFT3 EZעnR@ H) RTyt.*Wa#CJkVrUT(HE)fP.TYES2h66#ǃ^r8lRCMSR@ H) W[bތ^7/ڬ /v*BNݸEUso^h$z<&ٲŎDLQͶ~75R@ H) ]U죝fF23{rі\Bg,'R`C v*梼pff͋f&9 vRhrd/>0-+^R@ H) Xպ2ͬ\ZEoGgRџ*@n7 ^$xǙKD2˅m=ՏR@ H) @+ʭTzPk+W<(~]mfvE\NߝlT@2|UK8'ro|dK4ΕJ t /vWBH) R@ H7H zqB"7|vVNtdS \ezqpIhEeK]m[4"]+R@ H) T +ݕjo[:::s[؝uӋ]eѦףvrA"*W!TrQ$ Fd[+\{a9pq+_!)\_+R@ H)p((^{W%rYQ=9HC_B Epbׇ@k ; os *Fg市8Wr˨J) R@ H5z+oWCO/xe3j5UGg!kR $4jF/G"}ίěj."C$틬Iq\uc[xQъ[[(9[5R@ H) @;*yJ7|帖\rQvW.E(,~ Df"(|vF*~A J[x5,Axgá}zFR@ H) 8 [NjV̠_D8ۜގܖ.@Yʠ8,`T37pk3@[(-$9*_p6 Rm{n_lꋲXpJܝyJvWBIMR H) R@ \[k[d7Ψ\E=[̎{5X㔬<›n "1}|lz>}-{~ǟxwW|h¨;1ݹ1\/ϝ61q߹3Gz{_y줝ѧ_o^k|ypi4'܍wOu)8=?䉗vu:fd8s70Gnis+'\Qٕ9Igȷ\eZ~Q /(>^nkEgNE.]*QR)Y7R@ H) @+P[QŪ]+ZE3*V]Ļ[o-@|N'n /gN֧^ܷ/ww_unreg؎3/rb{ι>+{纏9眽{~9qrepΎy7&9ӧ>Sq?ٯS_)FYxg^>\tM>|bp̩_|G~Oϛu7t։O>5ќ|nlSKy$bEETY{&mOIg˝@Yx-sWFϜ^ӻ9LZ H) {TCͲ3{ɜ~68ˏ5g,ynւމszO6'Nܝ?{3#л4Ff?#s#뵳:ꇞ볻^l_q?'s&z[>ww]^c?~S{/y5 hgoHTqZe3f~o۶.R@ H) hk[V*=.Sd@/k56M/oDLnFL>!5ɀ>o5׀LV*{\{{/s#7kgmAqW\h.!$s뭧3<}'N(Z=;FϞuc{>q_y/YZ'8ˏ>yYS#?{quwem6m"UdЂx[>um/b"aP,31*R@ H) `ބx+x3rzutռ+ ܎NOoW.Օ6"Jo3lĔX xO~'wȹϸG|{f~X=Ovcw|z16sՏ^!Q27}p#LHP-}}?f1}믿oYti?_xE_wWΜ7tw=\oz70pc;/'_xqߏoNsr^/v/4 M9e~v_~_u ē{>sW=K~>Nǿ,XzcK>ym?|/w0p ns.3ٽci$^VbT3N,/5 #wTA2x15RN>]*R@ H)p8(PzjkE/UrC;ro}k3<ڎ-K/oԞ˖ G|zU^@p}ݳo'όu (di4o#;?ƻ w}o%ߎǦ:[{O:旮KWxS]k2NGO}W|  _#G >k{'o9cnEjU ];7_(v~/(A wuuo` g/$^ڲIԻ) R@ HT>^(d/#߻5;:s~vکydW9xەgǖ?z[+k齷 S~{:}י_{՛ן3|#ښ֛8dd3{.[vQI^u׿=9q"f>۟~kiCW]-֝ʧ1;?}ʊӖ{~桛>rDc|ljzzoz駟 >kNcg~贷9=5yy?Cn"'ҍ}>>m#N>K{y3]cg<}c¹~מ-E˭YJKVm h mVg_̭潠_r+#G+- *ffkՅR@ H) 8dZ9V*ZOb_*] Ef0}7}c}ɞEY,d=7>.{Coر쥷#;G{g-gxͤ߱]g"ej1\oIZ5\7oj+$9bKF{AެU H) R(ǕJ|joBo:<^n以Me196>e Gw#I4FSvyӣ-0[sha1ش'6vi NLl?ȣva]vcSAǚ6'&AǸ.6iLMԛJz}Yz4圼BF8{e*,xf~/"/)m D8rUquЬnUAnT) R@ HvW`%v]j!9]9VbN/(hRsz]՜́F2*WffW6"Uf!/?Rh) R@ H))ࣚZ\9r^V3QTŜoХOxZITd5Uھ&}R^O/'V$R@ H)@V+˜^u"ڙn*X)|nW.BNj}z]7r a8Eu+NR@ H) 8t`N/T=˜^T $h:L@P«"_ռr/sB'mm؈Q͠YV ^8OojnR@ H) R`]R9z*- )78r}ͭP\+W3RvIF;ٛYU rMg#b̠_lJʵ˶+WAoJR@ H) x= Tk=x37ήjFrz_йJHHudksz/rtm-84R9>^4bæ"ZR@ H) @3R.cD;9~%增3I+of"VQ^$/ժ c|Ub/zx~#.R@ H) R([rE ~]K|Gg3{m*V7 x>sqRpKe\(ZaM^ H) R@ 09]qA%^P.2EyWKR@ @\u5یpF-#t G1g՜ r\A$3HZ/vFNo/[R H) R@ \}z9e}frྥγKR`]PX4Mk% }_N+/gnqT(O>EmK) R@ H fr^D5DoW.ʭR@ /rz}$︋ iFT3~Q*M7_$ZG aD8<^^BH) R@  $9>9jwWf[ mjmQ~]$b^—˨fneC9#>[&3:>sߋ݉%yUZR@ H) 6GK MuYߎ\{l/lq_}@@P"<^[/f#_mm3#R9eoXGE_ZR@ H) @+UE Oo>3xm/73R@ lDy>~]8`FN/zxv7FZe/h .N`Cw/R@ H)`NoWKΡl*K] orz7B;#KEd2Rs2Wb"Q~`*}B l껲rszY[+ j{I›+R˗R@ H) hwœJ-zs:9\xoG YxؐQљc}f9Z"&9KApۢO 0׻|vCf8tPj8_ H) R`+^xzzsQ3=hxtt-'Q:U(1mjE$7 v|)foX6KeSnRٓpC%^&R@ H) ]F5UF5ZUW._Q8<>]f~yQ[ ~ ] EnE>_7Է D5'ߏ-m Zsf/JrqR@ H) &W{Tݥr9e<gOW'o?y>`W~W5:Kqǿ+z^x^xWmd pv=~g,F탖A$^-hq0] R9.ǕMK) R@ j> O/rz;Z ^7o.ʣë*d5[7o~}߾}>N +qw\R "QъxmHdJ\ rlFoiR@ H) \JΨh^D21(Kup%3w zBo-^<=}1-ܮ[ySywG#7j%2y5GsRrQ_yk[C-|u?_[|K/'{R ,ai+/pߞ/O᩹o?.,%3߹kO+GG_wf?]ܳKKwΟ 4ZZ8t~79pڳN/AB끖yEoT&KIN/~]tN[K*Zy<&2Y6//R@ H@?/珆_.spC k_}W|he/$[n /IFvR@ H) 6ns\,Vg xmT3I:x|:o?}K?y$8yշMn{߽|٥u?t)^.~?M7Bϟ0~ju~#!XێmE{}%Z珟xz}Fdt!k{z?_~g[~ۏ巗лip>bc >̾5[mdFT3*Q%>^5BjFM,ӛ򨌨f[նx؛+R˗R@ H) hw@$^D8Nd{Yن@S&`yH-_{K8vO}cf}bO.$9G̝7L?z~ia'܂F;K˟=i igZ>>|qō IDATN}y7{[&]$9\W߿^?]k% ezO:9#qvoΞdXAz_|nN4E?׆^^He*{Q:έ]QB0rC8_Ep9h+ݿ5) R@ HӋɞZgF|%ގ`6M[\Zh/^V!^_fcouJ 蝻{ӶMFmZ^QzO 8O10//^Oo9؛]y/^3zҥ~]so幅_@л_dʩ۾2sSjמ].,.2z&;;?yzG,tx3zoFQT$)|HE}flxx ;_t>^DD߫^HH) R@ q\*R*7^8ttuthʍBGۼlXtU\d]ܸfLɨsw?}3WN>p7n˙M{h;=33}v>z?s%Ϻ|,]ve;Ny<ǎ嵻*Vxw.oeLڠwv~i{;7 ޼iK;KWoW\ռ93Ϝ(_$BfEyBZ@bd·Ep9JqE9K) R@ J-pvW*5B2ߋhD;6tf"_Ox89gv~_^ӯZ?a&'>?)]|؟߹7.\|r21 2Rd*zg>|<|LfvCwv~/>vZwZ-o]ˢ}ٲ񲝢|Pb4#Y x\(EF5ݦ>9 m|q<٩;K 1͞f9_s'csKSN߉?ɎWΝ1KKs^>yzRS74ǁ},٧wiK;?z3v8p?=e"tk`Q* o\/aA/΁~ ^D;~ZKڧR@ H) pUpή0Ѱ9hI$rzKDܠʯ^,o,r=nu ׹^=zq O[{Ko_\ȗG^uRbo2 ]X1S/wiv~_/._kD-9+ʯWڱܢ%5[eqM+^z嗋jw.gy9R/xˇFq3,S{iKkNu"-js=Gec߾}{޺mFVXЂ~_Л8Sm-~h:awLſJBQ{R@ H) [8V*56q5\C%Ap=}~oGgU{u3*{_w<.3;-...\X\\Z\\ؠťťe-e,.[\Z\ZJB__bmD\t'̾_*CmVmOmhjYrv~𡇞?CW^FwI UMޝ$[&|36 R@ .ܮ\Ĝ^QxL rz(fT3,E27^pxU5y) R@ Hz#bUʪ9ܑO uqn `F H(+(g}m'>Wě$6w'b$3wL-ZvFUw}QJ) R@ HV{Z-Ӌ\ĊVlGg[!Un G  <^0_Q͈|f/C -"b! VNf g{m3}&/R@ H) x ͛\KxQ.)pp`$3D2G(_Q al]Oε7N*T'Tʍ&&^om{8_ԷR@ H) hk⸊[Z5>^/tt!$ dM  5@oTpQ [zZ&v" o㮊U 2n/8M^ H) R@ szR9T*]r}mneϴ7B73w7[}ߵE ^e.Lsn}h4NDf$젭oI) R@ H) `^lYU ٝ]9{xKF5x;roȦ8yȍQ͈;&*VP,#삥ޗ08F5_pR@ H) `Ts9qB/im0"R@ d) (׶I"exvsխ̨VHfPq.ʓxoI) R@ H) غx-9}܏Ļ {ڜޠ6xK,љA 4NA3AF`ɖ 0[O;3q&\&jVO|w\&p%HFtk 6]y:a$9 .3H)*P.;VfT< b1#K%7JCqu`jbm'Їxup!wKD" ʍ k_g̟Tj8:|lp'rěڧ5y) R@ Hzy(gvVM#t:ԗ`;ms%";!1fYd"Kw-%3 !Uj.iɟ0j$$<" 4ưCK,q tangvZ{:fz98dc 1!Bղ3&$:r86 -[4l|l j(sXκF>X&uȄUpVVr8mkv}N4n@Xl[3ѹ43vV@Dx{Y63=GjngT3ƏIJhg{݉)/6F.rk7yB `fP.ጘ^:aP9R@ H) hw[.fDTxPA';9jB wG89 H&DX%.)8\^)x):[aA{`&i}x3q-+Wg="ەL!: pm-  eN/swAI36 g_{pVNokR@ H) b$ތ-gw酰(_<볍q8`FDLc, ^iBK`*AK0N#`geמ9+ٮf 9"3ȓ N6G9+h<(>a^*ثX&p&sث6M\&)[yx|Iŭb+#7| {0ڨzlk]s bYߒR@ H) R G^,G6b \=n]jl9A VqpƁ7 fNk`>v9> -S3K Caɖrx( c 9|>қپ8˱3rp5[bFsOp/h۶5e[lk'Z֠ w.$En'|Tۜ^F/'}In3x 0;ٳW[mK) R@ qr\+x7C=_Me+bI2\fɄ i@Ksg%Sp4DA2DD,ln 闚ઑxVX^^A?8w;/}LMͭnʢE2d(Ϝ룝I"Q^sokY 'hgR@ H) mkQFN/a5@ Rc}jgWb!+T|;V,@ .:0+!Db3hْ'Cև>L*0&i$\,DzSk9m .\zM \[!g_+J&9\nC<|`G>f̐F@ǁ Iq=Njvx}-n9u7/B_;,E&/R@ H)0\o u㸺VNIzqd<ӽ `(\F,jy) $"4q( :t[ZŞa %g,3lU`$: U,3hCW]>3 AhW`ͷ^A?NΐjD'<ޤthR1rjUاG23'[5#IgOBI_R@ H) R(Kا7c\- x@5 $%D1aL !$%&ևC,0PziA>'K>GlAeV|d0ŧx9C.'M}px`Ђk?ftxr;!۫$lo6/F%^4K Ah#Gц-w*ߛ~ NR@ H) UQh3ADDK&4r`B*`;{.=2 KA`%d'ѱ";,>X[v9М 5\'g紩 Ƶ 5woEr hBNL 7 T^zZM% N\CN7GZpү>^piBjUɉ>v<砍f7f.,I_ k$^yz#!R@ H) re;,dEG C"SdAx,`X˜?qef~#a*`"i.-A/B#6I F-KЁr)Qvz&ԁ0Od5l=Ta2' WWIaqCl btjȷ3[#{|tE (f4-l<JQ &B $={ ~ rqR@ H) &W _(!q\j$<#l5;Xx A"c}@tLǃ6FH# vd qEMq{$P1 ҟҠ5VXkgpz&t(r& R3~)&5pڅ˴igřЛ2.RwrpWSC'G`'O$ٶ7|;_t\fT%^ۆsQŊBE$3βQ.uiR@ H) \*Vx7 <~-1I\C[}L6 `D h$0ֈ!1CKtMZȲ8-sK0+h}h&Bˑ+}F0yyoJa- >]Kr8+|W9ZxF>AZ[11[x*#tqF2$^L_tƹpu) iVf8kEjR@ H) @X.E^>ո9IԼgV70Ir݌pf.0Qʶ@toJ؁ŷCiR@ H) \oPBT3<$ $lD5dctq!3i o3腣*`ж# #TL؁0stDv ˤ>D`Vp>֯ OpQvxJhõxp.!/nH-_ H) R(R9UlB赐fIwdt Erᖣ8.m%HzQG X g$4և5B(oVf΄8Jq9ߞv5Pi/}gprSL 3eҗ+Eqސ's@ޗ633x^8խ w/R@ H)8B/ mac56c= a)m<D@hDUP&^R2%$Otgm|,%K1kG F$FHRJA %^΄D!a&4̈FpjxB3RjE2rWb3:S+Ζ5Mx1(=ˊY?ٙW.m^5Op0U\VL^k0[}O;~x`\X)Z3-_%xäq%ĶN_ukזj8_ H) R`+]KE O/xt 6!$B) ߺj="b!#rV'jwvE-]z [btX"{Ƙ{ p90;&6s&0hB(qLLN`P ,`i\2piOpo>+4inV͌b&.F>\'r sM<^Q;!şNj Zpیpvwz䢝}uP_ZR@ H) @+$UszN0w%~\sx-/?ɍmN&gBL,Td*IQ r`iG"/ez9+HAO0+eDhE53!rq"t>^n'64ѹ%[#g!ƛ7&I/Dp" tffF2;9t>$*;k[l;NR@ H)  Kn3ؐ~f {Gyۙm2<,v *M=B)f#<^00ƬvvV냴>tOgW0Ť>V@Oۙx"a6: qjYA7f\/oT=Ù4{8@>bhlT ~a$9 GH3U ^CxEjR@ H) @9 X*#7_(k+3G|:H) :A5hC[ɐU!4L[|*@t8 ̄ڬ!@d˺@,N'Wj 8cëGLt3Py'̄}2 E|\r&4H8"j;YM!B7jF:qppGF>n3vR@ H) 6NjvFN/ɁǛu `L# \*)A"'0|gz'6ئ̙>^t`P %0>X>:3m6$d{; ʂh`6a-5QD8Μ-5}[T{@#[#I(-~W7t1PBQ 7äξk8>^lɿ"|) R@ HvWʱb = MpOTpazBi斗Ԟ ON%c`$+W8C FqΐRED;C^ Fv >Xri݉+1q 2&ȍ08RA7qqpJIpS:h>X&!6 pai~y!@tB.R.:7[Mt(˸F,BQj^) R@ HVT\*ǥr!Z /f<N `|l@DT$X3t#v]5BO%KA;00!C& ZvDDԊ U"m#$%L(ftMhgŐ`;e}8dL}ң㖶⯡fmw|*h—0mVZgosS_K ^D8=_ H) R`+EUszN"FH/c}$-#Yc-lЈ="m^,ȟv8<,p&<>FrП#RLcGri9C!dk$&Xc_v˴G񩏽(D-0h˱WFgkC#A^ेk`63/´U@$\MV7h3f7W/R@ H) U9 X*^K/J%ޤ$[@HB HnqD[C*cK2-ܐ>a>2ۄ1_%M$\&t-KNLZ-[U+:Ed$sZ^)n q'0,KfY1<>\h23B!2N>&>4% &_XI+/|m›+^R@ H) n"xل1>: -mـay0z;z@titvYJ `f,8KfLCMH'HVvI8n6^= fi˝aKje]r94Qp$C@ d %piА0MVkӕM>8rz 퐅аD;vӛ/-)R@ H) ZoP*c8~CBX+G,z rBm7k;5XF"l2 % 0HX;%#cԇ3 ¢ #Ԋrm }'QdF@FhPxcG$r'Lx<2R>͐3᫖uS7*ٚɽ3k}+¡}T3/䥀R@ H) *WNjT|6bA ]6՜f* he!6s&jlgKhƙdNKj1k|5}'m]}078^5N;`Vyإvp >Vry.cnKW.ggen #@=&~ݨ F-C:sCR@ H) m{St3%`p2ް-XbхMd超qQF(A-?dbD21CǦ)e#$[4}w0`Z hbW3و>0B|#` fcW W"/} rp"qq5}xqLL&CN 0-ӹ=цFJ ZnX1u6OJ/3s9p.-}"2-銴=r`9}x"FW|ist^nc[և5<:Рߊ 'geFɜ6۹{C9Bʍ \3m|w%b|b@Ɖ ޮ\f}7i\) R@ H)pP(xE+< _|Rg~9yY٫-#7ևHև}L^n:iWnHCm%s{ot@7bC`K0:j^d\yɰ /7)k>3H{*BA[Օ+R@ H) Dbx0 o c{OSDb~<>04hŶ tt6kDYRrjD hg6/@ Iԁqhp]},>0N-X񖑨6Xlo44l5a}>zA(eOxYaˎ`93# `/=+ސ,Y 3IS.#a|aqUY P%p} r.ʿY/4R@ H) 8( 1B赎5d`KS`$ 2A \ 6>6s ! mi$ CbdN #4hG;3:LhF(6bi^}Bxy"g>\9}a a3'osLxuɊ{cO}bn\5KlV JT0ߘXM:~Ǥ_Gz$F|ȈR@ H) x^x2,bԎG|FBOx!dyJjh:+Y^\Ζۼ,a5q}Ŏ~ċ[9w c#I2 F m` ţ.yR9\XFqqW EWW⸊JT]yzף市>bRU*5́m7v;ҍ8lT*ewZZ\z+՞RwW펗Z_ĕnTV~TzvRu{Jw۷Տmur_`w V{ޞѮvmvv @kҮ uWzz U{ z{Fz|opkw toJڽsaX:\w}[qhPOhO`zPAGF0k{C[hġ1,d7mf'bѡ1Llnmn7ƱGh}l xWo=ҘiL`!#H}lr1\oO4&0) :704&٘6:>\o5:֘CcSt1>6CcĶ1wP}llrzlb>695Ml7GcncS3S3ããS3m#c?q1'OLmiL'g&N NLl1>93<:1ҘD{>l161ΓGM NMLOlہ6 S8qbێ$F#`1mjfgc|[2mĩ8ޘ>m;&Sۇ961=TdVđƤd,1K`BYMm7F  OΌ4&'gQjE}#g2:6% Ngtlrȉn~9mȘ[ Uc|P}pWF LsP}M 'gqcSOz|1115O}lr|jѱ)wǺϦ9adpI>&19m?[pc1|G'O7O&5Ҙpu_;8>82?.p">t1o6|vpc_08Ҙpc# _ܷuxpdm2RoNA~W8|㻑$vPp?7[F[70~2NU{ݿ70ܿu_p?~{zzzj=}[| =}ޞAYu&?Ǿgo:~߫5[׷Wz0P!'!|wO&[sO ~\պ~67#1鷧w6~@eL~zvKw $?4'~$ux(0![@&Qjݱ ZÉh[GrAq;`3NL*  s59}zc HGN9HKЃHcl9: -덩ə#Hћj`9fiLPG6Ϫ310Ih־Vض#1M` ʀ c/x}%Cqq,墍OT-/}&xcb{;i~Mwd=:.k) lS=&m /6/5 Vavo@풶'>&Ppc NjNЬjeI:wT 814 {RXK4vo e۾e_\4^Wo?Uz``Խ}[љ$_ Z׶RM/oށWᕊ{oTVk=ZO#(7^0K<-<?saxsaS{5*|#SsВJ !FÈv!+ ͞FxDNh'ܞ$'!Ѡj}-pk2 8 =[Q໧#;Q0rǙC,Z_eCf][`DZsXV1Qd`u z'Ꮻ򩨤̹ƹr,_0-_ؓ%ӥ,a>&Gstuvu@Q2!7|MtQ8h7+xriR+.Є; ` qP bA"HapKT0Š,k@`A8N 2^n!BBh1@L+qE$FT$BĈGJHF~E#ǑsHryt"oO(RQ]FG( B h:-DKr ݍ֡ u}`Sycl,K21)6+ʰ*k*֎uaq"NǙ#\xç%xOWx7@#a,!0PL(#l'"{H$26D/S% Ľfb1D"HXO*&#&#]!uzL\jvULE"c|t26r#GѦP)l\J9rrV]]\G}HH\}Y:T{*:*.6SoSh4kZ-O[J=j548|9uW4^i54Y5 54h^"kYkZ*kѦkhji/ޥ}NIZ'T@g tnAgymS].G7[Twnn^4J#z a0r7c  [|} EjnKSі3-k,XVkX}N^h]oF߆cShScsϖfh;Ŷ.ne{^h_iut9lphA3B˞nBCJBZCuBB+BeՄu{o DDE18՜HY'Q QQэёWc#Ul68&nL嘧.3$&%Jx,nm<%Y3y|ru򇔐)cG5Ba(!=g\5:{/c̈́iM4;$II )?scUܞ Nn_ V eg|埵*S(,vآ MrbsvSK;,ON67e̹3b21eŜs:‹vΥ͙F(iNMR( pHYs%%IR$@IDATxg\Yv$uDj5Pقl flgml[1ȡh쮮* ZDf"bod $@Vx}>~O lDVC?Ѩնbdd.XZZrݹs<<4<3ۭ7=D" `Bgg'"nʎc@&[׷Ś!`!`|x'۷ooo[ܽ{\Jőwq^/|>{__GwqMvC0 C0 C0~+!YTsιEdH&\jV^S$TT7mR\kѺ텅J%\|٩ꇔP( C0 C0 C0v=CpE.7771;;gҥK.,ĉꫯ0660D8 "e7(h.iF׸x"n޼H\ԉd\[3 C0 C0 C;d^YYH3g_B,su7Y)E9axZbmm:%Yۚ!`!`!`A`ܩ);)T̝D4Vk;7Itkk Z? JW2Pz^qG}Т>xKG^붇!`!`!!!gLPxh(]Rj_Ɗm*/X&^Bfiب'P##oy*}.cc} KSHzق8>怿alC0 C0 Cx')צcHT-CpEE@[:gTL#rk|/,U!<" t݇|TOUFa*J"*5~AwCA|ޝPQzsl0A+FS!`!`+#HRO#.IVE-l9"7_,3+-rEơCܲ5_$W׺Rj>ujJFE8ݹKWnaza Y)Fj^" D-<у !HEo$[F@D@"TkT(Ôred"ȵ2˸3>5*T|v+sNy5č>mu[[G%;{t. mбQF}HfzYd"Lm Mn({[ aaҙ,< e[ lmCIBM;p~#E.[w#J+DпuJJsXX هX{7Z"uD<%dWW2*UPv-R/sK-H! nשȮi\MUeo~ow&YDz/K"\K>̭QEYE>N|M2ybVwy_1:JG'ȭN`[;sƜYHQw)bu}̯. 8oUd1$>8[Wq+Ro18Ό!_vsC0 C0 C1~yE =/'NxXH>"Rqܾ};{h(@5oxű_G|~|_jA9r U*eani*vN9Üq-9tfL5]ا_G@c1wze +opɏ'/Ou=ѨZ+.o__w?*+w$oWpuj3ؤQVB'lQ=mǡӟ_@ {XKT_/^{\1$Tޓm4ZY2u TA|}1U닷qogX?Y?[”yU[|g\fX8Ä;/N1n:iNJ 3*Z$Ggp/EpgX`WS8u]Z1N"zcDc_ @Kqf0 C0 C0^ Uqkk+FFFhs|s@ B2Tw$϶ous׭:5yI`ĒhmoC$@x oil,+O?< $\TQIs$dNLV[(}hxHKswfD>CK.MBMHj%OŕsP3DNvh;o勸:_R!TUZ+"S(`92ZP:$e̍c5p\rYWCs*ռH ɝg0lɵ4--`>aG2I*qXǵpsX-`fNlHc2Jt>²UW̡ qu& ?Lc ɥQRQAwmQ ߏx+g, ԑئx7;a!`!`!\~RWd5`0J<^x*gdeܿqzbne>qH`Yùo/ンG)ƚ'PwCT+ilt*n.ޏ=KnyI9s`u/|'nX$z/n[þ#ԙk?~[+^?3lcpKqf<x6UXov G0EO2J̉$zF&4z4 E0_@"IpKNEv"=FpE|c-cw W<^@O@͉N409g3{yv =Hww~2יU2úkeIBAVet؅!`!`!<~ry<2fY|N88~WAG\],qLFv8tIUwzÐTSgpm<֍2eJvg#pp( p{,QX[zKă\!xN|kqf+N*[KZlIS^BQWL0ľ7|~J PeGF I"ѵ}p>wHJk汿z04WhE[q`fyoq¢GR),#Gz-CR[@yeDot!`!`!A"`}vzz|1$:1z( M d7+X=_=؎8Q3|,N5/Xuo n N鰯=,6073>lDw@I)hEfe #N XfN4}yKE nߦH>h9ĺGӁDM*+Q}]F:0"[(]:Gw\,a@eZs0*v wYz (M;vo/"] PF[$ C0 C0 C0zx"/>DR}Ż6>uD| WJtYb+N}kS5dZf9tyPG#݆뤇>?ZZ0ԗ{dvKW0 Y{8 #P @BlRM04yZt^bI !I t4R?k0姢 %zKTAH*?C{=gs$%y\?h`%k1pbKo/9/I12Qm0 C0 C nõޮ>{_)T><7LvaqܾzncqXo m/: ,jRд˘qOЈhKtljx6s.[;sv-0$$CUu)N׻ՍzUy>'p^1vתck X''1wwnKimOUӨÕ:d-\][3 C0 C0 C!Ut:{amm Q=IpC#ғc'$bE_~?|+.Ô`IߨqiK.5Qj[$\D|7V^ekU) 5pG]>TYVh7?qc݃TFpކ!`!`"?Z[,IԔ#LqgڢwťKpYMRAa7@j}G5f6H %bqTȕ64 ȤkX?# !эĩA*# 1[ann$ego?G(|[?Dfy 3ىZ[#L70vazЁ:jC}6lo/C*/!RJB'Rd># K-D!tb!`!`*3WklȬH~X"===P,//ϰ4/~ X-sev*0JB Y ƐIT\9u$ҍ#&fC1Ă$2DoW nID08؅ ^KekO~:Yfvs΢7&{䍁v^C|7_\{կ'9sE/'㤕UG{lAWB;oJ6%[Wq!vǎG>mܾ)_i!`!`!~W*kל2+Wꭈm8f [) ˲HeXZWWl&Jks|/.q8R1$*Qu{}F݂(b,k( ȱX+cb/V'ayh`׋C8}jݬO맊H.KO#twKwŏ.H Өf`nϬw[Wh n9Ul܋X##;7β9s=BES/@ m2ES]UQR!iSY&/'zGљ~7 Mo/'^tt"j|Wvv:'* X|G?cc?S $0 C0 C5~" ӕ &+'n =j>m,+Y!ʟ~S!ͻsTBXc-Zy-1yJEIp+}8x4F?凄Fo _`;7ū+u|GXgsEr~gq_8Nė8^ cpp͇srF֯%tvNEX\Kqtad#}p;zWEyj]-"n!4;k 8t"X؜YL">;q<H'ݥwtBUC> ,tv"*>b1>Џ^biI茶!`!`!#%R]EdGGGɔ“Edޏm"Cw<nW PML!jeIidd6<[fD-ʼnϾtHDŘ44!y:^ڻzp:2S}STvۃ"-"I þd2:7q Yf4gJw`?FzO1U=CGAVi=1t2%lWhBSf)&WǏP4v6~{اD其"iOܟC'!N% K^^RZ8q(NB?ZBq!ȇvw;mOuҩWJmy}vkv.nHl(SբXC1ōv-ImCi䓹?mg8ȗtB;)?okgm7ȭ#n{x$f-=q)>gspW%i Ҡm!$w~G1ڋht_$~?~@#Z;%$m=]fIy,ˊOQKMu 3O-݇qJG si:GOq7 CĞ^N># :pͶFg*AIyO!,Wqj> /%Ng+F'Ol.A3ڂ^ C0 C0 C`)+\;>*VK HQU(Klo_9"Z^mqD՟W=ŻСuܾ-Ivt*!q$*m!I<ҁ㍐F;BêOoEr b^t05ٺD#'/'T)o -?y|q&1z˓[е'? C0 C0 C`WSWQDI ɪe{O^DlSǻDnunZWDWy!`!`!`!""2,&+I=Xj-DjŢU_Dp7K C0 C0 C0{4YR)ln]æRBqYsE,m] ;!`!`!`/G`("rAq$w}}wqjvϤ6MD2whji%KU)qܸqÑl[Tj _~{l C0 C0 C0 WE`\f [Y;wOQG.j[" AJ+ոT*aee333\D003h*!`!`!`A`\U9.+)\GQ"ёZGҗȯ/,uNpT_{m_b(|z_u+ +u4kpR%ܕ{++֑ܪRf.u+BK qn—IrZ3 C0 G ]] Dz#}YGa̔:N[(Bž8* |mX(MP+4nB߆g}Jrtu k* ! {dj*K({xRkA|ۺ!`NpտǍDp9w5sY,6ݦO-qfTTos̬άci]XSZK!`! f7p~!W͡X/=c|6B0H)R} #0]ggo}M~3-~.H /=9}Ɔ!Rt>"OY=oPU&SZD,Ӫz&D~%üܧh!`ڸZFuéۮApEr}zWHװ^]ZuEC{0B="'QSF|)&iM:7C_Uy}~Bʖ:۶!W"5ٳHppVs<ɸpf r0VgN+%[vdC0 %ilrt4"2 ǜ+M16 )7C>djyt ^qwf3؏7i#l0@`'?DaSՕB+ڹRo4g{9\xrÐv+3+'<&AghyqSX[@.ԋP9F]1$ٱ C0 n).DX([2(ݤ"׊SsIG${)3n̗`E!_QJ$qw0:)57S}"\m=#jM F2G+7b?*Vy"".WfSł#.YUumT|E~}*< _~[pf[ĥq,Z>Cj(⑀#ovhC0 UDZeQh&m?mf:e.3[ 8 #5%x+Wn$ 7T o Qb#Yݻ|F.Qߎ>)zaYRsӘ3((dvzfٵ!~f~9CchhB*ʜ,yX;5낪YsI֜+5Cu !oa){ٕ{lu6ܺ} `GK*,#5"a$ѝ]ɞ(IZE(Ξy߷g놀!3~tWVmm6uy,ni}^Sh*YNu*ybd, $ER)GN$ot3wK aw?|f9LO0L9F`R1cj$!#C@cBt$x/p #U@yY9M*uZU$0e2<}udh@J oãXh-mAtc' naX UۓnJSÎf&Ujnd7!r[$ 6jܖ[òk[ȭ[fVX7]}.e\Sz:YEm۷OC9~k$Q3TIPbXj6e/AU)I_'(1UaIW]T;XʯkakׅN*g` w%gO^քak9DZuwGn2A$RrynYIa _;'] !q2E*FB_ Q b1oӵsoo0t>)ds?_gRw?`>T wݽϰ V+ŧPð~=w-^~ٹ WḤr9 :Vu%8 O1BRrJ+c2Irx΁yK])v?r M]?vw0p-Ce⭈vaq[zW oc&SxkH jC-h 77|oo'&ENt//ԟq v0Wq0/C0>@8YKB⋠rejօ# |W ,tuwT/:"^ߊ/\n9 {$I|~ !RdAᤝμj;!XV:T$ϝu_]D3Iޖ@:8r9(#8K\(7SM^cDZaaAäX*#sשJ M♦]%c1'~ꎙGJ 1Q X,?xl߱iDq!w?L(R' UEH9av)2 7Y@dAa]1t7p`4BٗewT!V6%.ӥniW]!^<%mCs].|s"MGr uc쩟 랮GWC){lJUբ lnl"u?--F5P0Wk6Ik6+K%#2EX- YVTuՋs_oq"W[fpajdjm{;͗BX[i;篾=e=BAc%z1?&4Խ[q*߸+7Ff!2$i'~I 䫬v\ij:2Ii9 mplzq_p&Q&iֶSK$\ҶFRtn[y0Ip3WwBy]%BMC fL5s .܇TB,x vnhQ #Dǁ<@{|*͙:j)|u,w M0*gf#!y#$_;sߟÅyo< 4:؞!`ޥ5{ȣ4hhGkN1R}%*Ӫ .$Vu|Jҥ20TToH;vB ?H52e 3T9D(ϲ51i\+_WeRhRxLtYywgy^ MWdMF4X;g 61o( 3ң>ƽ_IE~dt]ӛKN+WGfo#][_G:vV)UUP--T;#a9IZ܊*L97Vv:vs?q< 䖹|El4j'yR~HN[lquUolpé_G|qb_e> C-IB\ $PNݙ9H3yO"V4*Tw fhi>~Cm!H09]\ԵK{9@2;W@Ey!c1r89_ET{N'pb93!.mayܻugobzEL i1w?A>6p9sPq<8?@` C0^@7Tʺư'Z@o\#Di$T/o/|BF\Nd#߶S߬ p(6:7YT118U>.+ ?,<#+S'WQsY$ > fhk+ *1<[E3:|@7MN(H4{鄟yڎ6P1"e*26"RouLHCTtu:_-0D|ęh :`plFR.Ї}} "2U)̏_8L!G%V'>R=hbkww+C{d5p@eX JZLɩov0 C`w!Ճ{kTVH3o!_cpi1o1'И;0 jLp{Dzq#i!HL:̅ug$[9LyQ|.7F>/N-uakU*L[J3|bc,n8|{{)SEܸF>^} 8V ]Vv hMDBJWx´2IʳDM|OR6-{"E7ɔ*ƯiKm[CPͧȇ}.~w$ITN)u${#!CSWj ^H0&h %VN"^MWiD5W"Qp]JID G3 U9"2"$ozsp4"X_wH1b28\d]sy*ù.,ЧLPS1wo\M?A55I>*,gg0Ɔpt_'ڒ!4i=oPa-F8skC}Y$^c"6FL}?R+M*(9>1h#>|u $BX4BǞi!úeva3śX(N)w瑂+%+@B)W )KP$!1팠y )/a ޕ閜Zah\5ڽŽ=Jb;[)Y*7xo 'V39C/26MY\9E$ˎ@?FuĝGns /4"{z2A,*|s"U~IVnr:G6~&g"EjnAdIh ӱvvq4N9jz^SNtC?Wi @H@IDATT50H;|G*?wL. C3WȦr`WEjRLMkk+]HEjUճվz_RU\WVrr[˼6[/k|2oT!kpYҒC>%'$TÜT]}?܎t+wb,ll]I' `" Q]Kpa63p>c=+}~w}oq>\;ʰ2"62 K|\Kc$>j #Q8#Ʉx%W"m!xoz} c㐋HO.kXz0w0bvܝ?[ʻo <hi}ݾ!`BJEV*'r)vxNkMTƞw]u gBWHlgffz&Rd[9%9WeT|Q2dMw%L͝MB,}ҢXHIu YX .?) qwāi -d^]kYWYeB?KНbm5Fz+X.L=*(̺Sn!CT0/~/dyvN8iv'PhEf=ױЛi s7:庶5m]!`'4*R7堊9n  |g[u`q+0bv#ʼL*ĸA9iYq%x'psjcREר0֬peTl* a>"%tuיy4uG$ɀ"8+X k0!Mҩ9Cg5$`=u=m؇⬷ Zt~$pcN C杬essQ3kThB"a pj0Uɇljo!`e',hǍET4n?wZyC:L4zI݋3Vja?k(H}VSq\qĴMcbA6Fm#RRT]yj:9UW*HSddsh99*$:ΉsHe?\aQs>Tg ooSTm{@PYMZ~MT^7{)1n 8\^Xȉ[t vAi{xrcuٺ!x5&Ap7ɬSm61_L4WxtvmZGP. M~"dyw$dlmN C4͋D9:#2a_`cuuőjy~:.}4;9qܿ?syǁO dGY1Jrk$ `z %{E +'4Шsyq>4T;7$M8u=0hW.kܥrbʺxEXډʻB!`4hY[6O  05eT䩍):6J=kKM*lƌ$2tzHg9-#*"IZsRn0 t#eZte>/H!Vj*]cU1ձ',cMi83AC"Y;u]kRMO w ~T_S(ڀF[/ioz)<1qSˮxa 51lX&P>՗#IRx*ףEmJ%`w\,'ngNik@2%,@WdWS$zkJ0p=}ho@Og':ن۷\p!$mHh*A3wTmt(ѹ;2}]A"4۶>wΝŕwqctFH^My Tp)B[gk!`?]7.Q(\2qdivRNO63ȠJ(V}n?jJ]zY@&U iAӭAC$:H"7V r|yy}aU~*,=Mv=]_MI$k|t: }Y*IO~P gG!4WCx5~}V"u+(&`!Cɕ2Yՠd2Zꪒ? W.oyQ'̻`KR+bCm4,k#gwk -U^..Q̰E/:ϫ~aX}iهX ' o}>nՃ4pqC[Z{1p'(kno:}N $ґDWo:ç,~{w&|#`~9܃DuEx:#ig綽 C06QZ5$ dHrNMVu[,G\Xȭ\Ld>Ԋ|>>rjF""mɦJv0T6"͝9:F 2/!U񓸼n€Ԙt{F{Q~z+t}IT;֘s/ZiQOY{5A}`/D`\ 2֪NEUYI`(6fW VNgy"LRXnoOWh台~U(JC+3ΰg~A#W̭Tw3uγ+ BDq^Cj>|s6sX[͂)OwBC[1Zj&{{5r:cAzS=}W_W-rzyL/i"B}G}VX!aC/ QP~!`n!G*Uj TG ڴHr~+4U*F/z~ߤ6i|~<>B6{Q4K| GG~訢/:SҸ`3V{αxT+v;!Yx6mn3虨\57Y-ۮ.l>i܃G#7 #'nG X„Elmc6Pɡ`#,#brBkk>j:u!̡pȑ,JoYeH^$Q-$Qj:* BUH*\nc8~ZoE3:8µUh3IRaa-I*wT] *IJL<bhƑNώ`2@<8f: K}ȃp C | NuP?aZlS?_qqMaҍAȜMm}'ʵ3S-El]x—EpW训yy!u}yȭ*5ts7{mЪg+ˑ\Y\{ub{(y;Pc*9>ּۿc¡p^4›?L>]C`\Q(pPBnGg^. $h)Ҏ}}ݑ[DKb>SR|9JBYRr׫" 2\z~)$ZIX<3ŕ /0ךdH(nA+<`v#V*k>}v{0 C`!$" ;^h"d2 -GfsĢLSNa@)LTٴ 6+Hv殕NJ؈5uUQArI.?#&r[AUÝx}gkL}V5Sə2K#=nL4_:_Wm זaÔꙨk 2[C ;k{iI\Ŀ6޿}xWhg4~<!R<$9G?XRY˗Uu*n!{UY!S-zo{1nsRkHmXdW_=X^^ymFͼ# yIyp OΧp C#}hk%b5TX pܚgmqtM1=0ڼp^5l؁~nCJ.ɂ-($<7Q m[q C0v)Ūfx+\VB} \T"t_ި#,*(Cn& E+TJ7zHNEjc,uU3F"+$K$EDjrgT :b|[fb;r*/WiGx4kם7rVeX,gan"4ϬnX6ƾ}8>;2-$}ϱ|:K秃M%xF0bλmOZs4Ab 3nk7ˆ8bmחa~q{)F>evڏ%d囸zF[Vtjjo;!`KYY\",aG '¦qjr|yW>*1$""T\mEVs8X,Aݙl|=b}KXLawJzp +TOG*Tbi$rsCU2h4!;uH<˭Mb|NÙTi4)TGz?'3#)&R +*w"NjWpD11Ip#FNpSg%@4@ܞ=]n3Y9G<,hCX_}N*\tu/gC^$0,p+B!sg>;{f=a7?ov4I8!۫!`OQ66<796< 5gI0Dpu6fʋX )Y#)dv!6gei>oԝQ^RK`rΥ&X#"q430. ~Z eyK.c2ֵ=aysOmZDL:1G0TӕYUf8q0$6Ϋ#.O2(,*|@Qy RSpc=5ivsym|.wꯚx{}44 > ԨyC*O kS{',o?]zDƒ)bwUwרFfBP?S(e"hI=)H $~|@$uwYg6l ZNܚuӾD O2e_~bd2;f^.دO+ E[L@ E*Spj̬MLlUS[ INQ~=xg5 79ce);TcG##G_xfQ+O9sPdĎ+gg;ja@XPwj %u}"[SYab֎N(¨Ыyŗ*?N딍N>F^w]܎C~ 6<p6EsG#xvFGl;m"a c ĉR=m\,K5? 7s6;j{̑C!v0vu$󐙆#ۯ!Ma&,c!WDHkXE.6뽆]u7 :66mOan{m5Fm'+'tl@%tN% ɻ6x:oy_lyw(ʡg8'!TsNஇ:7"ۨ[@ڗԸ .l}^n]wZ)|7plgCrPW nlb ݹ&D (KOA`_\JXw9PP\,rJ&)rKAFDW#jV3Ũk) "tę4|]!RrE{Dp ^{)ǜ6S:h3'e ,7eҌkR- [Jv62|%MXzk9DdWG#)iE1:mE:+% WrgӮ6QQ6əSMUHb.Fq5}"BJ1݀T*/U2۫n"R&R}ö7Z{.rb!tR } *Qz+723 lpMU h{ڿ2&Ô(G4H ,jb54Wܶ"SIsRW &EGjFuc}EuP}XkU`2EZF9!.)Jm ?걎7vF؅!%؅u>x.vfWQSx<GF "x8#_ۇބPDsG$ęR?M&,8R,J q{GRBS=kC+[S'KtwUdA'9DğP5jDoD!"/ -a u0yf㧭p{[aaJwQSem@XЕB}-DDB UhF{wk+x2~ciVĀ$[9 hSD"6O$ڸOj车I0H@_c+8خ("Bɲ$ǜfAY۷<"!{C1¹{T@1U΍NP8>-cݾo& &$^.[*)F3ON4gD:/23;ĹU\Ls/߫a¸3 ܯnɒ>OG#@ЍJ"2x L\WVޗX>/VFڬXnFm+ IK cf&Y!D$h nB)r^1/S#x<;BȆbIIrOqΒUP!\)7ɀB| VtW COCZ)+ȹJ*qu٨;dC/::9e^OT؃.>GuWίroσIזpOFD3n9s"ןTw{):cQRTC1\[sG~9ƨ|ӫW:szzڅo}yymrrvj;h"jթ On,\=z|Dn~[ȵV7 4_*DY%h 7iG#</~w!=iPRO?ݩ:NKۣ6(WGFA:7^Zʑ]X@!"Rgf:"MB`ELDK9ȢsEF S]_߀ /]^u!"/iȒJtVk2e!}PC,a΁+RuԱh"g8*XX+0 WrhK]):ֶ3 r]? YǨpb)IΟ&Wa*=TÜz CV>,$W.:gz=h]a&g wڿsypupCkaAՠh[\hyMJH{#x0m(Rl숭+XM7}ݗa 0埛qʍ)n";&R*h`|u=G @{%ꀌ~<G#!n P(i@*3 Cr Gn F\7B5gT]ȮOG## zAdB?Qaw{#x1qt&N5 LH`t=Ss %k_""<G#! mzZ6aDIS8κyLx<"H@K\>fyA@!{=G6x<x<@$אM]u?YW W)Op9x<G`_\^#x<! `nTAwfJ̀ W[Z_P 6i7Iw}3o {<G`#oGG#x1.Z9mQ?TO.GȪ#cpfz -\Bo1S%~]x<=<G#h);CQ ;QyU~mu[i[P#nNHumئmhh#%[H.R9sC>y<G`x<~Anto|V;Z[.mb %Cn3Pf"5ϝS'NArGX~T /G#x<'G#x~`5Qi]Z)Rl:-v6ڲMoDUNm%Pkq8lK.V?>>ncN-L#?x<}KpvO C|7&G#!'.;v{67w:v'ڸ"+b"A>mIHr7;t^~5b1Pjoߚx<'NpEd+j5v u;Vux +JS8BaThԢb1c4zOG#دӱrhk@]>mWgb7x^nYIfө2P|^bĤ,3C>m$gp"Pvll)~`x^~<Gq!ęnZukZTGբ5[nCt!}4E "DT*B2e2-8,<~\x<G2뼍 dƺWT] TZri7:( ^x駯(d6VOYw=aѵE+n܍Y*#Glvb|!:ICG#! 'e6J^ԕ0Pi8nΊfԛRa)HOX.L6K>7ryE"~)rx<}ѪdO[wyUrgu|Z\ѥu*jmXB-Zz5~ J,/Z1Ut4oCh/pS."d'G#< Hh8h5UB )eކzz֓-f3n.gHMzO?>?fG# Pc2\P2.nVgvMkvm"8wd,^!(*m^R~FV*J[*J7?y<G# g2~N\N%7w@x5o(m^k])zm[-A«fg?/G#v(W:kV魡n0'ZW mݚ(M7{(Vq*mBEiK -8e%KKW}Rr;6h"EG# B:x=YQnɴ}+Ϲ2B*-$5wkkUή`E#x\rhE`nwJ[Vix;PfE~{x?timT 5U\2"h-Tl![P"t2rjEfG;{gx<? ~1I>>܁:̦ڄ-+78%WDp=>N^鰬pnB딠ؤf"]tJ8#t~@Kc^[(1ZhB?%$&ّ1{0ǯ*~EPC$[z𾭦Nz%{Ԩrxxܪ݇7-UX\i&k62^Ljx"[eօ%x“;=\Qw^V7)#R'RzdPymtAF(JPg2TS?G#3~rw@l,UVɷmB5RlZm Cd>!_WyOi/z+E"hW?]lt^87mi//lܰg?bV'ənj(ﯶm7/^uJ,;4ls(A:dOnxk>\\)>tn켵)I R::|,GNukmrTʧmJMPgRJۅ1Bq6JDho~:~_<G#I@ [JێF"ad2AI.;AHTɝ"d7) en] ゙!ev: 5#e MC[#!RN&m FtZ?Zt}{KWWlujeMi&646cӇNo^yK秧 j$-EtATN1cLbD(x)=CRmtջiTE̡Qm7!r7VZTY\ c[n=EE&6\NSR[[&>lCQTZ楠 3W*K V }̽J{?x<}KpᔉZZ[ rp(pDmMJu$\-. :5 =;Z>B`*֣mU+`[ߩK8Pr59e[]c;+Vny*h TDĝj OGd~x0 -oel[- ts ß'@ZRc^g@aDmG/;B[RuܟJ> j̛&!Ԥ i[сr $Al  JS5?y<G#wWeEi@>c+r+dWȰ\""=OΊ3=^(5tʨê;pcrԩQH{\Sc^c}OKed%l{NCyP[۴_:jaTvкuFz茇g;3uVsmȭJlnЮf?Pi(S̶v^!"\Z\T^#O#6ZY1Th~ n#<ps``Q?.jJG# K+bX +VV!MW'Fsm2\ջ-U%θD]x2 xk2Du6;gf7;*/eXVErx6HXzVY%Ϟym?^<~^:>k4e'09dE笁gx UVBS}sVZjZ|0Hp"J~=nlN;vuökW+I>[&4;Ŭ&cvШG&7kvclkuk3a`o,I۞:x;aSCiĹ8IJv-;]_-[=c!z,{%~SjcPۗ0KW.vmiهrTNd\򜖝^c!&aKH֊mܼN%6^it,%O=oNٱ.&بυ6X邥)%2up>bMpnn4"j_qZ<G#xPHnlnmܶ)I!ɹ\ֆpd}00NmQF4i% Y$HK&X%e:fD[Sb{R:{C`ޕWoڥ3g {W/bov' R`kkelcVsC!FG_wHi]ξ|޾nZߔEӛ9ͽxƦ-Ot,$%o>d7Pyׁ%scWeCcc6 CM% 7 fZ,[A{w7OEm$sF<ݮg>򪝻hL9ّ̎߰*i? %,€,) !'Ij _?;{Ï~]]amZ|P͎bV#|*v3`|`7mB]q)S!{wn^?2j#Gœ>@IDAT5;cjs^XF nbݶk_/|e䂭:UA i:j?wi{'=I!I[[vÿ>=g~~t,Q蜽F!:bfp5v@r*mw '>pǬ\_M﬒\hض ?-N4Rhw+OG#<}++*eU!FgVꐈ:cAX:=KtPmg6oPq+)+>eGWdm"S4TtƇ@Gq}Q۹a>=c[7,[3`w6V@!öm6:SXnr &:|^Y6E]qeͯ?I/޵v6WT~;ml{銭|{N 7~m1C =:vQK{(˶{?ߜְͼvҎ')~ (?k_}bߍ_a=D^O+Yq;VN"gVv6l9Ll"|:wg+Gg!J=ll0hVOqO!ꃰt$G{0*$!8gŝߍow=~Z<\G#DRE4lu:<#Fi]zMH˜CK=:ORk;* CȳTYSIEz==Cz `X\y]Ȱޯ!)g)юN!ocWE{Άlss Mn z1+&[}?ۗag.n_\ّvBÖ'836gFD $ ۸Q z gA[[ *)[t-BGtlOk폟ot^|U 6W!ekNZn%>70ˮ\9ϲGfG-_g_J٭!K/oU|!US]e[}7 ?7fC[/wB zO:$-:6|䔽֬ ctr>,%\do~܆&la;6F9nD>f y XK -Jlth̍Xr͒{XOem* k`mr`.6l4^bׯ_4Yx+؛?;lo uQR gE oߴk7'lv*y 9rgݨEE!/xA;0xsמ>|!=r)] ?5EM&ld:|%PxxOg_!{$=UHle5[kܾn?{?;yZ.,ْ%Rd'wmu}Mߌ _r E-E`yoVE86)O,~JhUy.܎i.ʬrki^--[q@bXת^ RRj{POZ6f\lcE7Z&%M5wwc|OG#x<xய ʣb+' ;5U!U9$+,Y˨)^ rS}2#䔆#rGFJȈ F ~l iޮrE@I:oy/uQ\%)OT4d^9GpOG'mx|ΐG xn>:j1oai\JbVʈ&< p%΄l vKw8aҸO4n;۱]i9k&ĺE%aQ8n,;3=⎽[L ȭ۴Ֆmfj |~i \2Js`4-̆UʫTu}~# 81:t'dwLRZ$]HZH-n'7V~h&&l'kČ56lKS'rȬWqO kmj*=R PhZGJHC+CuЈ KU>-sPk-%}95h2/Z,9?iTa2ԤH5?y<G#!ozU)M6! T+K\?:0a%ʧJܱQ8AA&goo˭Y%Pqu|6&7\xwA6-\tNJ-=K\9h 2֍&TU/vEj^Emᾭ--lY8T5o'oڝ0aޱ|Pj,_ 1a NfSkwl`fP leV9O˟.߼`1 :q0cb\ex"gmG+Lr屃!¦w[]8q̎;bGaK(,g1NHei(6c Z F2ǀC{PϭARMSR4DwC; eMoܘ?6Y>:_+",.aPo0aՊrۓw n|ןDd+ŠzkNW["e÷-)n1Q"»%vʷj Ϙ(ScF^;*mx;KjE 4> I\qsZZ5Yy+#x<"ʡU G6 \yv5F:ણ aɃ<#AZJ@P)=VbQ@(F)b/Z;Ys=(t6 8Ry(&К ghg?>jQeٯƚN bX}۳{< G$Q?#]ܓfi{,VsLg˶)_Ԇpur_+n尭fĞCA iDoD7PqFW^&T;r6/_ eZmmǏM(!.!rbXW9"( :HujC ẒVr}wUo-Zzȁˡ(ttFJw}?~2+"Q /g;<󮾳?mQh|1U@rPy}7\w+KG^5>z>PB#Kr9h&ĶEDAАpr/R @*Ƞ)XD֓YG#x<ߊJlW(z)c4=s5UzVB-ɗ&7{'eU1(tyߍ ]i|.sOj\VKT7n^}N`l5KSG)=@y$;?t玄w;Cf틗텋+iK?W;_~Մv<{8[B!Us.D#6_CvҸ$MN聼M<]?!D@Z˄!i~,AEt̶17kC!vyv fvZ`]\u{-!ݭ5>v*>nBF_jO,w S \}~䀍~^<fx"1 Mƫzp'w$ɳt?:nS61%}[mҗSԝq;q|{s)_Qyv<J.g?u:N $A:gv 8#(mrOܩpf0{9 |'ҏz=~B@]َRj)8cBpd)ArR XqZ}%u/ѵ93Rj >v0gE,>P*5gQw׽^M*7x<GB`_ܠUu͑L) :"!ǂ"ӶHFEG~́vEuڌ3+PԚXGwY,Aʢ#ngnAC(Lٱ@..s/ک y }z(mak)٩^W_=aLJk5)V )k}0EYbpJVaGn'ilisI5{,P~k QG67j9AF+oۉN-;v [mS„y*M-;csOp|FɊ a<3Uۘkn0cmGui̺""; mգUޚ[zWUN**߹-ՔE(oI[rJm6=jXW0{<V{~G#x<WuD$U%WM%EBDTJN"„ iUUs9` ~ UFHRfմ^S(UT\уRmܑAWIg]{W P ')rʮU"oV.|jQ>6Q~ c.Ͻ8r(J줕F聜]lصs}:܂ℌ6b" Rzvز);8U5?K_ەSՔYc<4dž(?a)I#0ȟ BOR#m̠j"!U)]!nAh{R2u*rt\GC"WPjM#V*mp뻋Jd4ǠP|ZŽ)UOM Ԏ{x<G#77Fi7,+,Aʊ|_eqtmk(*-)v49v̐7ͥ+YUqRpۘ(ym=Bձ Ja0EN*Dg|#Ʀov>pii#ިԾV6Qup)ѷGOu<`v=)>_9=mV3}ؙ皮ѩ͍1Pڅ-S(CwW׿}ZYX6Sv|ȶQ\k[9c)r~P |_1-\ZJ[shH1PC+#!Ȏr[Uk9^uyj\U-*mµOx=RmHʼnƉ1(V)%G#x~>@ıZA4B ^G)XT"׮CjW7\0#!G%8$ 8a"-BL`:w0zknYVs)>_(wkWLe$}vqپ^>;fTbURT7etnceS-%; [s ߜr2QFG,bPjڰŋ[BN0~MA_:b?moRvf8o9t?y[n\^?B_f KG6Cnv7m%[//`Yɖ8nKPEyj3?Ϝ_"@#B[V2OQ>m/5vUV|h]RhJ%;Aرs>fȩԆPi#G ziK2 ԺDhHUiP[5M{ɭ'#x<Lj!"j" ?VgDΫ.duM[Yߢ!"0ZJ %#( uW1O)qe Mm60]5HnrWR(MN3t$+,qU~tr{ QȋI);6}gV!4J)1%>]vlDTu'(/&o?ܾj>JgHycc6K\)͠\m苖N귔y>溝?CjdezG1l" ڂ?Cb79|+a^%$u'k>}B23ȑmxNm9TepoOz p5 LfSiUG*vgi=7+2+B\ -VYv??#RX2\|F㊔ڬ%EJXz -T1Tܧ5DG\<G#L#m)J1S&w\$X)9y6M2Uk2Զ Yf]tKԏv\g+ V/jolCZɃj[i\PxĀJ{j~~Ns(D!|;!-;jbk(uhj?aI3?4nYBn'_{i􉾽֤qC]Sv%VB.7XqL&噘eD eMc T.Sv_`\zJ\M_mBAMy;h kD%D]PO DEa\c'NزGmԊmlX. cG4y̎M]=`p̢6ɏmjmŶriaSg:*m{}t9%9hri%SӠG.-Jm*HyHrxdRJ:Ax$|O G#x!:DtsNVիXNOOZ,l699isssP6jǟ|b.^ȩTVUfS"|)=H]hiLsd#MJgBfC̃dH)ʘTA:x[rkUo@RqmG^ \vǺdf%Wy'O/]yAos#,B:ٳRA.rĕM c^-MhBCղBD/<kbسIB2 [os2 |I5L:&Tq0ܗ|`@5pq&O|^pBDOle ZZ) :WrgU: gImA#B`50Xl[rU*XMCԢ4QjIz0-Rmȏl@zKzN}OPG#x@w{>S!jӾtɍU.IsRDpTQZPR19:l*"ǙqR/XҿKnSw`8.K>NVQWVe??ZiG]x~S0 2x4V/ʉ%gU[^s*FovZ\gQR,({zk]2B]G[P\ɀEhoZ=}\J2,v2YT}JKzh-q5jKɒShu/Uo̡x<GiG`_\H) b)RI,D2PnXຎz + ]6K|q| NJ_GPj2y>TngjRlY>Enog g UmnZ)ʛrjmTZQ_iU HjA6k:Jmz"e)U;:\ mRMRV0]<G#xV7W[RGh!O:189zqO–H k{y)~ބ^e[Q7|o_H EJZ@m@d˽5Y9/:uqG⳷E!;ﴀޮoBI^G^l֎YXx3PjO[(W6PiU{Px<G# / d9pM*m2K egȣB\b R ;B ^#x<g}ApDq0χ?tG>8rJrnEn.K*Wnh`IH VƄ :l["+MC%֑:m2dV9bn5淓\>ěy{O.m@wsiQjRi)YA!*ӆC& TZʣ\OΌx<G`/!# SfG9",s.,֫Pv6d#*r-rj"[( T\Y^ʽvR"ZU.۞:Tڇ|ZFikiJl`l+YadȊYaW֙AhVsE#x<Gn<G#iTJgJ)v} vO$ϵZ^d s(:2aA&fz0[Jm]kAdV-DuxAEw2R' *>u5'z cIثg5^ 'eLr˸Z"+}K&"1+ⴊq:NEcE˼̼KS)MZ[G#x<'B`H u&b:pt&0ZxzM /Lg}^֦8~H25*mCT@Yr;k,r^ "|Z0qڃ!嶑jlm4L>m<;-jiF@VwAN>.YXm,lR\-T~*-^ [mN]0b?PS !k6heэ$q?y<G#x"G@nǬcHZ3{~;+TRIT4b c6 5ZC!`!`v-U%PUlKUdNrcΏY@5wЪ\F-qYd"*R6dmA[% yiroW t#ӻdkό$ yɺgGC3g&d3< `O~/!~Rj?4{@ZSqi̢Sm-U =rI_k!ȸuQj(+6UjS-ri[wPe^k!$ZlY5KԪJ! C0 C0Ǘr!xZj HvXTiB2qIeBl6ƶ%l S YY.H&>m!$F7Atr6.7Esn-I)FU],Nrc Pr5Qf-EyȧnPEif1TB2Iهp3C0 C03ǎj.g_[ GNp1I@pcmObE7qYbT2?$X۸lnS3ڕV^'PFxʂa^1P<2́EDңTJIx4vׇA/iQqgs(hO:^W2LX*%{*/&s@@@-Wb.eQw3׬ ePzMc٨ C0 C8f;;Uo5 {c.ujԦ7rJt}? V'؄ YnkMvCu =p\w[LA&)Iש2-BۭKس6Bq}GI$u/'2Ef ,o}?ߎeA'd;E|Wtï{ rVS|'Ǎ|#n #A} :`  9Q.cVː-e淚2kEy(8d{E򫱾69^1oe\4Z!,᨞Ggb".=p?3`~uD*#t9,u7D'7'ث1sW5yk~JJB';Um趸fm-L˜>ېPM YB𷏓Didw!`!` 7Np`U%+1W$kRfP'YF$Z:%UKw¥Gs쩳$Lw8SAݼ.^ݖaɹbSHfdqM+Uh  \~PRnKgc]B?#R’/ 9'~.͖p]W'qf^ޗ»wҽ{io?oVd )L{;Ho=XXyr><8#J3sX:'?`Kz\ ปxȡpZw*o@j$UekQҫbPR.% (J=9LK!Pu8ϋg!`!p<xWk\:dT!SuiqU7cm#r;Rn;6jkL) }o9 ?W 26 ^Ew.QP~4 eܨBt-kIZY}>qaBEZ8A"@4:$ٌ 1\"r$=?'IƭU nH¾=?@M$>"%INB0[p?Yve†)݄KSxMIr 9nG5'Z[=uJ3$>H@%=>,S?wQ'Kqhr Z<g%^`ΓesoރmmXk:j ^a] (z G|uP=OCuJUǛ>LC$@[uQu;«m53$L`HÈ.~^!`!`xWo6׶G2Y<-BC9uT Ƕ/jmJnUQoZڧ%V]ͫ#_ЦOG7|>"#uGUr{F^ph/K 3RoukeA.sd^`a`6.q 1LEGyexTF4?+_Jj!!ܹ%k5}W0>4y>ayHcPEu5o٪lٔLM$MAm H@Pk9Q)NPǙƭpêmug|G n:E%WK1 N=r85_8FmLIJ)'9w!K'E{\@ V*T<,߅Ur Q\waӁRaߓ<CLpo*F1DETkl.ӔzObZ҉P_ CGd;8MAtVp֮PxrOjyo_/9ұSS=Ob@`ZŔECYλQ \85{Wqƽ۹%C(}ί9tresbBq"\n<9'|@mo1"u_C)^N E?1b3~|4snM枴n(wW`,q7:'}EZU9ҼAXg!`!`7Np손*<> O[΀u S{CIM aRR D>W; Qvk8&q\S'׮oꃊ̤ Jvpc @KZؔX;$ Zb^% U{ 3ƕyrTvXkTj4hxBaRu'|cb) Uå]iB-!セ*.MJfyeȘ:EI#su_k"Mw:8%xϱRXdΞ5ZjqwU;χo!#p0# MZ+mB! ԌQ;ﱐBh4 Qndn<3xV)3g,f5c7_S)i$R3?hImc,܆ \家kP%t¼kJ#qu $"/&`4s .гe!&'4`EH,ץfR;#TGtqe0,:0@dE:oLpgޑ%"_}!{]Hjkm# mۑ4^rH<1dhp8st opO|lOh> AO"oۈfY< 2` c1r8qJ "!s;Px|to!`!`ϊ1 pJp@:dB%Q\.mmX!'ӓ!H.BMi~:' LR>geyJ&ۻu'ry-Yr; ܲEBsPJʾK/6FZ7 KIRr%SSY܏ 'QTCwB|Td/Bb—X$7 77Ks}  ~KI.F[SILP:![wB{C&)Cni4u97y kݼGN'x4Yǃw|:gBq5/-|̥9Sd:Cl*, ֮&!`!`!&87 qI p1ѣ4"+ã&pQp[!miy :B|5ykE!O ԍM$BԘq#WPJ (P'1ZBX)G4'rE} vz wp%?W ݵkw -K| I[!)  4bİAGYڹ=5GRRDIGy4W{|x䵞(e\:2Ƭล1BqG_|iQD83$_&]~<Q@'$B]$<Dڸ?ڱ✤O~F_1=2 C0 C0^=o઱w:\Ȥ==LpGPD1>(\JrHu1e2M٧DLa TK:|>U@4<ðGk(0C%$q@&VYT#lcbH1-]*8%F J2q#qj޺. jvۦPBq)K}ʨX ΜZ .)y3^p<zqULNuXXP_UJGaCXg kHȷzyTDc*MW>ԀxUt}N?E>Iɟu!]%s%|B>7 C0 C0^-o*A3RF^q="O[5#.,j%,DP@t 馔@Us/uv8/ J6s\[8 ZMV5<|9nwʃO 1MOU[%I<[SX&P_H9tgD%INf!;5B)Ӄ$+&PQhQ©XᰪF̋kz\UC7bLTti K&r0Qo}=:gsq!kϸ\^$ϲAM$a=0Uo{9 BFⓜw.1GYp\*jvDq#P*I9=ܿObj=mqAUK`2 ;|= \Ac7$XF'%stB)K+Z %WÒw?圮HmXoe<]iCN#㹎[so2P=tv]iα$t~9 >j5`ǔlm7 C0 C0 q1K4ějؓ*T͡({1™hZMv i{5izr-nzJQxSa Ʉ."0o1Fxjy%WGC5 QKgꑅ`Ih٣!`!`!xWOUC~cxg#qiRҊ֊ʥDTITsi&i)rKrvPͮL$ L+ ҄PN- b6Fnp 0 qH^|x}QF(a=[4h>-DDSM8WQۗ BW7q6XvQ8PlOҽ3boO Q?8qhٚd'go J}GC!}=l:.cwNBzNZ$aa,:⢦Z׆Zܗ\_a,U4!Gc=oD1 ܼ&M]81X.AsjUOIxAepqjRKEc͂B@i N9u< 1 ,h2}= ߄Iw \5,{ԐJb`]j7X\υ՜hN C0 C0 C p,{(;h (܅!0U5vL2C"!j0ꬒX-5 U 'f,QN%o$W !w;: )%,׃x%K47Z7%_SB=D ?S?Jڐgn*u%e}ThQG# TUE_j þ4{A5%E홮zu!yb z ~+ݞzkl4':7 C0 C0 ׂ zaC:Qr/)F6gDD}Bz{(F51+tQNMeqaFd")3UT.=PK9Lâ %f4VUf!`!`!p,86WQ&N*FSQMC Pd6mC .eqRNP'9wfY ԖMS mr U-é5U99N4E4ռ ￴ۃ0 C0 C0 C!plVZC$U1*Ւ'LaC*QW6(ިKZL3'S.49N>S.CHf0| Ye){CrBs\(!Qv 6#ᙯ&]D;!`!`!`  UVBekƠfdQҫJ3R'3LRTyV% zc̤TΊ1# r36K*|f0 C0 C0 '8fC)ƨeR< EKhSB&p^ApSyEԾiB=\u@c”77w}]) ȴ%G)% 9MXr'D9M%T*c+k!`!`!` cFpqV%neV^,Ŗހ=SUWFz\ GakU^<eumjzZȣJx?rl‘c*8 C0 C0 C0~_ )>ƨCm%2gTڃ*R5 IAh!1Wv}i4Z6bb<89P4yjn!DY9kLe͛V'l4GsV6xpdX3 C0 C0^ NJ*Ԧ9Q^DSBNJd|.@*(k뮾ᾜeċzhd}}[]!kV%hC|8r2[hQhs[@1Ux8dgGL~-hϾxfܕ'Π1.rQI2޶8emgnk!`!8Vwtz;H&UUr$aTR\\RܔZ&9JiB)JժuY*#IE|)#21.@:'T<\%p8ш{`Be{w~!Soq5|D\Գ}{^@|fSv^!`!`yU4lj7en8sxEȡpPPPǨqۃ|;QQ7WpM4%Z-+%m‘c7 Iّ4%848fZ&iąX(01Z?4^ڷvjt*Ӽ'^-ӅM_e2滊 !`!`ǯL(g`JrC _١qe61eAUCizb\\UlRQLy2A2Y*kj_h)٭;Xܒ'u rqκo)GlpdX^ ;sC0 C0~"xqB2'/L"=N_.2%}2[ä4{dUKۭ*M<)&2>>u-oS2o7v!`!x#7w}/^Iف_?v}Sr{T~m7- J6d&odhDv0 CxsksRyKmj|6nQ Rɤq{ <9Ǩ'dCJ8?n[7na1E?=GHMxe' Vݷ C0 C F[%fSTkUUkj5nIۑf);dzfJ "D7%tF+H>[Hu1%//[n# %7[n;Yy>Tֵ#Q2;?O\V&(ee({0 C0 C0^Ǟ* Pb}~O6d% npĶl<6e/ʞ) }vR,ظ(N8"bd8dҒH$ >T^v@;mZ{  Jtv_G$i=So& ImAl0 C0 Cx)(7 Vײk)Z db =o|.J_BZ(V[&0'yQ p9VvmTRi(BxޱL l E5)վU:n.EtNAru^9\W!ඇ>} ђRR/2 C0 C0 eqPNڐy[jũNZWP5%9 :禳ڨllw?p-KsNJ8C:;㥠m9V(e]gJ_ʝ:c(!D{>tYJ'%rp}~!`!`!`3iHjcŶ%;{}mL# KrpqV YROSr_|;vI3G&I9wWrw).6;"<e C)YΠҪKX҅"FL|VQ A!`!`KFX\ K֐ݽ]jD"r j Q'ȰBN R,P_Cu7~Ej5֤ AcbQy JJlYy¡_2v7@KRA]zׂBj: ̣PU Uqo.(]`Jɂ8Vg~Msz<9 uo!`!`8F7@1rqN{8!.Xɥn]ڨפ^ۗX'49%־%&&d&RF=n61ZuqN%Vf=*ڧ*JrbZ#Bӛ o!'frr!}*n6J~r>EݬJGIB(5p5; C0 C0^=džjPմz/:+I d2\%iȲ!jRx1 rZ D:O.%%*έ9 JxtY=W% J[hx\?^`Nld>r|G$W&Sjv4 JdJ`AW{˃leE>$D):5C햫wi+&FHkQ( oRb Á 1~4E%|[Oχ.e PH-:j%R wGi1QQ{ 8bk Xe[pZf)NǪ@>K |mNvTqΝZ Ͱi+}ٸs[g3BV,=ﳀnI1H*/TΕIG)Eqϟ,^ Blh9\^2r[f*"ReN֚Dսސ9tQN$:>|Z&\M#XP/T=5H%<ٗ^ $3v) QIt H5)c0 C0 Cx# RZՅ= ܎)SUvn^᫪mvxE]YŒr޾CZ>WRDV`Ї8vd2AC6\?*Dg/%(Ihkʭ7w2[/?s'ge wWn_ "u1K&?.KeYE91_dsGn_wnBx{;He41'3rKr̊R4γ^}]۷&WolKWgrbMrn-]ǟ_߸/.f\Y])Arw.;7tŇF5n,RCNKp6r}!՞kJ6Rne]: )ς|;r)cj^z_]7/)0x̂l^?wgr5zwSoդEI9s|;0UqJ[G!`!`gc'``J&(Unא̪2iFmȬ!i.Sه|up&r-QFHݐCv&'=rrcCRuYs#ᷞ/&JCy|+3YXIϗ#j~-յgo?/?גӿ }=iޗ>*l~*i0)ոԥ$++!heH˧*f8@SrˆCIQSRX/~L><7 ,9p>Su 1;[?%r!}^ϥ}KzcE_23JŰOU5׷L8!JpU ‰SSE6\&Fy mt] CéP%\YpUt[궬O~uS@IDATB{] :L-i:ܓ]](K' yI}>$~u׼{s(7ޓ ='S9Z~v:ןQ>çreu_V"#jUrIeL~QwwrR]~G |H~+:M(Kʬ䒄qs:O@̒ J_g'N$@~"Y*{jUnN.&@ƴc(H cV5W~1tGbfQ#7/I|]i?ݐ=k|~.!WC77 *Q_k7r $sr #/A0KٿʿͦVޕwߝT[ԟ{|՗pnScL Wr0 C0 CxuqS (}{QH8 wߪ܆ߣmuOz]m < AP25kmjP'b#άC_EAS巏"79N;`?c)fSMTc%ǥ 8dk|Mي̋򎬼{FRLEq.9pAb,j+ԜPþY\#iVd{7)fXl /UߑfeKp&/kAœyQξKP .7= |< 4]|x:>׺SY""8§q Rb:CqxxOhw{疬޼*sqŀ2!Q?Əs'8Ť\>ga)7)_&Mie,0&M!ǥYK2V^*+ .e\cns]ֶ;RKuݮ,i88ʞ{eq.yӪ G'א JNGz ;w)_)w6ʲCnqHiCs/NTZs^!`!`7Np`r|uE5 r'e^G^.ɗ_ȠײrWW~W< Ջ2$l?ߤf,%8C%z !`!`!28WË{8B>pD3\>$zªjl'iogsmXݧ6:A6%)0˫.󜝔)rIϢ^|$KwnIAYH$ۖ &;׷փ<7Q2HBt,x,SYO2r,^*RV4$Z-}@^ṃ cnɣrwBtW^"Ԝ J] ]T +-42}%G)&Q$n]IݬXl(*Mrʧ0MזPd`4ExuQr;6'$ Ak޸#+WF53k0X=ՕҜ̟WVdyv-" =Ǝ長>\+%9}#{0 C0 Cxq\9O$8(†)ys cYrbԷ8èRQ/}Wb![%:$*=ܯ˽f,-!=wyc"jK6TyGn_!(?S4.9p=W8vnʝ;q}paNƋ9)BL> 9#ܔ+%^|MV)t}6 <̲RR#?iE ky|!\M/2&+=(? }yS9#3?ZUb l_Y`5+ &i6-,$L1Ϭ/wkY-Im7҈!g.?Dx K GPuݨC]A_\OL3Y!  ?ct1!f/-l)N5fOɬdߓt|o!*nϝ(,7Afel :[U)Iu3 C0 C0cApC)JXѣM~]>)K~x[_.cTktEQmG y7qWw#bi rC=@ձ|> 'dv|Do/$Zwe5666Vt'udL*25$!w =`U߃8z r{<7!G=iYԝw$Eذ:Sz)qn윜pI~K2/ U04C1S=js[>x֙͜쓫\bu(:։?){V/F~Ҹ#/jg Em$rnk\h 1U56AULCio,+9[Mze8e!`!`b ɣGm՛}FDstzoQQzxLϋP1#7*iڑ>RG٢ύAfQpqS'_ܼc΄exmXǨ=Jl< ;ߐ[Q>WvZN_x:ɻ8Ŕ'Z\o hޮ`5گќ"ɞ{S")+gǾl ^;$Zac%:Fv&1]l FƕX"̽b5*zg8k!`!`ZU2j*V㧭D6ˡQ`ec挩҄JV=9u yJ.OORʱOL:Q}v]y9)AZ_4,9g?>7|H,+r(䜤&ORc!D۲}!_[זr 9EM)Xjp4)"9(JfXtڐ̍ZQ#s[nGȕ/%>|:SYllIMۓEBd ^PېC7x)y]-lźQޥ+a-ωmJjcr%Y_5|)#ԩ#~XxY9M>MĤX~O#pA@)!!VE:=sR"~[f@ C>e<‚ǩ;Iػ-HPY+s 4RFX~Y[(XO͍;g3{any#w E}ԴPҫ75SnȿU֥8%E <ָ{MZZȹ-C/G2[s¯eus^>1ŗGp2@šv1 C0 Cx#&H1N%R2jѨ+5Uw5t9I&Ph;_&eR&!![v'"-&USpmԟ>>k#Һ/TU̓hhm9<!JoY[cofd\8 -V1EW^As[%|LW'?zZA]iKS)i(1yɈmZ6TPW Ye{%Z‚Fا lS,0KxAQE|WF-gPHڵ JmzZL&^~w d}ׄ ''-M``+lRf*d\ *b7gK7ToL(cwc͉ >L{iV[~췽Xlemi(Rte[NxQ^t0`xIG)$etA&Byc!4j sHMrx-]F@#h4CD\CdSM\.B A [I^?D< k(C$ (jHgKH M'ڴfCWU&_h0v 34N+>-bצzrFڲHe+I/lQ(:v[=hx"G>3`6Gb_4{wq{.sQD|z\Ύ!f3,Kh4F@#4w+)RWfM~J-$ 'QQ%$b@V-aIT%NHk˭K@b +b m|\d|U/H2N/tj-k7TؘL?DgH"Ν;s!Zd-ev@ ^nYETWCf[GPΟƩ$^&1;"a3ezTBX|W`?'spQ0f.^!Q>9Cf4-^Xwu'Ĵf[¨2<9T{҅"k.)?ų}}5y? $'`e_=ydQ7K[:Cܚ[N]X޳,HcFPݏPa j rx/aIKq/p{ȴl,\;+&16z,-sYʐZ[I/3X+,IxjÀ6h`u7n4) 5pq?XY+4L(lu qmooX`&8Әp~tnTݿ*VHĞcB;),%3֖h7?sx6vs!#Edm M#<>8@\ wQL}XF˓ o<Z* `$g ˔l;ػ͉"$Ni 0m~>NH씨I6s9 ,or==Y8RYuhs38L!<- v$Vғh4F@#x_w+j-I5THxb(SBKpZMTH WM\.-bY!.es\!/S#Xswn g}[꭬ǘ9r%uG޼N|[KWX(Xv7f_sx$^yʰ}ag~` 8!sl~~2=;lS9p'€CBT}v,oh;.0ŚUt~LՇϻ 2fW,]\u' 4Is8iؙ|,P!V&*S폹sPa 2yhϧk-kfoPPd"zӴ(`3U\pG6/r@qj4F@#+B\^ފjꡒ+PBD"lglIŬmޢc9vVZژ /:^ڦ~V.Sk /Ym%mYl!C$ɬ`30ZUp?'Ӗe*i/$mr@, jh B-o1nbf&o*\TrvNhCu؉xСҖE=:UN02zhF@#h4H+Km"NcJY[.Xkݘ ڐK52ڤX>)//*mP l!Dr+$:HK"Vjp%Tʪۿ/z,dC`׋-ƣCRͭz,kUVs ke*MCB6R5̭0ɚ\Ͻ<Tmd]fi'F@#h4{Dn5Xٮzj:+X|xxt:t&$$V;?s*\ uR#qцWuZV+ng.-e{5oGӘlYp o$.+$uӖW9QqU.6(F,W«$j(q7ĶӺ4kF@#h4F\ErI.$V^h$ST\׋ZHZM+KˠC2;~VFG) s6W!-#+E6ap`$7p8~Eٵ%bn' z9" [Z8-cّj%tw%0#:w zܣe!Ų_v_a.d0L[ xNI#h4F@#>8QK PO H䵅꓄LYW\I[v"I=88}9,r_I7f N FrkRuu\vɭ;c{;IS#6/:?noF<32[ myZ[I\6rJnW2a*쥻W u"B+WOF@#h4 Epe)OȭEhl3G^Q`Ilo`}}kkkB&Qn"0H>IG.5]BOSz4 t Ӗ7JX&s8a֬K\U"@DB+s͕۬ey.~(ս2]܈רJ.kzh4F@#SNnNbyn$g7+Pv:;11B@R|5%f=#dPg9/Ï.č+q=;.KD cuN+l "=ZLQ\򼤪kRvfV OGIxvJc{A&,v4F@#h4 Ip t!#)zh^i#$`«p%{KE%-(lDvvnV=tͨ֘f/7]*N&T}H:vF@#h4"p" Xsz#tE0m9sLsXXϳP٪8/U B% /mt`di]z,O{1QdWmJF@#h4U.vl(mL=\ͦz.Vd Rȼ63Y^Kn(]jdOP6U U++=*f?(iWlb[I[v# *!re0+"-dWOF@#h47EgGpR\.X,YVYX~tFHHrr^Wz}^a`<*;źׁܰ餟uE*y,osBJj6ٯmz]i-T' ^gkRM>.w߃1{yJ#h4Fu\Uj܈*+QBdfϣTv9d.(: m ! #2D}tUT}t/ .I Ρ󗈀Xm*]3qy׋>xmpMW[mtFEW@*#YB0qYITaRt\Xu/hۤh4F#p X[Re%Ym.S$Wn7iézJɰYuPvbQRTWzIp=z fN[ FLCbNjs!,lp5u&.6Baufi%J2߯T;8tpVLZ;M;ϕ6,| =RF@#h4'Kp aeP0,-lIk|٨fi"vRuV fSչQo(\Ix+Jn4ǮQ{rvF!UsEѕhQũ|vD9x /nZH\Bt%J]Ex6Jֱ!L$g7~\QzF@#h~^Xۢ $uۊ+)lIV$f2imS bd(@뱅V-{ i)ڌ0*ژhosh$P(]&RՓF@#] uS{1"9l3Вk*SUxRskR-ӪqP ^_j'F@#h4 p(*6MdP&Sq٬$vT;%*$mK2 [K5ژ^h[=lNUsKZzkxKTLb,umbhRTBX#;4!YE].C$R Imn6xK&Dͤe!zh^X%q'¥p5{  Pe(O4F@#h4DD\!RKtZ=MRQd,V$We*eIjQrrq \ޯX2I'1vTRsq\^]-V5«Dɕzk+鮺P9!`e o qY;`!TLOR+ mVS,n Qt|^c6~C\!R=`A4@uoF@#h4nN ")RInH\ˊ`d c!BnVs)xM&! ~(k$eZ %ytLvx|!عLQr%xV%˖ЩmsL-'{ [$}bmnw$  sOBJҲGm&3ci'.pvď!F@#h4N Y"h' |dժ[Qm!:J?m&&0/ Ig* *Z^tvz:HteE+5B%|J?SZ ӻF@[C2vl0?^U_{oPY]`QMW|e̤eiۇrL04-4F@#h~~+6`!|AkŦREQ&wmf!:tz+v2*{:V$YYQ4;uAMs$5eEz[ʒ*JәWW֫ݽ;Bp+(v֚؋" I9~=mYnG(1­:,{Cc]þ,Zy^nN,6 s|ܜN%Z~XX\Aϗ qo(؜n8X1/l&Jؚm,TPIUj9[81"q Ck5?fQxb~~q#%W7)-`o#"泑кzo幱~q,c(6߾?g~ 0. FHp݈Nvz~|ywϮ!_5ܲ!:p}oXaXS\ ._q WxE2,<\7x">r!,(fxTZiԍ__pxFD 7g3 Wn!Q2iQbuXFrRmrjBBnjuE- jaʾyL{qy+5ś0ިfG"~kF@#h4?7N,å_F!]Un" T{e.rN0tJTJ3Sm{^b,jRY/W),YXP6}Jh|{, a^npD0іY4:n|L.UPo{Hrz1=ř۰,-&P3zC]\ES7זf17Grp [-|Sq͕?|q*Vu Hg,nc΄8 <oG h< ȃdq#5ѭrɹ. ˆ|.+4w{V#;C2 *?-zh4F@#E_ɅaOp2iؑ[lPɑyLLюwi_&qT$5\!frym\b WmQiL+3r=%2.2wʒ,q^٥yBN)ck>X>:m 1V#ZlabK$5Zlb Oaiۿ?EU]*[wKcN 6jJbpj *jW_*fo0  (?4[ ``ٻjߘ:J꥙p-=UػO:yn;+q:`cX7Uhii|3G_1˜-9-Tp7P.VHH¬lU$p6i(v:Yͅ2@<쏸=b=c`C#1mxK;hQfqx~G0E~ZD#w!CE} ۝Q$^|r!oN6Ƶp(FTVwxwf ٳIYzRDE" &/{i7 0Rs*˔tPq#aGs /Me]VSrh#!c7.lj̯I_F@#h4BD\K s+B>dSeTF  /ۈ}f2GJ(n+ԗ䖽3Tw|^(e&"hZ8yuw/WlѢwI{ӮQyeS>nd~tttr !h&v/z"g'pP2S!o֒W'0eU<.1H)d&Ě-y@>ƭY֡Tclfs|UZ `t&'.&>B q}!\wؾw"̚їOTYZy},,ўCְhGӏ˟bUwp㏷j N J&O]iEOtITsXxwzf<L_x.\X$Xt~>?*u*#>S&殺XyD 0{9|y|p.@ lbnvފArП$ i5Tsk9<漸MW XyF[DY-pZ-^3ZkuqDqj/Ӫ2AqppwaommAYKb1 OF$A5Kb~Yb`)\&ԥ)Fzۋ!d;;)W1ue,q<_},>Ox!gzlC;ą^Lnhhg a̶;wҪN[z; P:NόdTDh>8naYmbypS9ro~mŜi3e'nòl#!EePh4F@#pxWq5vC.O '/Nm$fH=^OTs$6*vIwIrZlck$i3I9@}^\ FXHL3idkau|MѰq׉8]$z.>Mu䗠 Zḩ`Uٿ$G=l |}+ȴY9 H0Ą6AIEě J1l l&@A:BaE=!diL33JOCr63HCU`7kXΧ b:Ysҫzf| tEKRW!g=g{zA4*Ӗ83@ /[0@ 嘠-Wj\n80dʼ>BxwSU[ajG_s=*mh4F@#p8ʋLIZRLG reE{\ER`e+!VA$y)+ J肨2&.EJ\S!5>nB0M&Iz<*yuVu79Ʀ OdOP뒶K-#"c` `𶙸[ӎc(Q-c貭MHxL5ޏ>xqJAf ??}XE9Wa}{&c?m^S}=3Ti_ͅYkX~ñ R댒``c¯o\?cQ71u%n}P%, `bmhxWr~6Z07"d΍ZAI^: Ll*e%{/37E<>"l,TͤT\Ģ RNFXO{LF1txQg{d6΋)tțHc3M۹Ջ$0XOXsaÐT}+gN eAmU{;e;~B>C~bdyXF~5Uxʟ/敽2r҂  WCWI]֓F@#h4@pEe(,ImԭhM5^JV ʓҼhgK! Ea; s"`\֗Lns3qpzb ߯z>OpG!2哱MvdmS~/bf/N`dl3l4D۪AZ>_t^S[_bkkSٴJt7y0$|!&>2 ux;G,~O&&qsPc|b 2rx`ѐKPcPà:dWpÊ5]>8a r^*z3uE-,Ya-4rCC2%qOFfٱIhmq6T-qYdmm?r eٴc@f x|tcpi1ݜvpof +PAZN eፁjIkϝVʹ-Ɉ/ +Y.b5&tw̼Qv[jRHsŶ]p|]Sݷ0TF@#h4@pn.쾫*_Xֺ@ɤd=(&XgwWq%tτ1B'Pķ &V+[|>:Kc b]$= !  h8l wI H0dOb8%Jl(Ip%VX'ʶ;u\|iYұSu zXwg}V>w7ojX[ZlS8udMשObdj C /A0Kc1?oTxjsd&Jږc_ܡx Uq7TL]Oξ#x3 ՎKR2{s~7:Ʀql{2-nrU7$f/9ⱉke`i][c]/Z:9#)9~QĉqI]58dD7B Bs:kG~Mv50J$fK4jF@#h4 4L+RJl]r+X*W*L-^LUu6, kʴ(:&~"I"7`*-&i1rt~V''W,+Ԇz<|//cee 7q-AL\ ^{e3TՖlTC?|< -See-"a{c{Of, zR4%L1B bA:xDϰ5t**01%D Lc6I2z&A9Y&5ԍGWa?;\B7kRUr(śnyO% Y,~RO vU`}n}9(c?-%Fm~"7h_&qFX6Q}s25o1Ϗ8|[4}{N/R#h4Gu(Dr])8lQd-TDS-f*TXUjؓkq3$Q 5YnKک Zyq]-Dgxb,2)Ō]ZRݢ*X_ 0*Eo,Fk/X󏖰v7PkHdd~ M¥sݵC[-{V`?>E-3i >C "_] T]6S+3E"(I}iդ*233?/ ?×cd2kn_Y)N{osTEAc:sQs# I5:#CrHU.uL2~:y[>cH6%F:;m'ٗ*$yYHoXHvר|Kr m`{Z+GjDatR+7͆Xs%ಈgF@#h4OpyleTZ^ w.h_.'{-՜J JL˴qB"W*m٠}Ug#4zDVgVُp8|$g. /_=neܟA(\!$5[hKЊgH7WQ!Cv[OIn|]O(Dž1mѪr߀(ě8X43 ieZbk$d\g) %g.oy^h!;:Omޠ` ݼ"ЃOkWtT) X;\.%gl!U杀kevyc|{t&cnR'΍ 5w-; iYЊ`xVS.L5X`}fl5{hmoo;4F@#h{'jܺ] "3S^5/΢vnIKn)1zD:;'ɭJTnREm&R!8"fa$ !2_Wb'-ϲ\vX;$BlTL^ܡ}\.U=->.]zG{j& Ë,N"lKggHZ6oؙ~`|tx4 aC#B& ۢ<ryB/Pg0CMgg?$dX4ؖ&! IGKO#(7cfpYj)|ŚhR]p_D<]2hxLjIB]k7HG0ƣ0Z/WK;Tm?b5NSiKŞC{6]$ n!uA\\(:%3`*$eUKUWX@AT93m $)*Rc(!>I#h4Fd!ޯЄ\ iPI-2HZ񂔊}^ עvhm:)Vei xXKvJ/K}wi% A՘$NF\QK=(kf?I>Fa7IR=YuBI+$!vS/E%K#9wIŰgLc^#QiMՉ= T0[v YRoPl&xbC[ΖT Vz"B2׵LMEs;򡧏털] l.4y\d<ϒch3;|G6 _Ws bq[{AQ2lS\¶Uo۷kIع9/k5tZHD/Zq SQm],WDzRt95,iּʶB.qyOsTz.os ">KTң/pך3P1d6l,6hv{:>3Mb|u92esGDw Xg|(H"l'1[ʷwqo!gCgrۘ,!rn]8=[ mV-2]rNɮn<҆mk!k"-No]T4F@#h/'  EW.((2ԨJ[6m6< `./9 PyK[bknpyxK~"A421J턼޷Ep  c wnlyl^D2$(+70J`OGG0y5e[Tvf`Ϸ98h'y%e]rBK_޹]/!!Z$"Q9h>:۬ m2`c-[Κ2{{ }nDOX}ZD1M;؉-i{~vxg΍Ҟ}M L^b#8=&85N7"q` NaԿOm_+(#/jf51I=3"R$mm&CU r-ܾ{>d BpIM1{F&We<AXgְ ZUW6(XT+!יx nZƣr| "˒!|VHHB|Ml0Fz9JPuh$4F@#!k!b]k'~LՑ-TMA xɕ׊EU |Q Zak(6CyyDrr!kW&Z +GWڵ7bnhs yph}vKxþ/2;=ʵKƺA.֡QL(rRnm{9Ukvh(q_˸8@Ou9&cmoV^Vo~ZcC =ѧ8". q]tl~)ӣ mQ>BNdH$t.O~ pP-\.bLvr >7\c CxeE$8Rg*-xdg !&0xWE-qo"fVẉ\t^QHoȲom+UfߚlSM;z.cp5g[ɡ$ xӍq<G e9B/kݴW$E*{cSL'lv˺ɏy !Q1T|v{Ws1{~kC6H?f'] bߏ5,m%{L\.3./WHnS7Րܝ2lC8`8,}g c,N[w~&]LR}w>a l:BC~l!|;/7x?I7{.s%՗b onM{{H]G}ixݲXYBf,rB6~Suu"pVQr|D5NV2r%*0 Hx+fW@=DϷ,hFEEEW"("(OGD\QIwVʃ+$R' ,90vehΏ%^'[,ڈG[ WʠCSa͒gQr1c.ʸC/PE@wc.PE@PE@H+?맱(%,f)K" ,,)vL`n-!Z`?q9 qT|a~)Z]$6FHFj kOӎJ.a7K`u'TLdo+{MhL%V5USѰ'.9O֡("("8q fN b+WL1Yb&GȊg[EȢ u UC[IڲV[!x,XqzhzL[E@P$GI{~ ,\l%y5[ͭRխ|yC2vs覂{BTs[]KPE@P$͒d+q8˓H!ҟ+ˢ()2yCWY!bd1x(!]T+?nT^*1\v-Q|=TبJ?*nA'm>v\ ČTTq3T{c*$4ꎸڹ \DPE@PE@~Nr$RF,DV $Nnq=*Wzrw3RKJ尹G$)ڍng+1@n";6ٶ,|? E@xaq̓0{XwX[̆_iBŨ!4FCtX`}lP&tE@PE pIM+ m2kwYZb1 Ƞ8K$%UfRf9TSl!rֆh$jdQlڸ!Ins~Ƥ?"A@~,8"㰧K  I`w)T9 *U J5UՑW1Zh:,&lY*"("48QW&TL\6a1 u˔X*::͍M<2 j\ aA>x?iVfE@PE@P^/N$}6Ģ~_A_"DDYz 7 ゲX屽_@F2a![Kҕ׬lEE3,VI<:'E@PE@x"jѭ)"2ܘő#W=\{"M3ٛ8FC:3YXUndC|e4CU2[PE@PN JpOˑy*k$9lF KW2J.st%?l::Ѩu^ 1?p\vz-?/S"("8+RwXLyqmm񾸭EMG@~c xqn0ǝ83rUYLk@:WbP;$wSc{LFɲDI9Q0E@PEBD\!rr*BX4ܐJOdG$,Mf}TtoEe# &nn{ =by+eq\{2?72u{K) wz0ȥ 1ҡ("( x–fqpp#6UUC}EHj EQfvmmmzzsm)덀*|Ë3qS\jس[EF5U#;W4.277C3*#V n8f-Kdġ)"(\QfKAl?3ܸqkkk3l+Q\XHI\Qm[ZZۋ .?4v_rs쏑" 63~ 91fX2HeF͖ҟJ t ˛y Pf8.4;赗1c]"("(Nj D#QB0EMm`!Ҳ4{EYU!L[QvY"(/Q\}nYlxƲc޶p1hB[{E{+K\fEsӕlrg->;\,EW^Qu*"(q"pW̌An by l2$/&B\lzgg(~!~ܗq t"L$R$Ian#m/Ϯe>" &RE#^jPmOBx m/ 9--yװtdg0ƹ?,$*GZw5F@ |$.gv:;qL/%1Qbn`g$7_r=~qY kI]SE@P^o M(iH.27!2_YGBrZD|w[kw! ON%/3G{d;xׄq (/)1ldOn} !M)2ź@IDAT\ΥBYq9U[E1:l$:E@PE@8 ;TI lž$r$'Bm6]b4WW6Hp[X2) PӄQBJ#Aa<5H3iܚS͠[(2?CIts[n˗؟;Nր6й*"(o2Np|!M($\gs^͌\y TL{-`%y"ݝG|JG>Kqmʨf.e'Z&DKyșrF[+Ƥ5T+%A1 iN m0F*6H#+*UaB5@QY81F"^D;GM%Jz 31,&Cm\G؍q``b8B['\3snmV3ɮu9gYKċإr/P`nU\Qg s@en0FwMBl5sz"("HN=ȼIM_vq_3|ۅKxG7I'C)uبQڛk/-87'~k  l7&?*7\\>k]/e׎y ׫dVps:el`?U@AKm]C;z׃YxAޟkǿG1';\};*_I+ʧ??~}0^qBCf'ͿYHyqn,`i+; ܘ=0.؋0I~qBF \_熂0ՑQl6b27C{(""҃%*ĩkEllݳ67ZIfQgWR6ְ#c"t݉3uC0r>7&mur nc4>Z~EK-}D,TDPa)f`s׋Z_ؿxNDDh5ClNQ{,,.PNc}a6DA.gi:FB Udsг⏀#`\xf+E͍,7NPѥ[fGTg++C;F{ {:ZhK6PE@PE$! 9P7,O#:AA[.boc˻HVH^zDoy4!{PX`'>Gzw7O;6]b,J#ՙ{70`v #fINͣ/nO^-ڊ,-_:ּ>L[3{5Zw@@$;';m]Kr_MAHnf KF9dYL\كgB44* E@PEB@ 1N=h^GO:X](U`MDql-M1v1a #7-*K^ $ayqST[qiMZbN`n{,E,Xmm/6mUaVmHָ8bqf0"]=2D;iMl)ώ.bwu>6cIB=C#錠7JMToo8u wB'F߿q\e6h., 6G_&Νŝ}TNU/Ao :'`PDž,n7Wdf?d"9?Mat AT˨PN,MUW&ò|,c VT z^c=)0idR7e85kf=:1E@PEB@ 1n Og?݋ Y1_Laen;Cak,#+͵0Ö;ȓz%|b ??/0@lE( K$H~ |ϓQ`ᗸ3{gYctL0cB"/{ Y 9/L2:vW~50.O[++؝Cr>uPm3H"?Łb* znl W!6s \gbc\HTH/cF~o'ar!xr7JW(7#n,\ipi TCh4enWؙ\+G˸|q~F,a?&><Xmr ?'xP+(.EB_`>sf6Y8Ga˾XbMJl_&6bX.̒I w8&;tkTg݅ F ca%sm{A _k7Є*[ޝGZç5Zc]ˏpv=V1}B8Jnm E\t|Et?`B 0 UC<]z˻0E+*%mX'u("("P~b;rgO3, xk}f.eI:L:nbea={d=AjSyРS9յML`sWpeAZX[DxsfY+ TIVqw. {%\z\ĥ6uԣqfar= UՈ6Hп*F޻w;]6DQ}lK pk O&Ku)}cu{="tKlsrkМ+A}4Hkl2><˽y|Lv;w >:6enO[O+]q\x!ogO&/qYsctU{@", >\JcׇnaJfŊE@PE@P^Jp_OnY[&0;A^Ni`ye%$莰Ė}^J^v*p||ĹQ#΀u'QJY&1DqF<$V?jj* !4c"N3bg' y\/ƽ|tbp|[FO=|fAa6KkXeN؃p|\/PgaRFƓq~fm̼勞[aq`Paf/RL1Bܙdu"9w1O#6]abL]jOoӂ|T_}nYlqcGc B8<+);VP4hWF˛,cRVٿ+N1of"("LLtgfa/bÍr gmC+JvTpptut~xHnʿ煮s(%Wi4G{nF<Y>!k7=#4qX Mgf.n5TD4[@'Ew ,K.I[ b(Ff.$8rq'[B>I"eǛ=LZΦ|w즼y (!4.]ҍ:5a0bg&?)'! Ý~; a{e3)^lƋ\җ;܁KU=X'("F(=ƃi,߅.!+ v.!^3rl.il舴ch$ 3E$nv8X`#@YG%FSFNMʲ:ݓ;{"<)zN[AbiKٽ*!TuLTKmn aeӵbE JH"$$f{ml5\FX4_JosH*]bZhZaw: :4sq]M!^r &۫p6tyAj!?XxA`[Hiv1- ,]^gVnXCHT=% XQꊢg Vkjpr]OٝFdtΊ"("pP{GDgf)F'cz-`y[qzغqAYpZ( SkuG~qT3VpX}D&q=Gjz6m#Ou9b)eOtdϟX]UH.,r]P\4"ˬ7};wq{T|)T6|K>E\h [녀N]it7gvn3)^`v X2e~7n02 Met`:E@PE@x(}pЕ <jC?fWS]6"+/.#Y]`g^lz7'JI] UQ}fZk3z×qqԋKR`s%GJTrRѣc1xrZl:;p +W)O >av`DX@D{3)Hi>eZ6x⽿G6m `d鶇jTƹTh}Ɛw릒4(Fkwx]?}}CTE@P BRL~u1)9uk͹\-)я<.Egx8R~ ۻf˰'w0x)g*X3~öSee wiaaeD{Rr 3r즷I$a̞.Fp6zպ *Iۅ 4zFp;,3qD}Y\H`LWv!2ڍvثK!O {hM]ԽXt\0MN^,E@PE pWrdeZI޸=)5!PnbWHRpYkKF#m82KQkao.1=?Dwb=8O22h4PymlcQ} 2K7cT` bivЭٻ#@%r 9l]$f2CdY #pQݵ}b_b˺Ȟb]<?ac6g&T oyt]ԙng\}6#Rh ['\wH`~ufkb#L*?N$ uۃp˸ty4Ut/b+YxȐ5L oG,_bCo} 3)s]^č;#tv1;O߰L8H[}= auRç)C%¶Fx%x?Jfq\:!~lT*sT{%ơ+\mY3tu("("<;$eƼd2KpY2F+WJ_ gn 蹿=ۺa뿄vұ8JWI<-DG Gp}6uܘ^Ư̧-)eT%?aD[$4L2إc>>/Xw}:U%]\Q;!2Dy)FZ-}giNՆOt. ^CReEYL~ \4wϴ976Y`vFֻf@^Ý]eF YQ'p'‘>L\|W(6Z#l<$< x*gd3c1:ȍH,- C"CşVK-ckG?4͋Wbp;/}WN!7R$c:|njY-cURձU/>-g!#.b€ء"("p\;vr9,..bff>~ ,6t_rR,E5YT[!4r=2JjUVOܦs gE)0sd v6UVQH'q3MHcc>_{' #44tx)LLuўQt92r]Zn Z0tmm{8d$vk$tlv:`1:9cj$Tbَ#_nP[9D{`,{$ O|P kA7rÍ֞1Xcf gW 2ُ0s]TK :6IצI`ljV> H٪\3aLK4<#1m\̾ND3ϕxIۊɋtaؿ_OM)?(QauΥ^du;uPQB~OL.0'Wo,^$1v/6nU,:E@PE@x<8urβbYob1lj@? F o[;kL ɕfQM)O,%Fȼ$9LbeeD79 ?{,~_oo Z~£։͚h;0r>SZVSyWMS%M Gb<_C]j(&5n7_E*˓Q`LEWN"<|#sGa"6JT9Oپ|}:6}nPԙ[n5ڕ0B,ang3"("AϿ( s#kGǢOѷ= &%t#ᅬqCU~z Dv8/ ?&靻' {0o ady˺.~<)P:#:eP?+t "; tOtYJ!}AkЁc!L %Es PE@PE y/9)=D"k*R$MeDu]ZZѧlKl-Jqs,rF>F׌DWYOG$Ў0G570MWc1r`^q{۟A1v-XrƲqPdPhmHgJxȨ!] h!] oQF|?fE@PE@8+is\z ҫ+$W\TXH qZR&KpѨQf,&VG"C%dWJý|{t]z `fD H0:!W7wuhy$xeY=ƾ[Jb$˕%fD -ndcv#N/.1^^,[XlΞ7ZaE@PEB h Bz~W!Bn09ߤOv~~_|ӫ{c1 XTXH(BrpKn6_ uM:lF-HATa6ȭtLu3t8V7.1>v=1:۳ f7l6_1zw;=d\Xԅ(iƟq"("!pnS5 YgH H Y8!Kɱ"ȝN2.NFNVO25"n,}^-BXm0WAL:z| anB5E@PEDDs$goo|.g+N_(+Exӊ3~vvˋTsWY_b+K+ʰ~a=l_^YXE@PcC\鿕ަ}Yɶ5JYJYʢ_FK_("$f;B$Den iG!)z3,a`~=rUٻ+C,N;N'Cɭ#E@PE@8m:w$Wsh^~;#swhh譕R̙3Ͻ}"(C\Psb z?\:.|j;Q~Ax1tw!>?K}t("(#~駈[qN^__ܧ4߼y;;;Av1Qn P4XXPiwn v0&bDwY0di -d s*[A6C ?t\X4]"("b8?Q,R,jy_JKy)OMM=*u"(njf1ȭdj-le1>.gOpw1A#<3ba.WJu("(#PcccFЋ< HE@PӁd޺&$U\ۊφA' /ݕEѭ`JnX2M)noq":E@PE#pիW!Pժ@!:E@PN'~-;HTۼx,h5){{ɝ]}^qaЈʭ$T"("-(=L7tiFDG{ ^#J!L0]UE@xA;Joٷr+>#bh;عR $5J`B>*NvQE@P#Jn5L"($HJY򶽽=/#~zWPE# j[ՅAlp%/Y,_eoݥ;ޗǻgZX" '}?u~"(G^Jb_.QT&^ݯbF/ʰI"()B@]ɞ6fN{hˌZgd L/'PZCTt;is!Lwf"("p8WJ}xG%JTR&'?YX4 N/ ֡("pz*qZe׏xK[ZH4s o|>+ca\9S/̼ͧ-Kf"("p;V+EM{{q$Q*I^mb+[H%wuuX*Éޞ^8㝳n]PE" n^yÇ,ㄲXEˢXb:<hjn*5PE@PE*{Z7?eb FJxZGp DYHd$v!J>ݺ"(D@2peluXO`*4lԍ7I+R7O4(n];ثa/K-4)9{ HoCF.'|G@rs,hk˝}fޚ-cCBcXDXU=3PE@x#8~ms NuYIkUk%$P`)*RL*Vʑ%϶m-B%-JTD9CmkcLjFr 6P7ɱ17cX)Gawyp~Ʀ P 2/!Go}Q?͏O(IP=HzSMaf%D +M1Z(yIhxm4E"("<N%0od@dʅbI~$ɕvX6!$2܎Kq;UnOܜK% נKבEBnE9UUeX+a+s,f S`kC{c'x&r^/ n.6WpP;=PB?E dWbh5˕,ݻ I1IEwZ"("8~K^{T)#D6O”H:E%}RT}.D,Cf&ҜZ5ťL+K Iom}1/ĉlnnrN9crFWIҹ<U|FُlѕR@.^X&W?Hy3;X}tU_p^Gs"UTdS)d!dx ͭX!H"ZD.U>8/XT8Z2{wp 1X/A$J՚'V~ƪ nI~ /zyV;pyC -AYS>,4K;OTRu#9Eˮ }' mRrlF[v:EshBEmQ,Wɗ\fnPsPE@PܧENE 1!XId?$d&A.xOgdk+h Rl%Oո {d@ O򋥢qxLYDX+9BBA&'+O<i1*Sd/O 5:pn`{qOG#!ep]fFP/瑋ocXxњY=22^F*#7 rB|s^?jgN؛HŕX[r[?A~ya} 4PNǑޚʂw~,'71w&߾nbc? Vm% ,Ƌ,\Ij6^4fkXIT)y!Q IC/Lxuj4ry,?k_bh $g3JIXr?A'Z+V/=4;aGM" !_l9@A ]FTiVvd.X->xE ~/<t7b˗˅"f]O,{0>IɅ .q;{mzt{rX~87o ;fwmOS #sS"^x|xiZ tfiNS͝3Ptgv;-,Y~r|[VE@PN#'w8Zk#=NBnhSJu{%[YB&W۩;Qb!O-G2w'bMG7ϓT ;X,׏ S{H,!IM:.xH^ZJ{W8.#TؐhI\ bTvH8X]jcG[g1hO~\T Ik?H S%$iG_i>cѮnD::Xwqg?)na YX9^Vt@%pmPJP } ݗk6 /˲dl {ogc-eٟ3je"mh;e?܏=*Gȸ/ L {6 Gh+mNvu!ىRz9Ĺ{{%ً+eU~e_r TpZF_7hP[R&'bx^Ao/gE1^Fy?ď{ ov;2I\kgm^ 7| s>U X&EGI3GocVں68Wb0@(q[qAvL+? /Bʿ[KBܓCՕ| &geU#s4rw߅ր?y&E@PEC s(?y rHXAsRlƓO$krBdc &ˡ!۫IɊ[Ue齵H3Ud|3< ?ZqYVSNL&s$+jkqPU% PpR\3.b7UF"_7܃PtS?.6#a2H|cxq#\I\&W _q}.˧7[1?&?tW<6?WsIll!9/}k qPE$%g0{|ywطИWk0~}z0s/?bEL* vw~+&;_b„%BՏdb'/vmhB__'? AY|e{m%kr{⓵&703Xz6^_7NȮNw c ytS٩^֡ZN1ݜ݅4 Sz&+- *= V("p"8v TxF}xR-4IX*@IDATO֥|ya,fQ3SHK-HS'G/ʱ`0aZBƕC=OԨ%<*tl?xkTՂCpsnb0{>|$!tGMѭT/IP*qE;CJ!cI쮱x+"]kC$&\aV<> #Bn[|kc+XމQtzHE*Xޛ7umY$1gR5R%Y~{/+嫬舊Ū;:ꏊ++eٖ5R"y oH"%k} 9^{^{=[aH8vĎAժp ٜzT$NOuCc{`NXsز#6t󏰼K6scM۶ugq6P i޶>Urd޲!=]-Z vg %(?i,Aezۭ)aq F<DD*DD{c5e[^J1 сiKNJ;3R;uR `V:-8וA;(T8X nЕ~c҆&R6P(Q60>|ڕRɵ$)RW;߼p}Y^`-Ӷu\m/Se~ & 8-Z|Ix•aȓt:(B`M7m XcWI¨o[h׿|`%=kl66r}+}ta*BLfly ?eN]u+-#m|w-V9f]`"&<ﭗ_8A$^o58+ jƇlU+yBؤ2Bqc;]ao~hm_N46ogNٝ;fɫ.pa헦ٷl7ogӪ;Nگ {u6la7޴/oY~wJTs6GX䲥,}Sr GĩPoe0 O~7쭻~PBYe|fíp2a EJ^~OkpB +2ec>}Zl5K$L͵lLh bpeahi"%641k 3Cv3raO/y{mDsޚz7ZWO>V+`K;lө*d3<9AkoC+ K=k$$@ u&ݧdB՝ o]:R%ۖ#Eq_w7'Gd󤬼>n 1|wv3?k.|WeYjz2#Q!ԾW!&Zˎ;f䌉{L 䒃 y";'Ex0-aMu]6_!ϙHk8ZgUM=m'zݺ3,ྕYܾPS_ Eůae#rOĔeoW=d9M~_ w:{Rۨ[;;0aksx}zPZĤvknYo5wJ 'z{a}~jn{Ю}6cANwvvz-aǭyp]F5AUd6x3oB\UO~(ޚax)  ?(+|,~6T"9ԡQa5nlŽ%FeԘ`'a`cp n*jJ+2~6?9BWm>IlZk'pmR*V_jI>!ze~e4P&ؖbefSk66hKh:!j mnxfSѩ]!ZdOQ{NX];_`دƦlj|USIu۱3xI`ZX[]"DVeNmrF.-Pj6֞ڷ?YүƎ7TZ'b [g Zr#M%J~.j*d6_^d}Q܆"Kqx@SԂr`a#ÈTœz< Nvn# n iR$F EH6R{7')W!DVǤ̼*:6I/9S ~bU@YVƋ@SjwJ偸%┳rnk; !_=@_eH9 luiEsWӶU~[k-*88A8y`j1jB ZLӷmC)GD3SN{TN6o!Bg9Ӗ^_wtr͹ް/%ڎh;e'>Y%皲Aww؃!5:NMX;fU]%k iWQ?_ 'Ar6dOav}?tk9Gxz\+(/u׋Ӎ@I׋'o$D%{:hGaB'>2@$X%[jn-9MJ;z趫D|{г1 YǼ΍02[%3[S*'U%C6IFMݺҏ~/k+ *Q*;QHrHU9G S6o&ؚOEs|rX̮6N sY<נQ]Jm俤:ϯ\?.W&ȝ= ŏG-hmZ) '5k;ڔ!,-[M,tSA6ӵL9_ |2jr!f'#VSe=? u=}-!v:ȡKl[]q$O|vB C'ANy3kɨuv[ ,L%%ΣWUH[IbkMTumurBK꧲8e7MHo?c T* !0ϯn"T'S"J\٬N31W~:-]WpNlrVE'.ϵv8$x, pu9saK%Ku߼ 3{'S&bK|Ǡ)XmPP1ef;T* زj0RlȦ !܄ybm>[15\0VIoCIx(PE/quͧmara;0{ece`; &ў uƼ{b4Mw@0T9J KyZ\;%«ٛ[rZ cS=c3 76._7xgrQK}UPs8ls* Vxs>5ԮD8:psT ˗wv+NxƲ`~j~b|:9rsJ:6Quٖ=c0k  ؉6&Usl|=ްЖ嚋80uQ7N&Q.S,ۓxކr ˼T4maGs\34iBtmk{瀬ng8X˕Y\X\9r-@8T(+T"v5Paؗwgmp;?Rq0dk6*AcD52yf~-h(M-)48KOڏa}7h}H$U'rzԵU KbiӢ=^KY|Uaxp^Q!̨޿m'w2-\;b 5Qؕh/ *CZc`{=Lf0Au\-IqջCˬ̾Ka۞cU26SnRR7jLMܶ}7lh0aOZ/7ثOM9 5OZu<-Rgrl:m &G8j[6OijW`VmUЉXbeo2J1p>36>2o x)y&끲CE'5nk6gs6@:"[[rYaYL:M+v*DÐJ:XTfZWlpepg[xŭ*B߹8rP&q_.{&5mQkHR*&ϚzjO I &kkB7Ӗ]1{8l[5縝UqonV8HYjB]q*DݡbuOϤ༻ʕUQo5Tn+NN4vq-U]}"RI[+y x c ||W ` #Ъɵ2%o1Q/tY.GJ\*Ƣ&TXL\-õl_x'0 s&Ի1S!*e*T^q -b'?fky?-=nm {g("7P"+YKQ(Ws’ vpR ]a5i [Gmfn;`>z! :Zb嬜005AG0ڷmq.l2PmUR.kh#c[f`k&*:]y}zߵ;egX]Z+upalrOIigykJ.'2KSydSIZs ūRȏM&Fng7,W"ճ^᠋8q| `|XХ*|WЍ;L&#Fa_Cxu?? ||/s@KN-V9b M V\Ҋ n#<,s樛 a\GWM\--\y؊(2aBAoӯhkȤ˱x8r2cd6S((]laV(#uji.^,hvRcu:0*ؚ&\ EIDjiU&˪j,aKN?m÷m@BDUX걡"][9Xu]u}{F_á){|)}+s EqSپtε2qBIrio:xC6|Q#dE9eur &},X{> 7rG#뿶9=8E[OcCHcӓɪ-jg]{81h #即Vv]vF>LtXgG,{2jOvsS q̸*tAVS(g-{4--w)<{v{Ɗ>PPZ.ӗ'J/^D$r. H[zzF(f9Ɠv]A1,LyuT!!fݵ! 2QDI!٪Fu}Ti|}r]~]燨mQK{tmߴ&F _KW/߂.~m&mvy nؿZ/ ٥Z;-ej pQ},ݭJ&--~-';n 6ǶK4q=ȕKe1n. Ö䚪d{/RƮl$~*ڛG߱rPsjQl[IFT}{t{|7GB)nxPB8KQ3!N Kc{P{'ȭvo'jR 'dTq]Yθ3Vw Œ*x+Dղe.gyR6N,@(R~%Pm~ 4 8-n6 b~jE(K2ϟ]na-q}zezUԮv;Yz\Ģixw}!f|r(Rc8!>|*Sq~n!1+לVz0K쥬6u(7vįℶ^?kWӶM~/%jCBed5YK*C4VkO&<SF-\\طAMVX e0/5t'5J+dZN ,"ѯ1ͫPH9ΐW =XC {FW@S$rkY~.Gcl}J2 L-+`C=|z2Y69:D6IӜMFV ^sljL^QBcj*\wTJ]u/2ꣀm49,zhO֜@E+:pm#ǃXRV-c 6"hN0Eךo[;۾5"Z,$\8A\** WL W%D=R/c͕UX,V+X^y #kQki궎A. SVw------ GK̏]v)"1wnr+.Q{Ps3[BTήPڤ<3u^NY/dhJALTOw(l_?"+Ǹ)yFfbeāg@?18 ܫo58'` lt6l, :'0[1z-/ɔ=!6Uߩ U9&SaS0JRɧ~ˑ"[*/zvyFR\,PDXoUбe#`KIΙџҦc蜭Q@url+M1fXމE"X_= K>!egm0xеIeh࠘毯{bq>2 |p+{%p[xǍ~ ,7PV jUV|`?bu@n :d[ F9gP=}=@PMPdumU[[[[[ga죀_i[t,M_ݷS.v_wK=[yngp QHnɾ~wd-!9 #n@c?FRl/WL8%aٚ/l! лD_ .ZݖVaMY*l9ļFH1̭\z!}p5[ȥK9+@Wb4~0o֌be#[̃̑-kC̪ }M % 0dai1/s^Ћ\qY7Kƥmt.ʮ dWst訏u :ދ})M`l&V(9Ӂo)6W1W$o[nJ&vO;e4EqxAnoV>Su Jgn6ndx8sYn(𰚟O;vO 9>x,[ٹPsit_[[[[[j<xr`LI@'KLof6Hs6q/uLY; SMHGg@ )ZBZKXqbpKUǪHʪz ,-45ZZ XSNb.y7pb-e.wgKYG BQ۪cV:t`ͨ_Ovh s´6~h,v9vtpW_4v)F\\AF? iLڗΉ 惘HmɅ˳ Z b4?Mw&ܻ{!qMoê3F#w.lsR [)Yb;]wy x nOgk膖$Pj-kxB!Kr%fe{n) a{5c% GΨp d)Lu 0) yUs+(ZO ]eĄBN@o;-Ӫ k['\ jƒC9}kD7 . !'t6Y=d7V+d[b_RͅCwk=1NE R~3]q@}cu\ E9uS4^|jR\TE&+AC[[p |p+Y,^ v}0W>x!F+k=ò]H?jSvc6_C]b5^9i®G]Oױ ^u mMPI\H6JלCk[9V@Q--j +zwm Pa}}筧^PdwTݼ:y׺_,/,îʹا29Js)/K}@nAWT76¨M w-----,  %]*%ʴE5GWIz.` PS r3U:gvQVTN*$ VG.Y{X60\lzk}*l 8 +m:]l u^{&@&(<1 -mr4<~0f .–Ef`x >ߛtxUͭۗ:ʑ"ui1u;6]) OQ"Ÿ́e<>܈u ^ub0AhyNKW*=.z*`uphGu5H _F$u;[ip@,渺3[mZ--|v_s:n*Gq]/]POXs37@rUWڭ c-----𳶀B`6Jl&&58-qufzӼt| 42b:"< 꿽M]x/:^-%fn-U[*tX`tӒx0\ؽMU#x_Bvwv}9x(*f͓'Ppm:[Mr_̞ڳ#P=s5S^ ,ЮUM +Hc3WmοӵQeU_T/ܻ#"9Sz^--P…-9+Xja--[[[[[bz|`O .[ ),%`$XM|(&X-PZ)d5X5Qʶ >"SiڏQMp]@W]9^D)Vb_`@9p+}u<9 J.0&!q'{F84¬[l:k=-PUM5p(" UӘĶVNw{9NC7oo-Qû~֨T87dmcccrs+py x x x x x } (@IDAT)W3K[++X!z&Q#):`wRAW[ p| ^E }qwl"-d(C( XyA΁/9bTUX}_~񾷩:O·'C`Zyo;`ooΉlq!@웷F 3%Fރ4wૣrg[ 2{]Sx1RJOF]0gخp_a2:ebկ7=ru|w0W>ibW½`7{~c5}|cG?,;Ԛ>xnsVH%8;-̧]~-K~ ܤ6SLWƕ Дn)7tˉD] [[[[[[ - 'ۅ{2vU-]SmX= )V]͚ |ց*(M! U>~}bB}*H9r@,gl7Ug$qPncR} SI¥}ҩS^|_l:%rth} ˮsdW.3_{:ąAv]@Q˖YB .o[ Ԯ,OLԤ-..|\= }vnMGoooooo7Jt,UYU^iBw]K :vp@RUnd~*SGL JAc9w}j#DZD(Mn{{TP|:{UeJ>uƔۍ#T)Y6ҹQ> vzmP46@-X`cBmqioYajkjjxpMfœoiqFFm }hF?OiI’s =J2}\@: 4IJJ8aoV *TtGfRi$:R2KyXJ\ c3ˆUwpj́\9jѐ gʾަ2N,:*]a22DY1My)D&shr<(^aʥMםob']vr3ȴ|UG_6QKgo_<9oɽͮ;IQ9arc٦yƽ/?fooooowmbb=ɾT.Hl,c05d1CpUv(t4 ({i@_BKlU5CNv\ԾYe>Go{m0wwq;o,2ٶ @b6 n)+,}t'S* pJq js=qVhR5r|But)yt~wkkӆ"G><91mSӳ?-]__Fmok  Z@ ea_);0 4,V#U {?t3yJp lq *緿;Me#oK8^S_ꣀ:l| ~,9F! W}_@,MXﻀ1>c}ԌŊmoi$ ֖6d2Mw[[[[[f*Gʅ4!M,4m˵P읅݄%(+j`bkb0j` l1z:i9@misJlV ȗ p__8} EZL{EaMȖ{&0'X~uށXUom!,&VUhȏW u|:c3 Y7f9=,gJ(H ,/&'Bpې3`F"Z yyz<\x*unn֞ ?HaT Mn<4Q}?oX$ b)WMz% T&;pFN̠=pѦvmzۢ<Lܶ_^_USx:pGe[@HOO;;^g^=-oX!z޽^g-zu6U\ٶ짗Q7m%PB+N׻nsܱr<GMā {xiQ?AC*#PS*~x{r@ ]a|V]YEpdĭX,r=z :?$u+j-L(w=bRs^])ۄj'WȊHrc4?ccl>w*F cGx |x uv_19BYsJJjgQ;a"C"UUߨ~I*6 %967ujHK7Jei'fx\Q/GIlN5 r \Ps Gr0>LJZ.`Ӕy]ڴtϒ-vZc;Glfffl.We u'RP-/Ĕ-VZp,n}4kb{%JL"KSgQe8[*D=H[(]/].[IǵUF1c7~S+60ڵ(HXYe]gۿ눇-Rn۹,ۗݼ֢g,u~SvxHpmڜܻmO=Bd PFBFK4SgOOXS"busٱ_--------Gpc¨n0XW=C s#4uc+hPUnU.Vrr2 P&L[*\:z{-..:|Cq䉊J-N3{M{26 Ym7L Xl5`hW9{70[iiytԢ߻Oz߸XpM>gܴ}d'm~ Vu+㊳&ά5Vېs;uv9ξ翷9[9Hv83Enธ'w9v{|e1MB@4(\p\ VVi'щ->\FNaȝ WM_esYJ)62b&nP(aa-GʅR*SZ` M:v~~ޅ-B]_G@o?wxn=[fSju1mX\JjeŶSLr&%gh9>Ӷ2о쫯'նY}NຨF3!{6ͶQ\s/iqsfS{Wmm׬D|rʫ]yX뷗K-62fKǢB NX"+JT=/)mfݶ+"Mrg 8/u 'GmEaP6u]xhu\L I&5tX] SL--e~MrJaoÔ2H6L-+uuҿԊjʱ>`e}_~ J'cL~slU*T]v #.qR]\l0Sy 2y)+MZ cMHکy+5kA{^k9qC+dW,O„͋k6648jXzO\|oꭊTCëj9غ+`߼~xi>ZޅJ{P ˷M>nA tW}c} 4‘Uȼ [U(jEV!u:\α2ܾj~WM!?3f@5z2^HWݯfoK01HGHxZX$ޏ> lqPl"o,,sn"~ ]E-->ҾEwr$&DٙCgf$h6D֥4/=B KA\' Fay,> $mx$>[tєmg>qHb>/ݶ0MNI^[C1g?"("(?*.knۤ kv;f#LRrjuMզJn(U\+KNRBAk%k73R;ބP7AZC*ͥT%mKH(<]mWN,K?2Z1Eqs3f0Bw[0FΝ[ȒdF܉??X[죢e:LgH MM0G wIr*cj?ND嵰:<̵D\G2\9^MVy3̺gS(obqRV>sZ iZ~S@Z`ǷPSm5V8ٵ4Z`y,2>Dwh]Q[gY“'x\D?Bܸq 9wIt24i~5҂ mvbm %0x=Z]AX}^J?"lQ?wgpwM/IsWva<^P_F %NЁfKE<\1,ϲ:?+ ¨a3{p7 YpY(Pà1K/7Hq ǗN ^򾢹Vy6tVO<pj喊e?6E@PE@P5Jz[J%t*tTfI6zg6{$ +#SIJ pQdf8E[RHt4GgR-آ;^vܼq I>!an8ZfîI#$>X@oI|_<_'ฤ,J?K9Ԩx57@~y ìtbt{Ԭ4lrSSYf "*y&nX/~e{Xh"K|e } ׬X׍ɋ ÜcKb8ؠUdn@ۡ꼞oFbRǓXFy. 2pt]7\mv8ɨY¶7,/`DC^Abԯo늀"("( Ip!i OvyqTJ@%yB5Ri+zVBB&YTadBrB-.V=jnT3e"2ATdcXD^~$5E;0.4w䫖 So~uAiz 3oKIhP$(2qYVfxnQpz40SsrqQF+mm&hd#+L.U8i%`$_WʄףKئsq^/9$Sq}Ü^ÆmXQX: S}9!UMKw6R= :,\Qi2̒Ezϻ{u/ zrK Đg1b5}Q ? 0#N -ĉ`{9{Xڶ 8և#֌&19FP8`uȇf?$R]ZƼ<7 Z'Y&>".~ DQ`~e,'QAon} ; i \"i~^6~-]1~E@PE@P~\(+|DTɯg>͊j")0Td<Fe͋& IqD%\$t )wcOd/%Coc_{q{sɧ<(Y=}lW96AE6k'L]~=GW&I+d N<.Hoќ ߽n^q^eırIDutĦt4D2yO[(2Zwj2&KTNT"cbTl@*g $R˲0ޏ/a]l,c4OMf '?O=~sƤYQ?Re>h|,1e,$qy"o*O`Jik6bIc׷1G._M#J433,I ڸO-ae^L&$oo}o}gHE@PE@P '/ ϓCQ4-j1'[TfZTBK' Z e EB\ŨWfQm{Ko]BZa M%n̕2 @~8G~%j f8;y=֡,Hwd?$j{c㡚p K׳YHDq]{mFq|RI&4 ]Mp:TN>rlj2<4#  !B^;$e&˦]аq? {7[_qilf\sH3c<%]߳@^1/E_+e[ؙkh?'0Xb"xtPڡc.c$~<r(è ;pw?fDbߩӲ&#ۭr"Iε*F9LK7lSTrFy<"oP9Xpxe&wqy8=]>.ʤ>{ G{5=Ô̏uSH]M8m…Qu*$;4cc^M4\Ow?ÓO8$"-28z:P~7B]_B5gKzZ~[{بcDɁ@ z E@PE@P #SiIJȱ#uUh埬Yr^pN~WAnJxs҉?6zʎBz.TH$\O27JMN&S %},>O- (Ldi>$d/Ui\q,Sm.附Zp;iVYV6IC,MC7ۀ2 6k\'*p+] %+!·ȞLfֳэKSfU,~~<^ji\+H!B1JnѹO3zGeBD&6x᏶IĀPtܡ<`dւ\;XyUe,=sI~,/.K[G!<䣿<2k9#]pɅ>..pj7q'ťFGGݒ>U<70Ó'OPdy\ܗ'=鵯V${uL?Ak:Ʈ;ĉݜ7,0;tz6~/E ,n +(ml^Ԛ}Xe^zqۛhKs39"("( Gp+76*aZ,CUTA!|)fAR! FWXÂz$\耍9v/0[03R؍{+F]$'S!.B!%ҹO6d5!vb)߹]1R )2ءk^!\O/b@&ҏKFk2'Ok5dpefXZW+=ƹOk-=Jap( cm_%w27E@PE@P~\H+A{_ĄVrgEVȭEzUZEV1r:mk#ɭո kJ+E1"5a!}B Wg1cm[$ jXȣ,<9|peGTO:pa(J=>yGK&lے* q4q`HhzdwGIМuu ˠ"`3\MvtA\:3|WsViPz]?姏h0bG 7d᫽DZ|nD11>tȊ+;lj' # Õ^jyu3mT(Jز8$z+警41$YT?3J&O?^mXbKnLR&&C}RBΩ#i5n&lz=}[r)vXʧ"19xޚ5ia82=)4?7Qb(JmVI L+>r)E-IVf1q~=".O]d#qܤTΌG 9O҇WY< 7MQ;4w &vר3;E8A~Whch{cnu/p x/J8W迊1.Ck-6W1uĎܯ ]N.ad:>=&nJ$ޮD ?_L|H] b#È=E; 9H>n2Pd> eK|e wԢ0*DrO"f_DJs`l$'ʹNZDGob,?o}Ǻ%~gxkG6k,1~r% M=!jE@PE@P fJWr!.L eۇtJV_r[%^0rǢS+۬tQfg"4a \1H|:܉6_M,"Kr5TbkǓU.yT%QI뤊uOl$WNL&.ZIzoj_Nd(VT bGze$na$GkqO$Aζ0qɄD\o#3\D4BNnrx:A\ߩx }, 7+BH2 Ǒ ƚ(Øy^mrfNfiISP^  IDAT]0a;~fܗvRMbulUXڢ6IAmL[X*?@]`g%n,9fH|dj}yK쀕5q*7 giF 있4MgzX !`M\c\xEN:^^=Tu#SƱL9;HFZrm?I&X (0'xrT-⪶F^1 egȺĒ&s 09]fɓ^f^mX`عYeEb(b1~~%d7ƫG5"("(?^.ud?+6dž*RB|8(Y$YR5e(mlLH~-IJ>_&LXowH.`Z 4<$inJOuu-ɛ$c%U4$$L6m;p Sʴd&Jb /9vҠ)|γTBOOrk2\ç5*uW~}L(sZrWb?] 95T&}e` (󚵄%I\O26Q%Cnc E>NedU9w7#H\+Jo o*OtGbf_}"~;/6^Dn/*4b,\-|d䀇^0n?bD!`a={2y]RZgo.5FkghA/'̵ vu2nhrl ~pRM{}&$!o)P1,Wqo9Re5pϩU9>l#!0 h!|rz) N8} d LB'ZL?ɉpQ7'Qx("("#A\ CJ룹N̡lJrq%E+ aqDREPv4!"2`w:%ւ~ոtM*DBDYQR%5Rj,%G֗{SRuBϩhR ˛v41BE17r[$0P Ap9[1$ζӉoI]\O;0vbş|u:]FLDR 3qkBRKpouIy0MYjͣ^.VPE@PE@P8w+cgqсXl$$VSҰޭQF?$Z=ke\syTv}mngx4O\K q4Mo`fB4HZXs{ƎGPE@PE@PE#p!$ui%'^kAPR֌rEJ%}Lն*h{dyh2RL Q]pܽK ;.sgV !s;%&BvzhGKws"*eҦ("("(E\*bT)ױ]2r.-ֵmQ}Rխq1OZ&:Yú5HDK eɔCō,"M?inj$U., qt{uogE@PE@PE@PD d:NDy cQnW& >[)#lwwڔHr+*/%m [5,3e~wk&Y1מCVS],a]KB0Z)"("("\,Ο,. IkDY6TH(Gde[SH_+VeP:)$Z]Fxu\eAp}{֨k|{(%FE@PE@PE@8;LjbHPKSVO(G_SrzYN =c)pEy_Su !O#M%\.YÔuq8uO0ԙ*87DPE@PE@P#.zQaמjC*$XD%b#t{ %GȨJr*Lk1\YRrB6*<AtL'Kn[.-Jۣ[G_E@PE@PE@X;h;USQo __]cvqO]C%Q%ٵP%  Hjr+DXHv =Z)?WE@PE@PE@8;($$Xn@]0Qc3mrATo3 InE="("("(8VjÁ@ `H.u/="("("(#Fv;srHf)"("("ϰ6E@PE@PE@PE*Ǯ/krB4^Uͨa%%ĠJ"("("(#5V(ಉBa {}/; fۥS Է"("("JpA_Ue,.-b{SFp\oE@PE@PE@P %ǠTC[܅HԨ C6fQx%#VPE@PE@PEP{~EQ7l堂:G#h5W{==맧VE@PE@PE@8D_wIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/wireshark_1.png0000644000175000017500000024372714062723141021764 0ustar00jonasjonasPNG  IHDR& IDATxwU<~cWܥW QD *jĈh]Ԩ(`J`%*"XPZDH-D߿s`.[wݻsO93̜:u䄀B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! N:GשS:uԯSN#m@q@q@qpVE]?>mPV-[ܶukkצ6a 8 8 8P@U5hCC0e3N?}Rsϵ͚Y쳵 q@q@q@ājN6kZ{veԩ|4"-K @q@q@qsͽXf(vwmټzq@q@q@}٧weY>qÆeIVoQu-RQq@q@q@r npY:uL4108 8 8 4հ+M+oP/xQTq@q@q 4WPn}Evkl[ҫ6u{.$%EM~ʗIpq@q@rg ʈX{5rƤQ&-Iq@q@rǁ/R GEe&%%%q@(lfz[j}7ܸkcg ԰r8 8PUbYBYt7-wiZXW)WwK?ۻ6ZYRj{{}-'.oVfعx7]y *\?#+ih~hۊ6ۚ.#*qsU^b0lHG e 崈t oq 9PnږyIWm-VZZd_>v5|{Klwnmo ll]y'8:w}imۼ}$gR'>Jζ{Zl#gmmnV?].d# sm[Io+Vغl@z2_qrDɗ׳O#"9ǵ%l jH4ppmFTlЪkksM^6fw\rvaVΞ+>f6:7-Z"[b>d#m?nR+-_>bTPw xSI^ lAO{VZ^.k8*O'jSe7q`;fvuuk?ԟcb99?<{֥IBY uq@hx6wk-'l}qlЉ&=%E%Vy n`WY7v˨ɿغض-Oe ~k?oٖ?؊?1[Ղzv{-[mvM;S6Ygl?˚7k-]k?lj[[is&?\|;ĭgknҊKm#췙v9 }wCYXs[};ln)[d`K,ҳ޳nv9&21'Xzt-y5,(y&E٥C[[ۺ[`34LwL'Ŷ/ZFMs46^C؇_-wԻ^nk9ٽnb[ۭgm ^FvnvnO6Yv wHv&5۶.'kء|m{yֺޮs~Җ v(?-%VjkZ8b4Eygw,%'}mo% GM3Ek흿֚`Ě7.0w+ywv83\LB9g`rR$ۅ8mѳvUOٲz_XbE7_} M]9$cmk_K sw-k=Zal?8^r6࢖"K'XQi}7Vk:Kr07xWNƎM\^`a2newwU7=ʖ#e͈WPށIZ+0LG(ԳOlϿDB98ޝw9_s̢~ιeƧVoqX,C$zY0JV[nvo|N{k˪ZlֱUsk޼u~KaA&]fO] [!؆+в4o60Tx㦶zׯ Jj`g z_ѺB9QD|{> tf91Rh$NsYQ\u68 e8|jԸs>'{$rqFr} x'-܂6pUYhkǾ$N-m wmKKmwk*E|fjs-\~ils,-i.}z[CZvu[Zb[6؊ekl3Kgnyk6VuXwPqﳤ[lgWr z 蜔IDSq\VBq%úնff[?kYbN[|kI+TW5 ŪN} U y eU8pq@phnow߹Uj7veߨ iok?>[~Zd6%'ۃ}[9 h^؇s6Yqf[ {ux.bQ.pm5؊؆lgջr]zTqv;ro߷9߮Mۊl t'SCҏo-Oulݨ6zW懭m[HcHxl؋Slњl[ܚ8I Ә}y+P>lKq%z>]-G\vmY̾wQ)Ⓦû㤽q@2meuqܫ0u[&K(i%]R龚ك*DAށ8 T_q@q@q8 ,,,8 8 8p@B9zzq@q@q@_H(K(Hq@q@q rz|GNNq@q@*9q@q@q@8 QY G=Y8 8 8 /$%s$8 8 8p҅gڄ8 8 8 @r҅ML0q@q@q@WH(KثcCq@q@q rFv(q@q@q8 ,#q@q@q@āꁩa),q@q@q _9 ,#q@q@q@āۡxNq@q@ā〄zq@q@q@H(`z`q@q@ā|倄zq@q@q@H(`ko⭞:q@q@q@*U.On|7l/T:!q@q@q@CTPF$#(k/M:׫#,8 8 8 U*dG̙c~n#_},8D}K.2Dq@q@ȌU*n]1c_6l`}]H/&ڊCֈ<J&JFx /q@q@GTP_ne/׭[gmpʔ){ЁNq@qs@B9B( tLz\qsKM/`>U8 8  , eFERm:&ڎC{\PUى8 8 tH(K(gU{A(\vǥ0J*Qq@q@ā|䀄rPV,ފ8 @m@ nz,Yb{WnIpo΁0xR(Tq@q@ ,U)Gigyv!Xmic9R`Eaf\7E|8 \_LMgaBm۶ֹsX:t`;vt瓥}ǥzC`uG:!jժ2a<֭:K?3دk{7>|*,WUn8 8 5B({1{뭷bъPƒ f͚s=U7Y/ݻoޝO&l#( Nϛ7k+hܒ{AP^vޮ#Q8|tų(B/-Z؂ l̙vꩧСC&>l2ªMghoh|{\jKAtRq@q@'_0ð⊘PF\B.{キZ cǺyՠAQFNW(/^8& LËD D({ yE^u'W^ʉҕJ(#|'L'38#̯\2vݏV {vj$":eP;͜nB2e}gG-ҕL(~3?eƞX XY\ƚ6mj׿_BYLmdNq]q@5^(c|'~rHA#|y$P޶mne0{ASB$-ZVxF4r)NscׯPj-\GP"IŽ䯼ixaByĉn1M2=?U2P((%*!Cba'K.u/ ^(}Dq@q@y'S:yBrx.#Zdn(0Q EO< e,uQNp#>VfgVOy _^L gnٲ̬`E@gJ״iK._(Yl 뮻.6'هEۻW'K>*;q@q@ā ?O.PN${>+oX0Sr:ϩtdx\jzq@q@GTPgXrO:7eK( Cqoq@qpF TB6%%Y%UԖJFq@q 9 EvuʉZ.{A r.⟭gx\PUى8 8 tH(gY(MUVڍ3ǥ0J*Qq@q@ā|䀄r2iٲev#ڎC|,4gUv8 8 5U*o We~1Z0q@q@qj9P΢E*Eԅq@q@tRP&"X~LKKq@q@ʲAq@q@q rFr(q@q@9 IDATq9 ,#q@q@q@āꉩa*Lq@q@q 8 ,#q@q@q@āˡgNq@q@ā瀄zq@q@q@H(`'{b0q@q@ā|@&L0m@q@q@qs ]^_& q@+k*UÚm|E^Lǿ"U߆P޴65[{޻"8 T&h̙3GeĶ6%^/O={μm @$#I(/^ׯoxw}jH!)5٬~m;]:m4{c8?769*^=ڶmV1b]piݻdkҺ&q7ްZ|l5TgvdԩS'n+TvYfOǎz)ob>}!|Xp+i ef=B(''h&>O7I|E_u'+6&3L>C/UVU:g:<Ƌlx5_Y>noٲ!b sڵv;gkڴ}i=UY4mذ8[n]PVƍᄆsaڒ믿!͛;LN;O(nٳ^z%7?Ys| 2E^(>@86ol~;ÜTT]rߋpSdףð38|X;>N:$W2: ƌ3\Ϣgñ}c)! |SX|͎ z]~wߕ;߹@v9@6k֬5 ށ/s/RWIN;vm7;#]yIIL6Y1>p@;\ڠAg9xs;z q]=uxk# w}1>OaQ4u8qyw5ߠOcb{mҥܘ1c N~8s P~O{dmDq/.ō ) b._ܺvj?pwƮums9M1>7y0n~߻d'?ה?p>-v+W}(P´%J;׿䦛nr nBE]v.BvbY^Ge,guу2 񫯾 ?!X(8y4/cfq›yrSND> _~*gȈ\?*!zʨ͛V7ou ʇ  TV3gΌ[y{E2cde"PƆaSRvLy4lϟ+c^sύ`Wałן'ol˖-nc&?|ױҥ -rQzX:bq@P ޹[o9<{s=Q :H$<ä(~W"F'me<|`%VSO=56L}_sR/q{g>0k-6'萤s (>u,[]/<⾰܎M yW:|\اj'<~_V¢Oz{#Hg9BCX׀=aG7Dy/.i_XAҸq_wfC,iΦ@)B-Qy4{I8зo2B)+cfCL%6 8B bv1C4iGeu2'K?fO%S`_xᅮ-<'u~:vh*c?>}{?~;B-/bS?%d_x=?,FEM9KQq"B8E_>}B~7:I;j?;|:DC,>x-]45.%3_G8Sw:tp ;D!}}y=X {tyюE"}r;^k~\ #ȷsuajLJyzX^$kɇa< ta}㥝k~c.g{O(|חyod,|moDi]0O>NyZ%˱Qf zpǙstG(>٨x!4F?])SbK^6aQxQp$O.Vi=  ywg e. de"QчaSBٗX)5s˃;:<|Sh@s?F' /->ka?(Q?u5QN8XxEka ٓ7 m?0?s'sO6J^ڥ!4( ,\G 6,ׇ}_r;^},XFi0ԛy/#>?GT|h0"/g8jdgekFP3̹]vn #i)saMJo:bO8-<~/chC:zX>_g΁υ{*A BW‚cnB1qc<~xNwWa!,ā=P1҆kde"~ð)g'M{w*jDxċU?ʋ}<^ux.Shsk{bEhpEh"} Qs{JCy>wĊ?^L^g4i|6J.,L>| ei(ދWM~$DC}>oqo4O0LW^ x~رyoǧ<~??t=Bkdmp#p(| O` ‡M>,)8|{B2XNiKvFp?ϡoPvI(WPfؔ'#/+.0Oz\T(3l\c󤦲T RLd@er>=hBgA\& q*9I%z(1F==,D}~xqC9+6hst1:g 9^>m2< ޽{|ýs78o!x(,ݭN:TkKDs }Qwʖ^: 19zNa<22t^#zs煲?KaGBRf\g:{8(XcE﷿Fϵ}VJ\0 o<\PZ27U+z6|7ܳiHf+X/_+gxGarh}2ף<Ăt&w޿]gA(칆Շcyr?$~ D'D?L((Nac-p3m"t?CW]>Kg|7n6,}a $e6}Ό:Ha?nD>W8d/C $>/9Ng^[s  !)ϢiZX̭a1\W 5E?Đ0-x\.L[>{@Q~Crot2MBs=X!_u@@JNf\|啟RK,78y7'}ç2#' e |)(7*'5 &GN US!P.{ᾭYI$@K/Ç'Bf#={[#oUY0}Z~wo"xVQKkOznY6l,瓮:ArI~|DrKǟB@#9!b@2>lW֩S'?ѣGè.BoGK{!R^>ziGa\RIē<$ܠA{WP(;3}帓?m:RT[/Y7xUg_N#_|E;Í+V5dȐjKpELB1b~>dԎk#GZiiil駟\)CTI(9D /:B m*?OeD(7o^ehG` N44sΆ}"7f;餓⊤D~t!H(De:Re_S2eD8^j{>h7: .t)Bzj',Y%< Mx̍FP#NdQv'xԩScN`WTe 9! @6@$#9&yagnݺ{9:$7pC[t PDovKy"{K$È`fh-o1Ycбxس>N<X%C?Wkq(-a|B?oĆMi,qT(3l?sFq2w„ .*$-0x=I(@(G$CL? ϥR- b",ۯ~+g-+|wم^0#\Ž( 䄀F 4N(qQ!MHcͲev*=_H6gIua'S .]s9U!sD U @ۓ-:7#U|0y,q("AGcwyR_K:sl;,_|uСHl?7—PβP.HD;b(![[nu믿O?q,ZɆ4. \yn~a ^gxĠgdl?̑D~UC _V_~ىSrk#;X\D]OA{ꩧ$bzN;?a裏\t~S eCabZL7nk0: Q Pb3\#/{7|^'sx2͚B~a2PѶxB55…J!t 0cӦMUr;PβP觡гgO :z3] eg(!Z/z͢$`Vd2=jXc0 c׵B@d pkz(?#ˈa4(CqY!/qn  Pj e{C0cy9蠃iHd0g̘ajSFe7P zH0:JìC3 X?YF e5n;d9Ig&MTڻXsPc< eL0\xz6y" Z T IDAT`痑Nl8:|ïdrrU"^[Uqsk7PG‰?Ea3y A8}g99! @mE<<~j+Jw# ,\,T B@!  "|3#' e |)B@!  5T(=zB@! @D5m2&s/~@ ,/\U<FyrB@!Py |x&~@ B׿u"]3}:/{[m&Ui-gsVHGΞ=۾X ipdQQ'p:uE-kC@B9BJ}ل<{dʇ~, & |oҖј3@2}OFm<[{M-'r@*7y$Ga|GO.Y ث/υBW^vwfF&u]g'xvYgUZZ2GuWB9BoeS,#Tpʤ'O?}m˖-c^2>D.'jL݈#\^N$Cyk2yfڵt(#t5jd￿oWfu}b۶mst|~ر2ܷoߘlOzv" \^rJ'\EGmSLqog*pb wŠs 7G78r:unO2;ifݺuKٸj/&~?qC^ܹa%`QHㄽ\E PD#p=U.*XT^{m֭[]ʡv1fg OBJ@$#9[(GN=Ըu텀B [f "1:"&pùr}'tk isz߅! @5 2 s?#oz9(*Ve _`Ju7u6~\# e?7\w/{7|^'sx2Mo e9Oyr V2pl`B`zk!r;PβPOCQɒs{댋W!sE 4izYOQޙYOG \p6oIebɹ|5z"Ξp_% s,W;@(ߟq 3Fw{\2 |QkEc^2~hGVeO,T~ݒg`҂<bdx׈Dwr4 {Hł5 e<&sPޛoM 221aecE"`痩l8:|ٲ| SB9B9 ! 3oܽE;S!jg6E|\B@V#z㧶tW=UB@! B `/7 ?rB _PP*B@! B@ ,!B@! 5ӧgdM9!/H(K( WO! @V:gA \!P@L6 |A@BYB9_x ! + \\]#zgϞmgFY̧U=Gp S_IL]-YʕylP3k/s4T !PW.OՏr2Lb9*8M&[l{r9! @U#79?;l\a#Fpe {ڋByȑVZZ~'H62BFTI(9D />ZhSH|*o&B:i޼yX˵P.))n֭QFٞ{iv" eLPy2 w+c>85l}gϞ+t?swX֭cC :}]>(Şw) B@T!X%֬Y742Yfm̙.s7t^"PFGŋ݈,/ԛX_pX dm۶}ǍZ|y;i֬x>3ˌX/qV]weşD7s! 5m۶N:p^(;w_Wf?XIN$s +W\wުq cҁD G: eFνrry_?֦MƮJ7˿>r80̄,0pO Q,GU{! +/1%^~ec~>ht<&r uG\E8+/^6>}K$#D0p_?cjG(,бxس>N%%C?W&&O9o.)XB9Y#̨Pf4~紳;aΉDCV^{P?XIM&\^ Y7nz-[ftP!Ʉr.]U`9^9LSOM:TߧB b#6d?+Q  C1OѾv l5SRz eFj%ʏPEv*^ D  cfnϚLB?kgaQ]cB9YO("AÚޝ|ɱB=zlҥen`FS1ϕ!t2wСHJ F#e\3XY$tɄ28GJ + ñp [d)|I~.| ! _aOE%[y4F-NzҴiSXv"PzB3?p̨ 2.v=s?#IЍM%,,;ηT^({l4NQ?,HGcJ('Y>x` u- O'zzL42',G3i}$ł5 e<&sPޛoٍzb u\xi@LH$,X2 1 G/} [6oaJ(gY(!_! @uF7t;"W7y8'MO<9_؇?䄀*UrճP1B@! +dÏ$%󅫊B@! B 'H(K(hzB@! @A`Y~^xuN! ʇ—gGN e:jԨ e.T g&KgO !P`Rdίb}'VNv LHݖst#zgϞmFSiLN89ՊZMt]PβPXZ~9crΠփ gu%sm۶55rB >ŧ^peΧ@.H%z&ifGqq\*?ge"4h`j`2> ^zٝwޙqxk]wщ{ҒI<Y|3"bYB,,<{Rv%s~W_m#FHv ґ#GZiiil駟2ʿ!*$ˋUT^p -T$>7L2o޼xln.rII>s[na(s=mr2dXN$mf}52Gmr;pC3fCƍgaW^yeҡC˝O?ݘMXpo?;9{g]￿G}.tԫWφ ⮽k6v02C*e<@@QQn<ڽk7tStNHT.^9gy# dXg}T˗S8͚5sC3<2EUVn]{]wYaas2u{֭c 7&nls-OB DE/>yk2ʵk΢mԨnm߾ׯDy|nQG+h[h#''NK7>}Ďiϒg.a<ЅE[?W?ۉrrErJk6mڸy+V0*Ͻe˖L=<vڹkd\pAL"w}w7Čk/ˬ6mra{ʅdO?]{uRqEk^(\U<;ϵ^kݺuGƒc! j.{[{F'_ΝݦsB @:Y(#wxch2ד;B!bhǠO#*F^{!HTrgIu(~ʡN%cx3J^{4e3Sl+x&zeNC9B)Qg͚ ;)ztR7m]o q2ig;^)| , 劈dBoNGa2W[U|,*oޡ{,C(Eƍ"z(w_~e7: 7?{La14̙"T8 5C[J,,ABV ?…=T?^DW$uξ¯|8PS:Ka3 \R7J(u,URKe7nB~B DE/Bً~sy'OK|a,!dD7IX,P?ލn:kʴÅ\C`zZx!v" e\OC񚨴}T[n+U źTdLVwխ D(ӋMO—p|FE(^3$\K4 t.처P&la 4.BgF8gWtG~QxWu;ԏԛL=…uǡP/NP_⇅}gM%C8B/u9*Sx IDAT3 X?YF e5Dwr4 M4q+cAƚ2P(s/mqھzA^Bh{lMH$,X2uv6j^W{ot31L , H *י3gVǨ)NB@9Z&34i/zO"s]N!P[(-ڊ]H(K(W= ! B@B9L6 |A@BYB9_x ! B@! rr-9a"B@!Pc>}zFd,rpUB@pNsG! !;lrpUB@ Vc=b ' Db:2: |E%Rwy'lՁ/ٳg()MOU" | '`N]QKyL] e 岌БB"~z9D `9=^ѣw^뻜U-W^mϬo?#9囼|2#pW*?UF&BAꫯVJCܫW/;+%Dy뮻}^qv$zf>PPG*B@B;0C0s=>|k#FO?'ro~^"H($DZ(}]|>&J%z_xÑT5eyUJs-KJJۺulԨQxr5C;o?'ƸGeGoP6m\#iӦ|rwcǎc汛͜qƅ_!7l&O7M4¸n&gbw}V~}c/W{H$C>hgiذ|ֳgO駟pXq۶mN,ڒ%KvSnXf&X_u9蠃.{aڵmذ!vmֶm[7$SN>v-W 2]=͚5s<205ڷoo;H̙3cܸqa"=mڴIBك(oXJ5NwQ?|USsm8ZnCnfGudmd~P;v.۷oΠOzv" \ͅ2qT8O?zV\\ +h]{W4+|'#~Gcue\;w69=zpD|G L4gyAL].**: 7XDGƍ'lb?}Gޗ^zɎ9]Ùq?!r?:?4~+Wtp͈n9LWZ¤}wC6{='|CK do9vw"#v$C4N:ٽKHPQrLr88lA2z}}]o1l:c/X FvꩧFh''8p+oD`ñXv"PB2߻K:E)7F?0Հù{oQ7e*jQיX߽{w7kx'#GB7hts=֥K:PABKDCdЏ?WUT,ȅB9U=U eQ"_|n /| _B e2+|tB?̽[o7n*B@]_>g+nԩB8GCXާ㚏ByȑҊecsżYE'52pHҹ嫯ڶnZf2s! [bbohqާNwwc=Z#_,EJh&S)P&S8,g1}~  D܈ʔ5dT'#k0P(j*BtpAl`DbZVA6H(Wckb1 zwyư(*L2=RZ6N@)hah/63cϱǣ0cѾv |Ceš0bYE#aѡ}cƌ 0D#fi4hceJ/"7pBE 2PA|:pf~sVY<խŸsEpBcJ$h؆{>NxwHYnR|!)B@! *$kP*BB@! 5ӧgdMƈ9!/H(K( WO! B@j—cgGN \(ž_5J*xѢEn0agB@A \jbSGVNw:[+ٳgKl:~b7W>͔j5w->u 'թ+j)ϗŧ%%o;(k|gsq 3}#fqǀgfG?@ApTD$(J 9眛hB;//Uu}֪sϽ{?gs)k,_ l&L߿w!eZF!`A_y>n&a}T Rf;u}|K%!ʬ  ǥXk,嶨kiI y>\tbT>}?&e]Dc]nBT,|J>}RRe֣SR@(cċ/XN8ꪫRv/rЈ M7Gѱ`!@X)9E+nWfGJbKv($ ρ{ %K7Qi*ryTTEz{:6| mT~2DsΑٳg%Dqy7n[oq'sIL(Qnٲgq<3rKԘ1cb~üe)T˝tjd….N{n/넏?عz͚5eݺuA^SOUʤI\\׮]夓Nrn(Un]Q2/aJvyvvڹk:,^=ekRūJ*U\j1L?+2mk]mk!^& X=Xٿ;+xNᾫaڵ˝ZfԮ]1^fyU8I,9/qK/ݻOŒ]{W),B"/#}K&cѢERNXz%i5j$ӟ*o8C2g@Xur(zy$|.y'gպu렞i'=&Xi2ځ^=^<\swy;L'taÆw"ңLҼp#9@y׮]+6lpwuO`QD$ 7gϞrˬYdǎ裏:KK.u ejӦs)((p !3O^ԁ'3V_z%AQ(r7VeQ7,1,7˖-s.sy-EEEBo)w3^I\,~'_}t<32VH('Qxtu+s=ҠAXqxX#~Ww 3: K;tc`/_0l޼^et(*B rKPa87e0 C P,v~Ȟ#G7+3f8BBVt&^YxK^zL$b.#'5/rD4Z&qjy8Dm۶s :5\.2;6H Q!-!JGDy *Pva3 ;i* 7vZ@xה('ҝ6Lb„ t[zt8T#i7Bq(Q߂ť(=B?2 @,!@or١_}U'ȻY9~V|<ԟ*%ź><7VqB/0Yb*d!`9$K]D. O*&R|f[Xt>O(8 -A#uB"9KvA2k8$CDxh rNDA]>Vݛ(C|^t!xX2ڗrT 'ixdӔ5~ ܦ.x(QN;s}(6M<2q0du[z-[:w쥣lѣK3Dٷ(#dp?"$W ^L?\xᅁVZ> @xp5`s A=s{H2!'E2qDD'w,|P}K[gHdOjEy䑀('ҝIeYuh~_V-7Le 7psea:J;$1ӧOwqɷLX[ ik4#~G?._](,A8O a<@bJuu}"c {!`@!AY}I܍}Wfi Ke,s۶ms!lY]QX]Š'\,EY|Q'_FƒygVocJK"'\KcЎ}=Q?u,.QaZ,B0J3 C(?KN!k߿a\PlaU} ݾ}@,M**(Zܗ]v[U4*Ḿ!^{m4uO>)M4)Mvi+/\|ŎKm̠\`D9ٳV(+5 C0G QN>O(s9+DD%<qvD=ze| mTF"}J>2Mw)?7u68kϙ3'٪(g9Q-^%z/yeϞ=2m49caݺu(/X@8@޽{ИSRfMwWJƍꫯm0 Ch#axِB ۷owda׮]h"93rJիx!Q9֭\~ΕZjpBW,c=Y:tCg@w?QQ%gy2q &Ǜo)\šdu]OF֭yOs7nUVI&i̜9=U^]}ٸ}:=OvO2E>SgL8so vf͚qt"xN79=S.$ ~N_ڶm{ 6,/JJT~9 *U$m۶ ͖pݿP5& "hg~׻ዱZwN'}GDn]r׮]wi?kFs(N@!&]tqOpq-EEEF޽[xЌab5dǎl2cDЋa{!`dr<w =# 4 4D9^9BJZl)UT%+1c[ ;!Eޒ[oՑ{t~X^z%w=dҠ$QK:2;;M6m~kĈr'&$ʿoqkb5 H/ov&NH) ?׏;֝OsM0Aԕ)<Ygt+rωtDu=ڕyfA˶"}_dý,!1tPAw,>%j Q% 0tFs( B5Qg^Hzi!͓'O(jD9v C0* x9J!|ufc| x7pe!Dčg͚&cxD'-Zpbzq߾}@<5oKHj`򨣏>:vs .3fpw o׮<.yZgp No _ HZQu9>Hrmtpi޹vZ ak2idr:VϏ IDAT(Gca'-D}ʒ()G w-#[>S\!bY2K~xGNw'Qm!:u8b+xmErr9O#( yw\ׄpL.7j!`!p$@ CbMқ*PAԢT(oXrDDɰb2k-Ď%#{@ܑ"=_->A\[yrwq]r`'$K<`1oXCX@6u2xe*,d]<&Bo?M:ExA&=ǸC!ԛPHJT,ԅ-|h¤gD9 ){{dxXQ>EOy1{S[nq<\ۈr9%4yɝw2Byac3QFNxJõz!`F2n QN/#{b=U 1!sΝcJ"L|kdC94+\(c]^f&@K.7%K2+c%er_ca ^" "<fU%/bp33IDQ_u玎x_.t)3`"WX8ŹXȧ?ӵ_XiL3:ҁBmM2uIߧDm}22#?d6a}N;2s1O#YN3ըpbK !`d+J\vm/}a0ҏVw%/-}%7]HHI QDWk/v/$n.k!`@z@9U?\3QɽZ&#C W0\A2}1nGUNx !`!`%:)Oӧ,seuYBC +0\ArV>!`!`@N"DpXV'JW8(Qpn0 C0 Ct@|lْԏ4 \ArMg}:p!`!`@IH/lܸ1 ..f4gNTbxRzL,Fs(f" m:V!`!P:묳j֭KMRfN\,餳wqn|nݓ*syI d"˚ӌ7/,,tGI>f?'|R4i[+뗿 n2_K^Fs(Ϟ=,9Z C0Hk%rnmէ~:  .Z,ZDuؾ}[Ö<(2D=zNBIId9#&;w^ƍ nGNܜ9sRq;&#YN߻^뮻N.yeϞ=2m41#պuFIo4/ ԿKB[rL׮]۹r3xTZ馛ɕW^)X0 C0r R͛K*U[`d',\;l0l2wMWq~wȗ?Ld8_e˖~[Ϟ=sdqIaҫԚR|8MVl!§rT^]}bݺusSZ5Yp˛;J4tjr+e裏vVX:5`AGf(jt#ʊmӉ^p5uŲ&SMz4oEEץSNC!j*UAtTq达E9Dg:Ʋ?>f+ GaÆҠA^D?x h˼Xg̘HIvGu%"mڴ+( 8,!`Uk$Q/V &{ꩧ5c_O?u餓d}/3;^](#K36!X]׮]:4lM:&U+Q`,qJr͚5όwsT(ױ(޽YP#9@'O<5E e^l\? ^ze(kC؞U}C0 C P ,S"Lqdɘ!C{bB.\7{-QPxA$2ycg ִ<|XZ6ZjeF$l'OR!J8:ie~$V~`آ֛wj_(9ID_'mz(#(RQJz!i߾};wQe4B[v/Xtv0 C|Jo7p>ja12eJp;Uܣ c9ҀyF/e]i/W\q#wREPBfc3?sL7Q.8dJ $w/ꢋ.f.3&H0F(gɱ20C0 C$ }2-[{MiHRP7t[s9n0 Q5y/2haa4e M2Dk>}>Q~'I&)7^&|^x ' W]uU%^xވ匶[# 3 CHHo1f^z9у>7?͛yWĽ" @ѣxROH;*MYc Q>sd)rΝ;矗qɚ5k/;N̙)/Qr Z,t^zuyg^s=SO/\~嗠mbEkCPPP kv~rW˶mb~O=TTmV#guL4)vBի݇qG93\P{/Bnݺ jժ… |YM6r饗 Ȋ+SO:$xu2C0 C dTN]+G.v^iX.믿eNs"Yd`;@ &橞Z eHϦJStc9F;psRJd@ovYvMɓq7K̓z؎!`!p$IQreiڴiP~tkHDŚm lA@I/DYfAbY4egGgyf12]cǎΠcw%%\ 0X ӦM GUc#YL!(c >Qw1^{Q'"XoG6Xog-}(k \>4ȫDժUKyYt^r͚5qŮÝeDyA<.pjdVG m0 C8|맿;7Oa|\9rd('# m>W Fxdi(4L]Dޜ B?6D,ᥗ^*FՂRu5Qzo />lXUk8d[o-6DY kឍ{"ʐ{\WpDY 2c bloE¶!`A QƢGŔ~dk߾}aЎDD9,.=XZC SFKXe^h7c!E6# Jl9D_tCQӱK6q:i((bL]>Fi!`ZĐG\sh"KWK:!Y}Wqof@K*5/ل@,ҋ5?ӵ_XiL'ҳK"'\믻!ůk(D c(]w2Ju֟-ZF(Bbz%P_!`&0gy8=.}ʷ`@EEHH}=F(.[x ե!`!`&#C W0lD* tIn#!`!`!eQ6eMҪc!`!?>)k2gX0r#FsZ= C0 C0-[Ƃ!+Q1̤Y<@z$3Z0 C0 CnJBzY"T-nJ,׳Q]trVV{T=,>}Q6֕dL߿$S冀!`@jХ>hu]'-[,P'xtM @eM^.eBGT,|J>}RRe(?ҤI/{ / _|p rUW^╙(Qvku6 C0RD5Q"YgW^b-OÚ(W\qE-(/D=zeJS$C9={vJisNyeܸqf/u̙)/Qr x)"իWg}=#O=_~ CPPP kK?ꫯm۶Ŷs zRR%i۶m߭[7\U& . ]O =/zK V*&M z'\ێ!`!,ȜN:KȰb"NNiX.eHMÀ\8Ymkd aҫm5ke~~~Pp vT){1y笱\j0\p|0lذர?]'®]9%]vu+ʜvi.uiҽp#YLz#g17|IУMHD?r&Lj Jr{.qORSp;;]//^XXs/?(/]}(pņiƹT ԟ8~.]83 C0XD|*W,M6_o16QNFڎ!%(רQC5k,5HM\X;Qzv"=jcl-իNܱcG9묳dǎKJ8aر*MF(C@QL'a.Kt](cdAޞ}gpY4nX7ˌ3ݻ]/?(hY^zם(ۆ J m0 CH  y7S>i:Ggȑ#DD9Ydh;@ zF 'x]65jkl(=;KD9|OX1D(Z^-Tc#YLqbp^zbDwl?R7oܝJDT^b&Y@*ZcM\In` ( [EEHD׮3݈X~}gM~;eGQzn(6M<*qf!Cmx\I28`٤|p(g1Q g"2T-gq,X HXff0|LC=$Z: X1iD9EEŷ(QѴ}C0 "^ WD9[%VedYپ}{NDfp(,cCI/X IDATn ScKxlOjgG>Q^h ggkݺ:Þ={fDYXu}ꫯ8c.غ^b:/0`J2/@d$'[:Ƣ]vneg֭Z=k,q;DDl 8AtG0l0 C @zu+VH޽]GrIgVlVF?GyBm";p3VͤY*bA>W ^l3u''s?r(!`@@E u5N\4Z +ҍ|7\RYy&b^\y:Jgz?M]D_ Edkc%3Vf1]G.Ể MG`;[BtkyQrk k@mڧ> C =4_GoVC0 C0 B1d~` F(J[z!`!`!((lwKӡRr}gX6P@w{˭M;oZhxr t8<)k2g3Νg\џ,#UNd k lU1*ŝ]7E@w˖-IHcؗ{03Q6ܰrt~;Lśv*җ~CmjަXQm`ŦR FZ _7>]J_Ztw2*f_L[3.NGnĢ#*3N?2WHGK<7*L:5Xk('KNUwɶ½WJ<'K{ڈr6n?Ao,,U)O[nJ,߼[!oȂ_G/]{3 t#G.MAJ{oSx!o58G!Ƃ2zy1.oW_n!|?3H{σ2ko7˳!*b*O51f^aM^.=sŕ';i.Ot(w%算}g|=r ]{#enbWdYq<@Y5m6w磖H+~/ʚm61uk4hhcpiCܰ0, oF(gJ"Ou.,M\O!Go?Se|c6xsL>Uf?EyB7N#q0Rc (9f3xr{.?_j0ZuDq~GbbGt w̖ۛ'cQi Q.ڿ=+ҽ{UKD:!WtR`@ϽOC{}F?7W jj=n*m9A DN2Lȡt&j_2DFҬY3eM|6K{)m1Ϣ๲y/)QL^kuUA?zo(gQNnkns 7IE<3yt<8oWyx<ɛ!dmNdR ~~[.(sL^m{۔cc&z@5!3qX37, k@ՆHEv4"Avvɇ,eH$-lA"_Y&-3Yʢps}Irױ&͂@a}HO8[`l\!皿wD]|^C6OTZ>'C7MXS (Wo5u `@UH/Zv+H[ƣc\.笓O\F?_*SkMY_+Q.((J*ɩ*k׮xl#ʥ7L8R糙("y#~lD9+ߊM;F_\$F@LGȃFK˝0-&h>Bb,߸#Hù͑WsN\!87o,?uUpLE qoe8j>tҝ a ou }f˯ \g?ڀDmrzaEWaUٿN!G҉9ߢ ){Y|1s9a\ߐf{!?%®ekĤz[He(S_Ores_Ct[2̸mu׼*QCq@ j~M(gɱO޾뵖E Pt a:lN&܃gx_XuG9We QZO ʴQgJN:׏kMl(0QmgBa+9(ӱwrdS~(gD9ۈ;ɭF?z!z۸Nh1[_"ҸDzTjG7270B$=?q#w4 g8XyAf q5siqҼI czֆaam v`}lVK!8X{- ?o'p>Q A1 eYH3V!7\<|I4ׄg"*͛-e:՚~D 1nŚ T2$M2Z2rr?^dOVm2.|{ngYu2CoqA9b7Q{tmBx2x/₩Iw|؜n~ʩο\pֆk X(6ݲeKR?s+f#(3#K=؋P2 X6`m [ۀ?u:guM:1c/Ag DG֟:Qˤ;ռ Qaԩql'\d|5Tzw;6rʈRQ/,KG¾i_..ُmx[6kmNRL.-ha٨{dcn/cwY,?'-e1iXg(<kfjKֈr۷O.29sŕ':^<=_@ k dO"ʬ=ݭ A&)̺(w}2Rwl[_HYQ!r*0N6\ ʴd+(r=䨣W^y% (zupi7HI+ד䁶6gO{sF(W`.L|lٵGNwa dnzBm½E2~&hgݱg,\W w\_q2v&W đ=Rlr=ԧAAc`D9{Aؾ=k hQDerN@qCa 'QJM<UNkZiB*-;8k6U@\,F7@VCb?ZeޚW?,dK(?ïK%=k %3Vmy̢oʪbt4L9kສVc(+wa8k^t͔-֣/[-[`|7iw_N˘e.M;w;}OF|7; 0hQm}ゴk:$mvƝyͻ0Q޿@՚| 'H~~~p>ۈ EEm+ wwgҠ硶׃ygySSV{K6p{+y#9LG[f"O2//;[';M;dT>2QPfŻg @B|#|}:xUpt\L[}KmD{vom @Q~gudDO0H2՘eX(\_׼!Pd1:Lq+- A}eZ-s叇?ll58R~}O(so<1$h@Qjぼƫj9LtFh9+h7걿e<+p>g|W|,+=<),/H(HJSOkJq6-z|ˡ6Gq^ƢH}/۸SpƢG.wvܯg:@p(c=LV2שS'7~ϚuDKo(63]0^ܳVL/s׻ɕ궒MA)ψre3&ITc`X}D$e?xو)~{}k dc~@RG,p"<^ /:"\fCAnZ?'\SNkEC4oܷZQ?4'f3FR,ĸkG`S2$M2Z2rr?^%J6UD9J r5q;KSXe)Nc6.-z,d(I*UYfd[eDDY:Xyƌ WL[Ϟ e^M-yx^ Lu!?#YNqh>t{x ,>12[]\pÝ≎\ϥN*Oy/p::W(;G%~E*#3 T6 qVW4C]:DYg&E_kϋ+')OD}W](xSGHT,Dd\G!x137'O%ʐ8H*}&rʾQ^ riTI(c9%/ʣ$g,gz?N~CO`X< 8WxqeFUPNvG m0̧DJp]bԱ츊3IiyyyQgv_6YKOf٣ϗDN'N↷Aq޷xkhH -dtQXfuy !g.H'DH+EzKB:Xel-2mډe̗x/uo9*-+$CXLN31*X.a=zgIyڹ՚f*r`J$gI{󭖙&<\+W\lkYeQ.->Q&/yky.J[<#"xjVHK .S]^gD9ˉr6,w?jaZۀ/'.>?KH={19b'ۈr6huψeSSgٳgcm@EnF4M-4*1,(Qek X6PAڀe#&(Q6娂(Geaڀk }6px-jq&=v J (Qek X6`mڀw˖-IHc$d$p*{(C^36`mڀ#mױztc^z.ٲY񉎏4?uSIw#%yAzԩSeƍeN幘ɜ٩jR:qܶ#F(N²KX2 țfDu~X oڀ\l,7SɩV\~X"ev-ݒ>K<ԾnwY,?'-e1iXg(<kfjKֈr۷O.29sŕ'ҭk}|i7bQl:zde#i{i=iz5* Z]aSs_p2 X66E!Ds20< X?zRD*;e嚖|J4x=ΚEO-G !d1iw/la!#_F-=:Yp^XU16J)^0ނoTZm"^sX{By%ұ7S{Zr׿kokl)gԂ ݤy^~9>Γ.c6?'/t,< GBg ]_ ҮV,w5nDy!Vk ' l#G/Y0xۏZޝkKbڦb;Cg8s"x*Hiӵ5p(ٳG޽[v%;wtߐ!Cw$^zMt|QG]T QْB#>b5yG@yybV^[LaӃ 6ġ# ʐ5HZȉz%jp>Qi Q4=xbHND:i:~x:Z:Z ڍC zoƊ(\Y'JD$Ox1<}%X*U$z]&V(0Q{Kfl:z|yC{]pZD+VȲeˌ(gE^XzǕ$ /^GOecz_qU8|E4z/s׻/@kl?Kj6 XL@U93(baŊJoQfkg"Kø(iƽ<'* Qz,YGSL%xQ61];!Dq yc5U HSl)j~M( }roӲ7jnz5o,;l,x qgzƪ> 9/[2zރZTo(s:urׯ/\MD2n<5'E动A)ψr'<\{̘$!HU`9v~p(EOKbQ g,ߣmp~m?3l8[6`mdm"я nxBc X~;aEye/_uD> p=(N$֚2Blo(<~0hN`fXq+ִ?eHƱee,t-QZN"Q:ngYBz(O9th-ژB/ O豐I\TT$UTqd bY(_(\ 2ϘqJi| 3|L< iR5=Z.Qrr݋Kuמ@`118|(ŵw0)8\aDh^M^ѻyDR cq?c.Mup=z}bT Trg*eհ,IH2S<>Lra@_H6|eݪWN~CO`X< 8WxqeFUPNvG m0̧DJp]bԱ츊3IiyyyQgv_6YKOf٣ϗDN'N↷Aq޷xkD$(۳l Q6l^zc^k X6`DeeA#\H-2me`Q6`mڀk Q6rnra[+/mr)r.'v!`!`!?Ėd8Ƃ!+Q6+mi!`!%@"lْԏ4 \Ar?2fL왥i>lٲO?-M4)`C0 C ZY@IHԩSeƍX%I\\;?%VOrEi&G83Qa_ǝ~s${Μ9r7/W\q1[eĉG!`DSOXt8z;8//;od;Qw߾}reɹ+vҔ='CZӧOJ'|2톦{ / _|p rUW^RHdbD9r6~㔌)**/\^~eٺukm޼9^Qv0 C(7Q.7n$D=zQGUB&NXD2DsΑٳg'̯&;w^ƍ'k֬/R;8`fF(7kLxCOKđXs'/;ùL4eGn&Al2v܇~(W^y<#2vX׫TPPy饗JNcϣJ*W?. ^2|͎|WTIZn\k;!`@Y#EAn:wur;txoL8U rW_I>|08 uu%\"?ٳ'v T"&j45ke~~vYѽSNqٳ>[wͽ|U& .tưt1y笱|OvaÂ;d("z۷N]vSwUN:$+Wݺu5IڷX_ 0DyŊE/Bpȑ K˗8>V;33_ 5l0cǎNHڵW_b[MXj-r>Q#GC{nJ;Xe0 C(%%!xrO2EEA ҀlS_tSm6m*ġwqy~rݥKc%x5j <bY4z]Ynѫ!{nL]10waC۷(sϫWvעSuYcw%%\ 0|[v9mڴpT>6Dɸ(%1. &ʯZƌP?۶m*Xx=kOq:'o۷;2yӋn0 CVJB=.]M~2 2B[9X)^z9r >}afOD oРAPD@$gڵ(B@d4Ǒf".:4ѤDf͚Ҹqcr[ܭ a\ƒ&Lp(cZOWE:6D?3<#oF>D"zk"zxiy!ʢ'nI>Q m!`ن2h?|G{ݳ>`uԼW` 8s=-2 SӦMgXΝ_Cr9=y rD؎!|0=CƳ&S&U(U 8̓^ 2uZM &ʸM [2dc]zř(IP~~AXG],'6lp=rW~\"DQoQdeU,ҋ5ӟگ4~|gΜ^]ŚD@ÌwP 2׾뮣 ]!SL<˯bef!Hs纴 LOȖrs-O#9@sQY} CH0!_Vǂ!`#!Gk]Q6\j`!`!S0"̏4 \A\iVOC0 C0 C02e#ihV!`!`!`e#ʹV!`!`!`d#9N>>ke5Z!)C ow3o^~iO߽sn&СC#4mv֣G̽{vyVӦMLf}dXti=nC=0DcwqG &k۶m8-`v(ԩS=j텀cWݺuwo߇~BճgPmFoڴiAlְaC[dI?aInD32[lmnNu^DN,O>=~1|+%#G J^M2ԃ66ќ[?3 B4ow S#H1}[.]2yD"yF-27“/t#&[n#{IGFC~ByNBQWw K'AB~бU+'3:iᮽZkժU83I3iy8 ~@ \2|آܬY3 8vXLs'|rJKB0Cs!ėt *a*Z˯:No炕^uܣ1BVA3,[!Psꩧl!Çu,((cdkKnb".,:K"V!:Zdݒ/8c:0S_c}v3ִ7pzk22v91D(cO:\~e.S>er?݅(2uZ/_9%9#CqkR(3uE!#*/zx+\2=˜t>L)(1Tᭈb,T 98Ǖ_'X\ҿW *Ը-<*^J;NlӉ'(s!*3$#f(#jѓO#v $GB) ({p :)lbQfGyP&_is|xr 0- ^ e= Igsq}>,5nQN-ҎeIGg:E>T` E{9b*γ>;=M#w\}氲0/FAKO47~Y I#" +mϟ O9t$0 ͦw: \ Ub~7ï`Kp=i|pSa%+I8(3L љ|2) +u/Ś*ԧ.sʹ|D,!޾I=PU),Էz|k߃JS! J.)}U>2ȭPf h‹ {,ûp8ü#vZFl{XǕ_ ~־jsZ7,;˜aETT\i{=,^00D:æh(BEUp8Q&q|J ˎn-EC:ThQyCFEʐAcuU(!l7.mw>t0 :+cwرPCڱ0ԯv!gM~u"yŁVJ;d.a h R2f<u*–<ũxFdV#ΈYg,ң~-^y啀t1ʹ`6PW*$'b3^ *6ڄ]h;ݟ) eGI{!PU@(V:H6lVG37ɆEnߺ[{~I_NT'b>Zrʷ\H(K(W.FVܨ]"@9Bf \3YO)@aH(K(ƔKBy/@r(g=! ,\SK! B@!PCPP!Tc ! B@!  C@B]wݵVe}ūgBCկ }YU\|Jn7\M(չVaTfTunvltGr%+: ]t+(%AmA1y/|o&؍92`_㘿}[e҉go(z*o7&]ҹsJU JSH5E0չ Xunvl*KtUs#;B` \O<1_~9g1M["w0?ŕ+Wv.D(?"]wV!B>ߏLX5@#}չeKAuܲЙH(WA .ܥKX(7i$,+V!C? Cw1ϝ;74czC!V4O8F?j޼y/bv )xE }}CKDmCܞx≡,x&Qs9x.6j(3rBtr|oF |cx:8^zw *,?K.  ,|D;Aov 'kW?4T皩΍  B6THz~h6˷1Zvg[ZԪ@WЄGf=UX^^bܬY lsM7LB∹8,iXݑ_$#uQ0X]l_,QZ8Et]!B &VD"F.F(#,Xs9,X \x#%1%,a);˅~ձc.. +D(5$h۶mWC{ s0&axʹvzO=TԩS|)1\խ[^|pw~m `8/۞wiۼGIs WxF>L8b-Bγ> qKg;:lˇpQ3xa^z m9׵k չQka^箩QrBWL6VP\,ɠqxb/Gu 7 UαPPK,_X. 8zGXvBzgqn#D@ )ZN*3, 8#/"?-Xh],Fa+,Al([,<#F:J łZPC]2E+b gB9ʹN{i8E9DVW;xdC]H" 󎭿|7c2e;W,IP;x:}= !~ AۇCski\@< i}%rB`m M(_iU2Ӡ \esU=S^o%P=zt0B_|2!Xh&W_}uE9^"CX߈`fh%QC񛭑fQVѤ@#.?cH3a‚ƜR"ҙ[}VzיU 0~ϢM(4I".eU/UPNZ}??GlBUsŝsYL%\c#C0E<;8C*~tZp#" nQFөCg ^X=~YFX|ƴ򉹐Ϣ1ʎk:Q=^U[ܵn*M!6@>,o4ߘ盆^\ƟB8Aǫ^Ǖ6^|it2,C}*~ϥ-XcABg:Kkdpmf"P~jK Ea͊m\zZ:|UY y`ǰs,sT}o&XN~>h"i#D#}hlb\aӄH, `."2[,`b.Ϻײ ej+4}PbJ6p8:RpH_,i!Ȕ/iqs8GGF6L^Q9ėk1/WzX}eXygڷo{_t|v'p{„ CyB_yWxôfɸȸr(>%ToZs4v`Q-G C ڈ6|P^B8Âb,LB8x_WVḢXhNtʐB2~[0&Wict^0,11 n͊ԱPO^Ge4F4jۼS[p q?P&Ds4ϷעEXVb|I ؼC \ W0#.W9wRܦOO:pk%xU[;չ1:54aø@6bYBy 劢bZcSjt^1% +̟B#믇Uc=KGy2VDU݅r!^y啙hꪫʴ;Kw7xc&؟^ʉ@6bEv~M 'x"T|OTxubDHmֆ瞳Vz௻L:u[zBׯoqr= 7`;S7vwAW_Yݺuᇙ{ʠD91kQ{!LJrȻ lm1sqĊ 4 =zaG;f< Ν;v8Hn-BzsGݺu Ln[϶vۀMrB@|PlNj˖-w'|2&<#CB}K=ewyg&|GIvC?7;_Xl+x\u[U-\6 c\+v,C.)c=Phkt,@@@B ez(3K/d3g s| ZYfkZj/=n g,t*.LR'N:)EaoDkƍmF/7aÆ~C׵kW;3r=S6m =<i,YP{C "~pcyu&MBX=PFё@esO;W ~ yC9!,ʗ\rIzB ;W9lF\q:qPjxp.q#y/{3٦@C3p@(t+a [t 4(CGǕ/lLF͘1>`ҥK.Ww|7|sp_|1eX(ovpWWKN>!8~3d0ƃ4u:12$  * |t]wՆn!$4 IDATn馌7*[zOq"ܺ5*{e=D&y@ *-{=;SxO%D\gB(}E}ARq_iTF_ 9rd2è\(3<^!i,rA!asY7M(c2dH&{Zyxb!#nFL#b'c!  A୷Ze͊ C:U;Tįw -Nq}F2u D_+_s}͆["_]JEc_\ل:'hi/iB76)c0_Ʒ7usc(tUVZ0lQ" q@q@q`rEaNk߿I_y1t؍aax0q@q@`L J^b WB/ܙb7FY 3q@q@ )cT%^x(vW_MMa%|0)/Dq@ā8 ,|g؟r92amJk:OQq@q@āp@BYB9K(S(g|ViͯY}Z8c),8 8 @-\rrnKP`d e}^<]/q@q@X3P"yʹ2z-M,K('Dr)GO>vYge6{SXp~_RX q@q@āp6,5[,iXߒ7('2= $\,Ѵt޽3BٯL ~w  8 8PPB9M{PF H( .K(c q@q@V$kHfu6M$.J('Dr)Crٲe[^V +Wlz:E8 8 q6,5[,i4Ȣ_lҥo0`4VJq@ā5[(r"_HDT*[q@q@@|G6m4m5 ZY]TbQ,f*&^8 8 p_~6uTm5R, ]Pfkm@q@q@qڳP.jժU'y͢?WLa%q@q@qs@By^8 8 @ e eYq@q@q@"H(G`=:V8 8 8 59q@q@q@8 ޢ[rW8 8 @ e e8 8 ՚F2a@ُ=(K(Xa^Mq@qs<|p?` f_uGBYB`TZq@q $p!ɏl%t]8 8 @eMZM1\BG}TFu͒Ux oq@q@āMr eDES(p8 8 8P9B7m5:KBlʖ1{_kC(?#v'g-]w ~&M<6`*5/bj 8k2bzgA o;Sm6dLGq@q '<+Vh }]iB{/ +ÐN9@4瞶o|_7\H#@~P^x=9?XIjyuʈӧ-WHlJjߚ2R88&[nc}M7wyg뱟v<{q3~0hhӧ7ńPG ћ˽KP\sM[>"Ƿ˗^2 >k^݄r)PCfoWYq@q  iҖ QGƽ]vvm7ܹs2g͚t?7A(c [Z*5ȢbQ4&oO܏"oq9;~7^iӦ<&"x2o޼p?ڵ ^Ef͚ٕW^i{z:묓sFzĈ&L_CCH^D,Ň䫯 ~֭k]v yI ->p~Oü'.Go 8,Z\a7n_/2;~Cyk /7K.5p/s !q<0OBr͕/B8 ) lٲ5:Gbr18Cy"HȤj8 U#D60!zit!\oժU0`Ģ=_$6ĉ A(>[:uvر퍡 ;8Om"O.ke9ꪫAcVsE{%UzG#)(\k @[4ܣN dспp3bzP2Ao=s 4kr!B ݷۇ0iԯ;tLXd~==R+cLO NA\D֏d?gO=^%}ݡcEetx>[nILsxr)v뭷f5 a?XQC Я!\ïXt#I'͜Br͕yxGfЯeǼ,P38 eFC z2B9q85,z={f8W8RYq@GyPFO҅ʴџ>6G`&dцرc>m{FѶ5m۶C?=,\iq0.k1rt h3&sLk՘s~yp@B9৞z*0CY ʈIDoP||Uɕcޝ_X):nt2K d%@xqeӛvEeι%2>$X< )yIxF:+!8}tP&P\a.y^<:(q<~?nT1l܊=,1=K o0b5_:iy =c.^G(Ǽ8 iBP8#€1Qʹ#߼qME:8yCX q@ā΁\BᅥQf<<XI"hƘ₍v ?ĔJse"#={ .ib죽i33qPfJ*K9OʹҢ}piIOvc67xmI#=#Ǟ 6 ~V4FzR5pB!%& &b؆D [gdI2BNH'מx&nEETy|P̵b5W:|\B${8hZ|X({lcu2i%9ӱ8 @B:]za!JF eH3 r 1B~v+2Ԛ6xr mZ3~?cƌ LAszJ L31W LCLa0Z8w=3Z#kҠMz ƫ|/~^VV:*)4a`PF3օrҵdrrkUVM\g$2e@O2X(3Nqqr#O #xB9,j"ȷ`I1-w Cq1wWCnyN_ 1 B+4=U| =OA^72rb}<\K1&^xa#ֱZ3U9@a,)\1E9/WP?g;;xל2rcab9q 19;|[H Tt8y~o0&8 .>iPf1/ΙJv猄djLjYD-Lje,X :?G(3z\aeJ{hWbBMh*# =-4D\C(V~߅2{inbm!4=#紟hӃhb1s1t\, eιHB,eOVh_s__Xs: r A]o1Z e0C鉈W.F([F eY!#cLzxUX(3z@.ƒ&İl&.+2@fGͽ`I12G2KCn@ C|hz\(#rxKzꩧf/yExF,I84cK|$`Ra/v A3"ŔktM(\\B4RǪ5CN 5q/ƘsX(s9cT8t8Yχj8 U.f ~](s +)mRڰ0p>u09 ! K{ƴL:V Cv2 i}߳+mK7W~ռ~N1Ң?{Z?G Ӷ0vP>@cYhsaX2c#oƿp |& F4q<9~$S^b8q.rU&^U˿[5*Iq@*;JȬl+ʚǪ/ʲ(\ |9ϗ? eUZ88 8 TV]XN˨;F2z ʚ 5|F eU|IN\q@ā2Co \6~DNa͖ʔ8 rY?zf5q@q@b r0sU H(K(K(8 8 ʕƪψ9,V0\;Qr#2^zhS/8 8 8P38#ǪLוB} e4ܫRfT8*g8 8 T%92X?jXp[BYBYBYq@q@q  rFUS^Ջ+8 8 ငzq@q@q@"H(G`7fW*8 8 U9q@q@q@8 Qz8Wȉ8 8 8z8 ,#q@q@q@/ ѣGq JBh W*8 8PU8H>|VÆ `,,\0YGF|… 駟6mh8 8PxWmС믿Vٶ-VOIƅj e R( յ]&^zrB@! H"_n>lm"v_ z!Ł΁4ۭOd^S/^h/Nޟ1YxR=B@!P#PU?ж1PaSs[."q?|8|{a)SZVizE! BF"Pn*:!$P^b5k̶b ;SG fj ʜWp8q|=ܳɗ;cx`*'O 6 ^!q߷w9͐!Chڅ7MA 禰g&ZœQ(&LnݺYΝmog}FĄB"PՅ2veZje:u y{ꩧ㎫̆J(GBuԱ 7 '`~i9A*~ ygy^{-\yk/ϤW)5|Q^|r ŋ۞Y5|Z<=r-e{.5|"~J^#GBG.W_}eK8[&S:B@@uʬ}I'&lW?~ +mƾUʬni)ڳY]?5L{jR {ryZ<e*xNS+PV,$5駼b6uwu͝;75XE R)]B@j@UʴiF=xP6?{lƏ]{(&BѱtϚ5.k2óy3e˖a$ij׮m-lK.͕̈́EyJ[lmVv" hPӐ > C]0Px׷388O> XMo7C] ›!rꫯKsN|v}ٶ{}g˗/nvqGz3,?mF liC !vItIEve1H igi%<7"mv믿>iq߾}mw6̶n25{ճqe |EIKga;aIC9guV(w0Sykg&wqGN뮻m{78!ׯ_? jvM7=P={vOO=z rV^K,1x>š_nl=8naOLlnO<,O8LsX6)sF_7v^zY׮]Cx_ceswygx/|'C{7CGc=:|רހBE˖|B@TOrکckiU](#hhX],?mG[SNiӦ{RhOw=h ,]uUxh7F3N_Os m ΉmXuv$q^f4K_'zڋkeo3s_63PFxx=nmFoP^2E̙3'k>AD P4{묳NF3a~U2PFиhQۮ];;ꨣi03+L/-/M(gq4i瞡QEw$p7^آ|K8^'ogx|\?<So^FD@Qyr.h"$M+\\pk:1M[sQF3z!͢p}!cG/( S 0~u]"0y7xKSpʋp)N8ާ ܲ[K'PF8"]ҢLG7|SL C(Iê;{c/cOD0n.n!FڼS/GA8s?͢O(3t@_: z~,nPΖ?8|J Q#y4B@GٙgaN?-l-lnÿ @Uʈ:ڮlK>_l:&w1!G{69'3C øi;6 禍299L[o5cTuX>sOnohp6W\a<2`@}y޽Pv-!k_|l%W bU¢= Q%y'KqGX2C؏G(cq @=s=}R(#Bi<_CӨO>=mwP3;ʔg"oiNё;! QQR[qJ;5͕G(;2 ùP󖖾 ! @Gr. U](#lԉ$~-?CٯN:i"|#&_'ܧm%G`!e](s̨0F<#ɣo cA(A@ w M f=xZz `+&|Q۶m0@ 7)pI|BˢLJrP }`c|_[o믟IX%$G(#<},iw,F;gu??> {Ǫ^6Èe,w?^_vrs0b2""<)=z\O e82nS|\<~xX߃w3?ۧ 4q"X2#Ao.g?\R(#j̔ mj3B98tDhfJi9zcIއ$c}vGܔ;ϻ /E(Sαc8ǀ=B4WP[><;[Z&5F[.W2sx_|LKn| M5L1G; ך7o #l4h5r68Pf6e|GӉnQn/nں\C(~vo޸m#<2߸_`kCVZ˅׿U&,f(3i71ӯA8>zl{Өe;.F('6jԨ06MBw eyrYϚ-nʉ)⇗Ϳ e/EX|xV',!yr=S!B e/,:fiejҤI&M啼gsoLD6ˢ̜j:pxgqTtI ' (3 +v<[ŹN>=>(`BrZޘ i.-oitM! #mlm|k'm-e?=cQFu(cr#k.9G7n{0cQmÔ6k\q!B)]_/qrK/,k`+_b[g.,/ š/]&ӘDH#L?2/aaBq|IB^a0a#=x_PN捎M|T4}>v)Bk-=N`e43%5~C;s҇mL^&ȏr.|2!Xɧ?.?ƜlJ!#>7s=S!B'',.q) 0/!ʋ{.ʈc% ezMo<;.P7T,A|/ȕO(#u0lB(#}ZQp5D–ǟQps=|P !Ǘ&FVkIƌS9iyh)zqiy 7\*8Σ:B@jǫچNn=m 3ϘgFQw2?h;zFmr6bPf$_C(i9mrsiGPvBcڋ,?mwŴ A~L\#!`e>T?}X9brLE*,dz-Vw>%aI1tO(CA J7}\>hc.1y"qfCzXqJO+Ϳ2'&sxGpp0\pk,*\B0,s0̳w1qú(D>8L\~B"moꗪP}oi0ʹCCEmƍg - E)u~łi3pNL7Dړt_2kpN;vG=63rؘ"6'~o[[J\%](orޗ1I!8 Յ /  nyquyӬHD?V[ xe5r\atM #q@q@N8+eV<Ĝvµ4ô}.\PP"F8q@ā[m%`vٸ+Wj+ϝ\-,EL#q$8 Չ~Ö¶~ζˮ.vy[aTd ?ôA.AǽR09lDse[l…_ۗ_~i O?4̏mɢE~ CRҬlarMPPPq@qrmo+*l;Gkk[XU-Ot*2f*r[bEWy bBzX܆= 4Ⱦ7nM0!Gmco6% aMχ*[ _dq@޲+_m}6dsTX:b!,X`=X0y'ozROЕ*#{YvOWգ6{ҥ/oFۼO=nS?|?!l)R5.EYYYq@ā"qɲ-Xh=-e}+殗rIJ`O(#]q㏗ 7m4w}VZvUWWloժuԩv0`Ϣ\ _tkkĦ]{ͼuK;[Oa9_-6fwfufr}ݵ6WK& ʪkl%F~]Aq@{E,+y3gkTkzI6nd=f]qsV69.5O~jGQ6xl:uw ~|a챩 wEf2~+zիWN?tC~ao1͇kZ] ijij8 8 5/ ͜35>z>:ػ|J:ĵqS˯ڏsYǖkoؠCmEQA}'OۺkV8bKgY>`AaIF"֭kSL)(.b6li#np瞳vFa38N8poȐ!!}O{رauhm۶y6iҤN֭[gM3[^`Ϣ\ `eaGfK9Ah1QFAA@!?🿹DbQFlgHرc38`ܬY3CH344䑍a fHvt]|B\`Q&Ԯmsɜ96+?f,t|w?za#?믿~0b@Æ zi 4(N&Mmii{-ʪkl%F~]Aq@{s,Yu{R];sn {^EM^|9rB{~~-W㻻uNhXZ>s e C|.}ꩧaз~۞| B],vaaal2|a NArU#XN0!`_cd\pAN_~e?R,P.1cۜw߱g7Y_laK~9GwdZiifKځҞ0v2rZ s)ZpM dQxxq@qr7{"1碷NwnǜPι*7CϞz9ڮ:0hM6Ӿ>Ž1V!^rM1'on?~a]qe8}?SZb2O>9̯g}w Dh1xwlET.P^c7n\Y.i ちӳg϶-Zacp~B>gK/4q׆b2~I+j|廗O!Kf϶uMOimF Ν<`:(Fog;fĦ~r7{7yg'锠S9ʈm<.pzsY ߤ|r?ʪkl%F~]Aq@{l&IDAT3-isrvT쌋ao7j`'^M5~=?gcMf[ի_zԦlķ߇p C &>xu<ʚԚ.pYI3-Rp`VPF,͝;7nl{gؿ{aA/kc "2(c<㌕"CE|`UoD7s37pC\\,u.y'\(g gomɧ60'!-?>Xl#lZe+_G:X} a2CęgS xʻχkZ] ijij8 8 5~bA[6yqtvUknУWxljnCFǎ'N5jWv`O(cC۰ >vQef͚(i{oUf^k;еsF,/I (XS6kucgXꈛ8*JVi5Yq@q7}2k^Qۄfڷ&!/nOـ/ڗ#F٨lܔEQl֤0'"ʥFmʐa6wCS6X[}0'uf_&% aWw"+ɇkZ] ijij8 8 5is&Mcδ&MoMkCG}kþc#`0ѾSRҨaLBp\'ѥ剟 WӛYr(cc2)͛3'/"Tʪjl%Kkq@ā'N ,Pf.ߪ>( ⿛pM,8 8Pc9޾6q4c6[[]G}aa\`04lk\*4%U1fF׊fLq@{]6al4!- 3~}aÆa+Vl °+ؿK(K(g}$B 3a&8 Tk@2X[~ $C2Ckq.,\cƳBqu|}/,,,8 8 8q@B9{:=*+8 8 8 T4$%s$8 8 8q@B9{!zq@q@q@zPPVϑ8 8 8 @  TLq@q@*9q@q@q@8 QѽO=[8 8 8 T=H(K(Hq@q@q  rzz^OLe&8 8  e e8 8 8 DpWu% j 8 8 8Pe9PKN! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! B@! ZwIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/wireshark_2.png0000644000175000017500000024327414062723141021762 0ustar00jonasjonasPNG  IHDRo(ߊ IDATxwtGyZκ3Oxyfl&'#`qLds9J$BIHB9K$Jԡs$U>UwOק]MC=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@=@M4k&MդI&M4g d 2@ d 2@ àANVWuh۶cI2@ d 2@ d F0b͛6M뮻^)A7 =cGYp>}@ d 2@ d 2hPأ-[c˃<Y&MjD+vm*G1 2@ d 2@ d <"Z߾ڶukqT d 2@ d 2@ȀswqNjc-5Kfod 2@ d 2@ PCZ5ФI3T_͛6BWw8}B2@ d 2@ d g77].iP F d 2@ d 2@Ȁ5Ϛ4i7EI&lkg/  d 2@ d 2@VhLB d 2@ d 2`dkGGVW}~E9 r>Cda2@ d 2@  q {=,O?" VI<"=,R(8cH7 2@ d 2@@3cT#wBQFP7>'d 2@ d 2P{ gvL(GQ d a Qқٰ䕮Mk۹nΧgHp١aߧuJ dQ29O:/Q(/0dL2@@@7dsf$ȌK MxY'Rpr (v]ҵs!d @Y}!ȹHgڇ((ʱ(Xaq2@|`s2m}|H9/{'' _oC[˻c\J$;Hˤ(i qY {.cW̒R@LQ·kOϜm^#I%eR^V*9Y[T6%i5ϭ_ d 4$ 9;WgeT\_;9mzk8($wy܃~ ̕Ԓ2%JHn^Je`~6WSJiy$/2. Hz(;"$H3S%&,DuT'gb$=@cdOwx}{$2&Y2s 0KbCQe)( Qa8XPye.yrAY 6VW퓓1'Eyr1搄|׺:صAJ)K31RbjoWKi; /TMi/B_d W›}ݫ;E8ˮ(LJ>2@@cfi[y!2iy)(.2vҫ]3qG.QBVzlDFIF yy-Qh9r1!§DΆ)n)+ha e%y+eR^rIV߹rx^`+<a{Yfh<9E]ZYGӥDbW} ld 2P \7G)[ y2@@ge!2wc AޟQ~O?i(9 9S9ı$/Y"kē[ܭuT`HlTLP's0Oˋ~$/͈rj_L{Urn kA}Q.;c$9$E2s7(@Z\rK˥8eEr ]>$eo<٪n_ߊ[6f2P \V 95Fu39|N2@ 3[YmNG힑fnjiinh!=wJ 0O]ߢ0^FQܡ(JLe(7;Q\NOHAq _}I w _ l1>z)a Tыeȇ͔cB3Qnu:yk'5v 8XV͓b9;y\Ӽy;2@@ xJ焚\w%M sV"6-e᫼ǽ~]hy%J n-mV[Ҳϗb 4P!3KGiJ+N^kewΑ^ ijS5|U &CYC#ˊɴIPgd=z,ʓFcdD92tE_5_(mKUr۾ͪF5ۍ!d ۝W`Mrפn!/Ii!9 ̕:, /Uw[Z E9rnr_ȴ?Fd /h%-wL~!Y21Y|v$Iji<' K4KU(WV*y)W?z45Qޛ/zʹ/S_RCo ǫMyLI f`i-.}pcxFy;ew!Q.0yIV7Kj޿.=6-+x/ vv/ d FV3n:"N-M? _erR>2@g S'ťŒ*1eOH %DJK $=7X: dwQr 0GvJW/9vH$E*KG"Q#Tk%E-{v|)ϐZ2?3IĢ|ֹeCV,E9;[*xcjJVjػP6 $o[%i_\"EYr!&Bv~3g.oj@ą (gK}E@cg]%|}8]Q۰ N  voT/(/O7As?L; d . ssz!DQE92@ d@@<+ZJK̗}D13d 2@zpTH9$B\#G72o d ߓ&:Z]m>4 d F@T>UbCͳ!# Dc7 2@ d 2@@by#s>, qF.(QJ5Bua{= d 2@ d <(Q#d 2@ d 2@e(ղé4׼Ld 2@ d 2@@}gE9*d 2@ d 2@ EZvx}Wii@ d 2@ d gE9*d 2@ d 2@ EZv8Wc 2@ d 2@ P(G% 2@ d 2@ d(a d 2@ d 2@Ȁs. }@ d 2@ d 2@3@Q""ET2@ d 2@ d Zf\-; so d 2@ d E9rT d 2@ d 2@jz)ʝ>}Z~_8`xg/t|ipp,]T4c yľ>%u~^WY_|?Y_'@_v;vxu~Y>_LmoL/GC 2@cNE_ҤI* /رc o;v'ooTvAc^EpYdɓ.3{NwqA]lu])ׯ{VˣrѢEҼys;7ɸq$++m Q_RcǎuV2eJߧ~ݶmϝ]vIVWiӦZힶ5uXch?ז>jXXE9{//6 G~};tyˊ;<5)7gqqq}lgc}j;[d 2@h< Թ(7~xIOOwj׵%=;`SFFF5[a6,:ϝ;W׿JNNN;+zm[Sy'N(wy5J$55Ul"?9sFEhƌ#%%%ɂ ^OEkC{w >vb/]DW.\DYbl޼Y5kj{iT$۲-h{~mڴio몭Qv_~᳍7:|k>˿婧zj!q߾}UŵZX؎`cxźQkڏ&(OlO~ɂcbSU۷iF(\x,ܻ[hF˟'!"^u OFV}|޽iz4ߌ[F#\nn0s=qt~ d 2(vZiA $jۆze! Ý֬Y>Luƭ%Fg\bXÎ0t tx7NRQlЁD'2MﳋuZ-1B m/#DtJ };!!i9JB BI@@|W*y!FAdٰanye'|:>~pK iiiҧO4h+ל9sBLpb3]ٳհ<ɓ؉E!(}ܑ#G@`&=t ! 0::Zt9,)~ Vk9Na0=G5k B9={8:Dc~tحw ;wB+̢GsT ÉϞ=+JQf`8;C$-lԢ4 >^!C8|c=Y=b#j604M`ц|gpoЅ"~;pg .q`Ç;l6z4/{ηK.>4ߌ[FE9W68 `?_/tA~ d @r\A X4h _|Egt, ҐxТmQx(QS?! Q_H %~׎s7UM P_tE||@m}1(@s-qCh2mW:hM(D;Z;#tXBQ hތy`"j˺]) l?OmזF?qh6-8/pw0hD.9 d 2@ \C>$cgN0 Orwtб@'m :bCa,÷d|طf: :@@T. -ԩSN ]KNH9D0MWFG3舎 k2AdӨ Q,/ZyD9 CP}f?-|,!j.- "qwa 0>sL5Lhǽ+j($)PG}a D0!>4(O3C_}0Y`!8G$LD5^vpq7Q+Qs:G̱c'|mϛmZhxk+pd-lB0}CCC[o "~VpacpbJ cyCuՑg'mTk rxÃh&p顴/c/Dq 3:URb<}Ÿ hhזF?qh߸e;ލsw_ysd 2@ 5@r:x";=DћK IDAT{28_o&j, rf0n0h%!c^_?si߾`ȕy'Dt!C DB,勝.0 oZ5BgGF]u&-ݡGQ3/MP:Q/|-@<@[hQm'F'~/=HA#De!n-Xrl9`#꩏! *:X *JќpLCy^a;*ip}ex1 X5Fx>CDC( k֝;wV6YGCeu+51>C/!좳m7v<Y:, B>4 -3rvm^fa!zIWš19l/pu,HHԥa_|c< c&;6$c^WL{'|nݪ|V{% ^??_sy]oP$ċ7eזF?qwD9W %QָV5.@N d 2P vQ  eƒ5:/(UQuf͚c@hxW UцC"vR? QdL"{ a;="&G'vc 젳c hЩKS-8DdHAM8^oƺǰQD.p [ bh+I7wbp@uXE>S^tW\{)4` eZj/ 8Ox."t{~5ka  }A <#^!'_jǢݝEa?s,1ܻ| seiFB ·Ql2Ga+ߙXp ~'pn1$p =` EFtȊ>l#::0۫W/<'3(ws;YD<":~U[ }A_2@ d ԩ(ocҟJD/}iKY!Jܭ47zYWe,$n[mٶd 2@@}fD9sD^u[-]=m:_}Xr^Sv:Oov9[ڝE(H aXwvzX֭j]kq`uz[}=WmSu}{; d 2?Lc#K|yraˆ!bY7o.D9؍!ߘK˨{KP# EA d 4n(и/?۟ 2@ d 2@-(Qc  d 2@ d 2@@-3wQ̙3D2@ d 2@ d  ]z0d 2@ d 2@ d9~._,L d 2@ d 2@pE9Q d 2@ d 2@jrp*b!d 2@ d 2@ ~˅> d 2P Ɏ;dŊPݯg> d E9nws ^{쑄IJJPclK}}iAd 2@hx P(gۑ.|)۔ 4<,11Qj%.ԉ혪 aM d fJKK%%%E ٺu]V.]*sUvhm_VV&/*Jȑ#~Ȱٙ/rQټy,X@%c!]9 e)dyƎ%q~m~=1\YxĜ?v?ǐUDՖ A\}vs?d 2@ fĢHr3g,YDmۦ9te޼yRXXvݻwH5q2ܹsJ|ALm?zIIf͚sN9q!"say`1оC{i9ynm<*QHx6Yؒ&C՜xi1;^Ίvs#e=2vhILLG{ ^zw-?ղO> ~>ׯ;KTT̚5]6lX'ܯ\eԘ w2]ݻw; t>+fRI rXjQNlYsf7{w߬8j]oۏ|^l2@]\AA:tHEia>eirjݻWBCC=z :Pʬ}ϟ(5lGڵkri[.^(˗/Wiii|W,cv8m:/onI6$ keDuI-+Tylat Iv ϊxs$RNqHr|D,R"VzڱA "KB(&D9ҥKrINDIt$D~o. D͞=[E]~]qF7n ow%ːIJrDfs8R"OLK%=:IV:/"Wc2~IMMͧ3U.+1̽gϞҶm[5Ľ^EuUTTi6mo߾^e||^RfϞ)99uD.w|Sl\N:Q2S7dgt~㪄I˙3.;׺wϑrǎTw*{:_}m}O>J~,^X6mڤ%00Pgzv4oo6{qi'GYfo #d 2R׬Y#vRb\feb%tX:,^?$w+q yZ7fʨ QeKttAh@:6Ƥߴw! ,޽G 1dyH2<3wm[,Ne-2lU,9%+J߮NQ6OD\}ǃG;.[)V$W+ Ys>Wz-UCZ{,I.d;L"NskF0qyTvamρ؃O?ر}`-[=#=16H>%ckbkw2md9q\o&7ه#" N_ǰKDy3Z4yDlrQ?4ILJPQhᡶœy稄u㾚\MQnRɝH4uk%ʥH1" p@}gc[\cAsU2y5~vޏ8*v>:E ds.ʡSK3_m0+tKt|c}K%dY|U^g)6Iy͖ `'s^̇ᗈ2''wr : YYJ.֭eIֱ<9]QΓ2J2FsJ2kn}fw?CFO2Lul>s͚5Se e#j^FW\w5*?kxߞ? 2@;JCgoREd+hN//G$ yڐYP\D@dCf1NGC~@=( CV9q׹sgU>[ҴiS>}Sy0ca+Ba9#3vJlZJӊ)baeJ-9ٔ|9'gtB#rdʖ r<6-Mx/.-k7jET@1$|***JuZqCT\~C+D*DJ#UHfNDr=Ym‹JxJDqfCڥ,Dٗ_ RӔAJ!3d1wީ -8;رc_J,X?o^v썛# 1cG cJ ;Uv]*TRR׶S|r )Dys͖M[vѡ68ODu!/R%cy[iQΙp/EwpEQ0!XD4_>my[;vP?,Ya۰lw74PZ6WY }{c T~0.mc 2@ (a ʵk7䓱UDQkii~7/Vqx 75W!4\VQN7ŸJ~ܸq]],7*re~UFTe(mrˬKlt)J'$+B\Wȋ-ʸz^7?رbl; ƐUtR]qz C k5zԩ2qx;/e2'MJ'SƏW s?#V"^Za$,M;i>}ʣz(7lG4m:;F eS QoɵAՒeD 6}o>m#}mE9]yOQj?KR^駟yc0A/z C0 r>Ί90VZFɣkB(>~ x.Z\\ ϟ ɒ)*ĐB='%?/GE}***_Q(g!Btz'6J}{ظXP~<$咞.;IJ|_?رbl+v[C?*X/TÔ/]ʐYd޾ήt0)D/'ƌUqx`b&7zo_:,ìqAr_n(w }R S{ccIP هmFA cG~#)WYr ?0}ֱ MCl@uaqF KwZF؏|pn\rmXv d _(!jٲejLk1`4{f4{z8'jeOC弱EQ B:dx[`'AЅ95++KN:;w* oTQrǏWgܹT%}0>e}v-R ->sI 'FE5L^핢DʫRQqY߫^qlU+WHA~?. ̗9y2B֮Y-rzeaA%%YaʆӦI^֡sWO{vINն={… kĆ*AՀr*:F`NJww\R~zAX/wlsfIQqJZ,vvwtrO6w''hQ:h߰]e~ǎۼŶ'0h /8_X5 yq o!ܹ]RRSgOlsկ}ȃ72kQl_5;bbX.e}A u8O?h!EJdv 1Rhy 8ݸmx}F du.au._ڽ_qF!. [G90Lěobh u4R 0DJC~7n"p(< oذaՐױcǪmQ=9swYyeF_D\}Q^^$"R\+qLe.FJYYر]N˗+}۰wnABԭZܽ&՘ /}@r欩q?رbn7|Sl;Q_~;xă>oDt+e1ɜ>JM.ߝ%ng߷LRyS7=ugF᫓L3gց=u%4^TBӼ<ȋcp, %*r)s[E^|p,pVn%zs*MۡʾwHا6/?*iQ(F!JJ|5wW68jwIڠByy`zuۭ!8<޶ ՙ/ 2@ 3Pd90܀Bn xbO d x΀E9nBcDuz(!ncušún:%axMdc?9L$u{JJ@܌᫯?Br;7µiӦUtLPkYcnH|ɯ elgZmǃ}yy@\+3s6HOJ3$>=W˲%==Yv&aR\^wVumsfÙy-]zT\~5f:fR[?D x^O`NJ3Aӧ7-[7hBN/~!wus2ud%F!iU-p=q䳲sCXu86@T;~3Z4yK^)5Tsm.;@kηhqrmI`(y,((2-ڂ}!8֝zD/zf)g|Z٧ߌ71o#aQ똗qHX6CT۽+Ւ6[=vq+s}E d;L\r!!!**k yBVyזB-d˾T%HA(m<#J:-Hr7c 1nժUC [V/z֭ 0@L1cF5aN\s=`ʶa3?+.K;P&6;(W^#VɓpN/Z yI'۷mT ;,7իKvVrsR=Y]V6,Z!סsgSǤcnJң,YR#6E m_-Gh!A{3{;V-1LΏ?nгg /xăz}ee;f# Z$&\chwW!?>ց|F` 8w#VcgǩH#GJǮ]:jN<,_X'pI}c~ɂLo/8siy5; &pY^Vmވ^>W~ƼCwUĊ,"Z ؆9*(CUm;z~;{72@ 1Pg" WJ{skknUZ%Qeҥ^=PQ-狃@#'`8c8| %%%)azK,Qeͯ{VD|CDՌ3FB ˁ@T|ϟSP>(ec'D&ʰ5i2g_Dv<^KvD%g$dxde&ʶ$7'Yaޝij5/NXdvm[1Kzdy})Q͆ڽSO"xM=v֢掴Lu)++ uUy8w>ۢg:l}Žn؏7w6Q|kW7&$Xp~p ު{[+Z&C䝿`:e|]?#'Suw̪feeeSjoއ~(>{Q;1`u;n?qos3A?Od 2@gD9 <\tɱ Q!Dbn9| y:TΚ5KRSU)%]vU:ly|I55ϝ;We02 -Q2rC,G .*.,젤U05';7Q ىҽaO bz%5U)4n}!۷oUq>NG:l}){!J?q̣zҝfr͚Usv5$//Xvyx}ؑjXb0xZ'O8.?X~kX6COD^>5(6WC|gunʾw},5lw3l…sVaxs9"q~yO"^:ohUwjf<-?G d Q]{/nSQ͑pf.KHK΋偮Ku5’^D9j6tXtZfC~Da^C`,CİVO:i!>t\.]|qm۶U"&裏t 0,Sy2nk, -kB(\rwIr18"W-‚x VIaADGVH||\(b?r929̘1ֆESQGO.fό'GJAdr$,g?ر\{sx!m:}.*C5uijj, z%;wW6K.||~@c(G*զ(bǨ0ֵ,D/=nrRvG>ee߾ARc(fzIU} +h]oۏ|f}> d :0teՐT G5GYp|<ʰɡ*cnGgix(E`/ e`4廳w6@Q b: joT;vaX6yQ/Er]}Z"eĦYvů elW)A41rJ6gFȒŋb)U+KFi+Id>\fg&5RX1'1ԯ~*7Op73Ѳh%dffHڂ!qPU2ܭY>-ʽ8aJ+ƌwBJD=q&A:RM دF>];KosW_E1NЗPljw\7V6o}I_2@p΀E9BRvv eȗU>^9nlX'QlݺU uUy4w^F$( eqAc8| &r`X0rVv˖-Sx`' a*vkż]ϗWɄe~M(ezY sr,]2l)*HKJY%If9r9M S$RʵˑXvNsl+f_yí:&fG966#><՝b/}oE k=8~1yɪ,6^g㒥Uƶv[gպ7wLSqʝw)G`U{ۉnvQ'~d^C dsj]Ã2"|jh5ܵ/e@d?? /|p|`Dޠdٶm=z1T !DPC1Ve-R;#K7L}xvCG{3t-2^ ɗ᫞ԭopwvK0~w_W%J~@c_=id!=Re:J6HO+3Ǹ#z}f[{c^~kGVEz~s`])ۃ 2@.ፇs̱M݄-Zt[=X!l߾}~OB[<"ul>G]Cx#%&Mh4nkEۨ;^~=X^Fq vmR3G'd 2@Z`5G2@Ȁ]Yx2@ d $~0?}@ dvؾ}$$$ކ]SQqrQDv]kg&d 2@("vÅs ۝05W*D2 )0?+_}~= 2@ d P(gۑ7t,722@@00T6t{w\ +J d P(GQ 2@ d 2@ d(/L d 2@ d 2@pE9Q d 2@ d 2@jr[l& 2@ d 2@ d 8gիW> d 2@ d 2@ (wea d 2@ d 2@Ȁs(QDJ d 2@ d 2P ]+//& 2@ d 2@ d 8gEDd 2@ d 2@ @r2w\2@ d 2@ d zʕ+ȑ#RXXX뢤E219sȒ%KD63d 2@ d 2@a|qn!}2o޼*zQ۪ ? wU䇕Q2rEX~R/?)/;ΛfQݨ}D2@37=R*gYF d 2P/!!e֬*͜5]Ϙ*ӦOS')d2aX7a̜5M:&r;Zx4Y86k_߮P> d 2@  Z;zXêEi1raw*w"<+:eQϪuGʙB׋"1-}XWKO [FpB,8\-NI~UyljnDg.I .,cvĺ>S-d8A55LsU5؝ҫ%"%_N.ľ4RKy&dk OT3x\dN0rv\{WPՆN5cnVsr41/6վ%b;CDmGO)&\Ф<;*er|@lQh嚈4%d|X-+qO9%x*ʍ+86)T|垘qT Rt_.;)_/w s3?Uc>ƛrvEr0f:7əy67^+힮׵(T%B,|MOer5‡A 2@ d2~qθ#jQ[ r3~"\qz.BΝ;爌3 rXקAr%%%bL8)ˇsw+QB Ad@','S Tl" lC$XDZltYB'v$Y2 %슠\-h0LR!L2 D9;fs*i+zn \)ct/J3_uVHBLs _ED9m p):"6Gg(_A|"$:BQsv\eJQuCED"Ft%C;]DθG~}\M/ByxuNFD3*EOCC?zN*Yh gQbC}@ym2@ d <8ȸ*BT>9 qIKKSsZ۽{ deeɈ7~]%a?Нzݺj#̕Yr?.?b9rg;CtJ IDAT(1SKEnE9]N9$| N0-61JbSKȶ426ðM1:B ɡU'ƐL=+up:C'!@x4 ZZ%n .:Q)@T-Rh\ri A*JxeG/ut% Q LE'cCB۠^w\E^RYӂ#ԝFFQ牨yrfQ }piI{չ]+D9DH[bhs]G}%hE7s3F hb;HtZbms6bp1zHOQT0H2@ d 4TЗb"2}0+A*RD/@b^y2R |Z?Og<%ǎW(8gK&fND9+!N rvTQH2fk0/c>n8DG>Z{=$BEAt~D :WNG:;eX o(ߑp,D#j8qQ6`q:D-9dOk|oq.w0N|bq: QcNrrn> mwۜ ·ern!HC$67[l΃{G)> 6 d 2@jbsazY3 rX(0LC!1YE8uB9ƋQWW14 Qk r0ؑŋB6nJ MIr u! qt 0VD&N,h1FAӑr} p 6lDwY%xn>cvr:/eaNp ,F۬ LZ!T!]S1?SDwD1ӪlWNj  d؎9o5r"jhM(wʁ( HGۄm]+o]q*|ft,|"t^+]E{]FO0?{eIQarL#bAĀ Q *(HΒOr%deK\` R7ޙp{L]]ԯ~VutהJy_ՀPj@ 5&hFÐ#B."ꌹ`|_r1V\?W>Vis%}J?/~񋉿n:-HFӔ#q!̸qyL6zgo\&3iiyέy|7h(~kIwgɔc9l"b”%Rn.qbLcL0WЭ7Zҵ\L2brH9cYyvLߐ1xClt3>QW'_;ol=a1"=]~.*)Y40+|bN\4E_03))wQChxCF-YtՅ3: 0CD>M+'^'OH?x}6âAΘZ|JSq#|c/" qF7S}`[ S8/+ox"[%8L9`R]Lz؉LbäRa2,#`M4 `OYtߥ.~c/1Ljp;n,g4R1¾ĺ1%=eeC"Ha cGLN icʋ%0TDa?11jq`ޕ];7Z 48_޼`~M9ʕc4Ep1庝+b@aŘrM+&(7DEylfŀh€#L$vܼ)󊴥)o^pfW;ɔrTe|cb)Y ޘ׀&.Ӫ5ԀPj@ Rx3s˘ twb[ݴjVG;:0>ruiΓOcYGSn4_ДRUVԀPj@ 5ho[nndar^`w}iʔ)Ì0ߪ:n{:)Lz(GqD:u)c|5-C ? hY*~Wj@ 5ԀPM4з324<ÆY5ZGjuG=ܴ[g}F5i5ԀPj@ 5ԀPjw eL16ttN>SP}7a'5ԀPj@ 5ԀPj@ "#m#| ԀPj@ 5ԀPj@ 5@{ hi"j5ԀPj@ 5ԀPj@ ,` h-`:bF 5ԀPj@ 5Ԁ )7{ ԀPj@ 5ԀPj@ 5@{ )w''dԀPj@ 5ԀPj@ 5k`MK@ 5ԀPj@ 5ԀPj@ rO=TԿkf`}e2x hP5s"h|5ԀP4Y)7wY&L]?qXǃti_JnkPj@ rsMtW䱏L\z||s`P5s"h|5ԀP40jܓO>W]u@x-od0A/G:E[8'Ҿ.kPj@ 5Ir#l6z%/yAUMʤX_ZqG;y:kmDR/ǹ{/Cέ~oNGҫ^{$xGJF2Rj@ 505S'HKE]4͘15CȈ4e`|\>(u].2,$K'./?eQ2YϢ|^/|az+_Yf6$06mZ~QG}t_h]{\'m׼5iUWMwy|[t9׺-2LuQݷ~Ty)Ww_ {矬dԀPj`4LWi 7lUC>`u{]n Ƽ{Cϟ{Vm<ùqf{'u]K~GB/gӎlGm|1^{M8L /xZeU:'댳*{q馭뮻.-i^Uj4j `r(НyX 5i`L< . 0"͉'fDIt֏4a}ǔ[WSO}S/}ig?㎴:dve]J?ubYI7)]w|;A/_f7ʀ ڿg=Zs=;'Nv۩ tqzhWU>g8w^{t};8Gcl-vuw'*?^9/}{~JL814D/OGuTz衇;쐨R<&1_~\M7ݔM)G%Ccp#ZvmQƬ0+N;-){teB"L+SLo~s 10a1B -N:餜苏~Amzye5ͥz>R = t 3s=O~f͚n'qyXa}9FCֹ\c#|++=yhAD1.Gwa̳[k\/{=F?ݴ}]ַ2cʀ(gtL0DtJ];WĶi,n&铟d6}is~Wyp[dE>uN;eZ'*w|?3rsL6կw޹M/|9sflGzwMnj@ 5Nft6pf󍧳M'  eD4]9o"|DŽKjS gG47 [(=eʔl *)&p\h-7tB00Xw9zIi;zAl;κsqxh_|ŭzD7ӻ3:.~WyTur<1HyytaAm,?3[[F]28m~Lfj@ 5Nf?Cʼn뮻n)|1"=SMTy;QkTzFDz|oL9re$Og?Yy40?֏|}کL`CD 0݆M$oQ,ј# \FJ+eF+y䑹|w9 F\>ԝ =6 rΣŭkVZJO " |"8w|Yy55Ұ,'zaग׸^cxv?&LyN[tY r%Wsmtg=4= 蔾ӵzzAl;9Dt6ztƊID.]pcۘwGBnZ,X+_J6e@ypoL 5j`LxS\t 7ܐ#~j)RC.弉S򱌧T>wr; xHrf"Da[ko|!eU‹NkWmjtF0z;t^^*M?>t?rrʣz#]}[zXS&8ъ%׏y#35Ԁ[ ,pS` Q"06݈H뮻汃&X*#ia񴛱[xs'Ȑ؞y'WLhL35]$  JYoTiȓyƔek|11ڙr,L1cFJ9e8?Ϗ_=)\!Ø[DƃΣ}cy9K 3&3|ǀQpt>˛r1X0V54c;mAFVeϺGZ-;t^?cqO?/r8Ç<%L9^@>Zg}x,M*:[ԭɔW<$Z (Oj@ 5DrTHM7j1?aj0`/OA1fH,?B{ .L9W̋H9s"j,oPƠǔ%otaB+LyN[[sɵZݮDT n];WgC1NxWHTQ.M~{q}{Ӧn_^QQwkQ&up_ۉH&car oCypiFf2Sj@ 50X r]t<e2 i48v'E[cuKUh>kPj@ 505S'G$ݪ1[&y.W=~y>z>90(ZP[Sb8_<ѤԾ6'z Rj@ 5ƓF͔c`Aa-x-<ǫ}<=Gm-dv*9l&Ky[PdL5H 5ԀO5SMx<>^2sqqq΁AD:ΉKԀPj@ tr?5ԀPj@ 5ԀPj@ 5S3ۜ=m[yF6j@ 5ԀPj@ 5ԀXxSnoN7H[poM|{z"_ [VMU0]~Oz,+zZ|iGH8!yέ龇HΙN޴絖?R2N2yCay6ȫSo} ]va<;kKf|0Lx~FG\?0'K_cjgĺi\<37GM=3C/my yy$z"z-|(=̳ɧC/>m }Ͽ=ߜ4s'qQWzO<`ZsKZ"Mu= n{ sc^guN>C۝qaD-+E^1SNv礣.3=w%hd޾x̳f=>t]tysb+ IDATʹOonY;1|igw:%tYM×{%Og ҹפ}4?ȣ.W.Lav-[_\./u\_T;۟vS4>PoGe䇤^wO^?{7P_aqέ9_ ;`[>>d1קrX!~bvoqn{ey=LEҗzTO!yrCes(KfS9#918eN=c&j,;i,2oβrW`w wH9n|KS]l^L9*RT`Vycn'+oDhܓ 9ұ.)7mX$J,sZ_K'SD #^6|1zށ&nB:*MDF\> wysk,(S}"WG_є#];s_5DQZVՔ.f__5(>49yZkMA9rq;w T&*uOh0f7zyu1q>bzEt9MHx_K0]ɇHﴯoW%i`̇kFuYF"壛0,USʲ,e(٨505)WcQ0|ߣMWЋ@1G4>uhP!˄`TDe(d}ٝ rVryOiSSSDT@,ux]_m$.;u7ɮRo6BD ױ`=vx5Qur}t3hqCT ev񌖎h2: =”WD$q!EV2-318wuxPѭ_>juw=i1Zc= o7i:"o}0Y33"2ы܇n Q<-{[GՔc?E+Rh2-*Çg{c0FdF ^ϬC<$nALrGs2Bu25V7 |0&YcfK#4aݩû]WMnڢ,xy=nߝ7@WCr57XBHrcT@yF^k1nSV|ʓ{NMTp0>=$eL9CZ$}D`.)ǓS'G bPb^Ґ+Y}匛yh0rbYWQN*wD^|t9~!:"?4F2(FBt+2=*QP.w~P BtN<"r{1܃QDDT1Vr ێHr0;dmK`bVq*ȡTvɏ4L0 c6ߴV=-L9b:c%̦&,zMڠ}Ewes/Xa]e=1ۈ<woVG4&W\tk}%veYƔz.1En_PYr5Q# ߫7vVTi,Oc^ݔ 0ܼ('"edIYi!tJ>Cjv@^"SBlh/L?Q7ʈC~&2P .tI)WƀW<ଋfr}͢.d_#eɼN֊ܫ.ubJ"ugs;Ӫ)DzN*˲}-50q5)WcʕH9)o KAEN%+nUv<͖Fҕu1xZIetƴ)Ǹ0fH[NHve@D*಻yknu\7bQtS{Aڙr0e8`ms BϸI7# qzzN;;ӈTapz7yp5mE+EW6|~sex?ޑ?\q?t3a!|"B~ hQýep(*MrM#Dz͜x˱P ju:~HrhnWcRDxQ- *qˆh.=vޘr5!WEgnot2>e;_)>t8޺i%Hxh 1GXV^!4c:Sʲ,住YXSO 1^E))G%/G~n'lq0G8}2+1;7&S| 1Oԛr}NcbO,)OdS`J]ڕ >xҘc쒺:DZTtOhM*i_5k08^gN2?T5☫ӺH9a6RofVw?G C?GY]R9N"ӭ|AL Y BzCo_eYW'Sfhq<5M)KtN9a,bGrYxxpq()W޿ikzCϓ^+cN{k5ef5ԀPj`kS4,Ӊ_eԀ? huUQ͟&75ԀPj@ va ڥqQj@ AЀ\+1zaWj@ 5j@Snl5Ə444ԀPj@ 5ԀPj@ 5F͔ˣfYl H@$  H@$  L~oNCАo^״^W}c4{W*}O\sMkY/~{ԧ>^Wu9vIꫯ^צ7jhnMozSUW]5=yg}6/lo}kXoy[ҿzh:(!>le/{ِ4\rIkk_Z~È2y^={ꩧw]Ygӝw&d&׿>]#~\{oxf81%Cvs\o<m٦veM,HzK_mQzgZw#oOw?:M>=}l{`ߡN>a#SO=ڟ/B -7{9srXy;idܹ?qZl߆n~&l^x?MGԩSN8aرPPCK:g_9.ʇcZc5_v!_|~+_IqD5_\@q$(Kxs=3w;=Y7pC+-_(_>?3c!&j,;i,!C&$5xZk w'S =MO)*0iEYu8!7j"~ʹ瞛8TP!:yJD t2x"^{Y{[O[3'ʑDo)7>>Tj{1*;O:VKtu|0"O:餼*\ZjtgM8f//$"YglFMcs/lr`{GRٛӾvղ<~\zv/~qB Δ뤭jYy]&5墨0W:rw{ɇM1VZiaTT*;s9wErs'D=n_{*{'SJ"'D@'Ȍ?ύL}$SW~{UW.$aMׯ뮻.?pC- ch3{^NǺD$>߇E2@'S{&Tʆ/tE⁙Rs=qˇ%^DDկ.Wk}_P$OhuZf/"ި/@jQW/֯rQ?6K=ӛrINKSDޱ00x‡^=M4^q]W[mo:И#èe#:snsq{ +ŧӾFiY,1fTC& lrݴUeT&.MOS]$Oy*YgvR~07"SXwu[]ʴSc4>t4(~ذߥ^MP)X)?O4O/K,;45KD"OyOen*t};兮07GNo6BD 裏?k !jv2 BjhsF$s˘r{){-I=|89/ØmeR/?L[^50a1ɲ;0Sk5aWic=Џ$  LLrL޺“^ƅ cT@yF^k1nSV|Jq \݇'.ҵ3j7kDuDԔ'Om@\y% n%ļC4jxl\7r.4ilŇF(cϔ2FtDVS$7xic$ڕ]c]yQe4 '.waP81EiR'"L8_r [n>/)#m}/5AYEtXݧ4rk]#};S.LB3?r~0$>O+2+L9@i/&uG&zmq7Nf<߿Ie=DKaq#p_Kj7CI /0m_s6ʲ$ )#Lj3]tSfє 9 xST~x@A1bpj3k}0̢{"ǎQi(*_ir-9{}FR96DEҬ D{igQGr#0>VRl^>)yĵ:ƍ"J˓0!2L7Ưjj)ۧK݌ |kӲ,"bH-Bwݧjʑʲy$0)7u2T_e͕XxJ6yEw##҇c~}~D4Zs L9N2WDeSli.0& T5k})F[}5A,LD"кʇcf,00K&I@K>t%B_S 3!8ꨣrZޚ͐+DJNۧ a'0XӾ Os|bYUl,2})I[eY] Wb IDAT&&50ha1c J Y'}2m'lq0G8}2ʷfnxc RFn1]'^_O$cL bW%l!u4qa5]9Lfj@ 5ԀPj@ 5HkOO3gF͔{gRӿs=:Ma"35ԀPj@ 5ԀP40Yf=өG]hlԀPj@ 5ԀPj@ rU2ݨrO=T;ꨣҚkY Snճ\ԀPj@ 5ԀPj@ A6eV_5Snܹ'wG5X#UsIuU{(WyC 5ԀPj@ 5ԀK )WgVW.Z`\iȱcҔQՄՄPj@ 5ԀPj@ Mia13FՔy'ZGqDZ}[2g^5g* Y5ԀPj@ 5ԀPA9)WJ OZFH<͙3g2Hgi=<ԀPj@ 5ԀPj@ sՔ” +x-PScCr^q;.?s\ԀPj@ 5ԀPj@ AYuWYHG}4=#=JMk ^Pj@ 5ԀPj@ 5ЫsՔ? ^r ^h:/Zj@ 5ԀPj@ 5 iNK3fqiQ M cf%/5ԀPj@ 5ԀPj`44pꩧӧY)G@ 5ԀPj@ 5ԀPj@ k/x/ϲgN@ 5ԀPj@ 5ԀPj@ ׀&&Pj@ 50I4@E巟VWe#5ԀPj`4)7I*#%ԀPj`jC.L~βSj@ 50q4))gtPj@ 5ԀPj@ 5rW?x\ԀPj@ 5ԀPj@ 5S.ˉziYYVj@ 5ԀPj@ 5Ԁ iʽ]bLJn:K_Rя~vWXa|s=7-Bol~>uԴ˦%X"=IrH+ߙ3go}[5yMz_6dֲN~׿|'M2eׯs<;[ztN;7xNH,Hz__Zwڴis\z+_38׽u颋.'?IB ||ej;#-3ln5~=s]uUۉmY>w88'OZiꦽ[y{z{N4d_"^oίRXM;~+W̿ow-njOK 3~S岺ur^熍|ԀPMڔ׿S7̆*k&Zhg$+M;~ /|ሙr^{ٝwye/{Y6(J鮻J7pC6:ꨮ5^z00V>#˔c4?Lr8hpϚ5+}k_K?>}љu]ϡ/~s§]>n0k_ڞxc=rŔ7yiꦽ'<馛m/F:~~ż^徍ȳu|`DޫFF4aDrKn,tH2L4ַ&> œ1w3}3Iv/Y)7cƌQVrlAZ~ ^֔[wuv  H#9~~C1rVX:Ŕ#?"8"HG$:=#BH@"Fi)O~K.e"T;DlE)&Q^Ǿ^|9_iDQ^'c=ͺn.'"WcFana^xYsDU Gw^~|͇/(o~n| sOr?fE6lʃs.H%?%/iHGTBA9_TFE|2Lڭn{5$}ni*~}"&Nס[.㗿+;W5ZZ-׷ldԀPYcf8lbU!Skgᄍ2~77@YCj5:+w){o*ǘR4`{_ 41qUSe}lw4Nc#-SBD41qQ#{&(B:MYbV5SۨN);5Fr^ IDATmmDC]U NJg_L'):⼁oٸ-Ɛ+މu+õGep[O/#"LK.$LI׉( m`8`\v3:]1;կk&c(CF\0vvM1KÄhkȶnMLa8apƹ/aKhۭ.kg?ٴ{ ^ute9;DU700Y#`m;vq,S:nmwbL;D\{{NS#u2x =(B1E'mq`$vi/~qO`Gt=0yU,pFDƴ]'ϴ<@D\g`whkmFPj@ 15sUu2me颁@ej+g̣M4̨L**SΔ,y\nc$+{# *T<]n9B^岺4zhP2ʀ.10W֩kyы^>QAM4c/A4wlVmIEC\r?2-kQkR00gl)L7saݱ(Ґœ&lV!~H*˙Ώ)Ǿ2~ך)/ZzP kD'>K+h/om)G#4 HSts-yh=4uDckV^5c,#lh C#p%WSH+8qǾpN퇱DcX`\qy1x)ȿn aMɔ1W&2k, !L:]IG,w1Ӵ..=ۙrˇRԄ>6?t2227zS'mqm53Ha8p) ]A:uSwpq}4k54ƾۖ'~2 ՀPj 40\\pLihU_p'  *Xҥ9OhiRG|"&b`;RhjQ٦] ”AbD, ̣AG%eûq19]>"MCT 얕_5 i\ѭ5x3%,}.@Evӭ*V.,i|tk1098`wE4NDEں#00"]LXa{eʒnT&elnJ㺌!^S5W6# .`D*#?a,ax7"ê81kfGi'>4s"vO9Dù6®sN6Nwkl:ARȎnek>013rDtFK?QuS >q-k32ҔkN)̵!r;n1s}:)g\FԀPj`$40 ^?Ճ@À[t!!@ҝL9GcZST#R(T0 r)G.֥J%e$̒vQe8@̤YeZ6|p?y  HG(bb쥲'y Npa=L4?y?LL1!619ֈ hlr4p#ҷc.QF1/4 ^ib|O8+#A)#NS}cPDz*Asc9]Ea6YV534@tC.Sƺ=ps(vE" hbԔO1)N|b=3bSM5}*M ޞz3fb+®N|LZ z衇vƢG+r>9x1JB/3n 兩E>0eqMÍ ]ԸNiOv0bE,g#qXʁeI9۝v99>^j#;^f5s+7~zLbLnw%-YHNXByľT2:݃הE9-Qqaӕ4%:O_ݔ{F-/i!kUO{h}͔|'_]ɔV|Kʃr8_/nZWγԀPj@ 3SFTO~",)sHAN3X4ϧ?!]&>X5Ge;,qGb90c&*k rzZWj@ 5rM 9*؉-X fY.ز$ꍧ3jU"7yDBL]1M6S.o; ssH5d;3YJ 5Հ[8ġI5&q0]-c #EF3q]5nu]%f]#3E\[/hZRj@ 5Ԁ+ hiY 5ԀPj@ 5ԀPj`k@Sn+5ԀPj@ 5ԀPj`h`BrȕBbx}=/``wL7߼HYd>o` ҬYZO=cW^@>USyu]a}YPj@ 5ԀPj@ r+WK,rtw*e/Kuen뮻.aE]i͜93?]ZWFWSN9%moqZz饳Vr[qf͚5̔cO}Si6j|m{pOH޲Wj@ 5ԀPj@ 50SSWCV9mqeta"r,H6X+K/tX{wLo}[.2j1FnL9 =[gum8o0N7tu 1 ?99sӏ"mr*Sop{IO>LGҏO=lk/3_9lŔR 벯j?9,5ԀPj@ 5Ԁ8Цܗw=?;CwΞ3 bYUizɧ ۟ZCHyeJήgޒr/|~zi3oI+xnii ;0nVzݾctk)7>QYngݒ.|jG\2#f{5 9z-4d^+龱t>6$Ng<䜹Ϥ}CZyҿ.^S'ƲԀPj@ 5ԀPj@ k`L3IUL)?p}+j+Ljw<0'h1Ћg 3LO!??g2)wt33Ni(0VyNZev1!͔;ڎ8K] RQ}(>(D:Ey])!&D$ !4RHH/o}vþs|Ϲ'ܛ}3{=ܙ3|{}ZQPNsdm;AUTX>d\p}>@@@}\Q`ǠAS*ХO\v>z]ˋr 4~Abmh1sxAsFҤ,5owl;+^#[[=A5ΆME\aLū6Ordh/ڋ>@}>@}Cr5Fz)346wKVёgur=QsZ }`7p[]Tk{q܂pk }t5ۂg[ަm in- ɭ-<&/Xn@}>@:<(H`.[T9rNb;欷W [Nj1j.WK"MX"oˑr{zlxVFmk}c`Sѫk꣗AE\ۯbZzeZg.EA9=OyUabI2_ mF}>@}e r DJvFmzvzzv+ ʝhz[v]CE\*{hڗ;xA˟o÷*۫k6|6`JY\!7?- z[m\Wu|7'e<}>@}>P>AF+~yHaUy:nힺu+?|k\((mjtq?|-\a@T ^PNo^s&X%@^]S;{ԝó irzSl^~((^Q +oo@}>@ 5S@M* kGņJӛNwM<[xFu[h\ízF~gU}TLyML?0mo[jzN\\WA9=. r ;+քM}TZ]]:_STvnwhiQPN2Rt{ꓖ<n}>@}>@6>aAFr @}>@@uNG}>@}>@t>@P ~:}>@}>@˞Cޝq}>@}›@@@@*U@@@@ k/#    P!@P    4W\s})@@@@ r$,@@@@@K    T a     (\_JG@@@@B\ @@@@hAR:    *HX    @s5ח@@@@ (WA@@@@+@P    @A      \rt@@@@*U@@@@ k/#    P!@P    4W\s})@@@@ r$,@@@@@K    T a     (\_JG@@@@B\ @kJ IDAT@@@hAR:    *HX    @s5ח@@@@ (WA@@@@+@P    @A      \rt@@@@*U@@@@ k/#    P!@P    4W\s})@@@@ r$,@@@@@K    T a     (\_JG@@@@B\ @@@@hAR:    *HX    @s5ח@@@@ (WA@@@@+@P    @A      \rt@@@@*U@@@@ k/#    P!@P    4W\s})@@@@ r$,@@@@@K    T a     (\_JG@@@@B\ @@@@hAR:    *HX    @s5ח@@@@ (WA@@@@+@P    @A      \rt@@@@*U@@@@ k/#    P!@P    4W\s})@@@@ r$,@@@@@K    T a     (\_JG@@@@B\ @@@@hAR:    *HX    @s5ח@@@@ (WA@@@@+@P    @A      \rt@@@@*U@@@@ 4-(}>@}>@}lF?ب ??x}>@}>@r }>g}֖/_s]9y~ 6}>@U (U.}>@(oP@.e>P>@v}>@(O (GP}>@}>@<(އ .}>@}>@4%(W}-#]o߾֭[G@@L@ ͜9}Q޽=ofC$V-HX=Lbᄏmf;.KxN?| _1cƴXVŋm-lq{)bgm뭷mc=l7϶/9&SF=Ge?mvuɓ o}߷78-ܒ%{iJ3^>/;V[م^QSb-lvz(Kf6?~_2{ym/+`36hР*yeƵ~cZ+K+*+.kM;Ku*Tǭs9.n~I_Woq@@X7 =6`[hQB}ݶ`kFZA."ESO=+W ^,ӦM?>"(֌3le˖Z+}7nkK, e|߶<٨緾-{B]nC*\AҥuQVU,yiIU XA9u]g|6dWpvm7KBM7ݴj;^ZF~˧]uU6ۄ|^tWoM;K׍&{Ad . d~A@@r tHPn޼yvwdA8ykFZ{kԪUvu0KV.H 9 G)pQy_җo,(_taY:K+wc/_RokVWUӈ~٨O|f͚U)8C '1vXӈ8BNϚU`ժEճ:+ )x嗷6~saÆe{m\tiVFW:4^mZy۫V{Νk'[4V+tzeziZσW/ͫ1=Cߏ|#ӻw0<~g  萠ܤI-yjFZFk.4I?~]楥{_|qaPNAƍCgi[ IGw)QQ(H[Yz}cn)|0[M7 ȣ{,K6㕹.ͧOhԗږ>7gϞa^tQ[jn!M{zՇ&NhiP.ͿbŊrZfMT1ߚ62dHgHkzW0оi^E?iz:饥eԚϷC#?^]Zu[9Fw;RA@@ GCrϜ9sn3=KO>ꫯuZQ0..RϏe]lz=vqDž yi^iZ|ӭh'|/_M#|4gi>+uꩧnH"=WOAS}@sTp?+s]<5LFL9mOŸ :}4A9^zͽ+^Ӆz(>[TkzF|-ze q3zUf[3SL/-_NvhV^m\>((MzI>G+=Fmo]btg4jTiF]-zzHC@@`C萠;=z>6u$ҭHנeMlz.Hr^WfVt܈#Po[G'|! 6rx~ `gU+W2;,̢ZlS#41T@z鄦'bꓦ网k^ZKK˨g^o!跢QUpԭ>n뮹,kzʧѤ?я_viߵʌ/iW/{Q;4K+W[-St#)Janп%cdn5-OUxkuiLGO#ӏWf  lӈ Ji C]46#-nh5jQ4ZWqKLӊ.H8+.6#O}Svty=>gL{7<7L/H?z{֯Vfe[iӏnT S#`4R.\ՈS;EI"3 ?c)~P+O6m unkZKIkm񣾡A秵ʌLǴ+K˗^y뵾~_cd{nH4xcr{֣mqe=@@:@ӭ "-r 曕VӲZxv;Vk^Zr^tI'O0!Rt1]ꫯC9bU=W,0Bezuγ8 FyFӧOM>z֑޴T+S34z_ 40BA8*P/":CP.H9ђOYej}.lUjFDz~{ ϴfeO=u)Ҽ2vhs}Jk~g ~c_{ׅ!  tXPN2M%Y3bi-Bs0hwnF/ZYo%O<1d[0U=N\[T}vgįTA'ۆg*U{yA?3 (ȧF@ o9s=O$wو6[оQ_os׾pK륗^LNye{4zҠ@F/*[ϧZvIBT6oMk@Aگj7`ǦgVfije*VZσW/Z=׷#{?렃 Gw=  'aA|pZ    A$@@@@ (W+!    vΒ@@@@K\]L ! ׺u!"  @fΜi>ux{7zizۧkZOt}m{x;hpK?o}oQlZԴ8?f̘|34(&5OkZ+{v)g?RUr^>/-_N祥e|^ZKKh|/|o;޾5~-?WTexE^>/-+`-yiY%Q=sLoi  @x 0`-Z(n[`AyifͲ{Ί/k!vE.؞z)jlʕY.]QGenl]foUW]e|A6ۄm*sڴivofA+w_[lYndɒPZ.Ҫ^>/mC(UyūFy{BÇۧ?pSW( @@7oqYNGy$缴ٳgyo\kz۪UvuWرcM kg֭63p@wk_; 6,:w\'?"|Ŵ.B;+r^{; /xei7^˾%S4yiEee^>/-/z2/vZ*џ_җok^__k'> S >~>ϛs+KbK+.   QCr&M .{gBKj/՛/-Cg!s_-:nȾ3@ܽVը8GE!Qw7B r_\S E :X|[իctM@>!O=.E{2/M|^ZQYqKV )߷qƅಂ޵>_2kFjgDկ}24,̼X   :$(6 ̙3n6scjMZZP2|Q4ve?mFOqCՂEe+Vozfn .2[fMi$^,^i>(wdz)׻wjE|#vꩧu5ZO#ǥp/VTV\bK+*+.yi1j3?sv{݌חZk5OA-2yWh=)?Fzi}#/Ѵ|9Fl4_|e%_~  ,!A9vzn_}mԩ!H[%4՛/-CgȐ!gjz6:H9]Lu?z~#hvZ ļr#G~aڎ)ӕMTPngY~d/Q1* JxY/VQP+xA#FdmuK_2}׈8rݫWPF}_#-e xf^ZqimTt#i4-_N2͗n;?heɗ_#  ! tHPNjuF YϞ=Ŷ47_Zŋr85/~=bz~:cƌp믿4& vQC#pSTM2zz]|ޝ)pnqeS/yir^>/--#?{r{rFKKFw[ӏng<|^ZKK{|9w/<   @@ ӭ F(أZi1՛/JKڵkTn-iU-Ѳi_IJGoq}{y{z۫4)g-zh]o<gm6}PZ.Ҫ^>/mC(U ʝtI! BTߨ_r{ !^QE/ɗ_LI{K˗~yiiy//    tXPNغ-S^4JnĉKi]w)Fs=VpE)s=mM6w޹MՂť]zGwzF+=0` F?6h?Sϡ'ԟ'uzP~/PPC u~rc]jS//'2^>/-_N祥evZ>޾ۧh[*K^ ]o3֛vv7{Eei^^ZV@K+(*[Ҳ f|^ZAQ"/    @@dB    4Y\)@@@@A@@@@hA&S<    yry#    d ͜9}Q޽=ofo_ҦMzw|F+_H駟b{a[lExj^ZW2eӛW7l3qX^Z\hҊʊ˼|^ZMlv6(X3ҊK[mez@   :,(O؀lѢE6fm!=#+SPoܹ55S㏷7߼"(n%\b|6tӰ?:{m]t{SOV[me+W ټ4\/eٶ%/z xf^Zqikz.i   @g萠ܼyM3J IDAT;Ȃp )ئ^}U?~6:{ɓ'HPN#n; oS /Kfd뭷CfߋfT7VZ%뮦Ҳ f|^ZAQ"/P0g۶{qk#/-+`-yiY3^>/lK (yiEe|^ZV@K+(E   @&Mn=]3kf^Z\7N]w{kZ/n2dm営||R ?pKK{|9w/izmQfgvK4@@@tHPN>6g7ް>}Qp^ZL5k6|ث7_ZF[f̘FZ}.FmFOqg^zix\4ܲ>f,}zr,hMj}g}li}JoC#  tHPNzF=6uTݻw6RZZ BLAe#2ci \-ov5UFm6l2ҥEx`)y{.3g~kw/-_N祥e|^ZKK{|9w/yir^>/-- NAG; !-O a}Hy@@6t pݺukl={ڸq²ji itrÆ rZYm ٳC@N/xL%KdYvi0RKV.yiEe|^ZV@<ֵ=b4,̼.yiB>  t ) 6҅ ȑ#M/l|4-r1Om >c\pAEdֵkW[fMW/XzuHҲ f|^ZAQ"/E;浑FV\ڥ^>/mC(@@@ aA9= oa('fjimU,yzɃ1"䭖/K{*zym&aO}瞡̝weҲ f|^ZAQ"/=GF^ZBr_|^Z_|^ZBr_|^Z_|^ZBr_|^Z_|^ZBr_|^Z"  $Kc9X0    @kf6    >`@@@@v (.l@@@@}h    "aA9IGݻ<`ofU/mڴi!|#G+_IR[Vw}wllw}fYl/VPTexE^>/-+`-yiY3^>/yezi^^{H;X^4L/-+`-yiY3^>/lK (yiE@@@?{'lh"3f}ݶ``z!{z&L;Ν[3_A."{쩧V\4y4,̼.yi]f{ov._~馛oJҼr|^e xf^Zqi,E@@@͛i1#<s^… W_5McMɓ'/[4xmժUjk@Ҳ f|^ZAQ"/P0 y/VPTexZy{mҥY[o :4^4yhҊʊ˼|^Z_4yiEee^>/-g   P,!AI&[OuA=3aTr7_Z潏mx믿>lZZ~~2}b]R,h,_!Cؖ[ni˗/'עb/^>/-WL^>/E!/^>/-WL^>/E!|A@@萠ڜ9sn7xFyi1unpĉC`|1:Iq첋͟?6h#{'㎳K/4<[ZZRD,eV}s% ڻ%3fvm~>E]Oo{E˼|^Z_4yiEee^>/-/zjF/w}?BZ~y+  lӨ=zn?M:z4RN{4_~k`fmlٲe֥K5jxaFV\ڥ^>/mC(s}ڇX=k&.r^yi^^>/2<3/Yz:ݑli}JoC#  tHPN#ϺuF YϞ=mܸqaY43bM~iӅWf~~kŋg-Y$[mv l/VPTexE^>/-+`-yiY3^>/yezi^͞=;Fz?^^WKbK+.   QCr VPQσ9r{ i]>|xi^*3#Dk]v5kք[nիWսjiKbK+.mR/e}g} .T^>/2<3/4"  HÂrfͲ{,U,իyAe#|1ӧO=6dymY/-[`-yiY3^>/lK (yiEe|^ZV@K+(E^^Wx|mUb- BZ2y{4,̼X    cn    @{ koq    u.    @{ koq    9]`+^2a@ZCkICۡ,f w 2"  Pb -{va֥KKߢBMG}u]Wǩv̋X3Z} -YM[º>6Ez??6l۶l{6urr߽|wo{i^>/-_N祥eTmm׵j%VTene׭wZTfy5+S+{7^OZ{eV;xF,\4=3߱˟_W)>ת{}|  YÂr~-\l 6/-nF;餓Zǩx?^_eKW[9GIb5~7 yq5h[v{ɶו} \/]xeziٌ}oF=ק2CgڌV5ZXfѴѾTTV\hP+3nfxzeNnx~B8]^^WQkjheJ5lq{]5N92ckh   e萠̙3CS϶~i]xv뭷fAzk,x)A^h?v!5w*;!_?V} ={+KV^2ތzOeFcbLZFđr3Y4՗4ɋlUc~m*SA^F_ʮzu.{9Kl+l%5TZWnzּA ܜwV^6qWM|گ<-Tzcwlm4Z˴\O# 453f_ʴԵ>A>@@,!A9N>Sɮ {Kz-s9Ə"(W+_KK/,n7"(+Ί5ϵ8/Z9vuZۋi饥e[O뾷f{i]|^ZZF~׈xZ֥6%2fPRkZhYQ k\~v|/S cB[EIld߽2y؏mvk-2+j|Y{ej}ޱѴX2Ou?6dۦw2-ѷ˼ߵh   e萠wi^zɮZѣGx\4z|:R+3 K/"8OmM~q〰?=2Ҟ>`6u+Kk}w{z|9w/Q4 XYϓ#̢֥ү3+ "i4iվz ʝs9_TzN۬W֬{e $ӳ[v+K&Z^o"j[fX[Muc-YUxv#-4H՗'-ٷQ}|  YCrv 'ܹsMφ=z]veHjiSL?`0. yeqwŅ.bu+v')H=o3=[ww [rʬw{֓-^>/ES4BKAAF,NѲ|-֋+PT2>8+WAK{˾W++./*'7`y\lt߽2U؂3ˆZIL/~ky[OEhZcG,Nџz$2t}߿Xֳ>A>@@,!Aٳgl٩j/rXV-M=#c =Pӿ[nQ4/^\hZt[5"&-gQ4,Q~~I^k{iy/yirSoTJVyVy{m5ZfZ7KJ2WԪ^9iZLi4*r}˫Mejn =εiT4 !  Pf )85t-z{g㎳ ґrL^EzH֎vH׭5޺x׳􂈽yT_=\L/2ͨTfjF뙖Ym/96(]rzꡲtK+<_#e( z?{W^Fqo=ʃ~s\VOu[mYX[MV2-hLz:wv5umO@@2 tXPnҤIֵk0Mϐ:th6jKKj\2ӨO#(?:lv-}z7[GiJxȷʜ8YFo{.nm2ͨVfywV_8=X^#4ڳm|{ŚUf@όdUfQsL#I h5<Z}L/wX]{m[MWO/+ZZ֍ cki^˒vc${D#e5ZZ.˾ !  Pf ʥ5[KŠ>hچoo2%ڟ^?Z/4}hO@@2 sZ lژ6Ρ ]K=zNUbPp #  % (4^k/*X:F:Gp 'w6ܾp #  % (4 mKCkIYs۬#}dD@@ jt qD@@ tXPn„ vaf]tAeo_Ҕ#<2;cWe^Ooԛ~Ϧ.\?w{- ??>[!k/2_zՓ4@3ck}D@@ tXPϷ.\h}>͛l^gaCqi˗{M AS޲O61KWOe.kOo\Y څWo=,[]r^㐆N1N#  @):$(7sL;C dg}ϼ4Ǝ[/ ĥ^+mt7zb;M/N xA5v]CܠW7mr^㐆N k[;tP0  BCr|-kW\q=)vG%\bw~ /rjKqJ=$ܖ^=ǮfFiٔ"-ͫz˼dr^㐆N <{b@@@N;4>}pK/ٵ^k=zϖ`_n>-Xoz?f4{meTGFgNoZ,{e˫],SZ4 lޒUvzܢh>#  /!A9j;lܹv7ѣ.FUKKiq.'x"kMk"jJ3ww +)wN6*;ABRz$( i TG((3Z   tHPnv18vꩧ/UK3gNx)Ḍ^Wf~~k}o}4|, 3twKSޘzſg<+3]2!@gHV3  S` /nݺPu;7r} W'N^IWgŝhx}u}Ѳ ACodoXmz kjey-CtFٚh>#  /aAI&Y׮]3 Cf漴gavitl^N~5.+6d۶r6q2;ឡYQےUڜwVOҼ jej;+քڞFfy$ qK8$  NÂrXG}Z̻k4!Anga@@@|6kjt FD@@ s fsI )=v v@@J'@PiF/ϩ&I @h)pI@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A IDAT5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(A5F@@@(A G@@@(@ӂr*}>@}>@Fm~}>@}>@}z (G *}>@}>@@;r Nzl}>@}>@,}A9"}>@}>@֋iӬGmOZFH k>e;vXv`~gW=1-Z}̳ϰ~imXh?}>@}>@@^zi;c㎵^W 3O;wxH:}_k'?6ڎyd8;oxIg1c߰Gy|zyFkú}>@/}g˗sΣ!֗GX~CG@KP@n}q~ն# /pWou*xlwڴiۛԭL4)c֬YZڟ֖3{l3f4eñxԨQ~6|p}_Fu<ַ."?U[GO}SMl?Nt[FȽ?ԫ.i7\g3n9L}eI&'{º:rD3aVZnV-o.#]ٟor FaZ} MѿnPZ2nܸPο[ d?[~QYo} _0)gرӟ4ЪM×4hO:s'ۂ Z_nXgm{'}>;n&LPxTUWEl:?E.lt>Y /`l6 tkt]կ~6xc7isOZ]-G¶]Zۉyx {qykuޚW\aKpz뭭k׮٣sϵ:|sM7Ԏ>h7o^]3뮡SOe}ݶN;ٷmsU'x/i,ݖ7?eʔ/~c3 j}Py޶ڠwp@hg]ݽzk;Q^k7ȶמcLX|l_~8ҖǃXghǏZ^=A9G:rS;Mbzٴ믱7^)7??nz'Oqkr ʭ )8c]uܨN:~g߫{bޢikO8;IKk[qYt"C\ϛ*X*͜9ӆN²=iTڻ]<Ҹ}[tQu 7@Νt8 ϝg_U;Ogut[%\.l=\O ʽ9}ESN9%86tMpdžtmCO~ס墳.(Nfmlkjlˋj(ZޚlFoEbl\V:չ΁:)`7C eA9[;v[Sdtޤ:lv;lCź䧧vZ8'/Uސ!Ciz嗇K;찃vmu|^T҈֐ev>}zE;sL0_A^^A?]GA#UNk߽cT7ݭjY,#N9'.Ӿ}V\ȫz+@tڧOp=&ϸ\-HZG׀OZO#+5TVk>^6kʌmmjs,h^z>~1/}˲^;k_ϽUu<(et]YE3fiDm},Vo[⫏WF8똫ggπ~1x5W'+Pf^{jzajhbIJ:{ӈfGOfL;l޲:gp!0w0Bo|Mә6Sldžփ0i$[=zM0&Mz{FӵGPNQ]# N:!}u_~9@ҋ*? >|Vw&J8"Wԅ.G:njrA9-ӹӯPys~~W:ռFbKEҹtWsZsD݆s|N8]0V5QumqeSj]-}O: )pk_ڦAy֋#ҋp|!k Њk#F@>s8ώϔfqꕦo1Iiq=`Q\7X18BSO=5׾Fr>rR}sn;0]/&5;$QVE*jSg3ׯUv lvuk^;~|% lܩίzXWvͥcKN׵N8E2׆miOXe_}M#tV,B1er՞)WPO:n뵧:ükX^ToPNuQnWQqiFqz]p^X>ᜳmYٸN!UPn$[-[m[b 2Ю*H=ﳕ+ʕeFd ]?aQ4Ȩ&~:rJ85Z?h*CS^ MUi #GJQ`'^):ݤ'PHk+995j# 6iUJ#xtަ4M#A 1uPPDeN `ꮌ,?m:-rҺ.h|x>tUo1ݞ?&?cZDA4un˽_@vbPYVH3Ո7=_LmKXhW;eqY m-W99P(mf͞{byj5\S-iRz=4?ݥ@WweT-5F+j1(s,#1F9ˋ:nh:V]wκXl]+emq;8_n kzr *x뮩uOU|P.i}ʥ$;,(W=0N!@wޟ-arlcO>z~HݷN4yg =>0O^˴\;7h[|Ɋ.+:~x' NlbP'n?S/(]N<2KfZˏÕtZHf{[/ny=DCq_ }ռl._t/t_XZWl\vQWRy%X/8Š>@} h.t4 XT<8s73"LȘXZJE_{S,ϛ$})Q\:wY8tĊҩF薻4蠠nտ4g4(QR M ܩږ.*}WP3Ҡ#x hwk]hD`~Tk <\U^[Z#Ҷӝ7r(B]gj獔KW i.YjTAX5A]|*YϦ#3z?i6R.[[s[U|kta .P^ .:Nc :.ĻbզaS>(WR EzynM3 S}rS@n[U/zPPEslܸQ6ixf]dzƿFX6y[儭_I k~Q$>L%4;? _>7f+o[~+/ ,\Ӻ ~)ĿhF|y:ȉӉ_)9V M'ֶĶ b\ƴ\}>>PJE.]ϩHIIWM89l9g,wĉLd!.:LD1(#:u,*(`En9bL;_d/'=^%vrFX]3t=[EF)uF |5$}aA9(oնtѱ&W:]vH2]2=ofs7-:[ӨQ,vZ;-s|PIMFlֱ$G[_Y?wXPniv~oS0 6l G[*O=;[ gڴo9l6cD:u͘>!,;w-^]r FN B{衇¾_a !H;utNr ~uupk ׃]_lb[BCN>*PVm1ZG@_W Uz$I@T':?uR_'Oꢶ/ ]oMdVǓZ",փi/80Ƙ>@6>^ f_.?)Тyk%bG磚Ώtޕ Yj9u~gzi$רO<1G}yXE(si=uUnGUMRN(:?u׋ބ@:o/LQsТzZ&[Eֶҋ4(g)(PkvERMuWA9wب~:媝z t(P5lذ`k rL:W`+V^R;(H!h:+Wz\ScLruPwuW7j'?kb]Z3A9](ȭ~nJ߾ghq_2' ^;w-6~QAiPV;똡khQ{Kֵ=\ںXz?ԧՇ>t;RN?+M}PriڳcןAZǧ]fK/rZc_lfMӑ|崣/=]#8¿ 1izcB:W IDATuQPn6t6jP>}1؆͆ /ma4ۚt 5jh~)y:Pk$~Dz;@'VޤA9QgKL뵮A9'-VQ}ruBv|K!K@tS/ U"ɘ [,CSj%JmT>.oM%rz;qbʅ"}>@ҋ֔U-(1]|:_oe]z{uuHnÌ#zsN=CxNQ*ǵޚZ:my*WR:7N9F\bUE_T[ >;U=byO`4(7tuo *)@]Sz_JLߢ\.X q:6k͂'/ϯͳ{ ޭcm|~\_m./hrwA|yuȁTf9)GK|h}Xq8ܧAoBJHw'+(dmms}>67ֶ~t,6n hr`nK3?pv/۟}AUw6ar{ wΆ\4?PEz'%6)];8p@=0wo_-s (7n廨rA?DIp8pv́:ryA9A9]8p8p:Pg@.3/(!]*+ 8p8pzPy=8p8pq                                           =gH_P׌IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/wireshark_3.png0000644000175000017500000105300614062723141021754 0ustar00jonasjonasPNG  IHDRY8X IDATx콉SSI?>­[u}rFYY^u%@aQpWQmuwT-aM:~_u9CXDǙwt:u~$y|i' OGlf8D枌=#Ȝ'2t3P*Q_w<(SR-Qaգ6w)0+G̒إqQeq` *Ka=~ ڈb^TdAM|., [3 3AbT8"1`6HO LW.s3c-38i0{#<+q~/~Ҹx?l[?'ܓ=B?RWѿM_C9wDGѿtcOqt+"#~=5-S% uEZ2@_/ B2j;e8 .twMFo0}[`B͜H A tx!zшl]r464zS n#bJf0 aoy&k# G>$^>RxDYFYF/^1VXx!G 6m@cF oI%2$#O1G31p\0zxzƋOᖾK;Sdd,r,r,r Xȅ_ >po)Tì5,ݱY(̲I4Pɘ oo$G{X[PfqGlǤ >@L 0^ 'O-Um4¾YCd틏k`kkpyL}<5Gy}>n,p~̷f1o?;m9̧%;j&5 x,b o\%\X8xKđy81y[dž8ܽmCoAT;5TSL228]Cus6(Lߤ#?.DC{/wZ#G,㿞S?0}@e #=f|~v-&P{\KKs-\^RK(`HX8A-#&MgAQ{#_ cMMlt3}&X-@YBb42`-*'z,=̝\N A @ |npG;G8~fa^Q (K?, AԐ *0!h=Z!1qGSp56Cg jbhJ̖8 J}`` xྠ,Y~%weNl-ƆA @  ;6ef;߳fb o%0p^ bc$z"ư(k&wD\@xPeCNolF}kꜧ8 Zx8gϱw JēA @ ̈9;]b%;@/?Ғ?zq- QCp 81@")$8X+$ جg"7Gs'G$A @ |Blmll9ں~{zÇF!~e@
  4G7hA1SUycJjOc6O%*4d0 rP=l#1e LPZ@@@`9PhO&A @ ދM6ލ՛tv;1[J,-An1"bP k-}&~DӉ8dtTψbirO#7] F$GfCu HWAr(m(0W"s`sE.DHA @ ٮPmIpiKGkKy4aumoBE@YH$/Y`5)b4ǒt+ }Q ']Rw0s 7gF GEGH!Gl)4s/$z A @ m%NP 1&Es_y̽D d BQC#rSv5:Exh]zlOKZ3P Lb C=@y S`63R5q(`3*O+?7'HZ3Uu亇FD:|9Wv<a\lT= w S񊤕 A/emW`Bu w_ R+a4Eʙ}A= QLvÊ 4]P>B< }@L0 j(*FqJ8ڱUЌ_h4# }ٲbKm%d<>'gY^n' w8~P:<Аyө|\A C@hS1BU W4]"r!C ul.+AUP Lҏ b hv*!*_H;gBo<Z,C9䞞!@%LBAzQZ BWAfԦՔ_ u6=dα;aO{ОՆї98{=co }%uO=mTwԜ߿ƙ6 ?VYfz\]?Al_y=s/\㣼vXq%78,`~ 26s1C|$>$`hC=³cV@0ti " o`s!L7>͌x?D[1Ír) Hf0J269#< z p7f#A @xl^↬GN=i)Ws|vnNl[=p2[GWW ގE遁3-Y Ս:8:m;[;GG+w6ֳ՞۳ݲt~j\1Pi']Vckssqd G' o;;+ב͢@խc1`8'cby.\'GGg ͅD]#tsuuws}N]jy\zZP;ٳb?ёKhk)$ݼ96xO=S)ێ=ct{J}fpO"stZrxpH5>}IGAH/A4qzmkS~_Vk|:TZwNmB관h.zSd wY0d(R 6b󂽆톘+ :CD?9fKUD,-cI@ @\BO$`N @/ciX c ƮX+pQ_  Iw :|koV٨z|l`jV3 j`{ɬվ.,;ߝg[:^h,gHU ><ngccDzK_9h'Xv3_*4flbPmx=QV%]S;.8)ϰm(Qn77a͞ #va֛qbb ^iUQJy 7;[we̓z~[eMZT'6?e^?S{~ߺW嚰5gTc򦫇փkhN[sv^Ui+98,GU#jn|`.`v¤sOG5qbtL?`T;H>xbqҎm+=*ݷ&)^ػaZ=VUc}a{?:hCٓGŽ*jh>p}P5iWڳlNag_ˆ=UT?Jcϲe\VoߵBqݒwotR6?g>?o #o '6B7GM4i8s<` ~)xQ(ͿaQmO3) Pi)f ﴷ ԸOd+u} *@2pPĂf8mڒ92 jg|%X.X >sO:Fز=/w:ȟEKvwLL*e_vxjT [Ckœ^U=eC9Ś׿M?xZ8`Mɉckld/*GƵ]OܝX2 `Gu ]~'{7%^{rP Y SlzEwL=9^Wo\5:+ٳWcgmroG;69S7uepgݓGu*ٵ34ik(ct0{tuG@Ll\DҞꮑ9l{{/ݻs;ዖ6;1)@1ڠ{yYèVpX/Ipy7g=o_xKuNL*_r#O<.jPwg%8+oL4?((mR~;W?iOLNYfٺH/ue +jm׳cAn +٣ ]/8˖{dT~:#H& ! @ք% Yw?m~V<p\ XC]D>Sɨ]kGq;p{Va<d1h F~OD֍l'ǀ8%-$SPoá)@ \hd@0}X@ȝ=#zJڟ#x/^>yZaUC|G@VJ/o+>.B`sp8Lm% C9Q^!rp;G'pv G K~.7: =Q~@WѫUyۛWMXnUW 'o򧇖ήBZ2zz~nvTzn]v`6]݃S l{'EŬXZHG;vК_'-神~@vrut͸ڠ~S=%m|`sSwٟwܳ.7@1S4e6:g4+U\8½WjMfxGnQdͅyB-?пWq8Bmvp׃}6 {>Y{O8l+4_4veAߚ њ@Wq>hi3fr vf^dtGZ>:ꁮ'^Wn\l" gI)Pz yZY-`! %>B-jl?#k(/Oo 22x:ECa-gdmtYE=F@uD[%Xăz헇N\spyEFym{'/zr ]ly~#se Q M=9]L/VvHoaʑ&f옋],'kz V4yyE׭{YaknuHfk3XN! [.(JܘvOG߽!7/ȃAdUl;_^ۗڋW[~l'-M=F=aTzR[:E}mO eCδuqNR"׵?Jw>('͜OlD9܃35^( ȯU-=C}ϯ'?@;x4?<;ځ#oԎqbDnNLobqD'Կ9:\/Zڋ!> ˑ'i\^zXa?/'A_իںe''lxg7 5O/^5QLzܠ!y"ODEXI-RK Q= ^((?` h7D”:`,>j7H/\7 Pp`'b(j(%1? ?tg~0 xH=-`7DL$b @^6t\vlGOhTR*m{}zR]ݟ 5߈ ڡ[|54j|}Gٯ+\݂ܮԷ2oԶ#W%n.^lŸ'YO_x6jd;W;#W ZIk pBNU9-#7+= =wP3Ys~F~ IDATpt?qZ7~[;dőI}KѹÎ9\{&T7ш~D uSA[5ivG8 g_Ȗ/ޠ1t\[%t g{*m]|hJrB]/Ya=Ώݨ{Ire@;^ܠk-w S{^[vz/6yӨR+v^tGmV_4{ ݼqI ԗZ|_C9ހ Įm1ڢÿ 9忐]hb%2>؟N!@QC`~o#2 qR~1UESÔNg@JH@*LZXrŧs@J0*% Ɂ|4 C@3l|G ZEKmYuS{Ef?//yt~aUD-Q(qHl|56@7pr=S?hmTǂy0 rzΑs䄺CUz`_ *nfSitl|R%k*,x:7|b/t_<}qCнwV5v z=QwqB3{s1y{yiyLW/kP{Iye/6;nyj녵:޻n6yzeIIYl|$ƖkͨA3RSZܫ|=}w{Z\:n7mU%Ug׃#yg/bلVPS5w=I`^k+F&4?yWo p}91QQ{;㿬aD6vo j_F ]6- < N`X n1)4 6x!0 ~%?<| Fd`DQ Hե=.#]JnZ!Z1V1)j`sFUF;!?_5ތO @؎!*ĄN=rp oEN}N?U =* 8Z-W' ѺgW%,[Xlēw*UZR^zh zecƏ=uvDS 8ul{gKa[/x{W_idžKn_!pϽlP1LL >) |Ý֑10oytߕòe?`ok䕴äa̮\{;{*_)0% '=#qeOsᮕ"G'cS:<}p`Ԍ4_-p`,۱BW|P93{|7'ٞe+H:܀P͓k97^ȕ\;h{Cʊoɶ?{ z6Pt6nUA4&>#\lA] ?ٻ]%|b0A aز¶)>qag'q?+"%AX}u,^ܚ\9=L=Af#  ^(? `D>~Xȇ ǧ=;П Q8~SEH{ph t@Šlp2ر x!41<W@[7ڍ~qAqP>1pyE:ø@њ=ZI1uACm\ /($XȘcq>}** m?  OQƴAAG`\ q1A @ @ O 5b x>1z/&Ә|N/yDQv/dvS/S `E?/Fr>ZQCe~JCo$O.s @4$ cA f24= A @ [x!O(2h=ee!jSm/FGJi5cLA<,CarDuaO@ڀ1@4x="=F~'%A @+#O_@}DF..F H 96Ts _Hja(&∎c,b= G͘X'0Y;&Ej?S! 5BOs}ԗ@g@/Bv~\#9A @ _t#^WdfTZŸ?IOtcQ{q5ObXf2 32~ Xپ>TA6رӧz~k8y۷n:ٳ܁?zeee@>A @I)= vcmҌIMN©LAkf3M3:25|slll,WEEEccٳg Å RSS?c166D۶?Y^^δvݤϫTƀf>K7n<}Buuիnݺj(O< quuȘ?{gƈm۶ γ jVZZYA>qgg֭[9H$*++ۻw+Wf644իWqe||\(666 k׮uuu1|tYӥ *LQ̙S<{N3 G ח\xzw֭ǎceFcΝ;GGG3իW{zzxzGzFsԩm۶)-_||H A @;#_ ?7}0 2@b)BMH~ٹ :, hzU= 6ڠ@igmZM<APXf-7 ///11Z_Fף߯ E\\\FF2>00b= S8uAs6 Ǐ|,|DR//UVݸqc͚5r#L.nwX__sjݻ7l0w3t.̧IwZXXPUf9z@nڴ)<<ի7n|0ZtiJJ n̼3 E||޽{gu:?l2{{+W>>200UXX8O|…˗44T"xxx[&==vٲe۶mSX޾}[>w˗/v^?|˿BBb xzz?|-[d2^_PPߺ>\c=wܶm6;wa6=0::}vCjj Sp877={|H˹'LXy(^~0R4"L*ʸf6MBfq LB?GK}mQ766k׆򚚚/;3gdff^1Μ󟍍W>u^iooءCBBB OXrݻjçZ]j_~ՙ_j/RJ%?PQQؘΝ; L&koo?qʕ+?"VcǎIR\N~zXXXaaL&ȴ4.#J/\V"""n޼QVVv%Z388񊋋j@ _v-չzjdddww7yBQQQxxxAAkbcc?~,H:;;sss AAA&;;{Ŋ\.XO*ٹzΝ[nVVV/^x6=p)[[FTo߾PxQBqi6-ɰHKKKII|6;;?o߾ЎݻwHV._ A @!@"|BB&(d(gOрOGZۃS1`bٔCKSZҕٔt0`H*} +V̨###+##cʕ\W(%'$$۷/((- _rE*N=`0?~ya@zgffx[nz.{ҥ,TuytttǏ-[q8ϟ99}ӧO @ _kkkU*sJsMD"A 9Zĉ111޽{X9s&&&رc>:Avv?C㏉`ӦMw޽wގ;$q`ٵkWFFܗF/4|bbtGA΍ j&'p=53ɽ@ogQ.XЦ<]kjjg!!!m۶g3 l߾}ZmvvvDD\޸q#66jkk?4`0D{&''9rex<^dd$͛KKKz}JJJLLLrrٳgBBBv5OЪUlقBhmmuqqy޽{׭[722b0>|hee5o`ooŋM1*++׭[e˖R{Ttyyy!!!jzϞ=8:e IDAT ..… A||<BKVZ auֻw;۴GFF9Α#G~\zeee޽[.oڴ)%%e+Wtwwz`08p ""mcU[[+=j08p322mvСyNJJ޴i8x`bb͛===\200@&''̨ cccOOO?FGzoMMMHOOߴiCCCiii\Fڵk˖-8>%%֭[ϟ@7 Ç999H? ݻw 򄄄'NH҂y~N N۸qcZZ gff"JN2 b9s ׯ_www_v-:cǎsέ_>44ԩS:nϞ= +V(,,d~.H/:ٳΝBE͛X^reƍ6l E!vܙ844#MKK;qD]]'p8nBz ;;{Is#@*x{"O=jĀ202@`lzrT/3);sD֙eF{ F}`:-v( yyyxAOZkڵKMzG/_>00P(Mۗ,YA;o˖-FF\+WZmiiϕ+W H$sѶ6R?T*$p\._t˗GFFd2Y7|i4K hjj zJR*MMMZ/kZ\֦h=<<;R끊 //Ǐ#w{{z@.gff:;;|ĉ۷}vS>}`0(ʭ[tN®]6_2s``@Tnڴ)66VTVWW꺺:Tz { &z`Ϟ=|>+%%%--&$$ի?onnzY.%˗3Ti͛7[Wd@llH]]]LL]{q 煀Ro߅|ġKp0Ԑ!bt3GGEjIS|dL0C+qoiT}}}֭J ׯ_'-S(bxɒ%|URR2::uV(@Dryz!PϭQV%#=h "##p{[[ŋnݪ鲲'cǎEDD茌N܏UUU4h0233CK*nܸtŊk׮MKKEz`߾}<oժUb8""Uxyyܾ}ɓ'111k׮ݲet=PYY _ׯ2O?-૲OIII+VLII6m;se˖'&&޾}o߾}R4222>>l  Bti>W[[B֯_xNgHڊ@[[ۥ%4ƍ/]z˗E"T*MLLqƽ{з&))J4J111111vEz`0geep FcT__BQTÇK$T*Ϟ=jg:… ?Ñ#G>*X,D"A7o믳`ܰa+VߨZ|2B5***--˗NBGFF"k͚5&ύ쌉ך5kO>-J#""BBB=j8}ttttTTT*ݱc<}Y#kWyXrelllXX4哳A3Ez|b/f`7 b)5^VPuFRP'03@w!F!D!N5`̖XLQP' ͕D|LֆNUTT-Z0^hER eN^\TT0TU(X477ju[[[IIIqqqmm-"FS^^Ј/^TUUr#P___EEEqqqss3PHOOOiiiqqq |Ё򾾾y≖+++ Qް`,///))ojjB32ͳWccV===%%%555ȡ֤TryeeeQQQYY2"=T*2lmmjt~4JEE\.h4vJNN.))@1c(nll-]TWWrttt ՅoÇ?iJ}DGFF]fCCCj;;;ʊGKVuoR*---**B_aV>0%%%mmm?btvvbX Emm-t``UVid]]]xG#^ݍBeR୭SpKKK7rH @ @H?oAH ! ƴJō1g43GZK#x6*( YI;@r0Rx Ip KCa^Ν[0i?CSmwEEEv0} TWWӧO8(΋-_Xrt6mn g\`A__G;66~蒛k2O91cרT*?,XE/]dr LsIΚmhOb|#Ӈ^`r/#6'e@!=j!#  1iĿio\gn18n$)K@ecU=v)v~   $EZ|77D+E2E}G'OXk4ݻwo޼S]xxx8y᧲I~PC,/??7* EX[G, ̕u8Z;Zh4xw8䙾@_@as(c(L;x84%=/N. @ A`Fz/@3`z ( Ebpkİ~?h1utEv;\hZgt ; hp J Oz`}ZK A @_y/y!2Pl!ztDæA?aN~ ͹w1J~QfA=ehK!Ox١w>.tw9E 4T10ڤ]ؿ{=%f!/A @ )? 04OR23ٹ:{WSycDL^ק;.ã`Zo:@o(_6B<w7 ^hK/֓ "A @ R߅~ QQ|b<sk%EQ3=;%:иfQ0УcQa2OcK +`Q6/y=_njj*--e<#5552l6PZmCCCqq@|y]]lczA wF`>z y{Ն0sg: Y|q_r@ݻw/_D/%`DCVVV{I &|iϓ6|=ݻo߾y7z}_oy۷޽ #rw޽Coߛ7oc^(D17o`޼yݻwEEE2$4].777OBVTTdO5VcccooH A @#I1g1G8=/6lʻw&''U*UwwwuuuAAÇ]VQQ1999IǾf97o ږ9&4;^/ˑ???͛?hmm}_P*ۙPCyv=q0/:+-/5 ǏvɰP^Vd@i28s냸{n^Δ ۑs$;H&c{r?zx|\ `hmmߦkP^rrLЗR?hA9}B-t߽{/AY__RZqGwww___kkkUUd24Vuu TRZ,ֶN̈۷o{184ׇߢ\.&]=3# Ec 7ʰ@8oވ?۷o-yIqqqEU]}cpyyy{{;Z/f}}}MMMꊊf48~MVd~F3DŽׯ_ >x𠢢}xxxllL&555WWW+~ FFwtz흮UwoF_շRe^/]gg[ޯ:rEeSΔpxTFn92M(gN>gA >KTIvkFBQrs!%d2l6.F0O Zb6N? j2A@o!{h,3. k֬ ;uTMMl7Qd2֞ |-` "--m׮]aaaׯ?rHQQQ KZ[RR%@'7 -f^!!,1 gZ3fN777:TIAǚbr HjNBnώ\.D"JZp8D"FX,V`XD"jL&߻wĉ۶m۳g>} p ɆN\odz w*d]A ixuVE`-~[jPW}W}#:c.A\`[o,nv)e"1/??Hl٦(TyxN(Xze|`} &GL\D5fBEEIw7[w\ԊOֈFˑguQX~H,ub*t:r˗/F|m<cǎq8M6=}?R8f18"I,a2JKMM$F=U6n$4uhO /訫3&)--maaa֭۳gڵk#G`0Z-\ k<ߴiөS}֭[;::]f7LBV"KeZJ@tyZF`Xt:VKKv>Kξp)4CRh)))|缤@PL7 3yCnkv?'@oo^Pը* g'X>(h,@jݱcǖ-[Djr'2Í7w\(*66ڵkX,H$dP3DBPڿF Dk MٰjGÊ' z_?8NHdXt:.))@qqq]]0NZ'T*kkke2͛7VXqDrK?ˡ[n`0.fTȴF9Ugphqh5u MCDHMT)dM> JeFNh{/ãccc#/F']}/Ga VUʨ,mAR^BBG_D_SF^]^5Cѻ< )>p8wؑb樨:8F\m [ť9J.M~?k$sêDUp1B\VZZܭk)j IqT;ظ;ظ Ľ$A@M~aJë`0TTT(W}r17mBRa/S&3~>R,(hKw}˗C##/222:62:*ugd VUDё.AWHxĒ+S>t:#/GG^ ͦ|ͷh}>sZ)Ic+dS#OpnVk0 @wRܻwرc 'y梢">SŒg.[g^xXN)*JM}Ydf׻BLޖ,#mO.yߚE9&Jee%i2)$bbH2nsѤOOMvMH=Pk`0pؖftI.0W,)8 w( gVav"8_(>p:tdӧO766d_577S({ګnٲ%##H$R(涶6;h42|ƍM6M7K}<@"Tjkk+NPT,{$~ OL]#5dߣr뾝Zjϓow.ݙZC|H 2U|O<+~:Vd>E s)wn>0l_5455dgU ؆_f70DP(~1|@"x[b===QH~l0r^ߺukhhN^/..}vmm-uoժUK, MKKs\?|[ѳ~Sile m*y@ˁ^\0J"j1 ǨѨFؘJ!3+B/]~#1Q\6qզdr@40>popPqmk>ieZ Y* zV ٭=&)))l6{2eeeHW*]Zmwj߮]_B{!v7s"V@,Nl 2oGImj?Siy9MM$&A>Uߨ/0$*<=I- ; #q8,OAN'ǺcHlfEE^N`>IN5U}YYɣG)Y&JcH$~@FY痔V{kkk{{EJKKL&x#[}}#GfeFәL&˝[[[)JUUէ~:-5z;/~9@˛;w`i…4H$޹sg^<\.SԱtWڲ]i^;iv˕jgP AA29?,z"}XDzO  iM[Nj7f|y6~Wx+|5]Dh4o꓎ KC4`FWuPB52*Յ(J{{{WW˭)J2 _?jA^'H=v“t:+**/\P( @9y nhk m3Wi|BBpFGx%L&NoBujp8, \.N?JFċ@cB#vح[སgff&$$דUV[ŋ ~>G+++uuux<@ Ɔf2e}aث= ww9׍v}rOzL|`1}D ~Щ@T<1_=p;" `T Zߛ_]L^oCLu^`r驯h4T͒7M8 ~Wz_ckyFPrks~p>`V5 c Z 'E"s=<22rZ655/..H$ܵk',grw=K횩'q[=~> "455hFJ7+ⶶ岍*,?:*7^0mTjSAaADTV,_oޖ WH:kxx`|!Xή3OMFY]6hLz[M(xb1GGG'hkk۱cGrr2b,ʕ+ KTVUUe8Б\\sbT"FpbET845tdQNCvvf'8g@ͨ\s6HQܾb[[[ 1 >5 bQYi _ĪH|eYBKl6xG?yH ^ ]^ Q(dm^|IPWㅠ(Nb6d|࣏>c݇CY,bHIIfH !!FH$?b: ~>%bʕ%%%-?%Kh4pʕQUe(TbQ%n<`^waanfN׊5Zp#!p> F_= jxӐdvXcc#xN_dIYYzmۆB mdDݻӉD"`0Lf{{;xD5L>ydBBO!.\{nhC[[[{{;'LR1֭[=zGW鴘Q F?huJ ?`fziC~ A;dy{Cnr@FZV|1wE h4@BʀXdC Hh4D"p2Luww0%Hvk׮ݷolyxF^t-[ۥRiEEO '_ܮ_-WެTT>3 gh16rnZ#z=ǨuY$.tX^*ޱ~I# xN.^%K]"#^XrhhoGvJ; 7WfV(MZg[O>s_7^|/>r4ͣGlrO޻wo׮]O8h[jN,Vʥ,BjP* JҲbtBTN. P:CgIIB* 8_tҡ6b1ۡVO w\Dl4ҁa>%n OΨǙL(.\B!p;9UQ^/V3n5¢6wL uwwuuf'"T*T|Ȯjl6P(+W˃6^rł{s:gX i $R.^xњVw)gee-[ B͔0o&33H$655h4PsKK MҥKW; }w4ɢR^ʫT!=HAAǏ@؟.}o8b^ybK?~`{xrHx O]C$A%p7sNܻ:X,Hf>LOc$Ѐ5 8ҏKh >`ǘO߇t:Zmwwlnjj*))!H/_6L/_ /_2Lwvv)Dh-**b2D"qڵ%H$===^r'b1jhh퓇WD"FT~TJ"B|G^:RA=jIwTb25Tccr ;; g/9:s9͒z$%"*+n&ުDPz]/lb^'pY?` ?Z#(ubkѶ*ΠBM׷J3,nRT:m6``yOOuӧO8q̙;w;-}6x PⴑHlrΟ- ;Sz T^US HL~T:u01~?KVGg q({\WW7*xÑ9j&HW^駟&''W Nm𗛝E%|> |B!J-R #D Us{ Z0̏~DmXn!BxhC|}}Nkllx}nݺEv;w, Db0>|@}S?=#g*lɴ| {h64B؀c[ qQa^':جK$֣uZ؀L0:zjt[YleJEWzp`{Çqu.E BIQd: rJhc1N=w,|T*E̝;bkZm#SS-VqsyOfx|j;-o,"̟)v}Sǟ9p+%@;[*\b`x|C*+*PEP?|G]U]]UZOeWNCrj_VTFjbWT5y ڤL ilnG<+ċx!ĩbЧND"r xhnnnMMMee7l_/,Xp`łb7mڴqƤ$ `Ϝ9?׿/˯ j zCeddTWWWUU=~8!!aѢEٳ#=/oVk C m~$9heAsW2|^xPp_Ad삵 "cD_0&}xC@ći;m&'iA>c F1z94zn0B!JP?Y[Ljҏ_.xuGay=l?5l6X(R SSSC\.x}d>`6Y,VooojjjXXXPPСCFw IDATZX,4mΝ'NXn]BBBSSbAP"Rr틬FM&'4SVqMXLMh3g<i׍ɤwtf1+6oK%{FWtX*$%K/_djzE,##6z7ЃE_q.-iPƤ'm9K{/ ##NIe??O?cѢE{xG0o4i4JaADE*Tsʬs|Innvvv$*).j5UA%(TvN֤ҢcV/*j<V~2^|l=gU*He*Qr:v]V2@$J]ɭʭD&TJ?TZ--EE(U7*⨂2 au:TN`Tiv 7x}dd*J+V//ï_B-[~Ν{4Njr8E}~.^;?O??cr`?uTppg};wL&GFF?}7~]vuM Ϟ=EK4RaQ^IIW!FMR E`D͏&Ș"-Z M b6kn}'1F4$ "=hĻ(@dn{“!g C3zLܸC`0@dk Q_3X?܋m,&Inӆx xua~=7"j}(===rvJgHZ|j53L=(4lUY\=<ק6xarؘuhBic}͆k_KG]92=Ip <h4gϞ/~1gΜw޵tX_`YiNHCO=^G^.8:/a2t@<+--OftR0i۱Xoov)Hq8vr9DubW^5HB@gA Z  ?tՑã?q-Q[h#nv0>"Pj7Lc8lX(0!Ժ@'0iQ6UGǽ6Fz}KKT*KTǫ!q8(G!? ($sbS!T& L&@yyyo/pА`+ydrYY`wwT*3ɒ800 AJ|ŋoݺAAA&I.WU>:)5ɘGg* \yWyt*.*蠱X4teIF`7.ypgl((#;;X.*U&á>}yܼ>0(]Rw'8zo/pmeu6ӡs@|p466.^g?ٜ9sz-BꩪdrYvvJyfdS5uyV3V322" [h%pm6k?G@ ($> ś?'PSHr;p8l^^X,vb^|@Lg|#uĜcuJ$ ? =0iv}41`u[odΜ9oΜ9fvb?5%*x/ZZZo9s~_ܹݸqcLLLxxxhh5kbccF#okkcMr=94Gas  E".F`1)YYi(KylV텂Ff\ؘ]*@2fZcww; 9RVu=-2L.CEEA|4<ԣT ZI :I]= ZNF3b=?[,W\oo׿޼y vi4JT]I"XZ׾|-D"s8|3Rj}^H!C d8n{3v8$KZEkPBaWFt/^B$tm+32* x@0#>Z} nvC7ޥK =ȫ(ol׮]d2eee͛7?)ad7-1] r z.Ïf AAdž@'+ޅ\+/ Mqvچ=Nbϒ<LF`=q8l6jNDFrb,ɓ;w455 ^H`H$N'j^<#4L%Ė[B=##&RJ _Wvt6QwѨlVSS=6B*f#YL^͠7q;UJ>i2Ygc=F&e {ڊ.{FGLFAQȹrY'[muʹVNdܽI2s~7;a0ڪ#AA+ J鿥Ztuͦ :chD7hc- IH$QhDʳ3ss.9 rYl&40/?H:RbO`鮴5mZb6?@|phs͝;g?_|Rp}X,^t~~ˁHƧO/~O>ioO *BUn@{X~!`g@~b0 Ł'V$v ZPЎq @>! y !JF@ 89ԫg+l OJVEwpR^f)7NF '#,ZFJGL:^$aDa*v]ӱAJ% ,)ʁ.RG3TH$a2JR׃Dd2iZTJPjkkldz̨j<"xRg-訩W).ש;jCr06B&a?3T,bC~CR4|h@׫۔SɪJSK~˞>æ:TNrprhvР~lbjjɰC <~xկVZVmj}f #sy*ɅIFpDoI0 %d86_D!Glp>@ Jy6_X޷ nBχQj5`TXߡUłR(2&111!!JNT8\=UY̝;whC5arNN$-=5==ͫH D"$X.4\Xs(S)x#x siOmCmMY^~\|eeTW N&a2Sy$@)w0^AAA@ȟB@>:r"p*x!(n=5p>54jW{qx p=Y- =@4 Gʹ>oC||^OP0LH eVR$( E V+4A@@@Dvwo)^ nln%H T?3Y^>N(iY!|1A@@@@@@@G`^wX ~ި⺗!~u.^-?BW3PW4U8/ttQ+x?G@$x(H F3U?DIj\;W(+= CA^xb| *lh0^Gmz#M%na㗩@y]p ݁_/D@@@@@@@@gA`G+&/~ؚy $sk@>>kU ^C^UP c|Tl?UsTp3X8Nn=1$9B?  V y6`/_pb P9◼>8yaV7  2fAzQ`)ˆ2D<zQKʀ W(Og!d n{E[G<ԝiddo`?`-o^^^Uw1H녧/Mᡙ$hOtH$驩},@>gXqCQiW"pC|Žw3 HT_㮋]Yh{qL]<<獮|{|w_z.f&1o,lQ@u_$~?  zFtb ?75ӧ\FJY!S!     L@ψNM}\X8fiO23o&^M>J @DN"Ot&H}d<`b@ 8 [ RcTqC#z n"=hC6:̎v@r@L2a]{/O@*(M˗!ݻw!\︞OOfWr.e*lșZmqw<}.$Fl}&6q= S\Xɤΐ'2Ӄׯ,NiBC FSv𻓏$U߬3TTNcK?q`oo8. gD%_9kRFkCSO^8UφU.\3OS4Z殩WɋVo=pkw޺̖o>|oߙǞ7@{HvbC7]/[T37&󳡒וQ7K.O9t𻚩Io15\<}b##zѡP?ҚD"ann6n߽,eӚo^y4 OǀB)H$xt $U4q8D"qr? 9X/AD{(+;(&v0hX{XY!>Ow{`KQC-> pqvp<^T'^r?0Wsۮ^izy˅t||,F+?Z8z7! +6qUסx/?7S~%j&q~Kئ|uF_lm IDATTTJ &[Cʺs VQpٌKw]ܶڎtʃ&׸3"w;s% il|+j?$G{t|ʘeO`ƽ1S>P"ȉ?{O1>ѤAW콶#b[ޫۉvdmMa_=lx7Sm>_sO o@ψ$AR~~T*vggg%޹)!I r455}}}}rCvOl+V' }?4֎_Ax!7N^ Y@6S}#~q C_ 4P^e ~N*TRVt|kAk;6T}`Qtjj?\uё&{Wϔ|AN{:䪻LΈ ׭v&ю!jn]c׾;cG];OSp] g7Ese҈Š%O8qՇ+Q7.z|dDž-Ƣ$^ӛ#FSŲˮMGYS!ο;Uɉ1H IAIW][OGvfcMՔvxph޵V^vs'rh”^\RoBaxSe 2kԥ-K*9`N, ̔.d[MІ$G-Έ:rhJ/7XбܰT|2Oכ_ȪVl8C7XзJ.G^T|ȸ$G·4a4-?z}̭]7tʃ ڣ#w|pxEX}޿컱Ic_gw!._!)wm%|s?됯FW_+$U 1wUN{ZVw v9ޫ;jo|"/j}9=8;c}PwĖъ?~*$G0qkBAgDWRRrbQÖH7MMMrd$>6 ѝ#ÖRZZB W^X>7 Q(䓐>?5"Gd j~d"C4 :@`| ʋ@8hAMR=`Ļxpi}!WA @6 q9+X%|@kr>`3hWr|)F[Zl>x3GwZjV.[vqR͍kͧ^a0/'8ڏ{nYI7ϐ-!㫕%OLܓT}3lc OIq+?;Yovv㢠CxoR)-Qn<M{qo/x;&=Iv%{ LGc:wc`1^EiD+ Dw펴,NΉ^hvvf{~pF2q5='ڶO\SO?IiVx9NJ&!DDY$^<=Ͽx"-rtR6/wsꓵ#˃VjzX  q[tEhLeX12SNz.PbL^SO~NJK > zAxIdTEHdyprCT}JT;v;9^7dR\yq˳S~ni퉱UwMMC HDlj<%qbwlB-;Uc.O ڱUaA cud4=ep@9Yg[7!ȣh2[v$GOQςSӕܒ+PF%aEZAg#|~@wtEOi;BN ؋&-QzP@Yïܾ}{@|ɾGj677Lf[[[@N!/L unhvswE2C^^ǃruu~,` Bra=1?1/$|ߏOԏM,#v6_+p  $Bڃ6x@!@i/b=,o-|VrΉ/RأڥjNiTUjvis] PUK5KQς=h9MȲ m;")Ь|Ir"Qk \u=^jDx g9ږPTsERF+ɭY*[fTeij79_~'3^)i˕HϿtNWG( c`=>JL岐ۇ"s1$6brZRQςk }/NdwY IsjN a9㩊4Q20׷v}f!% ~&8]Mt^>^}IBZGbSZ\FTSЕZ,~*rY|0U-[15tѭdV-+,UIA__~:2Y})@aQ5DfH%]jJR/^x/PO].Y*f)^aW(}ן )-*wR7SFŁU4 )TҗJkr򃵿i ԝ,~4A!f/?TBQR5՜V-8DBb     MZzz pܾy dM|1~JWWTf}}NC`eeՉ7xo{mH|@,؟XUHQYHIR@BpGpOOpGa2pGqC٧ 1O@< |,҃6I@v=A3Y&>@\8_  < %ر:^:@hROOF24UK1Crd4%K)B!F9_#JfB=Y?;YN[h*GZCC~/||'^V[4HHO ~S^<^HϿozx)d$#o!x2Bbv}TǴ Pɣp0 5T8]"|g1U Ԑ<<>M]|aH;ut67౷Ȳ UcE߇nKe?CDyPzG]+ǨzZ+0JJK??3L 's^j&>F ph|h2yx!0+iYOp [*uMtP2F4Hbs Y(%)v3;}aJNo@ϟWTTrjo`G.{>䜝9_!9CJ0aeTA5h 긣8#8? PBbRTG[ E]ԃv L I7WFN>d$CiKe1OV j8-M䴥sDT@b5cEK?%X F[SrY#QkcrQLu\ǞhfTK3>[3.Hr _辗֑x⩜NiXRr02cl3?"_LO֧BꏟșY/ f%9)gTK=ϊ*wMt@eS)VKe5$#˂jJd/Ah?o-iIz`AƘf!y9k|*o8Z>ctEHrY싻"J(Q>Jֶ%qv?B[y{K5ymi p[T -?g"BI榞5R R /q,:nyP NOyY-)j˞d\Uj/gT\#JD@>ҪTm;ֺTE3?*A$%^TڮS&Jl ږPTږpYB|MxR)DT_LR{**ֱ'yCjWO_9PQ-[> eg=bD+jvF`e    #ٙG޾㝒rxo/P$(:(H$K1\YYAӛ+**={@yy^ {~!=w2e+ )#|@ G4%ISp `Aa{Y?0!d^(A&P=oXdPٝcY <HH? 붺nz N}oE`}=4HI_=92,oh}@lY(>J}IH?T&s )mK02(g‰짯GDv}laϩldO>pT7Qᮤ/el!?/? T,;Gֺ[OTS#Xct"JWݒr!SvQwKDi`~b@VS8>?g!KtRy]BR;qg*%Ǧ^F Q&_ i!~&KOݣw* ο t~ ,` Ƞh>3gbgW /c[Tb(PtT. R(dsB|X,nR}Td~\]蹚]RLA_P?US{NL[\cGT6-eC/XE9R {zdHj_ Kn,(ʄ~W.IH +?N\|rf싰Nӂ0000~?@bRLL~؀BtQׯ_UWWo%ot:6::-} IH`> /HURF6#CBCȈa=hm1(! +FYW?JCéI@]#9h4:۶~VnQpr2ZK x;f$F蓵M lB-]KhR\p*&ʖ~&zY\t4jaP~Lr=H " nҹgo[H5u@s BԍF 7*B*^a>]Z\՟bq9$jKM7vÙ؆Z= g{g{w~?[Z.J74P3V'e.A=ᬗ%ӹAy5K~ƒ1иU֞H#,Ԍ\o '#ve`,ӼǙs,Ur٣u4L<ܶcblu4 nxrMt46,MT-Om712tѽ.#U,LjҶ#+ҹS)uOvvOv6t%Xz>LmOTK ' 8/czꎉC=gblƾ#NirC.B)\>kgpK%E'.v^`qXYfCMaEVƊvp{ ;/ʆʙz@岈 X#,F5t&dXJkO\h& UR.qєo1g0-nwMCi VMÇ~w|rr2`nNKOɺ};))ccc@ D$ >RxF[*}||; 1R`?1p؜:@0 ARPP?"@ A^ qTX?.H-kIE>h% QIHa5!=6*K[[Թ;a9otV!!!!!!!! 0IkxY7<MvCB_onn L===h> .yA)( )) )aeBB[nXPs: %@l#:RQ@=YZt X>(!0kX/c}}|U30M_ڂ޻-5Fb|f!!!!!!!m8NoM ^&$y~1556<11/L_D? ڣzbUj< a E$Rk _^ZG[{ˀ< E} g`ig A 71_5pۤi211\ZZx aHO O,%sPFzFB  W xG8 S|n X@W5~Aݝ5 {5 i\Pw"SSSIIIpuuuu >w~>x𠸸xbbB`̿> dv*pQhK|`[#jcؓ] >-}O!p/m^GUxS~:榱CCCCCCCCC"0;;2KA$eȁ|~~P%V8"]^daB}͌'']7D"6:;;CCCCCCCCC@$P zbxB;vU+Pdom` QWg cv#@ > ~o6         wPV|@E~謣h?1Ov* v$!E[᠌?J@<؏l} }tty׈Hr;000000000>9$K=1̮||2k8y %@PV" '#@{w7${|G_ Y jA+v,&yBG"7CCCCCCCCCC@w'?$ $,yF!=1N?0+E Чv4eQ7Pj-~"Ju:{]}w"B@=|;^HWFCCGr$3}섏16g˗/?@oÜ\sss__[SS CCcAR%2__ zCK kqP ߈Q IDATRG*AH P{f[@\ԀQ z?ۻ # |1>c?@o}xx|^ZZzs[^^RsDl,  z? +wHN<b!eUxb@9vgCG^qǃ H!`[DD##|@Y XjGG I;P(X__f8333==Æ,X__,oýa6N̍Ϲ-// 00>|@L; ~dr)()))Bʪ?1ovلx__";tpN`]`I~ A (_%> @Cp(D ec8ֺ?9NOOh46gZoogyMzjqq"~s[ZZ|#bca`|,> O, G'RT~@Y艷zPK#xXnH!` `v< @08!lx RI=DEE?;' X'oAo-?93LMM]zujjj;FRl>PWWgnnF|`bbBMMQQQf$=BPVRS>PbȠ{3 aBH6|bdBG%?r}Pp,{xq m1ܧL~?; EHQ]Y |Sxx88!aVWW) |7ǎ~cǎL&w^K$A2;/RT_~yСׯgdd0̷2;xb{{ۛŋ'&&v5[]]Z\\\YY(**{9==-""յR[[kllp8|1+""ʊf^b`|zb/$%؏L^Js@ ơ# |^tNEʷ`@w^vT 0`!?ymzKvd{3pI]]]6 '7ljgg2 >|}}i4ZXXgggg)JSSSgg''5ބ>@ӏ?577722R]]=222==pFGG_~ &FPfffGGG)Jwww__&''\}_XzABBediiX^^hooonn~>fff;;;jkk:ѱ022299f) Jhmm%ja..--222233>\.wjjkWVVzzz;::GGG?ՏssssssJJJ###===L&kaa:u*++Bٮ& p,F_===ksxxo+++\.wtt>FFF9@wwwkk 喖s677b&&&I$Rcc#`2 a3x ^HE尊 $PW= |;ZGQ@{@BH{A`y10ցՎ m2ؾ |0@!~F0zbO8H8ŋ޽k``p͡K.H$500`aaDrss]FR ƅ ZZZ8NiiӧO梢Ν;777I  0]\\DX{{^LLŋvF9F Dz3>4MXX˗uuuxzz֭[/^SUU%--???_ZZbz{{(ގ?̌⢋/ɜOHHNHHB00>`> .)2R|`[Or/O6 }x]r 2є4C>!C8?۠?|> hp%@0@فAA >5L6|Q$HDue:;;A  V|=?얖VVV>>> ^,+//ܼbƊX@WWל111///wwwqq4tKyqqqlllttLxjj ""D"ijj999CCC---!!!yyiwwwggg٩\UUӣ^^^Y#fmĘS(rffڵk222^^^D"QIIinn.$$DOO!--mlll``رcB*J&^z%##S__bfff ' #++ ǃˋxȈ)#HOH$>e؎w_ Dp 'J<dA{w>*E 8 #/&|pp:(&&flld2ǥ A܌gjjd>$d&$$1頠 2>( ϏbFGGGFFf08a?Sggg`` D\\<##Fyzzzxx0n%%ϟ#}ݼy؈ <mmm6AH:lnn!zzz999?)))a2###űǎ H**//KɓSSӘ,uuukkk: s#H}}}`vvv111b(,ɜ̜1L&ҥKL&ԙ3g/^`1`=׿H"t:Ddd$ɜsqquVoo ^z]se&I%$$INNNUU訡avv6\ZZ:---??_]]|weggNOO]~TTT455tzWW({zz(++ʸ7oNMMӧL˗/g  O x|@7 (鉡C*{8d#Q &  pW>;\C@@,=h,7pXPR׾v`>ɝŋSNvuuLOO#|իW6|msnݺ%$$TӧOLx|zzĄ\^^raaLKKl``T___UUHLNNRo===~ZTT4++ >;;900@{zzZZZuuu8.==}ttٳ`5 pAQ??р===*o"P[[+rL/`>mF Y|8 ʽUhQUS "bMT( l;^4I@\F,FƶgN  cvv6)) 222?TYYN600gFeii |2x1Djnnf0rrr Fd:^]]ySNX` vxx`9sرcϟwssR333׮]񝝝 ƍgϞȘpww7cnn~'O3>WW .8qBFFD,(aXVITTԁʕ+nnn7::vuaaaGMLL>Q4MEE%//Q___ZZ(\ Fww7^ =wӧ8`A]ttDd0 `s XXXx@8uꔸ'OUUU+**{>?uzh>u9#3Խ{zz tp444~#bb`|z> ICPH?TGlz&>U@5CqAkI}n QTKzP aשmەj^{{pm>9a*>\`2--- &SSSh4DPSS }Ksss`_Na`||@7*!y7 ",P|bC`0aoT5LɈ PHv%)EB>pҿK hZ̧ [!gF>caa& }sRc$l?윘699G666CCCopffc{]`00 E> )q@F,_H$&VQĐXd!>5ϗH!  Θ8lW "AK!F^|(l? [ Bh*-̐S'^uuuptuu} 3}slhhac|tuu={s;;;_x6{Ц>'9"6ǂ@EEHAFPRVRUU JY }N $Xԇy%/an=N@ˁ |o 7p*!%D%{_% ]7jjjviii;W oP~!9!eP VS[.D!L`g/l 7@5@d`7aEDy;{{v켰귶Z[[@͚G1 h`kk̖ㅇ,+!!˗Jtyccٹ|ss}]qss#\.} +''ЁuVQQ IDAT־~PAAA +++<8 ;a6jkkҲU]]m:뛛QPqpppoWSSSwܱpvvY[[cٙk766===i4x𡽽g}}VGGDyfPPbFF[ZZFssD"O>]\\|ⅵ5ֶzccL&;::VVVx777dccw^zeoo{^T{{{[ZZ={1X 2<33ccc3;; f866244t}sss288xssskk>==}ssEzbɝbI444~'@BL| (9_P L% y8 P9I;4r+~޶w "a3PPئ;VVVϜ9~l?ϱ1VSS+tK\RRbmm&%!!AKK}ffO[[;--@KK`}}[mmm<喕ə/uРɉL&#_ gΜA /^LIIy===666222###riii֥6*?7drRR?7(((p` ?`2111%%% ‚uaaaZZH-g?>>s--ollTTT>z(00" G$322bccV111***멩yy}P(ɠ077w-sssUaaNWWW>M޽{999׮]KII 733HOO/..y󦕕*B )--wލ7JKK_zUZZS!!򱱱j--X 6VKHH(,,tuuݗ/ן9sb!}h --HFۃለdddɃۧP(_~e\\˗/}||222rrrTݻwAo---`C@CC(;I2e|@Pjh>􇟷) 4h-x%b#??ݕ=. 0`g b ڃ#? O@BBa@@nnnnppcѱ%H򁵵5(.p.]Ѐ)߻wzssD"WFFYYYqssp8nnn=BZ]]MII100$ɻZ***pw ܜ[X#Y111@n]z*Y^^^__G7ʟ'd2ƍ<oyy977H$.//xysss%%%SSӦ&pHHHONN~ޗ|t:}kkkllyyy)<<.--OvXWWgmmٹbhhV__ɓa ]2Lp8[[۽<¢uW'K B$\nEEťK===6<󋋋]]]}}}kkk EEEVVV_jkk;qXƪ511}fee&677O8 zzze]]]/_Q(͂99}\: -,,ܹsGUUǷm0X||| LMMkjj/`jjztt N𰰰PVVwBꪨ UPp84ÇQQQD",&x{{x<7|9IIIfffkkk%%%T*UCC-(XcD`R;( B`sbC Q?xm#`Ve?!P0- x@p}/X`GN&Nms 8^H;WA?/^heeeii… pϟ|`uuᔗeHa~~8pAnBBr]X[[ʲmOOtII‚իWϝ;'##WWW]]]|`mmWmhhhBBBvv_]\\y<)+cdd^x d2YNNB9r677TWW ɵnmmEEE...eff$eeE@wu&yY"X[[rzjOOϻßؘqQQ% &jcc|{=w/##7t墢|`aa… $suuukkkmmѣD*c2ҍ7aI >L&]Ardd7X,VhhhCCéS\nWW˗/i4;յ ƃdmbbbxxX__?++knnNGGijj 8x<^UUd[[[xx8Nx222[ll udd^>?.,,;88W>!O%B2e 2JXQH|TUXPn,H^ !ـG|1d@kv7ZKKŋAymm-55511b]ppo41%%ELL,66($$Mͦ&/\.LNN~!FFssswyKK˼< x;wtt\|Y[[] n?CdݻwJcdž=zdff,Λ7oollܿ_AAҰxwٗ,..2L_~ܹsNNN/trrZZZzynll>}bbbrfoo/Fe{zzJKKӧOggg|(Z/--mcc t&-- АT}}=T*21/ }jjjL& ex`hjj:%%EII˗'O (!|$X*b kۀ2:hYumaHzN`cxwKS]]]NDl:䔒6557iyDbJJ Z8&nmm8p`xxLu/듖vvv)))᪪SSSo򁵵< ###}gr4;;;ocA駟(Jrr @%$$dcccee%**JQQQAA!//,ab|lnns; `aarܔMMͶ6?b>MLL׽H,66oWPPhiiYYYIMM}'&yҥ kkky<^\\Dnnnr.>__xQZZ:99 G.khhd!!!FFF?x;P+[[[$ieee~~^[[2!--]YY fffbbbvvv@񚚚n޼m ~/y<^eeeRRaHNNg2EEEBBBH,.>,..z{{k *{zMM )P( d|bww7hnn>{<G~>o v#v###: M*:33Q!++ |?-^|)%%555fڼLf]]E]]RRRx[N؀7W^Er "##l6|'|`ii)33SLLlttt/hllp8iii$illLWWɉfOOO/Tӧ;;;wD?|ݹsfbbdph$^õ#z讐xQUUgϞڦGWW:ŢR EH(--P(111닋w憇^FFf||d:ujrrrss"&N$J.]rvv>~>:::VVV ]]]1>,n>t B !;]/!<˿#} +PV=`^7_:::>|uرT> |\./488B&僂ԐH$XJ+Wեikkݼy+|^tpp 355L??(@Gϝ; |'>11abbxƍQ%%%d777)''']]8022zLtann\v-444>Á%&&$\Jj y{{fff9;;kjjDbhhhbb"DJJJZ\\C뉁0`~~IDDãvssw򁖖ooo%% lll*\.7..|vvӧqqqIII 7y!HMөT{H$Rss^>@&>|Mƹܾ} @x<Dh4c@ނTCCCYYTUU%''[[[### ixxx8@(..RҮ7k=#P9({H  ^|g d 2RBRm@r#B?g;Hֵ?ܪ[Nݺ`9{]/s9 61 9' s̫i5೻Uy4#=O룣CCC9NDDkd  +++%} K###D"jkkӍ _"VԢ޽ -'&&ï^*J𐐐రW^O<&*co_|AAAȍMMM1I$۷oQXk4r?P(^zޞիqqq( BBB?EgիJ쌍 ~:2􍊾̗?޸qѣGϞ=r}} y% ֋/rWaa!(**B`|(KҔ˗/~ӧ%%%h]]]_sR)qqq%~qh H\˗/]t墢eD駟\룢""" GDaa!344_?7oޜ|Njd;O>JHH%97776<< qqq?óg߿ɬry\\?,㓒wD"IOO}6N녂@ HJJ|rdddmm? z.]b>O^FAAAϟp80ہO0Clp8DfBEЦ xQ=@S<~hFASoF79o_- 4E6A @ 86mu\_ ZlXcbcfLe≵:(%AQQBC-4$bC kdA @ z9}6xB ͗wXx%tG?l@'8G+h_A @ $Z:s"&,=0o=GoT- wz5gZh@(@4m%XU9gVlv$ֵJR&A @ Huh6!~d6zz`/g䁒c^CkpOďa5}4jP,Xo0ʶ8\o[|mpˬҰ⾁9+cB(},>yS,pBbK$bxᕚpʨ<;}CԸFUJǍlhY709'>iDTXqkF4 V5ZW:pY~ʊeB1cܮI+Uv eOdO--5 ]j{/w-t.Ҡ{"㔪a ֖AљjGّZڑT {Jen)IE 䝼V;4m*CttL?ˎgn3O+L?J5"3v6&I(Kxst pdQ y]Zo-1'<< cċ(}w66s^>ܳ'/7j^¶.O_ o6DTDTf7xF]eTQ3~Ip7巍O&a{JOjy$}|fkM.kG2 mygs;Qx_Tt˟ʟǟd B˒*'BٓA Z@XăYp{f=|~Xz,Ek3M4Cͽ C R_G085W3D6m֘${O?+K {z'bVChg\cO^uNSo IDAT7$]+&~@h* ؏ G-#vmz_$  F x j 8DV:puD[ܭ3ؚТֱGjw] /r8^=zpfKLO2kpFZck7~oVpstOJX%B;gMʨK/v消SZvc".4v40ŚS}= chYDQ>@^rK C cYͅ=E鍛{Z8\vdդOJza?< ߝRJDk%rg'UeH)L@޶Zd:2+4 fwoOUbibUk.^I (uzͨKD >tStuH~,u;h4 lynn#+f4ՏiO&,ilOWO{Ff 5?6iJG{%;[ck:' ]RfTIzG.q)Miκ]Kk,ڛxVƱ6]71E쓬tTMSA5%]%*쓭̒S`Awy'_'>>֘6-.[:P3iK?T8=i 8\ oYeSߙćg֚cGҏv2S=n[BS7FV$S7]1[ck/6GXDT…Bv$$T/8u(foJd~<|-*^x1!@ߝU/] x_ cY-'nDTuNuO,xg!"jzF7m {Tƙ:T+ ׫P? mqvކe6f8{#tԝ~>n=WOkT垫 Vꂶc7ZP3(8f k?"=qM OkpWh(N]=&^qq,1,NpjDvˡa-qeWkF>Zs~Wn5y_1u[mdzZ=0ưfѥ]鵣 uX ֌hԝAjњ ,֖rI!p/(옸 ,kpQU Zon4?ەZ_5$$:bۙK'U,ޗjW<}yzKDCHKD%@8UJN])6pIC7B@ؚm uVWB]`vݎ#U$=dM}mfF=#:љǾz#fpj/))bcXM oH$dTWY5{ RYJJ(H3okJ4bKB(^3,Bz&w!onQ=`;]wfnM[oFVŖ絈U󲞩5!.Y-L#%nN(j:ky͢Vq=xs"EU/On=fMWMb]1jH4i!\ĕd =z[B^1=`nkiKO)g!J Qi ^kYBLՆfxRt$J`oi|S'BPcˠl7ne^nz3ݬLb^Nd~A8YYR9H[ҡM|k+)"rM|f}uz}9"d\ P%\R ZŮU._DTj)]fDz)߃Т޻bJZz?I~Ep)K,옸^;r3Z'cy7ip;1"j8AҨ"VP>xvt#m}B`[nQKα5EA^}M8W:[K'o]쟆.vXEnU\tbJ> ީ[mՃ3CEQ%J#V=`M3fQxQ/,z!5xzp?~Dvv4tnS zM {'WJx9; y!enQUCcw&4e:jK{n)Cɛ|W|\깶.u |;GT򺧢X7N(8ܮmunW]kbKֳKeJm6[\DT<}+ns>7ZJJ&v&'e5bkH׌Uy6 f0oJVfEZQЯ/B<x qFB<4tŸNlOolvw:r[DGZ4-BN~WN䭶y"V۵f jr{b}PO2z3C5*Jvk\m*eGVvs TNGz`StuRPNXjpLn )Qܗ*>u#9ܢg5 p''/_{iVZA\ 5o(ly׈嶖ԟKKQ}g&~@3 @E=S|\YGw6j@&@j6#֒t, hm,=.oTTOoc4rhwV-Z=UXPhaw|KDe؂|!r/Z^wElL:2_mf|455vAԪoF=qn4hoz#2 ua{ꉿդ'SzݮV1вza^@M o{b]뵣`\"z]뵣Q%q{t@ޡF?@{vOcjɪnOpOjkP).B˂ {SC~Ra-HA]Жљ;Bcjv$Q>EvA{=7ǶN(NP>Bޝڀ.j΢6M=38wVZ@ޝƱ8fM=vvv$4S82hvRZV9^=rfրnQT$KDkMAz*$p%]֘U/ܕRZ5TGS/tD^[ckO8\:~Hfs }-ʍjCgM $QGZ8}ך}I?Vn4?a c.򺈿}-0X7jke-0~`%_g5gx9bxVIk ~U !HԌ$а0t->0ֱ41[V=W?͕_}hꁫUZT9r}˚ {Oo>F@ޅ^?={٭;&ܯT׎wJ0\v.C({辚pp3h>1!VpG\m@v,[=4+j`ڗ ۙď HۖX3@k5ɕC3qOw][sq '#T5-YU7$GGr _OHC-nPRe6ΤZVk̝C:[:m'Cw_A|?|8J~Q%}Ut~7Z w_m >OkjpA۸ەMUu3pR]9*\O5d4WlaƕQeQ}jfё-HKGdT Me\Xq>Ux{mm vX^Y*P\N7;Z@or ǑA }<SC{XK+ e:'=jMӢ'tF@*@}xi4w\jf:ʞ NEi6;0!hKRy]=u~t -JJޥ}ӗ^A ?#)/A4e /mw\&KfpM*ɯQ5`3,A%;RMӏFgg ܣ\1%}}sB6[othIQW&G&(Re"ؙ̇Tr]b\DIUZ=!:duO gFgOnwc!2M=fNVlXQ/T?*럜RiF<}!yV3FK{>>_6 a)po5I~gUb)~ ?*vd>xFTે8nEc:~@lĎld)8C5y<%J a?RW oa; 2wc]KcJhE?u2mA @ ŝn/;:) [@z =`oJEC!'; `q^7S{# jM0,ؘS9=Fe%Q@?`icn~}FA` P9Kr-: ~@ [S)G-X$0SZG4G!k *` *j68*cqNXDRJ/# }Om1k=udA #T1z{|?:DF_@ P^C+\cبRE(^/~^cT]pmq4?6 }i1)ݫ *&A @ ۅF+M>2/܂ڏ^َ2 |+pc!J|B,qg$*FjMV31=`@ɋ @ A !׭4Y/;X-wDB#*ʮF?3HƯW JKՌ !qK:D3DGw>A @  >@B`Y.ws=N#/4`vy3^Q%VPźPp9Q|ewF0Ѩ)aJ̊%`pe1b_=@A @  Tĺ6fli=ܟj9_Ȏ􀝾 2ss#N@HJ_uՌQ*iESB*`z޳\A @ 7\i^ؠkeBŀ|2=oD !~Py՛1Kt*iJl0MS(mXc#qIyTA @ K@ /tbȅv$P(@AIDZ%y=pWG֧<#t;jNi#+M.ޢpy}|EA @X =DloG0z|7:obeG7@zQvQfs2jL,@. @ Cf:zI<~ )tUH"֚j  rA @ |>J#t<2f?2K:(ڒ R#@VC[F59Hy`CS.x,~FIN2&t_,\$A @ OlNE@q& k8We핣p[ᆼj6eE#ZΗ sޮpU9=UJ@yzPb /Ԟ rA @ (=s_AC8 tTHi|bqWV_ X \+ =Ym$hI$dTE _#O4rA @ zt2[eX<=ڴ';)`Kep;ZGCA3$-X+V, F ˈ JN! uT!M3A @ RB@MZoе5]foe1>lU JmU|[}a>ڡ@c(6@_FtvVT&LJˆ Hѥ,k%A PF,+,;Z.hlBx~!@3i` mz`9_i%`vF:vW.#.FÝyPH(60gј T4򁱎:]fA @ =EjF IDAT2(_7Z.w=`C4ZSN88;Ge;~Q%u-tHXhΎ z+O? A @ ,Y@|1?1?hap?Yho{ƓGE>Nq^BБT`1rR@/bn-M $BX94_h>! A @ ztG:u,u6ژ)# Q[mң\l]h?Q0Na-#'"*OR̎4";:ĀM֕P݀#6 W%UN zƭ 0CaJF&zA @X =`DGf~d,<`J6 kO=M)֎TfPc@Lcx 1T 4gXa! (3#P6P2PcC(,-RPA @xO@B0YfoeBLL[SF)m"@+O3T C_qPO{Pj~ȹ " fA@i偭>_6gT:(|(\3Ć:ߓ\A @ %|=`iL2YfoJ_h%2ߨB@P!5[ڕAP~|r==yP/`*PbD2 t*RDv+XdCQ}`?' A @ 0 tLtmLaz~dSE؏QQ!GZGa W2u a lE Yi|8l#8%8)u W;\A @ ,7PQzڏL/d 'I`OI:ːҏ-#@r5!a yЉ`4JIpP~G`2}j 40d(Sg$(yVA @ Wh=`#s:][:BQK=g+=ZPJJ D !ͬ+y9OAj_+3˜h z>3=@uV y.A @ t(=`L`z`%e&/t9:"@+ nVX>˘Ƙ. wapZl4 !T _/D|FUA @xЦTt~!'ΖuF[>ѩ`jhyqt3e"#FPo1$%לZҾA;1CQ}F:$}~ȵA @XQ(h= &CV,ף2ó٭@ZX;QYЀ[[T\fwbX tA @ )0_t2{sjNԚAѡ/ # 4WoaТ(i-C/A @ h7 Q.CLQ*}DDf(fP }.Nxb_<,A @ +D( γ{*$U8gԾJDBUDw>.A @  7遍Jg!m4I66epa՞lAk;jZyNA @ g+o xb|A @ |t K4[d1𛘽怿Bfz" A @ |=i(ߨ&kbQt bׯ*ld(=NO @ Az@_Fmh~!S_GeT[D.(܂`.*ޏ z@Fx; @ A >!: oTShIRy9 x%N7,$`4dKDOw;A @ u0f-?&?I}aX;B@aAaJo%\!'A @#:_H?`LWX/k:>bZ`+'`̟ @ A@Ex#wwb TL"ќͣqXxb'A @x_XD#[m[ނ㼉Pn z:/:A @ !@?Wq"ƹݐӑ5USC]-,RY7{ɋ @ Ghoookkknn~{(--5\i z`*~d'V[,wSiB4(>(8OSGQ-T5? v3@ Vf3? :~Nǒ{Ӌ="/A @ 񺺺a",@wwwgg\MMZFh7/􀕞Bz4t($:lVz2BHZ@K[T0E)?zaU*z{{xӫիWojEA @d ",ﳺ:d0ұ\c^xPF-;Y,w/4=? Pm?ђ<Z0jN>15Z2/-=4\.///Qnjjz%^CA @ D,@I5=PXXs[=Mm4ĀrgK&~b'~ Q|3 ߪ|YWêx3cRR)AdӁ3z h={FGG_x{ A @  R)CCC;p@rrt;e,efȜ@ I`6P@jH ,Fwm I52ATwK=^ 47p=~dd^QQQ.\`^zŋA @ #fffX CCCC}}}ۿ{RR4| Uz2a#!&m=@ c|(ʟGCG yQP2E}=u^h{ @@.#?=JN#4@nn;:<˗ϟ?kH @ @G@,..oH\B≭Bz!-3+명|%W&Uͣ.La^StH0Tjtt*$, ))G @wwsy)773u_x3TC A @ >8!^Gz{{o>77//^֢xbZXښ,3]2[HoA&}:Wس2w'e0bb<16^G<^v-y @0^(=GxwLd {={ܹs O>E5@ A! HXA߾}._<88j`遵YX׵1ֵ=`'6_ c[BTQ= @/J : yKѹ |1xVB* 6ZPc FU?S}-Tٳ'OR A  z`q6L"=wիWGFFgϞ0Ȝƺ6um6f͙|z.>Mg8e/#Px6媖LRי{Xק4O9]NM1OBK쿲ؿreSh!mZ+>ɓÇ333ɓ'=B5@ A!099I/Pww7DV|ImmڕH`8/w6/w6_[ѻ-(aDʁ#oBx)x4e~5FV;# x? bMCJ&f*>8X'P_[}7JPtѮ.M'O|pLH @ wdcc 000ݽzٟ׵7^`28/-蝉(!W #LJ@p(`.djDmsGD?J^|mVSSc{/d z@z7j 2-Gul9Q@fT-?.3 f&JO8ٙYQR&]?mbA @ +;#u:J=`~OlOlIX2?@L0 I2g(~:`8HMKzyU]/GPAPVtPA^A H$sssr\P¹ɹ LP(ÇȐA`I#a i=`L߰A}B z@dݩ_C3oH9ES_80ꯌ FG_DzQ3dl !B D,駄\@ԔP(|ȈL&r.dlA ذj-S,.<@o<*?_h7E ;15(|BxÇCȼIRAz ) Q)BTAX8Tr$U* P`U[By!m7m&Lp3GB#)M&@!_i"/A @ {?Uh!2H9:OВm02$4 G;j" w`1… @!\Hs&T Àt@;&&wag{{DE A @"*_Rt~A$]PYYi?nеxb@>FUԜ 7vV{*){uNLsmxxNRLm. }CM^?׳gϐxb---;-ݻwddDPb۫P(V^nݺDdw޵\~}@@B-,,w[ni=VgϞ6?;w8::^]]]0`iiƍNNN~g?~\,+V{{o6 `xxXd\199vb̙3t@@@JJ*hxCCcǎMMM)2zӦMpxaxr}}}|||__֣J~ǓFEE-鴷8qݻpRLvbx|x``"B166pر!ooﴴ4o'BgiaSSS?:tPj*=zt^,@bFkooXzx9D"ъ־oؾ}ڿtwa٧OE G?4x۷GDD5\VVbŊNRXXA x]ttQK؟Xsΰ "a]܃@`(\HPqZ 0 hDs<|cUH$ׯ_/d9Vw馥޽ѣ?CC~XnݺիW144^sNCCC## \>==}u333͛7?lZZZ2<>>f7n}~~lII{YY\.?}D"r}ZZ~? :… =211  Bٳg6#C@S@d?`H4 d|:SI߉*XкƏb594"yHfyIBk?0ҳ[9x`MM͋/J@qq5k TC.ӧOWWWwwwg-m۶%$$lٲ%??}孭G ZGݻq= HBBBΝ; L&߲e h)Ek nݺ흛!H=<޲eBBB\]]RinnD"~~~ׯ_ޠG=~P(liiO:055Ѐ?oiir?44fO:5>>=KP$%%b.wСDUUUxǏdYd^^^###\.b ;;;NNN6mݺu333oݺW_ܹf߾} _tҲ;ZׯGz@P\|Yq]@\SP\vfⅮ.{9::k### -"* ]]#GLMMEFF SN9sF"LOOo޼l#@/ڵkbbڵk/_Z(޽{Tr9wtt 9;;LMM;w.55U x{{'$$dX]]ݡn??dk֬Azf``%Jn۱cT*-..޲eKcclvccڵk>4m\^^^b755r5T`oooaak.3<<<ИxCOO/%%zPqbbU(bb4779d2''k׮xazz:..n۶meee bʕx̙s΁;rGGڵkA޽W,-))177G]d2cdd\.gXBprrr˖-fff=\xWWׁpB///X߰aǛMNN>xL& ooo_j͌P(4@ooiCC̵kjjjv=00kll… :sL@@@xxH$ZjUvvD";{ĄiqqqNN?ǫoii6eWWW󽽽+**曰0#Ķ6>r1H%A P333 p=`@ON /C&5Zy9DҡE4=Ϋ *PB:`m߸ؼy#Gjjj?) ÇSWIITeeի>驩ܼ?Q(_رc;wuVrrg}w^??E<333@KK? 􌊊ZܼߑΝceeeW&eddQS\\BX gffFGGKKK=ȑ#kyyy 887..d!(233p=gg͛7$?c4C@֮-++[n]`` 8{ݻP(<|-[333j|d}YTinnrB-Ù vUUUw Ѻ`ҥÇ=555E$mɓ'}}}cbb q">vZ) ;99ɓޚMh4]]]{OKKKUU466S㮮QQQ666555 b˖-Z/ajjŋNm999 6xPDkll Νp[ꁙP635k֜:u*;;;::zӦMYYY{xL&dH b]]@Bl6ڵkfffQQQ֭KII {9vo+VwX,aQܹs%<<|ȡ ĉV+LMMy<\.O` <<ɓi#G@DM Wqqqqqq2ҥKSSS.]𨯯srriq;wfeeegg^z +M= mvر򙙙ݻwGEErT^^^/^ p8 !??LJJw٣GNMM̸߸q.\pqqx~'ƷHKKu꺐}911q޽v >rBo$Ibb͛Y,?y`Xcees&( ! oxpOB^ N2==F=bvѯxHTUUenn/UWW;vҥKzzzBظĉnڹs͛7Я'Nܸqc!iyyy۶mCk 6o-t566uvvNNN .x{{Kҡ!ZZZLMMʾK>  ^tĉinn x=  ^z!OOOpقWML ]OLL>{,T>}z޽ˢ. j 6p7ntvv̙34<<w;vhU ~H$]|$88,0;wZ _믿H$333W\EרP([//'O^|.66v߾}oR4))i˖-h4Ls]]gEEȈH$:~EEXnݺXr%<>sN@@Gdd$ 5={Çќcivv>Sp񊈈Ez㑑 ݻ 遁jwttκ ٥K|||lv}}۷zS$ y1Aâƍ Eqq]U3::˖-ccc~~~r\&okmmȸ|rDDDHH˗kkk}v>>> gϞ=s M6effٳ';;~)[p=goΝ?{s\A 6 =011啔>>~VP۟| Ƀ}|"Wny#h:#,ZOmG5cY8p@iiӧOߨ,--q=u֎UUUZ@mmU$IEE"ΚzكV%̒Լ 2]аf͚~H8s)PXPP`ll|E]ww8{$&&HRpxٳg1gaaĄT*=tP``8,OMM ߽{gjj ꐆիWMMM޽{``mBϽ2337oތ5ŋD###7oDK,-ám۶b# 222RRR3dll+--ڷo"WW׎@'k`)T:99 bd2YBB+yCCj @&?~ǎb800,FDDH]v&==}֭֭C LLL N8.*],KZdH7p77aRBo$ŋG!ӧO>#266F"t萱1L}ꕋDFW\133;~iaa\*222@"i4 agg'Lۻw#G֬Ycjj:00p9<RfffAAA*brEcccF=ƀ%%%߽{utt d2B`#00ѣFFF666CCCmmmvvvZZZ򋆆ݻw3gFFFΟ?occsq[[[h`0RRR Evvvzz:\TQ__dfffmmmnnSTTg###$; Y֖D"ݽ{({=|0*//733211P(666&&&111t:}xxeԺ:SSS뾾>prikkk]]ݲȝ;wd122beeeddrd2̙3?R|*:jhhxqD&B411w)--&&&###%%%<022"@y]v˖->>>VVV$ӧnٲeϞ=  ??H$jkkC##Ǐ[XXB966F$Jjщ nbbr@W^zz1###[[[ӔGdpS333+++ss dllV>I <<611AJJJEOII PE\DB}XnT*H)=<fﮮ.ȨJEEEVWWq[[8 @mm-HY^^rUh4_a|| ^VVVYY"5~qQWWgii;ZXss3E`0" Ѐ?PzCCCcccMMM )--WCCC`Q xQTp'D+4Ap* 1r#ͤfX*JPgYY@ݤ鍍ST`ddVTT@mCCl冇[[[}}};׷QҚ1p/UUU------644xAKtvv 1 hhhNM/ ِ ' (++QU;::(JYY"{"|@Y('&5P x\|?h:Ȏ{VgLBeQ&eJO(//aFFF-ˏ= booחJ*M?Ǘ/_P滊5uux`mZM5Sv?UUUx}q#3TɅ #ުOXo E^m#}PgelT>x=?y_~ kuuu)))-s)l\GDD|\300pCCÏa'())ٲe2tnf#MMM\.l號ʼnE0T.Cx!"A ԰AmMH}2c+P_+T#IG >+@ BO@OOOAAUT <$ONN(9r@AK>_@_-2^:e#A/J4@ΫnI@/t7}( &Rt:ŋ c u䶶68׾|2###333//LOOnllDvy`NB!@ t:xbaD1ɔ7?"O!v2} ^_PmH A\ƚξb O,$ |WਥږEGG[[[;88;::~:%%e߾} {u!666zzzVVVƞt:=$$_~177755577RDB!@  R|-zX,$'-1 IDATZ­o\9 _- a 7]md㞿=@aaNRRHښPWWG S.[YYYHH`[[ۯ*7'^,j#B!@|||`^؛+s% !\a=2>H<}|G[[;..N\ر]vQTP'B!@ gY߱DwTyR?.!Sz0>˻xxeˋ=<<9y֭;w߿_CCdܗ=rUUU555ÇBBB6lؠ~[[[vZVA B!xW>[}k_u_F !P»N_2z%lQn=WJ+W s@pOOO]]]BBӧOSRR̞={k?~\ьRQQAǏdxHH B&rNB!@  w{󁃊G>Wm)Z# %t./x%¿8*(7X  Ȓ|=2S?~lkk[ C!AR[G399y~իW틋Aׯᇂ@͛7544JCB!@ Jk(>xx8_Ë_8e=I} |/+¤xGC`tX{,%%ںԩSrfKK7|KW^|BBBlmm <==JCB!@ @hP]?ZB]00D_( 4~Vx1!j=+ @ ?K/e0ź' #??LIIICCgff]vݻ̬fy|9V[[K&Ai۾>CCì۷o?ٽ{PDB!@ >3V.GvHXLXLTL$ 6/^?[,Hib'`:PY /$u-0  8x!x4m!C_!@ BE`u|@*Z}< #3",倻IJ`?UI$BĆ08QOC`i ||zdYAC B!XPZs> B〘Hmp.w\c9 R2%+ *@n{(V2~!@{@%iA85Hu"ۅB!@ Ow{طFS*Bk D ]p..Tcsya \ Dw@˗_pUd. |x:4!@ B!!0::y<XY&9===33CPnP-B(+UV<HLPMLI,o%X %Xv:إLw@2ADXwfH[` rHm/oT;};s挖Kuu5a---...999Ӻݭd2p  ?o1PQ<, ~@m-oҶV-WA$8 EAd-5~,= 1\t1B$6chL&͛CCC|>H$=}===8ѳLr_۫ CCJ#?nllLRKJJ8ՅaLSll,Ñ;u"B!@H O,@]\~kx~/ tx/Ņ@6^hK.?݊- MJVN H0~`F,2L2C jbbbϞ=q896{M6``0.\pM ^zeoo?00aݻwUUU1 Rzkk+aΝ۶m󟌌 RSSuttR1 s玎NNN@ P(:::|>0y lxyydzX,>_|8!!F`ckk[[[;;;`0d˗/|@ (((0,>> >>L&vvv lãby>P^^~ȑY0==-+"++khhhbba…zB!@x>o )T< !F%=(ً.V- I$\C ɕ`?WH,I*~஀%7>?յa#H9s8aKGGGW^`L쬿wUUUuurff&2|ð>ss󈈈4 ŋcǎ>}ƍpu @ B@.+Jk(QcTa~!\H 5rCSԧaN݇fKsa, =4 |Q! 0\A"maEIT1-\?0VGG'---22ܹs]v ;;;/t`xhrrrFGG>t萳豷/))WXp'NJvppxg***Μ9 >@ B!@ rx'>_0>XXXT 'D"ܻ^+| =+̔P>,Hd1-`ۈ9 nK>>xީS>҂aׯRSS=zFleddԥKBBBݻwI$p:::UUUs8ɹlOѲR|`ddѣGpdZZ@OOunn.7,+550`vuoo񑑑GfggPB!@ #P|@/tPTB>H'] wd%Z,9!y }/(iA"DUnQ)>`0Ξ=뀀//ӧO[[[q8tSSӰ0///"ߏaɓ'|fyzzIII=, OVUU=sUWWgdd$ ݻwoEE@ 0lll… 8f ŅL&;99yzzxR?ԏ@ B r>wo|WR >pH?*?H$ ?8T7-@;]u1I`E=B(MfKpi~>E! Æ^z󂂂ׯ_Wׯ_z*?? R\\\___]] AN211QRRSVVv@ڢ\ (//st: ++ɓ'YYYK0/_ x<ׯsrr @! lvQQQoo/+۳EO__~-h6B!@ YޕVCO,8x|<$B0x/6 Usp7{_/Ќj= x x dY`c z?L4dd?tB!x'V H5@,O,HbPO~A_#42Bx"u8^K6#^ `Xr $>ԙ@rB!@ <'PF5P]BPGǹ@TfήN̕.ǻ;;;f!B!x'>[qk@!eECzk58xbU=kp$(8:w$!(8L00$bEIŶ0哰0L333%%%wwk׮;v>|ʪJ߾}АT[?VTT˄o'OY\ ü) ;;;`llf͚rE B!@ |'VQ&U[lp~;0 \ދP7I.0 (Q"@[H\Kj¥?>'|vtt4<<\GGwttddd|ppP 0̈[&''卍aVQQ z=y򤶶0@000ѣJU[[;99t~~LJߟ) V̤R%%%333`0g4B@+<<<--ٳg`ɓ'Ϟ=VIF~~>`TUU9s&%%/YCC< @ g'޻Fs>1dyxg/Q`?PiGHJHz[r =KY YQL*x1AGcV ܆0ߨ >T@Q}}3'44̬9::^~/##bMNN7ܿppð/^\zz 87n000yKttt8tDD lmm0 kjj:v옊JHHHzz:xIII^^^6l\ӧOCCC@Ν;w^r%<<¢OLL%&&.qqGGǫW''' .''/7otuu~z``1 ۺuChh/K >GB!<X58}PAQec*@I2?K⑒WR>·P&B\ƿ0FD E Y ,lCh|ONNN>fǏy&yYOOϛ7oh4CRdO5k ptt뛛۸qc\qqqkk쬕ӧOAEpYV|}}lvWWO?d2;;;9Ó'O>[&l6;""</99Ɔd|+++N}utt e;B!@ XRRUQޓ(+U#|@m-VL@Q5rn b9p 4N Ճ& yI=# Eb _GDܲsܔuV6-wޭKd2---G ruu-[|77oԬ l JJJ'NWUUmoo0OEEU{{{۷iӦ5klٲH$2 hZZZh4`hhh$%%-0F;wx1 c2{USS{${{H$M(++kǎ?~ h6B!@ +0(~-OxLUQOE##5$HCbmװ[00 xK,hpT?W?,%O 3S|>>I} > B!@ zʊho*,7,?L&H`?(?- 'V{{{Rb9p;v35B!@ >cލ|[k4RZ}@oT`Ta^V{ / _ d%Cj`ܥq[,-XK -u>;}k׀g|pȯbdrOO4Z !@ o{'_Յ Xl x_-BavJ-D Wwɿ/+ km6Up#~ﳂ#B!@|~#PߍtA}b/Fx! q|ܶ<9/@S$;H;V_Ъ{ ZXXXZZfee;NNNqqqܯc-.\҉455{NMMupp*))Yfў˗*ٙ IDAT/فg󹹹d2:))ivv0`ccsZ 8NTT'O@򢠠 WWI''';::0 /^Ȧn޼iggwĉ.0'44!22fB+++{=>>zn߾ *9[n1 X,͛7kjj[tpppssUUU:u֭[>"B!XU/_/.vv.QKs%)(&2~ &>hkkO:--ãm׮]w)++KHH000ILLNOOrrr9 ~XAAgvccJFFƭ[\]][[[A&+**@"t(--MWWvbbðCWTTကWVTT8::W.++0,998??W^r111...ׯ_722p8Ν'H YYYUN599YYYYQQG&2MMM">ZZZfdd7ںo߾;::޼y˗V~ j#B!X>1A}@jWߧ 쥂 /A댅|F'𰭭mrrrbbSyd2@&PDǣ龾>|!!!/_uss[vo߾cǎW^/ ?ӎ;4446lo$[l?od2'''MMM|̌Z.//cccsss'<<ŋro}} 4hjjTQQ111d)))...H$GGGUUUCCJ@p%www.;::󓒒LLL䮎:B!@`}b}`Q~!| 5U {f/nc҆w݀DRQc,e|СCUUUl۶J.s\0 WVVuD",=CPŋ$$$ٳ`HדdX6<<ĉQh&x5@PTT966{2W_oڴI__͛7GI$͛lӧo߾yf%%`0lvv688X$fffݻp^jnnw5 {.a111nݺx"qܹJC B!@ F`|`d0ߨZCյDrdCVsK缇f+\zþdvdƍ7lذe?o޼yڵfffRJKKDa=|`Ç'%%?~500ٳP|妧+++oٲEYYFqH"I$RYY߀q '''*jffV\\gdd&&&fffٳ'%%eddƍ\.ǷnwqKK ZZZ SuuɫWh4?<77I为vvv$'';99YXXz{{GFFh[BP!@ x>"> 7(~@uKu[F׳ߚkރI|@sGh 455ӳs׮]uuu>077re^xv٘ fffLLLo>88@kkӧO9Nttùr功ao޼133+++x߻1 P(d29??ظ422 ===// hqqqdl x=yJKKUUUE90LbVXXtG]~w9sLfffRRRYY 5B!@|}k()4xKud>X]VZ#?HRxi7n>a͆|^@ccVWWϿp\.F-&<<ښd677effx{qĽ{^zfϟ? B!@ U#n|۷ 8ɿ ~+ŏ5^DCYv} dÅ =z477@}}6BILL9{+Wmiiݾ}˗srr}}} Ս\&͛7ϟ뫪jhh>H %44T 466Đ .8pIxzz޺ubeff3g|\.7%%244L&_tixxjjj>Aر_TT|~SSӍ7fggbbbn߾d2ϟ?311qٓ'O^tIOO2UB!@ @Ε>\.[XY&9===33CPD2RBa8]yvǏ 7l, 0GK+.Qr޽ؼ<>|}ZWW}ðd㿩466>PVV cs1 +**sP(phl#SSS@;w奧?%.ϟo[n%''  333?~Laj'Oeddܽ{7%%eppP DGGs痢 c [nZ@ B`}`xCw~iBv +g+zO\Zh*>E;Ԥaj/Ֆ;Xj *>^H[T.8{h B!@ GXВ .j:9O&?f=2>xx<6-$raQe@ X~2sWl >?;; 6b@Rp|>σσ x` 0 ? @_ J9C`y Vap8)xM~0-[/wﶰX*ð |_s?vtti)((~۶m۱c>Jx߰aÎ;ܹbd7r_|i``oJhllٳ;wܻwodd$J_pܹZZZ?JJJaaa,k[[[_Q۷zzzJJJ֭8+1yvv6222((hjj tqqlvrr2@ؾ}6 2>zhNN僼x@ Bό$@H .=v۳0VQS/^ƔcccGܹnzz͛7]]]Zf766NLLyQ`&jjjJKK[[[&a8pHq#z A'Oqpp xϞ=#EEE7o&ǎ|ŋ<e߾}^oܸ!t~~:oii122—zAEETы/:u y̥K<<<@ࠥeAA?Ba'j B!>a \󁞞̱cn߾-XH׻]~ qׯZZZnܸt)KKKsǏb._311uttmYXXBl///[X,kiixBSS豲ubbb|}}AEGGz{{1 cX`4oQ!@ DO*c rxO>@&/^ߟJ ?Rjjj =z(iii\.7""Ҳ>YYY',,,88xp8/^z j,eω;;/^[XX?,((hzzZJa/^jjj筬_z_|u9KKKSSsA>񺺺GGGmm@#$e8rL x'O~àTmmmqq1rʉ'`38~`677r%|1F B!?(~T׃jZZG[[ۮ].\}vP,11H$޸q#**1**j)>aؙ3g^*lmm>}aXDDę3g*wm۶zyyXXXrW| ==]fׯkkk7mt--Й q@YVVVMXX9qDXXXaaJݽ{wtt4G󛚚D$KD+|7PK&??߉a0Sΰ0OOOlǃŠG KRB!@^{1ڿ/"a󁁁 ^^^===ve0 T x͛7/ue@cc#@RVV9r$55FK9,\OOo믧jkk8066q)P //YVVV!]699 | ))BhiiSTKKKAug|~r€aXRR5rrc!w ؏B!@ |@> D{64~=~||>6oɓ'OLNN>aÇuuu'''!`X.]ڳg4MMM;w0[[[ZZZ?cOOАsBBMOO:l@@\ jjjٳccco|>4D"=s!777Çs\ee劊L*ZYY [ZZ x>3t}VVw=111SSSfffR68|hÿBmB!@ x> 8@n޿2>effnݺᄈhnnp8EGG'&&׿ Y?===KЙ!CCC--͛7_xQ xJmm͛7uffđ]N TUU[n׮]@ann>44k֬ٲeK``\i &&&֭SQQimm0ljj6l_ݻ777GR0ly>`hhȑ#󖖖Gzz://wޯ Lܸqcaan1 ?sLddܷS *((hkk-3`,/pB!@ `>hW|qc#claa!@kEDD~78ɜ=L&.>~_|}lhh pp4!@  P[>%-$hn4ĿdX---D"1;;)K.=aaa _`4$>s B!@ L?X e"B/88X .fnn.99w=ONJJzυZZZ@5'%%ð^v)??3gܾ}͛7QQQ Vx`qݳgĀ ?>((BP+sw^___PP'IOO>qFll7o@nӧO_vðﯰ:Xff&>yٳg㗒 """Ο?STedd\pڵk f_t $]!hB!@ ߉5"3^mi6s:|c%H} O1ھ;p2._㰰0SSӦD##۷o,uz XjT?TUUHMM---z^V=~nn 𥞞~IIIUUUW\y𡣣9Ndd$H<::pW^tںٳg۷oy&500ȈRRR~mmАBBBBRRӵk ÇnݺG&I$rw;\/h4t钝]rrSBBBZǃHPP;wLMM)++OHH@.{9//?TR{@B!X (+|kK+u//"r̰ ㉗[XX<|`&&&!!!rO0@ottt:::0 0ltt|dd$ 44f&ՃX,VZZ_~I &/_L"@-{-ooouu233۷uVggg@ի p5nhhWVVfoodkk{}OӝH$R@@NhLOOkjj.cpXl۶ 0;;l##@@Pm&W坙!III O<mc| IDAT511a2t:իϞ=344f0ZXXCCC7nx#===ƍl6ӧ+###o5v)U[[[ddӧOY,awwwmm]mm-?<),,wéqqqtt||\YYAiDB!@N|N}`i\H)KO\Vu?Xn[JZPTP^^N߹sgqq񲲲GGG9a/^ܿkt:^[[{ "PtttN>wĉNeee:^YY=33S]]iӦb:::I$aϟWUU{noo/ɔO3L~PQݽ ðb6}/_iii]z;44… oLMMm۶-77W.8-drEEfffrnڴp 555Z׮]sqqy*BY >{ogg0 v@[[>v{6N''r@ |||N:p---Au䌌 ssy0,'' ONV\S\!-~Գ<&+|)>_=zaXbb/ D4 èTUddC=\.f Fp5gmms â.^899Iӿ,|&7o/Pׯ0 p8sssL&388ޞfÑrxmrzz8 (>=_}EEErKu.׿ejjr/\~=` R0 ٰa ÇgΜ wppX/066nkkWPPn||ӗ.]Z!yyy)++ }wqqq0ڵk*\rƍiaaqMǏ ăB!@}H]9νH0lx|G?ٹvZ=yã'سg8N={cǖ:d| ** T*^[[СC6l;;;P͛8;;رcw|2pD"q˖-DϯJәLfxxٳgK5a>cccYYYfFs]vhUVD'OVTT*_>}y{{޾}ҥK^^^r0*zӧOP]] }B!@ oW~y~`>X!7Ȝz ?055dɒBq尰0>~AX6"r Lt{YY<::aP(tvv%  E@@@rrRG@`"h֭D?<==yׯr2|Y!HFFF?hdZLMMi 6\pAPtwwݻ|=P\\sN8l\zjhjjjGFFϜ9cmm RV|||Z[[ E\\ɓ'U*ؘ_RR+n>}OQ!@ zS[%lzGBЯHT 0lz] @,B8{yy_z/77W*B=ںqNХKn߾}5&nݺ쒒fff&QH䐐>ftvv>z, fZܹs`T@^^kaaaQQ?uիKKK5V}w}w4___>>$$$""8555??P ¿eeeh ݳ\. !!!!,,ٳw130 477'N$$$LLLH$++1 __b8666>>~rr|>}ӦMgϞsNAAAKK qH 2nJ|o߾7n?^.߼y3,,lttx˖-\.%%%_reǎeddXƍEEE>$1z5___/* B!0_|OBHSӔšv~fƿ/B`Mgx>~Ũ xaa!XqV[VVINN`˗/'&&^z&dXkq ¼AVK[ndeeEDDDEE%''ƹqŷoߎ|21񚚚F +**D"pX\PP#*)) ]ww۷'&&j;wSSSA~ NMM %zSD˗/x> ֭[QQQq#))i{* B!xM=-nZ"Fy^%^ҫ1tQ職м򔔔tX g08@EU$={6))Dk֖8]QsB!@ G\_hNz@no2L.o->`bN> [DipCq^8w'˷lr-WQ%B!@ k(pٜxf; C ʼnIJS=m-^kc(`|?sP(L&"DL&snRp@bDfCB!@ "fv[ 7sݏlZ\Hg+qzx Ay >]!@ B`#0'=WD:z͍5]Zma\.?;@&@oofpJ,˄[6l%*o߾O?~0N?h~˗;88sr3\ZسX,>?22`04Z X;vh4pX,4X_,3tXq*/J@0;h4uuuG),,`U;:::111ۍĨ(?kZ\fj5acccSSSj4G۷Jڵkttd]c^ܳgOXXT*7^VVh4|f) Mь$! R`t.V5 i4Q(l6T*Y,Á/r8&966D={{{Cl' = SB!@ ƷL3xW/jf;sljjx ա;Qtvv Rupphmmh4)ʄ\^QQ?88800bI$bGGG:s Rƍjի`o鼼/brrO|=ࡪ*55Uϛᩡ/#=`A oy&&Mx :4* G=sgϞϫ\.uZw455=P^^8qppp8/_/۶mz ::SP([ne0W^uwwqð̘(H@||x@ 'mbk6/(8 ӐdFk +.:[%tnX0zP 9z`ΝiiisO m޼ 6+WDGGM6A=ׯ_߾};qqqp9H==pD+v Fppp~~~OOOAv/Bټysyy9H|Vmoo9zhnnngg'pŜm@UU՞={AC0,66699^*ɼ tǽ{x<ޝ;w6nd2Paz`ڵ`K檪*0ې\3?|bsd@ s,?p\>t#uK7؟n3߀4L4[P|K]liapzꁨ.Zm4S@?~W$ݽ{wϞ=d2yl6{tt4888;;$8zO|tg}._ 999fRd2ٞ={\Rcbbg7z;Jz@$ٳ"V@ 8~=nOVK]uK ?`kxѐa !3:`\όzP凞<ꁈnDg{^r|u{ooXxxڵkw윝- \\\mfccSXXP(4Mzzƍccc9N[[׮]~_ڵk>|ީK$ݻw6~ H$ҁV\҂aX]]ȈD" ̔JMMMvvv6mvT* A77Õy z/\re@@F k׮]tnooz0&y 6ܺukzzZ9rBBCC;KK .([n]e,--cbbZmKK={ZZZ4lz ))~0X3==⋒h]]F)))opB!@ ֽ66ȀmN"/dk,H9KD3"ׇsЀClFFFyϫp( VT*A H~j\.WZ- 322.ޯh@ XS* \.r9`T t!!6Fo0JW@YRQa -aeeet:h_`&U*;JpNw G o ܾF!/X% qFV#0 ,] ^06$CP!@ ,~exK]Ol@a& C,Kw8C :!\=z]ܹs߷9 !@ wtB`Q_]Lh 4dD.N,ÆC.` (}{ԓ #uK^n#%^ZXXB] B!@,6ڂ|b?G.?~emu뗺Z/ ";eȭaHA'Z<rz 61?A+05v^lhLxn>B!@ ## Q(Vhj5*W(2L*NOObDR__[~0ٚ7ѭ/'B`Q; w]@C:NW i7$,@P?g 'O"#6Gb9e*C%O>L*nK,jݼ7#?gV3MdyRu\AKd~ӎ sjwqcB! {>C܃w7g.ݝ!q\|h |}1?[\_@F!uxfR" e7,@XгI@s$!PKo/M=`Sjs52~9F+V禳5Nf~~J*"L9{Sjv#8!xpLx}}ۨBȻ#P`i}2Qv1N:&V[?8hc)ů>%^Gձȼu_z2uzĐC /Nz& g@RKJ5/){Z}3[Zk' ÙUY.jbI!wV;%Un)uo IDAT@vVvQ;*9pz#a8>iۅZ] gASRgԠۿ͉h?7 zcQ9=>  NZdggsс@;=B0'V/q` =b?2k~dn6gOLX4 !SoƐP P<@ϰۜ\941\cⳀmןh GE)g?s䪛\g|jZ.p?i^uL|xFz9S}ʾ1 DKUQh~p\qict!xp8x]`6&XTTTl bA6y2BF'&C.N$  'RH֡=j.x`[h`(1NSm81gC6'?KxHʮyeUs\wLx)ٶsՎ Od<,duDŽU_F5'ʺG7%=sJ|Q#Z㽶纾*T/lIɕnNO\n\M=jhj^Cz`>4$ tz}݈l<]6'=J(~d~d6_# 6& :Ρ | DNWN D9|19)cBt5n 3NcBEjYSk!m7'=ݘPx)yߺ93[R*\KR5xZ3ϔ;&'?M)%9'UG&F~zWF3>WV ^iwyLM+}NK+L6%VhqLq>[~٠zB@>d]7x<؋T#UT*urr؅VD`T.TJlT*b TF+:=p.!Bwa>N=X~X_GnLп􀭅nQ2~P9lzPI$2wX8 ]F,CX>Tėl<}"A>PuB?bdTLNNH$*JѨTBĨTx@] ɏ A#Ezzzv&eF!'oC@Љ e'^"R01:%/,,{t) E}]zx<Ł$q\V \r90ɔMwx<#غ`|FRd24d RTZja4D8k4>?55˥CCC"HV3V2p4 Nx c||\x<`,q0L&h4. &"J0Je0ǷOQ=[B / [CýɅ Y.qzzJ7?`ERC=Ga+aZB P9mks[ 'F5{H3DfB7|>MOOM^D":;;!HZVT .%0 c2mmm\.w``@,c600088CGGhB);66Jb0l6[Ruttx<6+J\nOOZ~``@ ⱱ1ե `LLL r\VKөT*ϟ*#[A⷇3B__E>18.zH!Ɇ5f\lP@zԿ;ѸBCB!H V5Y=Ь;uBy<^SSSWWa"G(B0FaVR### kuǓd 0@۫r9Nlkkh`F@{{a*j```ttT/^\NR\.D @`611APJe__BQT?!@EEŽ坃Ë m"71_/#?@\o] u@!@GFNaC 7 P' zcoB|WǏGFFRT;;; O>0Ν;˗/OˀP(?Rzc|bvųg՛>d'O 7m" L&#HoVi!o+"h``@"d]V溺: af@ hnn5 EO8jjH$h4F@GGHj W끁D"jlll 477wtt( iccc__XiEEE}}= H-|׆( _M/nmfMDi=q~[6!qH܉^N=>`1!,, _\\\H&W^ىxkk]eee^^^hh(]jU^^3?OB!J 3 r9a?En 0R)9tC` KaBBB||<`366lٲ Ń|vx!lvsssww(`~`jj D᷷ONN|4==aR@tĉ~==2g&IRJq\PPT/. T* d2Jx`$ɞ3h4 $<,o@{|٢xn"?kV/2$ӀFC2 zWfZQF+JNb\.w޽PT*oo}n>Ǐ[YY|uuu...8DDD899mܸܹsF]8.oH$P;;;wwݻwC=ѱrʦ&QiiiHU"mmm,K;5X,njj B⑑vF399 2$IGGQ=j9L&cX@KdXccc"dVpz{{e2koo-+Ń ZfdP8==r97+Z-Z")qxMxMs0_z@?#ZhEKX ˰`\5!f:ZzҥKϞ=|](J^^ޮ] rss׬Y3[h%K*++ǃ233Y,())9qDDΝ3AjuQQnnn\.W*C=`0"##i4 W uƍH?=h1:_$ 0c0-hH$"t:D"Jf2 & ^jLLL8X,@@) XÇb|2C"lR3t:L& |ZMGGG$Fr$N꾾>0 ?r}}}`hFz{{An$whhHosAB@z-{e=E 2iƆA>0x66ۂ)t  "9s^vx6=_,44ȑ#Z6>>ĉ8wuu?~| z>]]]׮]gm߾ۣ۶mۧ;bccAիW' ںu?OOr)B!,XػF#ux׿+5$F=̝!_-4spJL&N)JBIo\.W(D!*JRFRd2 8x&l$RTL3mbbNh4*JV u ɜMWl6z2dt0!oo| ||zOlO }٬|㗮V6f0TP E+'B!~#{_@z}f͹7ne=`*4C&UE1?`l|#|##w\,?p|i?떺Л0CmF& ~gcmwGD bB`@z`@}g\A=e=`CoXXX bLL`vVzwO^h{?}JG e2YWW6kZ򎎎tCCC7T .+++--P(4`:>11az#9е@ Ꚛ#7'/>;TX$ =H4 @z`Npgs`5n-]/> yQ.N >$Dc 00̨  .=Z,|b\n߾}>>>ǎknn6]rwL&{{{w/_&ݹs'33shoo߻wM|</**>11|s/ srrlB(--}Fq|`` 99ðǏ755D"={6!!ѣGbq& j1,E$jO//+WBLq)JiiiYYYbbb||B0F]x199MB=Vkkkϟ?s F^^@C.p\FSUUpܹ6?>+++--D"8^SScee7H):rÇ===V"-'''={|޹sǨ=\(X(QAA]o;W=jLCfOl|vUuaEF@}GH$ҹsKKKϟ?,#""Mڷoacccnrww/((X,駟>|(Ɏ=zܹ'N\|/H^UZZzFZյyK.xSNYYYK$H?8><<q*ߦ>~ǧQ\ ߟ#6-RRRbbbJJJ|}}o޼aXWW۝;w.\7448ДONNfggܿ?33GR .<~8$$`hv///??A(rrrlll<*wuppqFaa |RpB```IIIHHիW% /))illdpsww?zh^^^IIZdgΜ)((xVmz?77޽{NNNCCC$iϞ=fbb庸'$$zꐐ`Z=88Z\\ v6:ZV[SS'l&ʴlR +J͛7"^EB`T*eXFK|;`& \xxs+?K`>hL"WQ>Jȿjϟ??tPuu}kk+CCC۷o'ϟ?moo_5n $$'Od2BA&GFF:;;n [[[`7o`jjɓg|||jj ðȠ Zb""" q/..mgtdddffQv^;~8ʕ+/ֶ}\{L%STϟp[ .|Z뛕vNNNjJR.TGf<0l{A]XPTYYYJU*9,ꅙT IDATEt=t=j{{{;::|ѿ>S2===8LMM􌌌}[jGGG v&&&FFFol3B@z@{oIZbJ,?p2 /R~Ex6&l lMDp/Y[_}qgϞ֫Vo---WZ?ՉD"??;wZ[[C.^k.cɓ'_ʕ+---;[[[XaL>|7Fqcٲe`+Vf'''ٳgϜ9ct1Frv8ϟ+Vo/_RΞ=82/_n;VXQ^^hjjjۣGp ޽frD"ѪU@R]]ݡCq7Gttt/ Wz 22իN=zť0Ln:{{{ի{zzpرcoqqq p@dt:+ p"#G#'C`>jmmrt7Xfd)o#q08Ϧlࠞ1x +Q B yq2W=7"xl}߬7^X-u[\zð}}}VVV Lffftt[zz:|(۷A@do (K"($$"?))xﻻöb8** t;rpp 8ŝ?^"&?Ǐl@"juFFx3M&]]]]l>99٩7M:66644&|~GG===uuu VWW744 s܁v10:^__ZJ7U[[ 2 &xexeރX_Ȩ0=z8pZ}?OEEEnnnyyyZ644466f?<&&ٻwg}xh4dX brrrʕlݸqczzΝ;,(55?99r}6!H333]v-NP( j%%%ɓ)))_ꫯ.]D"vc3z`bb"&&&55uhh([w`VWWyڵUVjqƜo>>AR;::;&HRRRjkklÇo߾X,NJJ:~8J*sssa!Jz@P Axz@(侾>Z{{{BPTbXR7<0&*ɔJe8 T*R9==;>>j9NGGT8+N %_Q=pZZZ&'' Eww7% pVеL&knnRTT*!1J%Ƹ\L&jFߒ׊| =&n- ObH$;5*̱!64';~b9uB(5kں!|~TTݻsVcbbaKDD-[H$aޞW\dt``ȑ#ėKV?gǎ ;w޼y+BBB6l҂xOOω': ڵkccc=<< Zq¶o~0I255 @cc;N8q̙ P(<<؈ |JFlC=aB"L& a6<<  X`\n?뎦&)L`0i4RM"B/Lfww7qxxFV 㧦h4'ɣ"į .TF*H*D~yp!ѷ}y2XD+ lb&s|>|LDz 9WW2͛cccsmgV߿ȑ7uNZJd2FP(BF`XZV, LLL =d2;;;rR$l6[VKR ÔJ%Bd2hZ5JD"QTI$. 8:CѠJjxq|ttL8r]]]r0Z P*X,{s=BD4~kysUDmL|ol6ol,<XB;55cRa||TsDV9Bo7p' eddDV ^ B&LR4b =0<'$$̶V#:5^Ǐ^|JΝ;w ] zzz6nX\\{n&O[[ۆ A~P B!0'\!H@z`| 27/n~/d"zKU-X$BEݸqСC aŊVWWYfɒ%˖-[|9qD"mݺu۶m)n ̙37n41f͚x0}AO?4<<\$i4oV*eff~~]d[%BYfMHHȺu >?OLLðs΁=jիWO8Rܻwoii) GvlP=B!0'ӽ co~;ߵ᝾A=`z7vg校KKlP0gLŲ\.zJ__ߎЛ7o8t!_~=::HǏ]Jfbt/bhhH-_<77W&[[[HqZiӦF>==X\RRbkkaJ]reQQD"ٽ{7d^^^:?066rpJ2...99yttt||< 77HZ}<TxxP# 8tE`HQnJAsS@llRuK{z]yzСCmmm\.ܹs۷oϙ0GOMMDGDDH͛7b#'''6 555<DaooJ:|Sjjj*++?裩)*8vX~~>7Gtuu577a(ʨत$0!WommݲeP(cFB@zC   wzhۺ &t܈HxӜ$|hElkzd@1xCΜ9x)oo~ \]]g XXQDnjzRO,iҟzqH_pt}`0jnz^T)439bXD|>dők׶bԼQ?訷7D"Q@@@XXpdӽvv Wq<33sr\(ruGEE\.W(gΜt\.Ȉz`Ϟ=F.~СVpZɓ'===^^^0ݹ}֭(-C_< =x 4 =`>V??~e=o^UZS=wJ0%Wu"H'^{eb~wΝ;wqرNDrSN]ӧ8رc,K(y{{'&&8qb`-[ ۽{7Hx=xcϝ;p&&&ÃBCC:Рj;vK.W\?tPNNH,ǧ^rAAAv:<K55U[[ @:00-***,,lmmM~~~.]BE"(***))BƁI??9.MIK_Ӥ_ϳVӏo\ضC7Y!ȵ+~ d܇ȣEч0V# 3,GLJ?ZqD|>(XlO\GID3\: |:YZ$tV~d 1OWٶXEbxr !*xݚGZj*tU٩noIL㉥<@(6ӑ'}w` ȩ vAkmLHܓ?OFnsi@Do!(X >rkkkc0\.8 *`ddf=d8NEE_$Qpwbꠡ[6RSSb D"@mmm}}0"ꎎ0#Κ=jii) P KP(TVV~!}|O{>zzznܸ1//H<5J⁅^}|( PaXSSSqq1<ett$77իW=}ɓ'"jNj/!iyIjj*FS}VWWwÆ .\!,WZ޼$@u/}@}^ 6+h)F`0SΟ&$4HGcWd+ Qit0V{4r*1/}`6qy=龾vJHHnllL&&͛7'x@Z@k6LLd2۶R)٠;i~N*}+ ]2[8hk!_d}xϟ?G<0:: IDATsޞ^pŋo߾r'Ov횫۷q/**:~xddsLLLPFUTT6۷o322޽{'O􌎎ya}}}w @ssa/_trrjii9{ǣsrrpp80sDD a###8766ؔ{"WWגyqHV\\\PPpA?? :k3330'R<_S<=y,@ȴvr!0bt}6S 3Himq0 `򀾾>͜9sfttNoٲt{uQVVNKKc2L&<##GDD쩩TX_VVug0 kkwڢ?ވ` pssp8޻vJNNNII9p@uu5hQQ޽{GGG‚***~|]77"''ɼO>puuMHH:$@I@QݯӧOڵkYVVfkk~ݻwf?zI]]S,GFFz{{]\\ܹ vjkkxICCp||ٳl6ŧO,] Py$''FݿbϞ=ϟ?ߴiӞ={vܹ}??W (,x glG<{رx744":;;Ϝ8qktt㹻GGGõ^~maaq-Tbyll,&&رcb槟~qvvuF+'[[۽{NMMl>8P( >@8(ʺ@eee&&&尌۷?zȑ#镕...nnnb8**ĉЍFR]]mgg'gϞ_}={;vx$FeH/9~]̋6}5|J[d+MVixv RڧSRe+"s$TڲHb9kiB_.۷߽{ZUUݛ8zo@ )//wvvNNN.**211ihh$&fswwG&C}:<<޻w<'))ή /[EBplllppp```ƍ&t%KKKEqQ-( |& CYYŋiiig0eeeZZZiiik֬~葝]__ܵkˌ<099qƬ,@yf9PWW?uT-qrMR~!H\V+uutOD KvA'Bt$ky/. EBrA8`"Í o]]]OOώ@```YGGGi4NOOOXv뭬 EƌPdd;v܉RsԘm޼ rnNLLn۶ÇhO͑'33SOOOEESQQf2cccΝܳgOUUao߾UQQٰa L066~5wLLL#CCCݻw ¸~ ZTTT={A7nD{{C6nhii H'%%eǎjjjsW;FBN( |<,++stt$:T7J`( 8%J⁍tU>4ߨ,~@Ib A~!0@rO8G9z?eotwrbN/ʣ9(X$IAJ34. WR7KIP $0(=<ĤxbiQ..qWt!yHE< B|.[ %zmBZ9`@k4,T/Mn%"ׯ3"(j(XT^̼x@e4BZq@kb=2s" z^p2$#BE$#u7O $Ȥ+[o0u9JPXZx`i=Oyot!X=D~!Y< $<@Gn }*1 AH/'vZ/^H4{9fPTef)il'Xenkܷ[>`hv }ׂ%J->Zt@J (P ~u"f%CI~! ߨl^Ǐ~x8'!}O?xzN]@g B *sFB8'+kG8x Oo޼z*Tڿ9aXNŃ ςTccc q\ h4qǹ\nhhGii)ygΜ++QakkO&:$ %@'&5%?La^/lRVII2IN/ىGb0666MNNX,h(_^^)CCCAAAeeeaaa544eff^~ѱi; J_((u;_(J9/ظL/<@0Hx`F!Oے'T|^;hKzyELL:BX ylXtVZlq@OlF>0@a{{[nzdbȑ#aaa3---8ONNFGGCB===555##Μ9yO8aXMM?lmml??/ _FFF0 h7ox7olhh0ɩqo߾K. ?{ð;w8p@(޸q]$MMM:t(;; HS$%@!5%?^2_0Darrqݹsgڵ<fũw`uܹѣG<sllLlKd2|~@@kHHիW\bCI _Cn+_CG<_ v|,&PX{6?@({GHt5iom2HSkK6䀨]` l<В9 ͖Wlز}\x⎎%%%H===[[[UUUlގ8r111U 455]q .?|bhhh$ ݻwOP(x"fC_ ~ᇶJ}}։ v5.\nZZѣGΝ;ŋ`(<66v#Gc&Ξ=뛛[TTp*ЩsJJ|*IRP#%@)v7zR!ߨ> )^aM XKrI`L+D0WlA/%H≑M`^A6Y#]CN$۷oؘ@ b"9###))HYY999妤hjDL&y(%@NNiii###}18^XXX[[PC( ,(X(/΋6}#G.i!0 /$SalRw P֑?lCd f >J(a 2Cd=AAar),`<1^^^p8t]N:ecc=[/^x!!!aaa/^q7888$$ۛk׮h'' gvvv3^񩩩7o:;;_[[C~!&Aaaayylqdܽ{-[(655=|0==8˗/޽NG˃|YYYo߾K8 ԇvuuQɅxW~>9`q谰!E'G ) PXZEB]]=++ q֋JVN ZFN OId(" 613|+?:rA/d܍*tUQ7KI8N]]\9>XRRw ȍ:}AUUյkllly``` ;;*//OGGnڴ U#ٴiX;;;߽{wȑ`KKK5LVqrrx@P?KnԂ{ՖS_.I~!yЖ$yùrƁD|0 $ 'R/ )0 #0B;$F!FR3P$%y ==*IfժUo޼ OOϫW*F:N<&Ϟ=kggDDD^ʕ+, oݺIE"?`0 qƍbqQQFGGe ˇ:_rx`=OyOJ4جdE'Dy@FR<< `|)rJz$%PPỲt%28!IXw<`hpB!x,K( =^/8Ό~9C:Dѡ!&p>d06 ҆rY,$#F]#O?r_p U䁁3ghkkoڴwqqő0[BB‰'+**LLL6֭[!J FD"###77ڝ;w^rΗ(Xr.x<Tݫ|r %M%#MZ!+ +(l뎶d#"{DiB~@D-Hx`<@&aI)Kl:ǧBcF, 455_7#==ãIUUuƍZZZAAAt:}ݺu۷o:%gW@WRRBn繹vڳgOllGP'\TL&7c3SSS}tPAqR'?66& ''' _nÇBaKK FKNNV䁬,KKF@&Ϝ9cllb8ǡyooӧbH$ )//WVVܱc9GZo׼~9`jhh244x;wh&EwttLOOݻi4X,WSSK kk낂߽{ՕN8q"888--III(~ðPёl##OR@RD0V]xĉPNh#G2l766Ž~ww\H_]]ݫWX,˗/544`ZFR䁁]]l.[]] LJg\4=z///+@CCGVVI~H$Ç'&&RSS U\]][[[rrr0 kkkswwz*z =f\؏?:x-;;wrrR]]500|fHI`Ks'kXuĉO8'5%/@|oa^<HJ(Ne4%{ 9?$C v*`a@ !`F .*+~W 22<, = a閖׮]...QQQ:III͛U GS}$GGGKKy`#G(@aa+1W DDD8~ooNUUժ*k׮4ǑX,.,,\zloTSSSVV xYR䁤$[[[p ՂlvKKK~~#G]62Y,Vjj*A BiiiŮP2 @}qpppprrr``{UUUuuuUUUkk+t:}Bmmm0ܪ 6ՇWVVFq|h*ꄒ"咇.!RK$$@g쒘v<Dr! H txZ$*>lkd m$Y$= `ԸAvif@ Of,X,^vݻw<==CBB&&&pOOO߽{9k:<?22 ð{444ږ͛7¼<:^|) L@cccbbbKKH$:vիWV>{7Z<ñt>~8;b //uFP'?,u S$9$@Rs<MR읶JrxCQpb?!?HR @ꏴ(Rue so[)a@j W,}JJJjnn_dZZZ&$$|߿==Ԡ ee{xx Ǐ/[lժU1118?~XGGGSSݻ gϞ?߽{ ½{YYY;v,33[NYYrƍsh%%%JJJ6lr 433ۼyp/--522eM'''┕WB ;v?ٹsg~~>㕕?uRRRpgX***666h6)++}}[__{e˖۷BQgꄒ!H&'NoXM/Dd5ђ@ r@bZ,yf2 S`B "l`D['GKϕ:_( ?~<<<|X믿r Z꺔>y@ p%'p3|߇KeMK @{4fزt+_Þ=(z<lTh $ beDfRIQr0CF>T޹/{ (nxƛhYYY(YݨFJKT ZQ}:DzKKKo߾MՒ",ۡOI+_Ƀ6_H?FIJ]8`r'HrbGIb,VaZ.(B|Q( |=xyԝ~IxKz##x@?E7J/$7j>e{`(pE%CZ*`t8[Wk}4|}}WWW߿`` <CBn*4P,N!X Jk$ úT /4@CݹsA B{uu9<<۷N8ahhxEE"ׯMMMmll|||._b={?>}zdd$;;UWWرcPK𱡡ӳ}kaaelllbb#&YSS//^ooUVTVVvN ̲aiii``@U^~-p$?? <<xܼ ð"uuur꜒*'Kח-;͋6.ݰl'P[O}f%#2h$#RjL+QA0-!@ 0M-7;h0Q?&j O<7ꖖ"ڰaCqqX,޽{322X,VHH߽{wEEE{{U;jbb>66&Ν;GX,0,;;{Æ sT ƃ욛O8qE.+/]$KJJX,P(0tϞ= 999vvv8K3 M(l(>>((hll qee喖NSSS(^pA_txxðȠٖ1#8[W^0L.ׇxUUFCB >>FqgϞajmm][[;55%2y}5訙a7n\j՚5kK}KIːgH499M^^^#P(dXl6$ 9{Z0 xl6bMNNBzh#-B- |Hacc N?|0 wbb"Ŕ)?#0r>~8l#'V'ާ@S>`B8&II24{v <R>D Svn JЖv5,P[[ۊ+ փ<==[ZZTUU555|}}{{{9NXXؾ}x= ζy !!fOO'OxT;%E(`[v/Ù]&HJJ۸q;N81#rP<0H:Dozzzn޼YMM(%%E,PP_d﷯'Tݥ.I6*-JFcưqB[I`dtwM@ǓABn Ft"s:ZH%|prXxX|yGGMHHqرcN_H,Zjx~כ8d2AW䁮. . PVVvAmWVVf2s$HsMLL,,,AJEEEo޼҆⢢""hllloo555G/_l8|~wijjBSFFFQQX,mmm0+Q(666X֜ jioo/+++,,_A_nUUU ݻvW477W::: 9=zdbbŋvabbRWWS__{FPo||<884++ѣGW*((B"),,D P'ۉB;U%j-XnEh@9*O,>DGy?@dS H`ZY" q0Ar!䕤l-Ͷ<㸝kLL۷TVVٳN8p 222))nyvܙPTT4::̙s%''8fff'N?~Ϟ=սrJff333cc۷oC?UWWG F\rðVĈHE{…;ws\R䁈;;~z{{tzBBzPP[ZZ|}}oݺL{ECSX<044deejggGoڵ===[lhaaa^^^qϟ?e0HMMM4çOc'NhiiAGFFo߾Zडرcdgq777:^SScmmmggk׮3gΜ}: C--8_ `IN2 ׭[wް`###@Ehii122"3dZZgϘL&-D***t kkwޙڷ~qIrjH&H|5Iy@{7*xb-_hJ`t V_R w~A\J<J_aAƈ{Hdt!3Aq`6xDp!}bbbtttffȈH$v'##… gϞLII#ccct:=***;;fd>(**$&)\慉WFFF wtt<~eee荔;QJKK{իW][/gϞH42]p!&&, K.vʕ{5E(,,|!y] N޽{}pkk͛7_+7HI' Ê+**BCC@%ZvmHHX,nmmh7n_1 |1 X3gdvuu r''OvwwodJmm5k޽{'Ϝ9311tcc+WLNN~7^bXihhd2y<^DDܛ8r>~௧5/lmmE}vXXrPPPXX˗Ba{{ɓ'h4D"у455kkkn޼p(_H(ǦM>N=qj|---bX$ B>qx<355^$)~@=rCMi~!oJH`  +Vb__ =if@B AC<0!2>H-w(zdzzpS9o@MM%ؼy3FKHHtx=)d}`=QL/*7jA[lQTRKb"@YG< ~1Dzs I‡[iD"Q~~ q=zՄ FFF:::===3Nr=˗/|o.W`P) |O恦&|~\l}}}W^|-[b7o͛偲2cc&˓7:}񨨨~aժUd*x`6Kw= vJh(@IQ?`uD}GA{uVZlxH*AVC l^Nc%f #$,f,EILakk211z8ɕ+W455G(***,,7ׯ_>}zogkgّ***_߼yE"āA=8CCCnݪJNSX<0֮][SSCx@QG}0 KHH@- %J &̋ ( TXRl Ж[!'w܁P0RO-PQƀ9Q؀ k̨f#>(xo`II9TSS0G^zr8 x@(޽khhhjj kkk[[[l[SUUU__r'&&cbbkjj`~2WmTTԩS*++1 " jkky^GrvI:<yZSSM6YYY9;;8. srr-,, l/1޾}pѐHcffMMM_|) jeeæ&KK?vvv}}}fff̡+Qsssv![[gϞh4(_H5K4l 6> 3 L~ #hQLQU<$$e%u7YC{9_vl՗NNNɐ?11ȑ#^~=((x/_z/:Px…&900 YE"Qxxxdd$Ŋ8'&&|||GFF 8p޽{ǏaIIItwwwtQHZk>mooXooﮮ.6Fち{A&4'ƈ92gj$%"p] r+W4779::}JёMMM>>>&22ԩSKMMniiALᒓa0NZk׮}Οb>T*! OMME3<<ѣo>zD") b?~lNs9Zj9r CMDoS!X>0R?i@텶ybg|@П?U?hB\4+嫣\>aKI*`rCD" rK.nݺNCkkk ^[n|\n0[YYP(:s}y4 \P( C}}={M&fXz>`eeu:޽{Q*!!!999ud2ݹsC1_@~M6UVVLrpu`2@bYtXx<^|||}}=` 0LBP.[YY:b1D󁉉``> d knnau#Bju㏿曨)sܾ}{Æ lggknڴf+Wlٲ111?sUUȝLA?h333۷o߸qc||<Ar۶mswwDR׮]}>>>6lFC$T*P(VVV_|űcǀ1Bٴi}ss3 D"y?snn% NwrrBsU.b;vlݺu~}ֱЭ[n޼9""XvMG`10L``>@|Z}}}ToG~8t4rpPTadr___۷kW߷{{ogcwsZG#l9þsa/?ח}FMk3¾iN) nVa2`f/oRkYX{ VKTx+90:5 +Mk[Ia|yrL&ۼy3D6LZ/bww K~~Fqtt400~X~G?e?:C-Am!!؛X\陋|>-٦eb<-d{?pںVdUn[n9rǖ8/v}ܿ#ĤYZ?r.j4ׯ:thèrbm0B0>?l;g1>nؒ_ |`Xg  :Gm[П%X1{BU} wd6#U|Ul Cկ? Ӧ߽g/Vne64d~:ڴ\\-T} TW<H/Xެ) 0,n-R%uy#bX"iطK!m@ˎ_{OF['WWk¯(ɆYƥֳ900l@d\`xRZ1&{YO[rMsWb(2'x~N>{sgO>LJZh\?$ Jk'3%шҔdD1siJdz򎞧&IKOWoO$)P>M; fF&;rZ+9Y>j~?1ceo!HmCCCg͇`T*:Nr.ZȈT*h4zh|lr)RrZ)WJ "0==r\D}]<33CӇR~@7Uןݭ?8" Tj('VNfNFw f;й,%DRCA򼻪a1%@Q,^B = 1vé'%!Q} #QΧ'2<&_9ww`vK}䜜?>jfDqS\)R\k #'tR'GDcKjPFQMu?$">DE ("NS,`2H$R{{D0d2*:33/0 2/@r:>Aټ):, 8/67^0 b** T2 ˬ1|wvI?[i=ٌdQ3" #I3BwJ ''ٹ5pW7j}5Z=FprX%+3M5cޡ7"ZZĞ|@&vzADe|˱f/Zƺ8Gd LX8rh_RsF[ר&ϓP'D=ZP>p0} IɓyDTfr!pųr-$rHK iPI-[hSGݧ$\XS茣k-,|C*e\*x)8dܔRK` Zg AU0(n,_`3 A0 7:: >T*Y,RԴxppP(dx$H$2Lp醇 BWWׂ3xLF-ӀT*B!jkk#Dž&500 JaD"OLLd2Xlyl4 V F1t:Lill cbXf[V711p~l6ŒHIAd2fWMMMX,A %<>>3==L(@,aLF&I$4Dh@?  LɌl6&R*` .t Tl6q8xhߗ#>00lA*WYo)yZ>;ҀHI Hu5x ~Dj4wG'B`DVi}lzb}q>̼?gtě4W>뗋@uDlB~ a^p$Qe=)MXLޗ_]M៹ΘۗKxt| fuBjoMnW'-Hs=vMiS7z\:I_p,9nqHkvHk>ZJ9g<ɍ=MN]rSVspI@qXCU#pFOY#  ^aFx?88Adt*b4&&&,7cHӣ/d2 C XVGFFb1 BB ,O3332 B NS(4M*}jaykZR  fYt>o4Lfww`h4]]]JRVST&i0Ba[[X\.H$FtaxppBz i4xFjzn6i6A|b6\%YL,|ཹxWIZp,64 IDAT>Ka6#yex`fN?0#4k4ЌsNV|_m6#}셸<;R;r|ǟUjH(S d&^M6<+  a)|d{Ћ@l*&CD4zu B` _Mۧ6'U N\sաw Nnj9> yϝLk.!p3(؟Ș\qV7_1h)xKjtF|>psWH%-%fv)M = Vx|;aox|@Rx2 |>r i5 C MMM8^MFV ) 6 bp0o^&H^*j4 Z&tHbXՂr6R133344T/b8AL&0 dp8B #00z=hQ* 9X <`.l62 T|>Fq|>Nfr^Ag{qXgQL&0 jεAaX$QTH433 Q|8x~O ݀eQW.Ȇ(R W4iwyIQ[^-PtKR*0 ̠>ggg7@0H$T*uNxB&SSSÃ3332lhhH&Fsdjkkh t:]GGsFpL 0:If'''ܤR)ap0h4CCC Lfxx+`$J)Z=$8:: t?b0t:]$- 6w \NRu0:_(IlQ^rCCCX,f2T*e(dE*i,J&& W,B[R#WP>mXRC97ٌt2^IRÜ<<88g/ ȽjMHmu]9_qS\+Y) !gҔOV Uj $mXȷwO+.*N+jנm%EC</=54F>F76 Yy2qn4o"ܟٌ\ҍ@GAH I?wDɄeZ~*`|L!88G)tAEDez'R+O.jKi:GH;gdiIW;(|XVDŽDs8VJ-j(ƓyGTAr5@jsFƃiM#]+@BPǸ\7|kj >hf]E#` ?S7zr[Xq8&R#exxO.R AǣR@?@&jlp8pt6"d*J`>Ay=,JN " ` h6Q,TP(F 4MVapLpFFFVfH###0 ;!(z5 jbh4`ׄ|6 qBftt備 K%JjzhhHRVߏE@cB0l6NAhdXccc + Zwxߖqn.$7Qf3^08^bV ,EvZ tC`| }pشW-+nǚا6ŔSY9<N'&Gz[k|C#}zLٻ@J 4iDz:6^R .)劋G wJ:91T,OC=5ebxppP.`0p8>O&?88tgffzzz\(Nθ~Aؠ[Rdccc#C$H`0||̓bx< 3L|?L=B&''l6Lh4"L T*ظ ~P@&lry<A@Jb. J%#p!wbIL&X 3 &`xɡ!^l6N<ܝd BL666}@^˟EP_p8] 7z:*C8F'g/+gn,B>ޥ;%xBx]xVqJo /wUF93wNInŕS(nW"WHrHkVZ0e\j:lqz9?7FN7{++oOMރ%9 ܲ;׈>]RW l *(}I'r ~sCD`R%xZ0V~fDYU 셀Br\.GVBXة+JX,JU*D"CB̌X,.f031zE(F <$U*|+ `rYE: 0XH$bX.VN p+JB!,hjH$J  Z ]FEO P5b9b}Azd遻 " \*d2r XH$rd2agffB!XuOK|`GǍ?}$xv?jO%ygş9?t>;$}Jw|{.ksm?M'ܩU<#lL KJ<6^\->vCB`Ku|> xY,юXC`5 `~pC^3 >yY%VkC@`aJ!F jdz)\.Ҽj5O~)xvN[VIQZ}Νͷo_`aa'/eZ=}ccc^`>MMM_|, R\v?شq\ѣ1ɥK}@Z^^aÆ?رchF U*͛׬Y|GKc͚56mCd2]|?裏;;;`+֭[d2O?]f9]1vDO-ξEWIX|OK@:zΆL sľza?_x#">ݻ੼hX,t:Llwr\&IRu:h,NƇaXT^vȑ#a`LPK8R) A_E$i4X fAJR*Rtzz݃TyBP*)DW@x`ɤT*9jE"X,FL&X [:Ry᪪*`ՇNIRL'Iwgnpuuui&>邃ccc%Ʌ x\.|>Jusskhh`2Gmiijqqq~嗱(JkkkarrZ;|0˭qss_|p폋Jϟ_JFACMA i \|aB55tD_~%zj"H((Ҫ<==ћױkDO$!!"i׸/ax|`jj*::zƍ@?p6o| WWWggg711a2RSS< sTV}_|񅕕վ}@ׯZYYedd19888;;_tI"ObqTTܹsg]]d9v옓@\{{JcA߿/**ի륱ۻ A_?}.]= {Yf CnnnLL ܹRv 8[cccpp0͎GDP\t)55ujjp'<Rinn. t bT*u||VVVLNNUTT\x133vܹ&@P({A}vZA FPPCAAAZuqqh Tx <==[[[fsyyC>D"ptttrrI0>鬆_c``DWR|:ʂa?!@텶jwjoooX[[߹sۻq``N_~rrrffϔ),,D`d\QQߖy~v"X\\w幺&&& õvvv333hKww˗/%H *++KJJ|vTթc׮]܌nܸr>Ƥ$kkk;w>|l6߾}ĉx<=&@gT%++ɓeeeΕ ^ "%%ŋΝ>u_~}cc_ffࠛ[IIItttHHHnnnDDahh;&&K>`0߿pd_}DGG燆%&&zzzR(__l??ov)>PYYXZZ:11!===D"x  p2p>x_5A&F" D|`ff&999((ʕ+mKV~kvk  #`2|Q>q!!, (XE^X?H .\B _aÆ8=z2.%|͛\ 6/ZAoH0{pblll[n9rh4߿tcX }]KKKNNN```vv64_[[[RXtW^hcccǎqRI$J{.cX L&D"&%%!288 R%jsMMMر@ vuu_rbEDDDFF2 W^^NR[[[wܹ.1yyygΜILLp 3::J$]]]SF0 [@֯_@EEPA:;;Q@PYYfGsY=gC`9>/QzYX&qE5! +L?juKK BYjCy~ҿ:F:u> `| ..'OLHH0\.N"$''9sFTUUU!!!F%$$СC5449rD(J$Jp 499i4oݺ lnݺAe?^]BLMM={6##q8`0 ?|LJ`۷X,VeTzxDѣ-[t//়80% C$9s&--MTFEEꦦ&mYY߸qckkP(IKKdx<Ӗ ;;|X,NHHHJJ fxxF133ݻf933 D`YR^ ǻ{.pywttlooGUu|4U i`,NgytwT9X&;#lq>Л7l駟ZYYp8xʕم.Vc,SN'?NSըuP(*J,K$4S^t l6/$G%i" v)J=ZUU NJh`QAhKX,P(^B[rr7Y E"eLL0;;B<A(@ X,qt:B! ͬ[@' }BAq0[Ըd2rPY|MKT*а0 fR@a0R)N Rف(@X=gC`9`|-^e8O- w1L/ e-d)`6X z]_0b4ߔl69 tڵH\~ 眶LJ-D+.&b%|7?}$/')Z600pӦMG}Wv555@W IDAT؀m۶ 6U8ϷpJ+L&Ӻu]vǎ Ԇ+2[O拫kMM B1 4tos{o%K@pƽ?^@JӱW!;&7,vؘo'| CCMAU۷WWWTk׮8pL&S(^ GGGz{{% tlv__B0 tN411q8"$D 5 LB!PSp\"H  jzT@@@XXPL&@rM&R4<<ͭkjj PC X,۴ۛy~B~~:W4F<}^߂iϔJz=ev<OW;\~XAf`/tG^~@.퉉QhпLOO/NƄ hrr޽{?~fVRՂ/9AA2"b^`L>c`` @mm-Aݻw=w\rr2@8uꔏGZZԩS.\͕J\.7$$#((!rYgg𰰰1} .{c =zhrrD"dGOOO_o ?77۷o߆ N>&"""}||p8\{{; aaaܰW C`  $4 +l j(AAUpSHzG@-E^xP .b|` دK>ԟ&͢g477;99-~J8.117n-W"""ӵZ-A/0Hmm-{N8!v"*ƍSRR<==CBB&&&vj6BaXXXJJRBdڵuuuH$@ fؘ/ȘvǏ }||bTPPЭ[,MєJ` Hht:O>a0z^&-ZYY]/YYY|`Pqq@ h4yyyZ6,,ӖoKz|%55Xq>p 68p@J\+sFA.]?pq$~8c l ׈)5&f`s0FfVӷt2HZ鏆 IRCne503N\#Xl G3ۓiY z,xB06&233H$ T*B$!RRRBaAAAdddrrP>z}}}QQQqqqK񁱱ֆ$ SN[.$$$66vvvVVUU$22AY߾}2L>p"##򀩱F<|bb"Jl6;22ŋ.\|@._p!>>p*RYSSS[['X1#NN۷GFFfݻ?~ ݹsc"_]] uU7o<|ppMbb"dffj4A@L^05bqaa!矓H$LЀF ]|}}sy+++p'N;vL eee*/\i+RꨂRϯdiiwyowx|&qu*)ڥ4ŔSLf60 % K ʩ]Q(eWFhSZҚsY}uÕ4+VyX_^3vvvEEEF*""bll ڠrV+{ЖRꚗ@MT*NaѣGgϞe0R455u׮]---AV+bAAy6weeexyy577/0{b> 㛚a:tPuuu\\\LL >|ŋuuu'O幹gΜihh(**rssXcbbsrr\KXw 6l߾h4'wDdll1==}uֵ-ӳɓ =󋎎| //z؜?p`z߿fÆ ߠ#;;{ d@@@hhnLBz>775(pmmm  /?\"{!]cmLaWrLo)*8?hf;{FqJ;hn7j群d2Oz4ADr .6@J :q8h@J6nٝ1zmrcbPߘBD| 9n[xVVVwutt2 ϗt0 _L^m6n|p,.\p=A***@=-v500APTTRde˖m۶]677L&, 666 8VlnnpL&S.2d2]~sjj ޽{)l>q'-0\^2,>>ӳ: ?3DrmWWW A5]\\lkk;66f4t:Mjuttt\\A_~egg'%&L&Xf$ ?~x˖-CCC|0,, bA@ ܸqo}I.yNwpBo1d iרkT4\|`_R΄Dw$RG.>}z*{Y[9%YL K-"R+jD8~`N"?1裏N\p{0O?a6}uO?tuuFuuuKA 111Ӈ׭[W_}@ L>~𔖖vE\!FDD?B >|877cӦM׭[_[,ԩS?wa.{ѪL[0 \xq1i`b`d>xHHW_}z~!!!_-[naxxx|qvv]ZmZZ?ݻw[C[s```ӦM}7|xz>@B}'׮]ST6m7nx51>M RӫeoHyDኃn< cl|0y_R#5"/G7T?G.ċ}@E&7zR|%aGl6xb;H[RRr|ٳagg\ь9ӕ{@& `p8@oooN>0ATVV:;;#Ry…BA?sAlNa8:::##A 9sAѨFczzZnll4d*..vww&`VZTbbbPˁWAccbeXq>b|7###/!V!|R+zli|+E:g$potr];_i*X P]n;K+Omnd N\#]zg!!j7dgg/@$_tipp077ƍF #%%e޽%1##G._|9""Btvv9sFVުž x>+쒛+Z)'oe9fwwpƥwDDъJKK|||ʀN=t萯/LFNGGGܻwl۶ {yyeffkqFee`a8##08I̠҄|FCR===q8˗A$"Lv///OOFsNEEX `W00,X%| rXCCo4(X-,2u6mkkkC/4l!aV  #- C`e-bX_FVhX%B.1kGQnF#-8Olq#AL&m5&T*ږpX# =SlEm0>F_#(j0_{V!!0>6F|4g/thF׫藒f\ X&"Ye~5 ?áCG ۸q9;b-10VXU "򁭁9s2؊aoȆl|e(薗_x = d2944kL&Sjj3gKk!!zd0~޽;19 A0X&ܱ13W'm\\ÇgzقG 3'Nlݺѱ ζoo޼'&\.700p˖-7oEw T*ww3gΠmT&&&"""j\._CC@oc&!"{(&XC`9x<O-ӧP(v}PBHLLj%%%AAA૓'OZYYH$`bTVV:88 ٲ]v-:ҥKh:VSS388R֯_OR~x#Ib`|`!V?_h1ɹ={@ |`jj522ڵk666<AӧOp8=<(xlVA޽{p18t萫k|RA0<oByy@ '''M&Syyy```rrrhhڵk/8Nhh[g}699U00 ^0d2(++͛4h4vLzzz1%%%/6ޞ$ """b1N X^ ",G`!Vb>PRR(jddqqq.]2  KMMDD"l޼966AR銊>31B !PVVEˊBOII۶m3L :9rE</^ |Y/^X"hfff9 MHHHNNə/Fׯ_wppXqO~q_[ɤP(W,j:-- XDTT+;,6_ |^/ Zf]UU vvvL>v옓Ӊ'&&&f3DxN|\;;;yXqqc®]_gddg<>>b1 *ĉaaaK0 wuu:;;;99a'aK]X8tVVV SSSz^"h4V222<==H$ CCCKPPPXXJuV}}=<00 'Of0 OOO8qƍgϞ(++uwwOII H.GEEyxxpF^߿qݿ [nyyy=|p9*888!!AVwTsٳ{}__7zwE]WĄ%#9303$(9)HFsa Cf ݿZ3 uNMuwuշzT=" 2MRH{N}}}\.FFFLOO @[[OKK چW^΂/EiiiZZzGbBBؘx! l6;++yttT¬ΧRHJv,,,,..~PT*իW+J0<}qx___ooo__f2}rr|xKD"mooKKK{{{QgV111A"h4PG P(! hnnn ЌA2{00t 8~@BBB>x```wrrz_hssիW***}(##raX$!0w@[[ ?Cgg'677ooo=|yy<ikk_pAQQM 9rt~~~tt˗(tuuIKKϗXZZ Y[[G>Tq /_:99UUU]re}}2xV@/DRچPǸgA {.Qdr˛H>0<;;&}6nb(p<APFFbXXXIIIjjzDDZEEExxxG]]~Datppg0<0>>ի|yyydaaۛwEZZZd2*))VSSkjj<}զ&466fggV___^^# 1|;>v677BAAA,+88XMM-00ѱ /$//5>>nccvKDRx_W555CCC2***CCC}}}<?99ѣx:yӆoIIx}}}q0jnnncccdd䚘 K]>RTuu##d;88OLL:::0㴴.===^~][[[\\ R#-//geedPSVVVYYQazz˗45??b <Q a!+a& xg_bmmm>xqrr"77MMM:uj``L&VWW7 y.b677]#h4By`ffիccc"nnngϞRTHd2\nXXX`` x/..VRRZZZ 4uyyYAAa``A`y >:o|~UUU^^*--vrr/^^EEEeffXmoo'OgggJ{njj200|gXXܜ@  ;fEGG'''%%%}}})))$RSSэ>z_F-[J677>|Ob%a7SSUPyAG:::x$kkk@gmmnl( KJJ|}}===uuu322\nUUI@@@pppTT8B0((I9994m~~-88811BZXX2LTYYfccc쒓0@,[I8ɓ'gffoA?ӓ'OA@Q''7IYY [ZZn޼ŋrf333>}*> uue>%//N>?88Cmll\]]\nvvSjj/@ ; Um^~- ax< +x? `<~t)pP  VFFM322 ATAPH&KJJ_ B)))IOOhhhrAՕQ[[KR\nqq1|ڪSSSuuu[[[555\TT?11! +**JJJjkk}H򀆬_O\VVfaajkkVʊ QAb~]]]wܾ}7/))  APII׻Q&q8LIOOOLL|⢒LvssKKK  ԩS10 /--H}"+$y@هT^^LlMMM666`moo;v D鱴?zzz"hllb,,,eddwvvN,kϰ-D/^`2|>Fb'4,))~xSxࠔ d,?`^))޽HRwrr{LZ^^g !`peeN'+++aaa>>>]]]D">Z"|WVV122!++ G,))>x} S+T@nxփ``***raA촵<==544\ؘ?t89 rLQQQ\\ط X'Z[[эTUU577 wvvnnn.// cyy9 @III]]솄 @ HKKsrrBh "oVPP}V))>x} S+T@Olay ט|l#S`0_(ڊF,thЙ]c ` ` | `< .ֵOR>a:؃n| A||Ã5S`+x'_u>fx'00>]0t٧b:|Zt> P({xxpzJJJ7o޼"}%|>? @QQQEE%55Uw^GGNLLTRRruuE#[٪o}uss3&&ccc0 C444dllϟ633srrvS@ KKKR!V gG|q'Bq?j|@PWWpVQQq[[%.Q' $@ 899QT4t18R*''766FPs;'OLLLQ(tzzz>۞HfhhܹsFFFDyyy]]]FFF;;;ѣG111EEE%''߿D"+++MMMFFFgϞ%`'S@@PjkkLLLqpp  )))666YYY222'v7[l;55/_tA~x YKKBƫWnmm+9p8NRRҭ[Ο?孭zzzIPJ}$> :xdU~ΝbbbJKKGFF]?<<<::zgg A_~%//dVVVBsKKK>?22 ) HLNNxno ZZZCCCM tGGG.:|Q 233s=oooA x<^GG:r}||?p6KdtjBB'A>>>--- N_XXpwwˣR;;; 퍍U~ss333}iiٳ ecccvv799%%%e}}`Ip8\WWܜ B++Vt5@266fcc㫫ׁu޽{Otvv_zAu++0{fxkSSS{{{oo/.a`2###nnnUUU7oDd-**s{twޫW,,,\]]㾆 ϻ<}TMMΝ; ʯ_ vqqP(NZ^^VPPMHHpppHMM566vqqy򥱱UxxNYY ###Y,𰜜O?iccfkk[XXx\CC=ڳDutt<@BJgrrYII B^y~ ׷555(0 .h4???GGU/1$ Ёkiimmmϫաr"""֦ecccmmmggxիWhxYY<  ˴2@XYY)**{. l6;""Xx`ii,aaaeee'ҥK薗===cbb***^:<?-- bud``q"0%%%=x𠶶?00f֠%x<>11Q7}T><0;;v w9k0S///t/cbboddT\\ APBB:0755P|8BBBp8: 6pvv622244FyYZZ=z(==Ap}}}cccGG8q4ŋ&o~KK AzzzOx@|P>66Vz##F$vaffFAAh?}>Bx{{>022d2wvv^|iaa`0a d0'N.7+55ueeNSԕZ^~r;;;O:E>|+VWWyDݶx<0!!A (###[[["8880pͷ` ...~ss->_VVfffF$kkkE"QEEŷ~kaa& ¼8|}}}KKQ5??_RR`08NGGqYYkkkΝknnf2D"qaaaO100Sl6;..@ ,//]O:955uy**>+%ړ_? j^^>}` ?3Ҁb~b EDDH$_={cnn177qd2Ǘw&m6X611QPPeoo?11a``&ݺu+88@ |MOOObba8))_cǶUUUN>YLNN# f˻1;;;---`~T===o CњkkkQlXXX HV d2pe!mlllkk[\\cX^###lЌft sVWWWUUUKK xx%ok8fWTT엖:::dr{{ܜH$nll >ˠMMM s%H(E555giiiixx(Jgg' \.wtt 1 `a8z0<33S]]]YY0nggg* 6:;;V TTTա,//'mjh⪪ @***˫|>mrVYYYTTTXXX]]DRI$hd[[p-ݿWya"ellIӻޤM`NHH533=yɓ:::fff666$irr@__z``DD ^|yrrR$IKKe_(11uuu>HHHPTT455mlln``*CY@PSh4jrr2pmҚLHH|555JJJvvv]D---'NpoRuu5DByTWW799\[[KII133߸իW(K***< [[[f݄]鋋$|~zz4ؿ[9vMmmʊSRR,@ {9K y HMM&DY,p8d2yxxxnn2R(844c"hccclllhhhbbbyveeejj ͓d A2xa2cccÛo: '=ɜG-axx%0=:::<<%FFFD"s4cyL=ؽKahee%x{|σW)))):r###}Q hllht:=00^__cccKKK(LNNZYYXSSS,JJJ<ojjTVVBhkk+x<Fkkkr~~~BZJJ }y@q_?pR:榦U.]|gw0w Y>tX71>` ;0hnn ֚D"+WꚚ0 p{Y[[ʢ@2oҽ{yy_FFGy,ח>w! ;;;7oޔRSS[XX` O)++KII0<;;kbb`ggwY>s1t7_|⏟>6` ` | p8OX0>>q` G0 OOO2ŢhAFv?x7 4RB>@`0ր ë333KKKLd2i^$mnntkkk^?;;; sssև? kgX:}`xGQo4lr_|EPP >>>]~~~@jvFiii)**ED_ [{yyihhC4::;wgffGJff&n2[[[oXA\! 322T${{{2,Ճ)#vS#TくpP&)@~4<_khhPTp8j166VUUUZZ:00P\\L"xxx///ǹa01[k*Tjuuׯ[ZZ.aaaǏ+**B#||~~^II vɼ}ŊjjjD"qss3''````w=0 S(qӧO ccc``Ձv.ޞ_PPr233Ah4Zffflllgg'hv~~~oooyy9Ԉ՝;w999B sss---mmm`qׯPkpYY.%onkkRNNN!^e0VVO6LVVSU@8ᑓ򀊊Jcc# w%fffZZZ8N={fffd29;xvwwpNNNAAAYYY{-z >|~II 駟BBBbbb(k7n(//ؠ7髯akdؖ[*:###utt=zp*s<!RZZjooikk`0|}}=z6IX]]SSSwq℗׳g@XQWWW___???+++SYYyMPX\\d2i4fxxxHHȳgPnXXLА<Ǐ{b X-V/wddd!  ^WWwqqڵkt:`AqI{‚nCCH$vww+((yx8 Hxxx_0OPBJJx ?22B jkkBH$111aX`ccc77婩).kjj' 222X,@ $H7L&ӧ(LOO嗳<˗NNNBpee͛P(LMMh,KYYniiJ4b±c 6>>^]]hIMMJgggXXXLLx4,)px ` |t<Ν; ƆzWWDXXXvvv\\\BB4X$p֭Fի/^trr'III1A.]"JÇV(=z믿>rݻw :7;rZL?Bܹs.\&|6q5<> ,...--~<AP[[ܧnOU>ՑcWb |* ɕ$BaWWWjjjzz {F+++KII x@?gffrrrVWW\nyy9S)A&ѓ>r`X---)))/_{ G3|>ڵk8B^=?NMMG<+<xwט!@yy9zf##Ǐ .kffdaa _dZ9hkkp>>>㠧9aɓ'o[:@x]T<9Do@(((`X"($$D_Ҳ>%%E <?>>^QQ񯳴>} H$wHR<<AՀ[[ۀ>?-$y ǵSWx(..NJJ{%UTT mjjjnn윙 cccǏKKK^xl6랞8.::zhhHEE%==Ms#rTUUKKKƏ?Dکt:}qq555=>88xkkkaa뾾CCC;;;!!!666եwͥhͱ)))4mffFAA!&&&""9r%**ԔD"K00ŕssswޭCiiiy`nn>|888?hddS#Td 涶?& W^F|>KSS l6;!!UVVFEE3 &&&BCC>} p__zRRRccc]]쬾U__boo/6Ҫ 555...|o$ȟо677\nvvLVTTEW@M$ݳgSSSD"hTVVv % jkk~u`(++x<`nnZ\,< lښcHHIJJhz ǃCa~a^^3$$$00ND###Pnii922>FFFjiiqܭ-kkϟ3#Glll0<>>niiYUU%N,))8 +-- " y$v:88hnn^SS`jjT곸r֭'O>zheeeff¢D$ܽ{^~!8w rppx0r IDAT QX''Hdnn~嗾>>>PXXXEӰMCJJJ7n C__kccÁ o6 `yy===MF\ׯ_ %ZZZddd@첲<==777ц`uu޽{O8!&}w>0<<W_:uJJJԩSGeX!::A܎= j?y]*??$q奣#>lccc׮];~ɓ'ŏy`<066=00_fܿ=:: --}ڵF2lnnk*A׿:u~066Gw󀛛D|2Z^xyy9rǏz*#&,))8pDh\'Oddd\/lǏQ ~##Qt]]P( 755+ʱ ɔx9 |vvvP$#~'АyuuuBBP\.\. èP~~>_^^a . |>aPð[wwwPP(<OFF8Skjjb믿p8>Ң?66_h{{fadd˗'&&SSSMWWWY,P('O恟ƍJJJ wA  NNN`!g677}FMM [J0ޛ؍0>Ocbbz||

uOއ{9Sc;رl {c:LѻM  鉦.}DOcٳX{~/fWUU7,+44yyyy/cX?JbXwww:joo?:: xssV|H#rrrWVVn|db0( IU^mP>FG8Zhnn| ..a"3330F733 XdA[[Ett4XDLKKgϞlPAh``)))a577;880Ljee@x X///{zzLJ^%@&922>ann^]]󕕕---wClll?  FNNDO>qㆱqYYP(711H$*))ڒmmm===MMM_z%X<(T__oeeckk Apwwwss@Y__ ?waaa!B쉥#Tnpvv&HښH$AB/QP6(x-om PO~P?ꧏ /t|ͻ\ťuvvظݐ,vx|uu5>D"1...v7-222w x\111qqql6R^^MOOΞGSSSuuuqqq111eee}}}<O1+++͈NH$.GRRR&YWWx) K fP7B8P>p$0@%!? LhQP|8h|w=|h!|@^F@@|cր"["m^k?pѕWЯ`B""D`/DHa5#A|ঌ&8pdBѺ-W#b NE݀KWDsw$(( CAB8I>ڞ}OFM qpp8O)))I ޖL|88^??32޽[SS&yPPCP> $C@!?4%N>(-:;;WWWa$|Dąl>drUUU1GQPgP>>6|`/&h c_󁯾ĤL$AAA bmmdzzz뇄$%%p8###[[[ m??O!''wPY4EE=GCBR(ocW^?pWOCt`0򁒒 C"\^ff&rsssuurvvv4m``ҥK$IKK+>>ᅅgJO& _Meee+++#|@,WTTxbh"1B[" $@(o|crߵcwN:522pYY^>p- ꪨhoobcbb`nii\^^.((x! L&>bٳgA'++!555)WD'|=(o(4'^Qj<'Osd b`nmm|HMM u77CbO|ܹI)~󮮮"`,//lq4{AA"p>K [f  Fh?v[>W_EFFr80|zppP DEE H$ӧׇ޽ cXx_>:>>#'''`?044W_q8-f@@xp8###G6,''QRR"^?[[["H˿>^VV&7s8OOڽ_|(++VVV444ljjӾ)---X,vmmm_¤ |LE7|B_~ŋ>|pHHΝ;WUUҮ_~)ccc/ ٳ:::ƀ@pӧO~I48_}՟'Nvvvjhh;wNNNh:{X˗/pVwww*6ED q;:󁝝SSӂ 5(K\9[2Z7v;:Z HOO?PP>`lll:u E^[[;s͛7sss|~ssGtttn߾b|||bbbhii) 1666D>viarrݻwggg!077o^*%Dܾ}L&C477xY999' ӏ;&##Ѐ{ZZ"Ñloo{yy;w… [[[|L&khh9sFIIR$DDD<~H$}'gϞo>}f__A&&&Nz*@FwA?}G ~RRRuu}KKVֆ""#p|ĉ{>422233cX^j ϟ/// EQQq}}9;;ϋbMMͤ|k(?zzz%x'|i}}}kk+$$ʊ$''_v ///''M,켺Z^^gIdff^~S,{{{c00K.$IVVӳ|ll heeSYYiaa1<<9::*'''\|B;wfgddXXXPTgggwww_|g'{˻\9|=Zzzzlllp8>PP>`TUU>}ZRaHtQi4[BBBss:P%b0?nllknn&ϟ___G" |贼>@R^xCP}uuÇ1LII bʕ+{#x߿?77goopxazzA|၁MMMduЦ:/// `?0<<|ر'CCCtttrssR$HHHA??zCCC<򛛛|wAnh H$qvv+,, aEjN~VUU%xH$*,,tnoop8*)--bsss@apHAP!x%ؘ@^zYZZZXXxt999BX^YYYHa\A}DQ~G|Bظw^hh(655hnnDMMM0TWW[YY]pARu-"{GLJK!XXXD"PΞ>}JUU}ښoll,Ǜ0 ooo&966fmmmSRRnrrossskk+11QWWwddx{{X,X 1/|.KR7771 B hyyY Z[[0{ڵ!H`233y<EԸ.> /)xN>=;;(dee _#$,aW^WW&5</_ZZZ*++c@s)77&''z~f==t77ϟt` ++uHg!B4o޼sCCCrܹr7p^GGǔ|grrrӱ B>y"!!2;;ᨫڂ]YZZoll ZZZΞ=|@$uvvp8@lA;:: ӷ&&&piiExxURRB2wIJ DDDhiiMMM>088r ݻwDP(/--kkk ZZZyVUUaxxx֭[``mm/((͍L&0$++{)55gϞ1Lkjj#""1333&&ȜrttTmnnVTT兇Ā $I <ظB(}}}ϟ?\+444??yyyEFFKKaﯦv8P(^VVVnmmJHH RlHMMuqqǴ566޺u IR_D.feJijjJq_`-@RVWWbH$Z__ аH$"&YYYLwEwP(C #4 ,E`&H ЧIPǧI$áh pxN1r333D"qzzzxxFR(*E|>vvwpp`@bhnGFFLH$"H@CA LNNd. 4>>.x<L!`gggddoaaA(i"8>>N"vkhA`8=-8q͍D"yzz>{lkkKVVG emm|> KKK8nggd^|0 `V}x<ښ[xx8X.--@R===_xfXx Ғ^FF`svv]\\DR«W0<::qƍeee<O<<<1Lxx8mmm%44t]T8Y$뗔0 __ߤǏp8===Ќ5eKKKZZZJGGi*aD6xYYYR|\^^0 @!:>}}!  oaĒE&''>|- ̃Q?,hP[8q x쉇"())uuu1̒1 n&955㳳#""6pmm-++eff䙛#L&3--ooU&I"***Lfqq,LNNbHٌE& Lfss DrssX,BA]]]Xl{{;LHHXZZb0X,6))@ `NFh4innf0t:p`ᡡQRRBi4_VVwqq83L `:::c\\܋/A*jjj@sss𐪤a_Ȉh[AAL,,, \"X^^&GE+A@x|m#"/4^}!j&4hooGC!oL6d?rd2р""!Q>۬ѻ|x}KT?BxvEE5{ ((>u*ڣ\9^O,B EE^>ۼѻ2P>pCKKT{b۴EEE@}}}FFj?~)Q~/|2Rh;7~P/r``` \.ͥ~ɛ(x߾[h{PB!O>!D>rhhhN{eoo:Ś-,,jll\ZZbXT**##L&XU<;::fj򲉉ɉ'Ν;Nzŷ~{t>/U\BBCCAll,7?Hp1ccc*zH~'w^oo/ óǏ)**BrIdeeiii0 h4wwSN9s&55U;Hm$IAAĉzzz,3 b``_~wHNɈH$rw}0:nUUUDd~$f322Ν;wԩ0:_Y2d2N2իi R^^<ĕ.:H  hyyHljjZXXͥh{)o<Aޞrrruuuƃ׮]+)dE"Qӧ%@xx8DE/ㅻayyynnnaaJ,1::z}}= E,+((I+++[YY177_XX+++ȉH)k52}}}],X9*/ H<|pjjBp8ڙ1 9SSSDR\.F s-`x&&&W^| 33mqqqllYTX|dr}}}X,pɔ*͛7x<8:%%ihhDFFr8SSS6`oo@_ .P'ODEE B|fzzٳL&s{{HEEEII >A---`\~} ðqTT@ ֖@ XYY[]]=H333t_PP`0X,UTT())/"85quu͛ (x9`j/>d2yfffaaa/5 õ`lo R)mllklTPPhjj'4wORLHcc#dXT*5''`XѶEEjjjB__(XYY`/#骨եz{{jffw 666RSS>}*ttt,--G s`fL abbbddd``/9^\\TTTlnn&$ `Ν;W]]A$W^ "/\V/_#K }_~9;;㣡$''WQQR&~###8իgϞ}fOOԽDddcJ)**wppHKK htttzzZ$-//:ujfffoO>{{鎎etܾ333<'>>~qqܹsݍKQ(T꧟~ RrrrRZIIĄ}ttA|`ssS__ǃ 4//'<ApnMMMmff??"矃V]]244211ƣGSyMMMFFF>tG؀K.X,|oh4A#Uq82Ë~@n@6::*2HxSSSmmmoY~/ا<0 }}yU>jo"baTe'ޗ􁁁6@Ox<@.**Z^^ikk\ZZollP(===o"P(]]] 8111222$$|`x{{K vĉ nmmEFF#]]ݬ,HD$`믽1mooX,v횣eNNmNXegggkk R]]pSSuLLP(И}vii^>.OOσt% !!!+H|vvdeeQVVfgggnno^{aaauu'O  555Wrܜ⬭FGG]fxmKK <riii...2zbbbgg'""lvvV NIOOy\]] HjGEEwtt>  }@ooŋWWWKvSYYdzzĄ9$7 Er+ uVeeeCCCt:}eeecc%%%kkk}AZ[[0<oaaAqų@/^|$#C$~`qq<''hxxpQֿ)u7 8쭜L&'{%Svvvv&YPPQZZ 65$ "񾾾!6p8—/_VUUQ(XH DW\ٻ) I$RiiiNNNqqH$*++300puu ãfffUUU8M"hnnIտܺU בv"/u %UUUUl&)`0UUUyyyMMM@x|z FYYMLL" CCC`0x<"|@| G&Z[[***(JaaDCC X7777mmmwmkk344LHHbqqq &00Q`C[nʙaxmmƍO><Ν;n}7nnn<tT$޽{wttt}}ѣG%%%x<͛mmmfffQQQ...{A  b}}Z[[ eee$I-rX,F$I:lmmmoo`0$ gΜ322:Pxx.[pggzUq֭Bkk}@jjË/BCC]]]ųg KYt:;))˗&&&Y>```ڊLH,{`0[[[ y|8𰖖VRRCii?H$\R__ojjZRR"ٻ5󋊊$", ‚>}啖F$ϝ;I],Ďɵ>N/--Hf>s=! i-PVV`0FFFțww|J\bddehh$$3'H2m\nII1"(jJ6@,,FXNLLGRRA|`}}|***헗i4څ 677% pss Aؘ^T%0 | |޽hI~HHϣlvAA9=~xdddyy驪:88bCQQepp3JExI``P []]500qFdddoo/777bSRRRX?T_hR$ >p122а߿C燆Z[[JJJ( Pq===+++o/ɼ{nmmmrr‚_k)>KaqAWSSr:~h+**$odP(۷o7ZLLL`` F,,,@HUUX_pF%%%yzz.//K򊏏r[[[ ZZZbX(>~ VVV.]zQ{{(Y\^^VTT2 ˟| #Hl6;$$411qU>411qNCCý:BҒL&%&& µ2//ԅfČ.//߻wtp>d2jkkSRR>}~kklmooc0 裏T*/D"ѱ_EEH$vuuϫc)`o^R_ l]RR200p…4UWW"`0+++V<onnNr_M$H$/Tx/Hs—_~9::L&Juu ^/O>\hdd$''gccD"9;;ↆBɛjhhʼnbI 0Lhh(ؼ Á  X,VnnnYYnkk~Ivv6ͮWSSdwÏ= =XϕrEEׯ ¾>''\`z433 Wy%G`vvVMM-22X|-//wvvs)++wwwWVV333D|>`dIFF x<>%%ennѣG...1'bMLLp8 bccß5szzzAssQgggrr#͐KKK4ejjzfmpABFFFsss CMMXb0<_~}vvV 444lnnbM bٛϟ?񙚚())D"رc(8Py#}!533S[[K&)JEE (..A뻻=qEE[YYy}!X￱rQ[[kff6n޼yь]XXh}Mw0B0<<455MIIZt`̰`RRRL %''K$Trss3ϿsNYY򲳳s||h fXgΜioo{{Lfwww[[[KKbbbb`` Nh/^PDWfeeX06naaq4|,Mtuux6jff&` GWWgrrRUU CPĐj*A L&?L[nӠ 333dD"bJKKBPAA!??Nمx{UTP(|5;;-hJJJ4-444**jcctLtt$Pp8RkkXʒ޽KP!999gϞDO>i ؖ: {{{,88,>( 322bbbQWWC$.`=TXXx Aӯ_Njjjlmmx'###VVV111zzzUUU0 ;88777WUU~dѸbڵkR^VVV{zzh4 x^>@"~GcuuJ||<`MII >lnnD^9yW:;;%ezyffFKK+33I/I"*Yp󁮮svv&''Ij 慄 X,ill |b`W\>0STTddd!++illΝgK; IDAT!| 66ƻѣGBڵkJJJFFFw޵lmmasΡ|`߹{O';nʼ> }nQQQaaaww8xbX &??bjjbttt d2ҥK7nܨ `DDD(((\z5!!J^eddNjTRRRTT455z&ݻt钦fww7*VRRҵk###2(77ի z>>>W\v˗/!sM&-,,A&ݐ___?s挌n000ӻ~g}vЧ@ ܹsGVV(\MMM~wmjjB>ɒȼА;w߿Cwm JJJ.]277Gbqmmbii)p.]  p8 333...͛;@qi/ZZZR(p PτBaqq6Ʉ b {KCCCY,h!BNdvCTT nnnaaa 88\ B</==v{{ z0^ݐN_H$JLLTUUH aX:BL`0/^^<oss3??_SSŋJJJE߻wN ggD. EEEL&SR_W3g΀xWW؃^zt\$}ޑH?h | w\. =hD"P)RjoY w7d|>=@ ,rw)dj) #HKT R +> z耂A C H H~xY҄G`=B:Z:mloGL>–b0ԂaZJJJ8N]]tĤGGGq涽3pE28"CpSNEE̙3Nӟ ,"0 ?o'Ο?wߝ={ mnn655 AUUU1 PC榟ߧ~no RSS;y򤹹d /@@$ܸqcy>T*DWTT$H5"x< π+}}}H,pi7iP|NZdoC>[PP),,kTPP( Vbb&BINN~>jddɓ{7kjjt쬥%Y]](P|`uuop`}}}``d.X\TTdll D@111*** "hzzzhhd&W^^G_0E$R#|$iO ԔV]]`FFF0 p;;;SSS)́>._>v…*RB4hb>7"m/_ZYYŽ-deelO> i` kZH$ssgϪٵH$ruuo]S˕|@b\]]Ϟ=7766C2 i9u˗occ#00… Ovrra:u*!!akk+66ҥK'OTQQǕ߿ݻ@# N{&>E;""k8r>0??Y[[秫;::lll[XX^~|._`0[/ R|`jj˗񦧧;Z& x|}}ZCCÂzbS{wwwgeel;;;QQQ_upp0X'''kkko)))@933@ 8::&%% H4@ARi B`kkٳg`-;;ӧBۏ3tuu׷;99s87|>Q< jjjɄTz"A"^+|`~}sz @s6䠝z;f/i毩aׇ߲I8r> ~ttt\\\:;;GΔaL&3>>8E;:QUU|5 [zBӳXUUh9;;vp@ӛ{ BxXhŨnl F UcVi]^WO-i~Tx`?qP mN }V9jp`Awjǽp;rz#jC+F^Yt̃ab$&&-7/(̽# *F beggKd% 999###``677K,{{{322bxnn.777==8ȕ98R*''8 YYYHloo&''95ْ^^ bp8\jjjSSP!eoAA^V\fN7O14O!y7Q\7 GL@P\\,qA[# ɾ'~OBIMMMJJJKK&,˗ٱmmmL&see.==,222b`IBr3)k>s,-S; {)ys S6oR4L*](TQdKd P6|PFJwZҷV~5Q9DPB+G['s/ seZC[Z{\r-${>m&0W1t?AE[f;9k>\ڷp-&/$5 _`ڙ9a{;x/>_ie{x>?mLo#_4GLtQl9{Mƍ؜q{-GFF@|̧Ozzzun E"aaaJQQQiii111j tzppHH+/bH?{ D644xד'OH$Z^^~>22? V]__ '''hVC{R9tCmFF؏I7ʄ]ZWtXa7Vv<OeQ4 TB%{+靍i3F[$vLOjzӓ'Ojii9;;2Jooo/wJJ 8>^__MTwy% =00 H@X֖f|_~ebbB =yDiL$%''ϟ'ccckkk999pYPVXX{\xq||Ãx<, g&( +W⎎++O< ZZZPv<8?0>>.>|bܹSWW''&& Gm[8LQ-rWc? )df)詗)'EWr htQP z*5 +эc@:sza 9 O(! @;r' GGG|}}xfmmOccc}}3g} Bݻ 5pdk׮GlSSӬpV:/((O<455)<}yUssݻwAW/_ Wlll@w),,wbbi4څ ̌sMMwMM hljjvbbh___4mbbXlllQU~K94ps@,s8ʂrͥဆ@AAƊ0+#buuuZZZ~~ׯ322{{{}||>>uuu.\P lll\YYiii`0D?xر .t0Jmmmedd>zDP~zrrqr8W^YZZ#2kkL`333 F[[1111666,,ٳg>(_zb=zמׯ_/** '9}իWSSSKKKo߾M"9RWW@ $/<` 7.C052a#wҏDyksjvq4~RN,n#i.5w_Vɯ~;茶;85ou18sU1gU0ZoWwOT $Lcȼ8Lñ2<}O@5Oẗ́V ?(,}re9; 58܎+Id.=~4j ,&׌d*|Zt)Ed51kGɩ詨Q"j!TK^X5i+!B&v:9/x>ɓ'555QQQкL>wDPNNN%%%H$===fffP{@"~'U__odd4::D"ALc A{(]C`M@~$a 6a4Ax05 ;@bށdЖH7c7EAx@"LMM577, kZj8/D"illDl6{Ss9q_2_]˿__?;蟐PSӟ>39;gOfv?3Uy *o*ȩި|~g7ukW`[w./I:'ވ] IDATMu>7?X^^pl6{iiŋ---l6NxL\nQQQaa"aò[#suV8܎2x>è֘F܊fLY,y8 wMp'IٚWx%wj4xjdHeRG+yd z"cu'C?_8P;8j `ĴE1DӘ٥[[&2Ӑxyk^LbQ8M嬹ywL.ތFV 0Վ2B8rex49cmGЈ9]rCLԮFxx:S+jGcѣG boo- I'D  &&&jOY,ٳg\H$6vvvP>vh ~/))YYYobqNNNppFB$DEEΝ_^^ LKKS moo_WW'677ϝ;G  ׷MMMB-]]] "J-,,&&&322@ݟj666,,,⢢"??Y.Db]ΜPoJC4h8O 섀?aDv9o>]룫{`'7HHSӟ+$f ⛝|O[::m]EA[7WTP{Pg򞿸}NwvFmW羸 sKss_' Nn+$~緡QTC?'&&+**={F&|~bb"rB\\\LL D]]]MMM!!!KKK䨺r<#=:: X$vjpS ڑnZP11kȀ$]xyGNP1N#2{nƴr:g93ƴfḢe[zh"B~L*zJ-@dT1 G$Cuq9jfRbip|D?{yK\2Q32v> Pc|x9_8Fnd{7+++??_(655677暘[[[nnnaaa( ֙x<>##MUdSSSnnn/_ɱ+@݃jkkHdaaBh4ͭ(%%@$-..EGG}hvvksssttݻwE"@pss433S*BCC]]]*.X\UUVUUwP|!wwzD`"""\\\Ժh+M[eR ({?Rc@9Xq|ؕLVr6w}Bo!,L`B>P4A@vN{cYz.t-\.OIIINNb=FV ,kii ..ŋ)))===-111%%ettT 'h[47XŽV/QG\zV =@xHqvgk'; <:WM'2ëGnXé7phpqnH`=^=0PD0Rv&=ȝ'X]3 qQ:X=觻d-#Qfq!Vy&Etdh w:o? wTب9Wp894""D"oooU%J\]]@ɠ OOϧO %9qH$^ ,";;{yY} 6 CCC~~~uuu0W^999BF{(.AHT|~DDlL&,**ruuQuz:YXXx!`cuu5!!>$$`@-ւӴp@Á<_7H׎P}+7@-'];ߕA㳻H@~$ f},:t/C}0<DG>.ƀB1}]Ǯ~-x@*U!38*Ś?&4xxW9~! ȭ!{!pNajy`H$:~%t9(ռ Ajli? {'C'Pj:p@lT9ӣoN~O{r3:{?d THtW*{%[ÞB)݈C BE mϻ|.;<<\ZZZ\\<88r|>N*,,`0l6Xi>pz{{KKK'''G@@]__OOO9qīW:l`&&&VsP"[L6==O?yxxE;;ի9D .hiiEGG7ӹׯ666 +"Baa!~}_~svvV1]YY _utt:::d2P(9{cRRRr8''G^|yppP&''' { 455MMMVUUfXloo/Cѝ|>tt 邂 T텤R) o߿bkk5@V8E*ZYY=~X Y[[ Twww:!RbPPPddH$.--?*,,,TTTP(KK˺:Xlddmmٳ&&&@H$zzzD&$$:zD???pР_uuu/L666>r䈮nee%t:}kk+++Ν;z(xT*/O;T[[p8YYY EGGZYY 577O=jgg'JAԴ!_r隚;9Xz, 5J}ї_~yP}}'Nd):),,tvv`?ouppl+++"w}Ǐ~A[[JBB h4D"*hAC]= ut{v8ʒYҭlr&Ps7A$O`/NG(^<#yX^^mmmx|>D6l6{lll~~3̪*ݍ㗗|~mma# %B=`O< !lll|||JKK---P{_ottT__0ܼySA8Dv _xCю9~WWWC蝝gg簰0/// X* o˗/744hKKKTUInmmh9666XlRRAWjObA''F~-e2ʥKźsNwwǏ333'e.144dnnb ܹs餦FDDP(---`UUb@zyyeee9r: Sg^| c``P[[>LY__,vZ[[ׯ슊/mmm`A&-))];w _(mxqsjxӊd aDDDrr-.\HKK&Hǎ[\\ﷰp8~~~$<f e ;\.H$vttgzz|qqJ<'r(j~~spppffbhAxT ccci&&&\F ===kkk{{{Lezz:?T8y$񩩩y&x-h4'''w2 ö6pbq8|KX} D"(++b%4ILW^^?77gmmPTTDtt'OT%Nfnn>66F CfffdddrrJE"@yttTWWw{{Duvv[[[ggg֐d}}}6211@ rss T N UYYiii988>??ooo_ZZJfddf [ZZΟ?/x; ^~O&CCCº1goooUU/A\G ,III=R,`0lmmX,ñKOOplllzz?22>( m//wnx̌L&P(vëTpT*76ǃ+[$<\o#J\.$.d2T9F",,,loo4IP u544D"Ϝ9cii@ 455.,,p8:88wP`*++K._ommqFmmcTTT@@@cc\NII[{{҄AffwttӧOi333༫zzz T*H WVVP(yj`X}[[[ Z҆++က8U~.kjjJ gggMLLbcc###(y^^$l/bD]]#ӫW%TY\\r Fkhhs 陝v0>Vy<~wwyww7 h4ZJ& _xN'O ?~|eeBuuuARcccH$299B=ztaa+//Wvcc#,,,99FaX 288ɓϟ:-//߻wE`hL"lmmՂyfh{{{ ҥ67|ኋqpcc#B9hC\zgA,PKD`0L&reeejj FDt.$pIY6`sssll .@b$@r R#lmmH.^X[[+R\bLܻw=Bzzz`ddD[[{ll XYYuwwGDDXYY)`aaOd`vq?U>뭬|~UUԔX,2,++$3KKKb1U\੅\zOVWW#șc@WWkgggssfS(@GGG5RbX(P-Td2{@tss'Oh͙D  ĕf2+++'NP5R٩wtt$XRAU:|H"{4 8ѩ_^^ ]YYqrrJNN h4pq`ܸqdonn^ttcc###DED?fCCC?0{nss*Kb{BB@ bW^]XX(--E"T*ubbٳp7/h\H֕>}±cl6-Hdii!8NZZ?ROԩSUUU|>366vccŋjx<n^ՋZ*XYY ̘p3g466xH}(H^~ GGGO8`0(IOO2/b8 **jmm̙3drrrjjիjQH$211^VVVKKK111І޹?63]vÿ~0#N8q{ DNNNXXX,--MMM999;88ܿٳg;;;vvv...T\.fLLcddɹw/ ;88ϟ#KTJ$HY/^hccckkkeeE'_5뎏:88$&&JҬ,GG#̹0WWW$ioo_QQ!,E"QEE*_ εXYY999b#H'ennSPxH< }||?pJx`vvիW$;;ƍM=ā]<GHQ?"P$#;ٛ2:p /B~@.WV@zx!^ߓws̜t>n<jWww"ikkhEEE Ft&!X TWP}֭'Nyyy_}իWo).!awvv޼y޽{---RtjjPqz 29H@qss#Rt``̬J-!5AAA:wtt(nݺxO?b>#]]]UlSRR@͓'OnDiyy90x Sk%ɰX,hckk X߿T.Pd2YooE]]T*RG 222244tvvJ$BJDBй8CpƆJ- rAZPD"I$Uhf 5><(𰳳3hVXXο###AMtt̶Kݡ omm 6|eeB, ڻp84JC ===@ JKKKx<ÍD"T p&xRBt*.Ca0p,O &&&}}}>>>5333wttaN( p8%-Yoo/ jmmj$?\YY f( '''q8R H]]]T*U LLLt(MXL&gff~ X,Ȉ*?, TjWW.(=fh4~R)nnn VWWq80rFtX d2Y1 K vi}}}hh2[[[w|33rNbz:RtmmVVVd 'Hx<`)ٓJ,//YtR Aye7!ق]\\*ÿη*dmmmǎ کjF 9c} c_?ɾόu ⛧JҼZc!"n>GPdOCH>TkPj GPo*x_ӛh8pxhyo)T.D_kwh{g!>;v@OMN}"PX x0CwSI޻ $s?{r}Yy_z|tTe`JG7ġ_ԛ|x_9͊4 rL3s  ?S@CCdzy'T E"PS`~O5p@Á4xkH?h@~@8-{I{ʮ  j%7Am*~ 0}0ިl$9X^^fXL&B4 p\*:>>N&|>c0 /&H]P? ɶDbcc#F"hffBdN B쌎677}}655 ;m<Ѐb q Jۛ'''Mkll{habF0 `V?755갳3>>\$ HBP2mhlllooD x9B& -,G(755h V1@W$`|誸cݻ...Cq999N&[YYyzzP(Thoo䔔t}}=::DzzzIҼ;wx{{%}葋T*MJJYՊD:;;,--EDDr PoNKKS\\w iaa;w|||\\\JJJ$IWWǏb)|tPsvv~D"!HH$@UVV[]]HCB0>>DT*-))SPPO:;;ARSSޮ錌ݿ@ը(DsrrR-!A `?{Lif-,-++!B&A j9R|p@Á?A<=~x@ȠMCwHnbHVjxC)|R@R=WYO^on<@&[ZZ@(񥥥*LNN 6 bϳ첲Y%0V?QommQ .P(3yս" P(H$jii ZYYAB%Z@777SRR=zx޽djTdT*U__|||yy'''gee b}YY)>V"׻0qٳg111b8:::""byyN\.ѱJUEcoo_PP3227337??b~7$(ooo={F ,+((())i}}FikkS(UBPhjjZSSŭ[tzBBBhh(e0য়~999~~~\.˗BpffF[[[~d@YYY IMM]^^|r{{NCCjxVTbmllh4ñlnn<~FbbbXXj+XC377;66699idd499HdAA*>##᫪ ƍwnx 6|DDt:ss355lx+++J>xMp@Á?/p ^~Ouy~OnhjtSSOn T/4Äx`,x>Jq?a ПfRa{ W)7 ;`0L5sss E1cX]]]D"`cmmvyiiiJJJXX8[HIIQ+ ٳڇJ:99544x{{WWWBCCbUB2?Q bjeeڪJbaJJJxx8@_@AfffHHOrrٳgaYYكTcR@hjj[SSj"##Uߺu d ܼ{nEEELLL||< trrz|0?? ȑ#`]]]L&/MMM5[?;}43:55@ mMU(yyyY=z(..nllLKK t﯊VWWCCC333ׯ[[[-,,@MbbgTMqs>550ܹs to?XፍA{>*Y 4xy:B-bqmm-:jH"h4a$LF;VD/ZJÍ0&dnn!11qw 00>0=LB~]:ߨeH~[8,/Ӫ|%Vՠ;$@נZhT_Urտ3t0@iiiKKKSSSyyyUUNOOO'僃02 *ZSSbFVL&񍍍w޽|rBBBFFǍ74\WWݻ\s~~KZZեK***,,,5666n޼'$$ٳg&&&4̙3d2ޞ@ h6;ydii)pBYYٯrxׯݫDGG_p!111==555wnllzÇr8__ߔ2KK999 .\|ͮ@ ɓ...555O>^I>-󦦦2c}}p000>>nll|}hYNһwFDD mڴ fkkn Xs>`5 7o޼{.x={755rhb૪ Xjo۶ wtt_v-ׯ}}}tѣGo;|_W_Y/g,~oTM,C"5_`GKY"/`= ڲQHP(t:Jr\"X]]=<Ԃ]]]Z;*,,,k׮u2 2~G-60=}z@oT/'^|>8\o8jzQ .z(fQRy'@ըW㝝yyy`hh(++6d2 HG\.)'''//WM Z 7;fkk ۷o-Rr^^^O`zz:66i|444bbb Ͽt钡koo/Dbq}}݉'?9ѣG/]422充SNYZZhjjn]033c333kk떖AB!Dp晙K9˗/9;;Ȅt SNd<hwwG/(M8U }z +++kvuu=vصkX,䤗ב#GMLLp8Jư J[[[Ϝ9cff2W.## & @PXXheeeaa MLL޽yt&%%O:k(InBrʮ ]j~S Pbz`k xz@(8p ''˾</""ɓ|>ڵk݃zH$deeMNN /_433Rť<uƍ5554-00044433… tuu\v-;;;55dXggNNNfff޽{YYY׮];w\WWT*upp& 9***//7""XC/ 避O/$SJ=/Qڤ_lRzԵ7YCF!PfW[YB b2 ettp8,ƨT۷ozzz L@XXzIII111122JMM]*k vvv޷o+>(m?~kkC `.o>++fb1B8|p\\X?x _|FFx^ԩSnZ*>lbb fffGINN^*bu||<((h zvv611ѣǎ+**Z*@DO>  g(p//GMNNýea+AΟ?y Fcjj naӚH :== PXRRbnnnddyUhBCCutttppКKwxz@,{zzЂ<?88x>>>ƕBP h:::۷o߿]޽cccR)tttLOO ?ϝ;GRbqSST{{1D277'H|~HHȕ+WX,f@RRүgϞ?cϞ=555bxrrR" ٕ~bbpkJqqq||T*hK/3aj`z@ &f`?*=sɥV#є W) \Pk|Uwe=`0 zzz޾}`0|~nnnMM@YYY]],,--hb:ё+@>U_~hiimiiiuuDEED͛7;88h.Xs[nuww߽{W,X'O?~\p8B.*DT*533|[xV6Y]]moo_VVvСADrhŋYYY6mZXX鱶~UuuY]]Vfe//ᤤ$###PH$H$Raa#A@X|]v;kcbb;;;_~c#mTf_>,,l)CCCNNN˓'OÛ7obF"444Z{ZbccC *++umAԚ۷/,, ~\jIdd${޽{AEEř3gJKKۇG{ܹѻw8˗/.?r||;w\.z@*x$99ڵk333O_n]GG|rڵnU\J޽;..|||aaA*냛5`@*>|P*B= ___J"wsssTT?ܿ˗/AM\\_ooV=2`ll ܹ_۷Z[[x|qq ~嗝;w۷8瀕1Sp}ƫj+y:{Vë ,45("r@re) z㵵4,+)) H>ۛ]VV0>>NPjkkT jb@?Pttt BDbhhX^^ttt&}WWMmm˗A{&_Zihhގ 755#zhmm500͛wܙeeeLN$;::mٲİ0pzą`(v >^޻wKzr֭暗 999zAx|bbH$Rڼy3H9@ ~[9r$//>>? | >>^OO/33S,/uɑJccczzz@pww=3̿o\.711wӧO߸qC"΂{GGG;ܾ}qrr[XXc~~^*p{]p,]TuWXC`E0="D6=pD;uZQeu& x`yB?jj=P/ jhy z666H$5::JPtzFFP{{;f=f+++lvQQQ]]j\wޅzTjM Hgggff:;;} x<VR۶m233qrrJsss|>Ɍx"n Þyaxx( ~zUU^zI8ׯ dz~ZZ۷/_2oiip>}z q\OOH5XWgdd 𰫫kiiioo䤍MbbֆKU= J埽{e3D055:::KX`` ...o޼ tzPY@@fXW培?#;;{vvѣGwy||<44411qppp||ٳQQQG\XX z=.p0陞{_>22poܸnʝloowss+))!i^a/P-v[4e_}Y%!P'E(TjYYB$H իW===uuu J666|X,VEEJ% EEE555{j<ov_iiinnn.wQWWg``흑qĉ~);;ʨ!SN%'';;;_r`Szz:!!0ŋϟ?oo˗/H- {Y5h IAMVk! U`qjP #g=0>>\\\' xbdd3;;9++kpp0''/X\[[ϭ~7.N&aXz~MII&GmmmMII谴G A+**;;;I$ҵk׮\dXdaHÝe|~ww7Hr/_f e``F,((R d3̿zzz`KP(yF w@GGlrrݻO>;gh:Gg0Gmhh`ϟ?_XX9L J]\\t؎۽{7Bf޽"8NWW/`iiIFFFlmm!߂D"Z}}}&588׷gG===߿sΕ+W8yȉv0qB0222<<Zz󳳳,k׮]]]]j+Z0SSS~~~gϞf J+++mll:;;LEII-[AQQ@6H$AAA###t:}ǎt:}bb"%%d:::hJD"h?c`` ++ܹs}}} ZP(ӧO]v…]]݆۷oG*RͲ.]c/ű`zCE|k>ر_hQ*(C4؆_\ח*Z/#ʧ2 z -AEQ.TGyG9Su' 755t4B|>ppD"|tfIB!`3۶m/@HJJmii B#kii蘑! 322xxָLA?niiQQQ!JKKK]\\lmm={݋333<:s挿ww7׿{n0Ϩ(m~A\bccckk{ITɓSN988xyy544hzO_3gpk׮I$۷op8;; $(H$gggxDbgggcc wwwwpp{Vd`CX7o?ϟhnn nnnZO~===Ϟ=g``pʕ3gΜ={6$$900O Gxx<~333}vOOy```DDDhh#G=,ׯHCBB&w\\ Wggg``͕+W$ ǻt钭}DDRٳNNNVVV>aaaϥK4}l5ׯW_x^SSS;wtww.]ZFbA>BYY͛7=<<ݻo޼ظbz`@ތN?km1K U"U5olm۾d;!^&zPȣ@w-D\ )I>CM=#: =p\WFX}?^M!@.;;ecQse0P lE;2KJBt"}}`| d2y/cYb|:`z->ٙ@=y!EP(!dB1CYzPj)W3PRy[:PrƯBWydbM*!`dra|HR \Z,1>0=LTz}=!PJ-7a gCf,!Z 2c%AeTP)LԯGPvLA SrP|KFql< w_xPlY`EB'@zE40rrf*-gދAmIʕ"A;e_%'''//*..&D K"@BѣGŋVVVO^*ر͛7gNhM.-flL_|1x?~QM(=≗L='>pӇ[C3;؏WۤG(;DSeY~4EynP ;;Kvd2+++^d2GGGwA:ׯ_ ¬, CCCL|Q`0=~pzx`zE59"KO g1VQj4햧=4* u3(GG~^5^epRw,b=0>>RZZ ܄***SSSAM{{;Dή$uuuCCCcccd2JUOrmll'OD4msxxXOOt'''2\YYwm<@#4XCsA/ 69P `qĠrƬ/*ߘW&js C#"P4]\ ((CTp gB9mhh(//ojjjllP( D"566 V2\\\ 6@⚚x, p[n9r$,,lvvVsIhr8^XX̖X+CC?A@,q\؉D"gCO@lllUUD"-..~Wo-ӵv`0ݻl6sssΝ[ rDce +_F@lFem0S?  2xEAyv7mZpqف %}_F-@4h~Ԧ! !`W$RJ655544H$ d0T*|pppddD3ܶ xSAAA]]q8\EEc}}=KMMW/?yԔFi={>}:''ťaz@.C! bBւ`Vxz@,:u7?njjH$qqql6;66n^tIIIZ{hjj:}4@?ܰa:ܜow^G;;;z{{^׍6Loa~BF:+5Fzn+^6*~WENAP ( PW(;IP^`+|4mp&&&Z[[O>0= V!JsssjGgdT\D]v;>>zd]H$Z[[??KmRLOOoܸ7>>аA묪>EWjG+c0=N`;z`ӚC ~`~bXk<"_Q GVt\MāP]5-W~0 xzoooo޼a0|>?;;vhhvll@ xLZXXёTl6;55_loog2{պj"##bT*H$x<^"|\DKa KXof7o AAA?M8Ή'<<@kk+ /_Dpݺu500Xf??99yqƍ׮][XXR۷ouĄˆ 233E"@011ٺuQ(>yDWWoݻw/Ǜ_6nx= L@(R@oF<~@~?&ەot $Qm4Gsn@exMZ%Cj!|P30_Ł"@8FhiidfggWVV|^zӣ</00p۶mA<{Ӌ{lu3g ›7o[lս~ֆX% C޽;11qppphhhvv.]tq蘞pgff;D"Ʀ`Pz`jj*(((88455}}}PP(SSӒ+++zH$oc``WއٹsÇʖDGGd29888,,>|R ={ڵbhjj >} @LLLPPFI۲eKXXH$JLLnٲDr{WUU]zˋJف4qOII/))###D HUU_TTLׯ_b|;}C!T|w l]Fv\~>2{^D޿)D$7`_iLM`@P46YMA2B@BeR)JL| !!#>={Ҹ\.z ~@(2228ɓ'5DD? @֚:u t w^XXH488U.,,:tt`0DzzzOsNJJRnnn`뿜|%P8\.s.b``zzz`[HG>ӰP|:GM2$;ةXO: Cwꏎ1[i'tQ{(! M9Pp&C+ ;6L|N+{E}eFGG>} 1yԔX,rL&bxppprrFyRzAwm"xJH#J@ܹsW^H$\.f GyP(|>x9>?uTPPP}}oXXXuu"7((h``6xݐe>>>~~~ׯ_W`K.<|AR7nܠZNvss AAAx<700̧1!L|'= ~YsP/$PmBG2U c*Р>R1xSepo^A }SJMw mJP cz^00>$ IDAT\HҪ*^eddD*677 HRE"|T**))),,rJNLLp'''AD"Bd~~v277799P(UUU%%%hX\ZZ:55x өxhkkKR:^RRRPP^vbbF勋Ai=ϧRBP*H " E"@yyyaa!Lxj `_1VŠ@=ot2~`8sI]y!]F;]G* XP)HAsWHU*#V) : BJ[`z`%``|-s='GEcp? 7//CRl**=f[F?çJ4釲AR 3P|j2`Ѵs޹:f!!C#w!j:B+*= =v:h3%_'[*`%y'P6ȈF"9Q/oco!!_k!QG?|wlZ fR(kt\$h逬+(;4@4@wcqHŕՄ`cc`z !;밪JgÁk|gcr1 f;)GJUɰe5(*_?7[=_h,x<~`:S] ߨ"NWU[cGVC٠ḷ5*aZ{Pjx_VPNbzX,10> cC`s0=!:z@u?22>2Oc)P[6GqU5nGw W?@fʲ@3l adž8MMMA_->00>/0=y^l1VŠh'O @~b@{^ɶթ?0fhέͧHh xz jPaϚX2=G L0  /L|?1_$"wRě:)≷5='^D{6QV16Ph{?P*7(;Y4C3 `ꁽx CCDkG_ X69}yr!CY01OMO+?g;FqhuzAˋ EM-s^dMa?1 JI2 ,~`3:xzX,6=>> JWr||\$h=p@s8p|D"A?Zx[n jUb|0 >߁`<17 ≷~kЌY PCR8,;_!GzXYD >ߵCC#H$ڲeˏ?sNI8ͶmZZZ_(hIII7nܴiݻW칲ӳaE˥eņr Dl: =.`<W9(yB$@nt쑉XNŵqw"cqruE>NRkkkTj~~~yy9͖Jyyyŕl6ATWTTMPB@ . D"JD"1'';Bd2966V AL`+dE`~!BxbIJxbeѕk9P+,ȏ2+A!}= ߸qNٳ766VUU{{{wwwS(cǎy{{_z,;;) ܹs\.`Yիx<>&&faa+000<<|bbJz{{{?l6[$k/xb d2-""m4XY+>2xb/'^\HqO+ 2oԞ #_J}aXXLͧ֬Ycooo2׃媪*ww++T0=88hkkk``)ٳʕ+"ʕ+vvv_F]|U {Y5O1= K!6UoTvwǶ5<`oer^՞m>ZP֫dznZ Ai遥VV!!!>޽{ H$ ^__A~kk뢢"AwU[[ A<ׯx|OO <opBB+2G33hA[Ο??44UB ѣH$ZZZ榦q$ɯ,4662L 777+''4oll`0 Quu5O<~xx+<<3x֔{{{ 刽@xzAK. ®D ΁MOOST?Z[[A\ussD"h|>?!!3f133S\\ 333ᜱ׌LNIIь/׼*Vh -y~!8N/[ZP3FEwl Z.XB00|h4ZbbzUHtM_ }WTɶt wx3yLD iL Q 8cF ((Hnh9-FXZή]USݽ?U{WMWv-4'Ĉ 6C="lK&Z+C]`} q0xbw_/^>`t td;&hT1si~R2n%˾_:?0G;F#V`>V? Rv|`U?Y=g7s';&r}Ѡvw,bg+b9vP|=mx}`, lVF`|`11=c3&͜>vOVyd,3: _Oodlv׫~m*W6, ;-|@$|xt|AC!ͰG6s-x``QFm|vsl- e|9ʀ ::_*CESbZiK0j|FAf#0ݘ|4_oQ3'z96#'~勏םp^G2@8G9(1 "|;[GIBZ0ܿ>/\#G8q9EqjGcIy_D#CfkY(Aȉl{!DŽ#a *'QYb&1=@~˾_<xXF#0|`3bՊ" Ϟf_OXs|,uP(?/7NV%0Ko #0HDZFB-o<zM9CwBАC?`>0X4B>Io7*~Z0w? FRwhU1׹J11&))))qc1ӈ + ot_p[:َe^GZ4?#ww#:0L+VܰaCFFL&X,i[reMMH ={u]8 #0|`,AO^#>\dUS|u?<(?BiHj0QFqҥMMMnڰaC``L&~())׿IPFv# .>ؖ8ۑdL^7)n޽K./FEE gϞ-\ŋ}}}BP._xqÆ 4JΘ1رcQQQ&??555}}}˖-m.\mDT9NHHȏ?xXFcZ}||FJ% b8xo`>ޠ v@ydv|`&K #G:ډvEG/?(-`FU]]=uT2LRo߾{ϟضA|-\wtO!}?l]|7 #x >0osÇ_tIabbb:ꢣڮ[*={'&&N6 @[||}CTT\[[+--UT EEEݫniiZ$dfQ*Vfn'0|holϰ|`^PBGM~> /\$F#0|`  vtADqF`l"|o# BQN1c{C[9 *.soЁ+`x%a-c0x)~C/}a>`0oo.[" IDAT|~TPL_ Ψ-` XR;Fw xPwܸ_8e0q0QAQ+g0xπ|m/##]0]<6A󘮅V~ |`;>/4:n?Vj`_3/?0F6n-F#0`>0b^ŇFRG/Wyݎp`=\#G!5>0}D/'7'3kdGpww,wk|` lTF`|`! 1c "F|][`;Sd;韖~~g>ۏ=f5ƒfbI̶ IL&RP1GPڈRT(!1b2F#i4LBP,#=&@p&F`(0  b<y͛3)~,XG;ĆJ8ԟ|.8| >jkky<^OOsF#ʦ++QbikkF9ĨJN %!h4MMM64F`X0",0_CNߒ$fg#D`baXT*h4. ^omm%ξ[֎2[ZZfsGGN3 voˑttt]b@wwwKKl6mmmz}KK@ [hooP(FQ(Рz=T:z{{[ZZL&biooz{{fX,bB󁡐p>mƄwNݣQhF#Q"0|rPVKX,JDJjjj(H$j b1L.k4/ǎL&6T*V+njjfwtt >bAww\.g2l6Nj p8LkooRE&侾>6 Elv{{jtL&blL@ww@ `ٽ:`l!HX,Vee%˕H$n4F 8?~rZ8`oߒi1F#`C`NwvD" &H2l6t::҂^_h4677=بjjL^'ɀHRM&S[[[ssN3ZBfJCӹ\gX|>BbnXzzz7L4 V0d\.7mmmL& _.NF,6\xC#[Do@Z^> ;F#`ZNB HL&&a2j"D$9,a$@t:]UU<ܼhr<jD|>ߎH$'5MoobQ*b544xÉ#ȱh%HB5rVwd,1B͘| qFQ/^0 OoX`bl6Yd25 jhd2OM,K$!aL&XjZz=9ڮ"\." BI%f\nWWWcccGGj*hfR (xBV0@|`X}N C |B(9O@bğ?oꐰcbP&J8S0h?=, mhhtFI"X,pFBhnn{hhdRX hmmF4 v`b|UԴuvvtp1`巶Hn*p ~'ɚ, .k6!&%> Z[[d2& Q__`ɆEv`>TUU|Šv|`d_l7_0啶amt;H%NYOLɣ[G~aG`=VhDϞ=xGPoo/J@-Fj_^^^SSCt+!JKKڮ2Bă`FP!D"RL&2\WWl\.ed2+lCV`0!:i6Tx-0-$DZA'vxe#w%>$f:rbPP2DͰ>'5F` >0ҺHp7҇ a$~>0JG ks+@!="|î:/~# #s|f.6xW`>Gz<9N^7wk(-Mm;{(+OL[܉]Y'xq4qW0C`..'`>0N^_e謉%X7y)#pn;y:#oOraB1pc1/|a@k|>3'ND .C?0  ;F# q?Nz3\fLp9}D9#'nȠ|î: 1=~ISc>NVE0o #"0\gLpU_N\5ofOZ3{Zh~ AFXPb(%Q7v$aF`#ŸkN '3{niAa3&9SQj 1m=B >0 k`6["G yCK>p$G8Q1M40 Q(0A;~`¢F~q+7;A'>`["xB D{LbNr<1M,2T>!Ҙ`ê0c`>oX_~ն ML {,o2EA,w&z٥NGJPUԾMd2~/;^&`0%X7F]!BvXpy=x[<1ȈF3!`|۟ekT֮ *(9(%ȑ?Y* bQ^Gw#x;6 o!K[,֮A%f^頙mmmCiT~L=7.\Ff6-HD"R]>@PR)S===FңI$4M(fTPFI:fP(dXDߕb2O:::h4j5LbdP1 Q|`T`J ~O

q [GЂ)vsȠG&5[G}P;#%>-֮a=q0߁/^tuuY,gXT*USSX,R|>FT*h ڤRhVUrbqf+Jh4VZ-4XV,H JڭSVL&wttp8*d2u:&I*677h4BA*枞uvv把 :DBP,s8h쨈NlnnjVŢLfssB0---PEww7yXZA}__VUTr8*fbqKK\.5 <O(2L@`4d BXb:::, zw7-"88~յ>:cʙgN5s'?¾Y=gOLM~y@ːkM#+Fv#['b"c(B?Qw#HX #:Z4FTfJU__jNwww#KbtX6(͆{R__A|vv|9Z>bH$---T*BR2jtG>PUUUajkkzvY֞>T*{{{brd2F[,Ϊ*I@~چB|n6 NGPZZZz@ `XE$q8|FcUUUkk\.P(<O1 ;xz=|>_"H^"@TR1L`0rlX,<NwvvX,Z dISS`jN Bی n3'Ϛ~6>D>h4UOL8Զ{fQb߉hCAT]5Qȫ8A*~`-Q~/p 5 #|~a;f\.?jt:pd=x<. 0Z[[YSSYLp\^hjj*//H$G>vUVVz5l6ۑ455t:Jzzzh4ZMMMd0Oxf0ϟ?0jʠޚg$|@PWa@x>`4B!e FEE  @ p8R<\}Žpbz V+qF`0TVr9Nokkchw) "zzzL& "`0`5'#ϟØHp>D*++~b^̶88v67Qqh#y !v U2iN(LbUڿ>3ٸn3|a0G]|ylI;򁮮fFa4 Br9Lf0T*`0L; Re2dNK ~dl sLFKgh4\.7L]]]5559bytVK&f  L&Sgg'Bijjb2Bwf<$S,J$;/>  L&SEEם].X,FVWFH$DmmmL&j{@!>Ǐ/^x w}x~b3'z~(GFcP6:FAL(HJ %ڃ$$@K>05q,2\F#UUUļJr Dsɰ>İT*%̣\hH$eeeJP(L&SmZmGG1~q};;;Rigg@ FV+JŢjFT |>iiiQ*4VD"a0jZhZ?!HbxY,]jJ́D"7jLV[*r81 @ P(dĖ@H$d aP 򇮵 ۋh4$`brvX@FJL$!J!&,&X,Zf nfN~ b?0%M4!%R`[d"^'&PD1$29C @~?X/^7#5F8`#F`|`Ծǵ9}FgM|ٶl|gd۟m!dy=z A-rۆE҃4;vj H$DU(MԆ^#RT?>Yx_j4[ v)%)!9cY_?TWWwuΕF]#hgXF}"nی 5<|`B,f`߼ Iqh|wdC;1ӡt|u9R$?3ߑ&Vp&/*zLoc1rو=^ f ޣ D-pq-ٳgr?`0ާ B>*l|mƄv|>?~= 4 ۚhC&X󧼶Uy,6:wko dPc9~{Hb&a0wIޯQUW;Qldv# M҃ "}PybȠG" P 8& l8*mZbX'|@V_Ͽs[SIv:S IDATv: LK'|G;~b䙠Rm7 KYRSahӬ,lT$d]}޾uZB~iII ѣ2pӧO0a'OJyyySL??-sc !:t(33s(|FSVVVZZjǏ?u6Vo߾78jUnn޽{ZPvљ2hxT*322UUUNގ>pBxx9LŇxYtt4 Aܹz &;  )ڵkWqq3*q4p]Ǐ ]lݺu(>Ro;uGNN޽{ao(:kRUU~LtY~FvV''H=CO8N;~9d}0߁H$jhllkk }ֱȰ9SN%ϟO<Ν;RH*Ñ:/xDp%nhhot:Xf\\.t}a@^/s1fe2`h4BF~v5d2FH$hƩSBXX,^\.d2"l6[$F~mP=jzݺuEEEz^ ATbNB!p8DGGGtoͨE"f0z=l!@ZL@xu:/ZP( \C&A|@.s8.|+pfp$jmɱׯر#$$eZ ދ"uVPM6H8U#A@TRsW^^^RRٳg󳳳Ϟ=|v奧'$$9s&%%n333srrrss (KMM=}3g DϞ=cZḂtvvvRRRvv6Ѡz( PWZZ4C/ĴDeSRRrssܤ$Q^^^FFFZZZ~~ٳgiiigΜINNNJJB322Pk999* аD$mvfeeCՉyyygϞMMM%OLLNn322RSSagdd9s&???+++)))%%%!!!;;;++ #0###)))999!!0RRRΜ9cŋ2 ~8Nvv6V~ї@aaڵkܹiӦݸq=2 jpᐐ0A^.>}:((:!!aٲeD(XNC5;}]~BQa322J͛7.\ JE"g0A"Ϝ\.b˖-G.R|@VCBLAqKKK۳gJe׮]7o޶m@^T|6- Ƭ>RPT** **666..L&Ay@|)Je0.^dTzĉcǎtGmٲmʕyyyjΝ;AAA>>>qB"Zuݺu۷5 ?,,5((h͚5mi&6}ĉUV8pN[BKbbbt۷!ľ{xxPTꫯ,FFFh4*qiJbh0dM\\\|||6n|L&8p#**?>,, >7nXjL&;{lXX^޻wʕ+z/6mv|9NO?{ H$JLLtuuݺu@ PTBoo{2̘oܸq/^Zv{XXDǏwwwwss۱cGee/6oo0z}jj~+:+taam\]]nݺroWmZZښ5kN:cɒ%wnjj*++ vww_vmFFs Vt:a,\!.20 ={D"\2)))$$d_uHHÇI$۷ad._|ӦM/^z޽{ҥofŊǏ֭[/_ɓ'֭[|Klllii鯿qF%$$x{{ggg_|_&HO*((~QO^|9..n?3ʯOKK[n۷ WEEÇ /ɓ$ٳg[n---ݸqcxxxmmm^^W\y?\ZZzҥkxyy-]۶m+**z%K^J"خCyxxXb͚5k}}͛7k__=z <<u.q oo b~^^ޒ%K رcEEE$)##'/gϾwDze˲8pEs>֭ #6>>wYlYff\. 9q/O Z+''&e˖ӧO BL̏?DtJQ( l6["DGG޽[O zժUgΜrUUUnnn+ Jue??FiӦK.gL&:z@ dĉ p]???N`َ| 33ݝ:Z6:: ٿ?EEEڵK&ׯX"** {2 b+//߸q'Ot:]ddSj5Pxȑ6pĩѣGbCCCZדd T*+^_RRS a<;wn}}=Bٰa֭[,q>}T$EGG=zTV߇uX|]vId2qy QU '|`ڴiwܑفl6;==}GV/]ڱc L&LOOW*eee6l(**r.zVUT1~xx`޼y)))r͛yĄD"pB```EE1?!!!((r?EӕJeMM_W"&ׯ_wuumnnV*t:=000;;<|0|EEE>>>7n(++JWff&Ҹ82e2YRR͛Ql6=ztE򲲲  dzꚚ\s={Ç{xxTWW @~ӧOxׯx"55Օ644^@]]ݾ}|@,ܹ0ےdݻw}}}Y,̔-^ӧ0mbŊ۷oyׯCq .d-[9r$''7o|Q3&U+JBBtLڵի ~~~@9u:ѣGׯ_f^|Yر#11(DXXTVV_zT*={ldd$ͮvBe.^xƍ 6ܿ_.:uKV_xq֭AUTT׭[g0pSN99B1XBoii%g!oY ';or5O_݇S$]2i[^E&!o J@T8oK>SgBO Tm4ȷ㇚?D;r |1fϳa\/V(qy[Î;9\fGGo= %TUUM>ĉ ,hllDhK.eeeٳU۷/.h^~0WZm0ry||</^p//\K&}X,&MKKW@."V-|>_9*4͜9s֭[uRDRTTd2m)f͚ƨ^OsssϜ9xҥDccc@@Çssskjjs|5+**R(IIIK,z/|/^𨸸8.. T*MMMݲe ,*z}yyyJJ]Ԃ XRRV*.\شiɓ' 3gBO~aPPPssNwx{{|EّHjF_L??Olٲ'N 'O(o>0ĝ3f$$$,qSSAەxb^shط~{1Xzs΁ѣG_DDDA-999EEEj 66vǎϞ=DKKK ݻw/;~^~9ŋ0\|y4M>x +++***::NGGG;`v`#iNNέ[4MAAAdddrrmۀ8udgg)y _$ɹs6m2;v>/ʊ>p1955?ʕ+ZF}޲e۷JǏbbb)33v)p/_@HHǏU*Ν;a]k߾}fΜ ξyyyT~k4%K\rET}sm۶edd[vCCCo޼V˿ S;|mm-Xݢrrr"""bbb?@(R[[ggu:ݢE~gbR?ju:]RR҆  (6T`)5**0 B~Ν3gO @IIOjjjzz5kZBpss+** KIIt'NS?!888++ YwK(pUVANFFƮ]?Lٳg/Y,7|/ UG˗/x((_?8[nM8bݻwoӦM0+78txܾ}񁨨(pMIIA|ԩSG`ge]RRSS~A`qqq˸j*rhɓ'H!,,… 0m7w\XgϞ;vŋ>>>UUU233{+bЄ;,"q ^`C H@bbyܟnTWWϙ38%%eժUuId2922ѣYYYǏ6mJyfdd$L]v/4Zc3 IDAT êW^5cB3'9qofOZ3vs_ |!fde^#;yO!NvP=%Ӊ-~ (!r}૿{EB.!~LN1tCmnk o|ghk4Cl2LnES&I"d"A[[[cWHnm۶qF>>gΜnnn2,$$FRO?m޼y֬YPBr\*VWWPɓbZh"DޱVFH$ZR > rJ}}=j)))_}^TvZx=d暚:h_WUUB555dÆ 111j\GEEt:T*㣣kӧaaa_}ڭGDD0 \Ԥׯ_ D"9n߾P}_d2YIIwCCD"پ}{pp@ x%K txΝaaaD>B~~~%%%jH$BQRRRBCCaR=y@ JGRi`` u: ˈEaai޽P(rrr_`0_|溺;wT*_BT*)Px`2Ƣ]5ҥKmQQQr\(EDDTcǎڵL&jLvر\t)44H ,]4''Gax<׭[K4QR]z5$$bOvyם;w ő#GܤRiCCB3gάh4OpjcBVG]- ;wNRQԌ ww+Wh4jK${:s`PӠ~ zyyT* |`0YfܹMMM޼yÇ5@ x98GDDHRptN`j0ݻ__pA,577=3}}}7>}v@ݻ'HbUadʕ CMMi4T*믹\nzzzrr2W|>Xc@hh(xnZ|]` eӦM0^YY`\: ~kAAA>3f͚t4))I*:DRL&Dk׈?ېd"-Ho|RiTTTxx8 ϳX,X&ڽ{wss˗-ZHA _pATTVV2 XߋT"CE"@ ݂0 ***O^RRh|>L]7qxb& KJJmD"qƦM`'2PQQQ```uu^OJJ6mR,))ٹsǏjuxx… Q`8s̡CicjJe[[E^5v̉6>OlyV/7A7ԯ~;H,lPXYhM*d#I%9DGJOQq*OKXW]w?huU{327sr2 " Zj,V*]ֵ~CZj.]Z4dBu\(Қ}~{8${?gxZwҶȘ?W\9Oс?8s+юn|=S QL;333>}yo۟.OR[###>vo޼^ZZZz<Ommm8>rٳg=t: +** ;ydGGG-[d&7޸; vykW^}w[ZZ6n܈R`? ǎx/A)ӧ| m۶544޽ǻns?1q޽MMM~Wp8xx~СC`߿Ư͑#Gy{C?{zܮ]v}ҥoC<88(x%q;v;qsΝ;ǰ|ll,?;vY ?x[[?/󳳳~o[nJ ~;~͵{<@ ]>lss3.]:y׿utԩSN_< %&&q_'z2=55v~?gffW[[;<<|ԩW_}uhh;w'UVXZZSzW^V{իzwwؽ{~o@}}=v={v>ĉ^'9qDmm#SSSR[PoΝpXӧO'''333Gmoozp'r_;v}. ?;;;m_M6}K_۰aOj.\m۶{ruvvb*_'N477ԄB^x]x~ҥ/ٳ}'xر{L6ڵkkjj={6 .^3`kuoo/ٵkWOOOgg~ϟ?`0zl_|W^Ʒk_?ϩ[vttl߾]+ '񓓓=q\;v0gЗ},q\?PiKX?66r~Q7 3 a$SzH?݋GEq ---=쳭>/>|xvv_dr:աPG;77wNtܹsgggC=z`vvG WUUo~󛮮.sQO\3~^ZMy@ $&@P8cƩb[Ə|of]|]8)Ѧ_û-0Y8$Nj"ԭ .{]/>1~8cff/MOyrSSξ?77t'Kb\rIßY9==P|Μ9} Ȝnc,xA7bVǒ  N*cٳgqggg/Cz96;;=Gj!NB}cC!̞9sfrrRH 5 ={˗Μ9SP12ΝC̓sssW`6ynnnjjjffٳ|qԔƗ/_>wb|f/g{=0===33#=bHЅ .LOOcp"->sɰLNNbO~o}[}o~߈ .8TR*Ћ .799)ǽ!f);s挜7:77(ޮի.]Yp~~ʘY\\ s;855%F\D" gggqޱow… 8.YD.Aܹ̓s8n~~ʢ_~ Oŋۇә52"Y\\_i_!充[6Slq6{saqt9qpᏭ(MNNW riזPo)N0gΜ ,_os&''t~;Ν;^_bfffI^axj&_ť%ǁ*Rn79z0É@&̙3ږ-..bq5t1G}4??[o}?x@L6J?`M HB[U*>҄|fG&QBȉAH ! $"cM]VomfwuF[B2 {ط&loi;;w/f|7Ovk'ɓ ᛯ/VY@rEϫXm._M2}ۆ6NZ)%[طmfx^qI%e%E+%M]3 0I呿8A$R6ǒF DTl%wV_]VOuF,^sPW]@k]]]uamk4?Zr1_[Gwg5[9|J__ 747$" Χǃ?~|W}*os#+|mދ+<8t…k׮]x?,&:HߠLedeIeIme }d[rh o6M9~0m\I`h6;kYuY]:#:#:#:#p#...^vOG}~y~ ̯%7*IlKp!Y4Lʹ^B# '^Z^Ն 4#D"%Tj2:g 1w߶UXXXXm׮]q‡~o&/1D19P#< y0H%;r+˄$M&MHH"&q F&*Ɠ5js#:};g''}ޕѸ Xr =(빓Eє0(4!0 CFO {g#yG ȁGHxIO=w~w]͋}r3SǕF#mrg.[s>(9jtA#=O=w'?cu}>8fUQZƲ bUӓBiϬD7Vmуf.D?]*wJr}sCtU`F.ВMiiiԦƲҔPiJCIJ=S$%P,N DPؠ~moLiƦNrieؘۨ=qeJT)l.M %Dꈈc!3PyFFYt*Maa3e ) BICqML8Sk/ڂ`yzw h*^FDBzSy: N\S%KiJdl/?jc 1sx|IJ#C.v'>(Gڝ`%]cZסoG%/L ƔkpbAGMC-F*Id$, ~6|kvcJVCZV1ʑ'o]];j |u&  WQxVc4(Z&^xD`8DTDnJM]ᣛ-< 1%M &*WJ%F-|2|UȅkwT Е{G-ںܵPZo:6FKdb^)$' qIQD_IYѶ^7K/~M%e-"ӻKm )wVEvev2;k2ګڪ3ڪ[[̂5CyZxT.[1x6`1GF*U%gwrt:"o;kr}uv*stgw?>'UڐXO>`$:;BteuX< 2)_VZaQ|e nHnKSTw) nLl(K dQl:ZD}A`BBW%Q#RdlQQF^^IOOa*V+ZYbm6d52 0KCd=8MݚZ4Ć@6IIbh9m 6EcLfkL(7iT)78*L)^D"`!1/LZ(wk=k=|L8[NV0n'3ܵnT]\$ V*p΋iӣu-_ڬboT^&8W>:ǽ[_:!QP}(!T@x^;`_>PPz@kɿ%6lr5 z4g HHH!w"@Rܩc%?SK%A+ӕ€6h2}SKe&[ؤ$1HVX̔G&\q*ScY:uH9$ Ri&嶥2^"Ud)0Cv_ɕ~ϗsg9zݎ"g"4JPstk0Ij=N]kl=9䦍!EKacb4ٕjAđwscZJ|UzGuzgu*0Y"z~cb#Zb8"͕æ@w9V`j2ր86a$渹&&kΚ.F *"I@cdԐތW)| $jYm"44qe2d2`M+l2€`M7i>Vf1hZ+C(BR%W\n{ LR1j˭/Rrf&ȭ 7CuayS`i-qdZwHsq516`/-/R|Ρy-$/ !V?e'F=lPG40YNѵl*EzH18YT piBV%ɁL WS.n.7o)f/hTP/< ~Rb*BbVP-:|!c%A]V/j@ vPU==a Xn%*^XE}\$ו>JTCUUz[MF'ן;p;P o8@^g9uV{/Ju&r;{)tx||ݎ䦒$PIRcIbSIb#$uj^chp:gOSH&]YwU֛qk6j)dϕisbd}Hu'Ft*Ht/N{^w'6#8lYqer]DyMf9y46J׽!m[m8bj9 &btN*Ki)M%7>\= TyFⷚc/ףkMʼn!~uku](Z$W3Z!TrQBCVnŦXN[!%hn"&5IDAT%Zk }MS>Oldr/&F$8!" ù5ʜ:é>O֯gilrdh)m;tkJ[. ׭_¬"1y6 9u=8]o3$듅Wz@7K@RjJy&d>hjI 󡾼O@x Gs.B q-GI }\ ^wV(C2]@\B|GXDוk&@Ch7›- T1F&#+ۗ;?wg^/x_/{e1CI?"H0m.!w;( D{})-%Ia+\e0ؓߓO>zVuq*dz"o#05=DˆRjTPܝTؔ&7WwIePz!]u/= 1{Z*ݎ>C5=&WFov5= g(^"Z6U]H҉[jSF Ȧ6yQ *GflS lK.Sl@p]}a"] Mżm8p7uH.iOG=9#0SpIYQH/,42#k 벝j,RH9*, NBIe!ZDt`@SLg*! 90h-b:PN},^6fZC"V^;GlZX ,McbL#=*7Q w3'9t%J6gX *|%0K \*E9C\@P os08G|4yG%h|:\BLsirKMF2*"pP e$A|كnG?g}쯍amf_mf+/IMzwOTJj.I %orevrS؈L!Ff)QW'^9v15sWS L#6FhBAf k͡2џuU?V)>֛__]VRC}me7%xiVId0- @x6Y#h5;bnM_BdZ$aDǑDB 2 YBsن 0A3|k/Ut`fSHf2!`b"vb2ik)aoUx<>%׽V) )Z׀V,9jf]=ּ^3 Ps"*V.p%E b3 F30+0Ħqe%4p1v W $6S1݄eB8cKv K<ͤ5EiP`BRTKјLep$;t(N[1|¾ъ=.mE#+tM^mVO ws}XC[(^wGÁ`h0wLn#@Hc0& n1 rFT-3ȥ*%9; ~>Jm/KTHOF(ihX $n53J`m@]<V\#:X9X1PF'%'s$~*v [Uq\ y1v xuYC];kT fȝ9(k24Z,Yb4N]\xR5n28H?Rh"6Řvkn@Jⱙ%WfUjGEJImJR rQa&FR۫R;*SMXHH[.^#&V`#Zw!aDXSHⷒI$1D[a6&^qA5Nygbm\"_ښ#ċM.AJl #x2 #B`D%Ҳ5,hHԭaA DbZ3Ex}$f6 6nu +㊼Yh-\$l˫e5H{,0TH6;':_1FS1,oࣄxZ b=9@˄fbhZ7I n&2W J3;t$RE qB̌_* VtMU"s9j^қ 6DJ'aZuPK{p $IӊQTd9jaJof;0S9TFM (Ëv h)pd$r.TזTx` ㍅[n 5݈Pܱq&4 7 oNH,\#|ճ6ȦQs, R'jl *Z R;+Z˓[S+RY\*4[rJro"U9~%F!ΘI:0 ƒ. {|ɾK6mZbT ,ءO)Y ۈiP oY9@~C#63lɡc>^Tbߘ)0=Q CrÀA˦ Q:NG*;pG()+E^ zTqnb.rF ~h">w!w<7ʛh ܔ;n65ۭ<!52$&$+4>”Y8$y<TlG44L '|[95=))U)^ftWT;=KIRA }#:5eyoְ2o0LLnLiNd y5>ܨG٣>?{̗=dx^&ct(xCELqӰݸl:FgaRqB6P2;\d>8^n#>ЕtMwui]t;=wHi7WnJL_mBiצ\0fD"21a%zL]iV4GF[3<A4Bd6$4ys1IE*ك+E19rݐ9,cĝtK#yocu7$4 Iw8o\OTHa/5-xfܘHɽ^Dq`2bV\@LWIq"@ɚ́sLJ "*3@^ #8L&!EL"僌8ihAJ o9N$6 ogrkƌ`Z)[xʦCYEĆ0DQ9Bm_*`Ƽw6o#&%JMdV @x"X(oY~B *ԄZXwL$o"D^ؑ&n+pB P; 6L4lnMJJgJ2i _UE:ldҕ4T gLVHj3R:;ӻzhШh4⡨ҴNE^ǨhLHS. `;# Ff~*x3F|#7sPM wWuդuפ$Y9B RZ+bYWʈMBa]9VLݎ18h z$F dWUjW9laj핉-Iت1f ckbD^7Hђ38PW51^"l0/鲉7TȝI-xj*D]MD:)-lߦ`;+-@EجИ bGEcVBq|To .⏻讆܉́`8 5 * (zIPm s&V2oֶ`TwnSd(rs$Dv(oK0w<>g>g"w[L| ɸI圎3R09 <=A*˦ 7mN6)$@ld#Am`m#)r*9F1.T *BL"8\ t'.QW%wg֤zc\齮M16]齵}S_a_>w3B k6@V2"5 f˭ͬ8`qa"M^\ TJm5 cjE(&#` όALg"G}"еap\%pI}9=8%46Ϊ-yЋwF+3&<ƔGƃU+`tAm P!:TT^Bem(ӄL!CmTJ!I%DHn>ЦE$F̤\J*BT1|R`Fۊx)KƠ5iiIENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/images/wireshark_4.png0000644000175000017500000040335414062723141021761 0ustar00jonasjonasPNG  IHDR% IDATxwtGy[ν3N{wc@49l&g8L29s9$ $$PP"﷾:n>Q:V鮮VwUW-@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ -@ _*URT^JL d 2@ d JPmN]oҨ4mܘ6 d 2@ d 2P]Fjs[ ǞzM4:>+%gh2@ d 2@  <[4j@xUT 0 u *A1 2@ d 2@ ԯSG _N:u(FT2K%/4d 2@ d 2`n 1`2Bd 2@ d 2PyPC:W?SJ*ժ]&X,= 2@ d 2@arMvÝʙ2@ d 2@ @?R ^O Uyb߳ d 2@ d]0J*5H0w+g>J d 2@ d|1Ь3UUj]/(d /""b2@ d 2 "*/=#K{_~FF6*I:/E ^һv d 2@( nz5a&y5}cE )' < d 2@ d7&j+Fy&LP(AQ 2Pq}Vھ]ݬZ=No}b"އ_Iߖa!^&z{p? DHwj0AQCv @H.*5_eϲOM}JxK%Hrv%/ kh72@Ȁh^䐍Ümʁ24w<- zs$KzrUo)_N\''eJnV<]>i[:JOKFޔ_#:)yER%P.&,n }>~s!"uZ'CuCFQoOB)*,܌4ɼxoWI֎kn/{F2_ H8mqg}%߲A__wVܳE NwVqDh'2gߞ,I)Α̬\),*H_B)xH~jWC=1:-F oFTIn#dȽ(٩Irr ya8,9}줤dHVI8y+#`n{ )O޲-xEWۼl]F] ; (o's?mCT Փ_&#מ|N,C{"Ww_ʶ|ȧѱr>~Y#7Qr. o]j@rIJ:nIgRPX$Ed'Mo֖E/̴̓ PT%;Ē;}2K &VJvA,HhH5!Z %}RW NWUhCBE2OWy钜_(.]3/)J9:I^^KXIT"DaA$'H6|%-,ACKި(Q(+M_Xx2pgd;u%4n#2#m(qk2̰zduRoKXX)fܛ;Yx2|V~ZTBɿZz+Ynhs2xk*'nfw*FJ^Qd,M*mlEEDQڧ"Ͼ7sQQVC4n*-:]RW^KQTk_HQ^LXRMzr9eeꗰ:e)(̗I}!=#d x΀+6om%loV_,wo PvDe]e,),̓S+Z5|_s?2P8T \mM5BCJV":-J`Lw<ǽvȀ T[P,Yn%JבF߼j'fKQQYjtURtkhFhH%[*6;c/J7mQM PBRJO!abvxЀ¼2VevD;h;eE\X Tc2 F!{-D =iQ~,Nb'JT~<kT}UQ-~wwmEȀ3\ #%ЎMC8J;,琀0aJPV1S((FhL2@J8$)gJj6&+^+Y{Tm^wd r@UeB˧%SׅnxsbLw^iuBQ'D;!@:d 2@ d 2ׅBxVUy٪a'%(J2@ d 2@((J2Ϩ{`Lڗ%d 2@ d X(AQ d 2@ d 2a+Uc 2@ d 2@J%(J2@ d 2@((J2c)5mM d 2@@2@QE 2@ d 2@ eE W&JvQA&d 2@ d ~%6 d 2@ d 2@(-L d 2@ d 2`E ( d 2@ d %^gq;U2@ d 2@@gE d 2@ d 2@ʄ%=*ӟyWou,<<\̙4~xiԨQ/m 6%eޞWf/6.[n-k/vw_~z<1=N(mH2@cLE?RJb 7~ΝC:nuydy%##X;ڵk'^u#F;C $$Yzn_pANbΜ9#O˪UԱ{_fQBۮk7%uLTVQbݺu+g<\`.+X%*͡]_s; 2@@VO7fQbĉHRe{b~CD<] }\(gҰaCmDZ! @ׁcqIC䒑53լY3ׯc?֧L6 m=ԩS njz~bevx} 9ӧq pku+||g$11Qye9R=Q7:q/<"|\F] ,P w묫Gח۫VX!e.yұcb-R|aZlRl_+EfX6jA2!իغwJd>FgC֭+=j+#kw͚5Uv}r}mN#nuex4bܯo߾|6m z4ߌ[6GJ`?<`xW_u B? 'd 2ʝ(duӆ;<Mv>G]xrkX8E)ӧ}ΖxDFF*g7pCBBS uFNo'vlga?X 73,m۶_mwv\ } ҤIrLq:0._UaQF)'vM!@3cL Qm֢$Е-pqC$~n۶mS\z%| |Ν6z4\3q->s>4ߌ[6x7}q ̡/j[rVڔ6%d TL\Kܔ!oݻ؍nX)nxh`( F ᆼ4D FQč8VOlS9?xݩS'u[DQ`_[~2pAҶ3htps'<G\]A}E 8zO>1uGN_T7nqSv G6EzX7I<ÙNn6q9vc"\Cc 4aΨ1k]YC&rT!<2׃FavJڄr%J@k\BPY8yzFGw]_#(_~E,ϛ7q,8pME>Dؘ/؁>~yz_= %7(D?A0Oot|fg;yQ.K/A!1څH-Ȋ.ιlB*DVmחF;q?i~q;=~ADh;pY1ozٯW2@@p2PnrpCCr7x߯ 7V2&JK xbg,6X98cDpᩚ. 781c(72t.p#%}[9pґG3IO3G(ܘuM"D hɵ%뇠YjHf Qui6ӼysG[P&LSx2 Fo1 Sl#'ȣN*nj%4q} rA֑H}.5+a\W`+&v}i Vlǻpl(`_q{p޸_/d @.2VZnϗ10H <20b%J'ny}7H !eOl|8L#w1pN8n1ƛ6\?ͶFO\~cÁí'Ę[8~V^_#T78n貍Γm~ñ(7qXgt&uٞ.!H@t@1ڌ7SNUuBP`.nZ[GQ[ۄKڄ 2@ʊ2%Y/O63U0G #b@Ų- Q`\O(J/N~h P01h 2@ d 2@% 2@ d 2@@0wQyd  d 2@ d ;.J\rEh2@ d 2@ d.]&ڀ 2@ d 2@cEHd 2@ d 2@ʄLS˸* d 2@ d3wQHh2@ ={d2RIkng6 dEo&y 2 ߴiKBBB$ԅ:g$Gm 2P(AQF}`@`Q  N>-PDv{s;d 2@ÀE 8ˁHm64i̞=[֮] ={VL"n׽qF D ı˼x8qB! H@mcD'*;GDDȁԍ.a0֡m>r*oo.(Sm?ӧ}ݵe֭?jٶm[iϲeK$"b>|P&N'˗/_Ww2hDg#BtwƍNۆmny:_5"iAK-Jv8[{뽻nV|ZmG>/(_ (#;vPO1>iEd}\r]6o,c nqSkBY(3P'w)پ}R,`=n ^*GUymùsd޼yWrr*M&[Í+OIg%2hٕ/1Ihz4/Kq2iA9|d >DΞ=㓍.2_~e?!~0~Gj}Nט`ykv |+Ir;wYKXv>b^pu hB*П)1Y "2Y9Y/Q"mC޴ʦux/H;*U=#7SH_Y` /Z#۳EȀ?ZQx ?c qe:-cg{V]w'+C yIJJ (]Vt_cWV^zjWh֬>|XEխ[Wڵku]ʢE _'HFF ~A=wi=fOO_"HD8vD-#G,ǎEt.t9̑T7*{:_6ۧlH'xB?\f͚%+WTCH U:+s5춣, dJQO/^,6lPbDj%="ZSZSCVlLQ9Q-J@2dO eB@{hu G 3&=c<( <6mH˖--1dyH2<0߰053O6ID3M-]-ߗ*$kOHUDٓ|Q\*"7dLi=;F h9;^/HplظB&))խUmZyHi8tvh%X|e˫D; }%G}Tz@,q[FƎ%˺ukeŊe^qa(ujڇvZ>)!KITJ#KY,U޽„3+SNR ߍ4Eu$ɝHyk%J$6@$o1ˆ-[(q u~ 0Q۠9qB5mzގ*~wLhsڜ TN.J֗kܸ`27D xHqr:\xZgD O,? IׯPT5F1.\D)@TpV>{kq[nJhѢ'|"}UO5j$=Z߼ys=&vܩB37uGq䕡(T(Xw +%wʸ%5CUrMv'%gEgg'݆ۢh|;ңuیmhF7ykr<l\RMGi{~mW^9J.}vZ_ݴhm#Z 0@ giKLH;c47o<Ϩu˓LU&QA>s^̇xoNm%%0x r2m*1믹ak֬cFJta-ϓ%<)y~Se~r5>so?~'щ"K D ]K!Xd2U&,ڥeq5o8R~Vy}}?G2J0ޤȀK˗CUD"]]MzxPLi= OfQCQ"nqc#oIb/t* <{3%ɤ9l9%fɱ[LQ)!C&dgdL[mmM^A\~C\.ס >7W_ׯɕ7uKnHu"אKjyudilCP/tPN6$_H6/tPy_yADie# H =֪6^>;رb#:;97g۷Onv5Y|?-[7kes$%cg}I,+5o-/41%yw6ռ6t6m~cq_,dخ~zqi_0ږ%%O8o|;#"(Dyu '<=14>9$0dW^jnApx<B ϪcB@(ۮ=vdyik_Dv3nUTT=p9uꄤ4=)s1$$WbbN$;+C=qF\~sTD cK+aUwG썔ϷSZi+11~oCK*2$|Dl#ɰ`NJ]Q^tX/wܮ\p^NAI߈f=a.]*W);+KғՕߺJp|h7xs;I)h78<y(in]c9%чd~% ǘ?&攌9L zzZa ;Ph3` iվHo(M M?ܧ *"BI|:lC 7\E%7nT%4{he΋aƥ/6}OˀE 8&8sUCOgBDN[˛K,IJJRNyZJ-z8"pCvb8pāב9rcNDDF %?u3yd +>3WDW^q?2YnzZR;ζco&Uq_֫YP[ :/#D  603PDvvOdK׷1"(Dw,/netsarwɲv|7A(1D^ QYBHMMDa {V_ :tZena5u"] =.okB(\p.H]̔{N'YlHFۥ0W֯_'GDɥK7N|%Ja󖍂H @ѤyKiV64%J 8vcwݻwWg'Jo^>Cx0ק x}%Yu|6VwN(wg[ۈkOTǁqS7=EteFcǭ_n;pM4̂sJ(u*>X7g͚.ߜiW2ran`nj_ %ɛۯw}jRi-JqD X9[ _LtgoˏKSdez[-q=B>~Vy|O 2;e.J`&k<>߫f"6J j tdWT9sFExBMĈ5k(JS( $aڴiriu۵kW%J`rK7nP"yh |F 94|e۵|jy*S:f$5Lmo*Qʕ\ٳgrϟOcǢevNا,''iCe7$';c'SUvሎ8裇چZ U܅;슌vh% p0sF8& $< e9m9t'V׷t>?^VSM8 q3;'fQǃr /VwSUueu;Zt{_ k`ٜm1N,]Xݸ"b.A^}eyc2aX%99Ҷ~m7em7Nϙ}1sD@D^} ^ѩo;a` _tV]AߨmO)*j#?i_0mH2`fLE &HĸɋC3;n%DDSϯ'۬'ڬZ-{RY7;IXaD|Γpg@@Ē%0Ɲ6%>}[x\vm-J%dII &(k$;+Un%9\)ׯIVfZ rl\ OD6$& "&Ω6 ^D[;:vTmG^(9arĞ={^S<Ӯݎ7G=- uuJ !s~C~|ܭS IDAT:n^ )tymGBTԭj.cazKA;C+.pkl5'Gx$-Ȉmlہ< #{ׁnٲEE5'`c"!^g{2f(5ŵK󪉻 ݓv ?>ց|V\=Tn * y 2D9j;֎jp@t`p:̙C'm.y vW=|Ӷ`q u^+*\"%m4VoFқۯcF+1/"# Q byn-!bc-8p<:m~bߘ7AV"v)͚9"}tďҝy̓L=T64ې<ɤLo'$;&WvƼMCᣏ>Rā+Dw5'On[\uvۑNG 2xLzJxsKTZɜ9s,na/>9L#GA<􄁣g~%(D D> 2uC LhT$[M6Uc7e۵<)}'ˤY~M(e۵ϸ76EEY]={vɖ-رr\̘>MΝ;.ie횕qVMxys\H $;6zR˖Ϸ9ڰ`\iնr4o!sL{ΧjRbfHbR\ ;OWy;Ij^O`NJ'G}*;lm6@4TN8QZe@`Я8s|<? </ȫYfաaesoh>)9~M 3J( DK'HT"m̌x9sܹIrs)bϞ풘x\ðڐ~ZZy1LcbnCO[>$'RCOaʛEDndĹr"6Z//m{U/Zk;رI_<'čbҪU 5ᕝW HJJfrq#o 6v)uS 02/L6}êJYP&67nl0O%ڵLIN>))r(U.?!W.K|q)*L׳%=-NU~|HKP΢m3p G!551WğmvXjtsxsy,^';رfc <Ԏm&cƌ WcǔK^vY, 3i =\g$JE/v]~EnXk(eFX%-JZ6oL n\^nE ;Go`bik4h:Mj7E-on}.iqFDWx[U6X=/Z#e~|ڈ6"dSD}ħkp s$Yx9HSHYxRtQB1؀6/^p}0N!`Xv@tD:haO0_^L_h ZQXPtV>GNgɰ52; els}~&dx[1}:Ovɢ%7'NJnNDGeKK\\\()&L`BL [r%S&*訣D>c_]SN CzZU~{iU'(J%JͫWQGHta<9c^_W_,L6|So~_)G޴hK_A0ѻwo6U6X1`':mGڕl2@@na'Boվ-$ɛb{}(`q!٪.g*REzvsf;AҦd 8cLD ܕqjHc#!Dn@H@w6@QbnAި1tP5D4 Eeu<-V=P&v66%"fE\N={vȺ+Pǣdr. sX̟+ȥd5#=57Qsyob7Op7cp@SSSRmAh^wv1Tê wwOo*VɟĖeА{NBY(ٱY9AKf& H`=q}m۝7sfW_֛EqqqNЗ[Q`8ouvۑϙݸ d(UJOOWO.X'ݾݪRoHHkM7HUJߏإ5k֨a5odNrPg1~׹7K`X rVz8bsU`' L`v囷kGӼ^˖oeB&u=,qcSTpAtT"y1i\I raf%HrR\M '%;+F^AD(Ý:y*sX1ʓnSO9&WYjK_7AY]0$ZyIM`UWPt68cJOfI.SUƾr2h {>s4y;U'T6X`ezbGu}O>d x@Qv~0<1@$wۀI)YcKw`,4𚳵kݻM a /*uIe ӿ elOڇ1t]b~[ o-~.nĘzlJ9|Óu^!pz0~˃>D9ms.uID:UOV TE_l{t6+*n35IZoOPK6ongnoogn/{vVsj^gV\?d (uQ3O46MN.yN2@(=(JP(A d 2@ dL(C2d 2@ d 2@Ȁ%(PD"d 2@ d 2P& ]Xz0d 2@ d 2@Ȁ~%\"L d 2@ d 2`ǀEK. m@ d 2@ d 1@Q" E$2@ d 2@ e€E"a  d 2@ d ;(JPDD d 2@ dL(*'O>6 d 2@ d 2d ,X@v%.L](,,c4i̞=[|@Ԉ6 d 2@ d RaW?Ao߾2eʔbѷ̟ce#ӂ2pa0wH~{P&?eή~S CQ 2@ d 2P ?'3fLǩ4a87~7Jƌ)F##ʰCdıW̯MOO={Ȏ;N۷osںuݻe;P⃳rD)F)F=$sJYQ(Oi^|Xy#d 2@ d錙7 ;1bσdȰdА2nb~'Yoa(}*(QPP Ƅ2tTD9lQJnvsv_wuO}G__ؚT|Y>Qq;˶["UT -R/E 2@ d `fΨ#vG{b[e2~*[w"$<#6o,:;n?_)an(a"oaE%/c G2˲H7ȝ)wYYeePK0_6<2@ d Ta۶oQjj#Am۷z,J#222ŋw^G"KUx*JlR:'D~V計#_zMgiʊ&J (hyw[L,H-3b'>t֥W0Ŷ 2@ d28gm;r1pЏ%H Wb˸~8qa$_ !J1cT"OW>ל]%[f'Ls8>jtO]=oZcO:=|wv,͜/^ߧ^lSWSeu]bt,V?v/|M?dئT5ˤMr]JcVo o]i߳E D M'~ǽѸ ߀0ЇGPe:{W2# Qzl1}}層]m`'M d 2@8zK}6"* c 999j?O$'' -JlܸQ\}d凾KXmn?o]GJsʬDSQF1Dmj)Nꂥ2j0hBMG=L<8FC"X:pk4\|o2@ d 0Qhw-H BBGLX FVz_olٲ)!H~$gF{-.~0WHQ㴓:H8x{x?A9V?}et%0!vlyK̽{N/ެZ H ]a!ƴ|iiy# 0s8D DJ|0c;#oc;6@].QCAZwi!* J‘M;Tԛ1v 4`U_tΖ3b\ IDATNd 2@ d0(J ޙ0ataCX-u%OE ]e%%9$-'dFH LR7jQBN=*Cm`"ѡ0{-6D. @GJ?< xRTb}P %mFQ>*Q?ډ|ǩb|y*t=%Ӏu%0%~c^砇|3K%:a?DZ%\b @FmXC$ :KzOwN/vWj(Cձwþf_2@ d |`%HmV„TsEdggW޽[ Z0/uBx1EJ'HaVFD9 &R|$aD,բ( uz?}So߸nAe;-uP}!mZv ehG?)vH 0ȇax33`&zmh#9Kƨ#Z>pUbDLE "{(DKQ9d0AD!̯7}$d 2@ d<146.#aD\|l16--MvYByi%>X ^._~TQ"77Wi޼yk._(Qa*=s-ȶg= d 2@JrTD9s獾}}۶m}ʍ7|92@ d 2Pɉ-G;CիDFFVgT1X1PWqKk ˥>;""Bnܸ!׮]S }._,EEERXXR~~ $.ά'cIE9O* [8S[+y5 T^Q;󨩪+k t $B#FNM@0 ́QdR0VI & M@, aoVݪKUݽתs=g7?XȈo_WG 1qH eD0BI{B|_˓B{}ΓR=Y+}2`eb#%r3L2"I[R|ľ!gd!=Sƾye}I)[UN>VZ2p1ˀ'QHIwyU^os>PSXeˮl]qҥ/J{1me-{Ye"(Pg?СCʷ9rĵh}K_r3fHvEm]F*JJ]֭Yo0gD7xY;ydoʕzN,p8 *Ud͛7?-9 -/[9x()ៗ>?s +@aB$% $zz~_?J)LΫ? $`vڢ >2s?޽{mۖgX)¡ c=Μ9{= /M6i4mK|as/Uc3ڈBB)}_82r1]QϘd+[t͚5sʔ\p'bkDjd7\rg4 N(4RgBBI իWWK} &_I =_ŔQ2FwsaGo}vH :%2}Cww]w'N׿uo߾wnE^+ Ǐ!& $8OΨ_4ӦMNdZΝʇ[nu 4HIwM7`צMw믿͜9;vuՑXepL}H{J5J2wcfVJ )e*}3 /1Q)74(˦Me˖Iӯ_?Kb qFjԨ`R͞=)u>}n%%=>,nCJB>3bm1l0 | :JmFYzIXW@LPw8OTSӥK!0tu֕ EAWR}(X_~۰a7MQPo[9fyYbK[/}L#yND8@pȐ!>pӟ 7Md;&0'ǔ:}O=3brmHs:` w^` &95KG=׶_0VhW!>s=:uΝvNr`ոqc!(|[1 7+S ?„%#?jm$J!%`G[o%<ms#^8Y2ʌ5ٳ'sCJ{R(JXFt{ΝBZ͈ cI'3G_Y77]߿kذ+ݿۿI$=#=tpZ(>t\ hc(+cDZp{}z:B1ec{-`>z8{dܹs9f͒%ifbTڿm h>:|‚.=#S9e97۵ktׯ_w̘1g}[2-Fi) 䯖:lڀ\Ώ ^2{Ƞ="[Ǻ2NfvpYI^b}LbJC 6z-;afetʀ<+g&0Y4aƇF?ݳaƢTmH YftIc=CuaDRi3l3}7Y,XP %c(i DZ :Kt#m#ہ ;VZPױk &P2La?#|u9F>Ns8ŏ-h.֮Q_N0AtۖLe)XJ۷oE 5Ώ҅o<uTO` 8G,H7dn,YӥwQH 3 ߽n2wcfVJ )"%ԙ#KNLfY?t萌?^oԩ)Ѐ2W 0̅N1Fkh|u[I n3Rm{AZ=ǖ;U,DY|(6l3UrGa" Vc,1a| c)CM? 떇e*mFG%%N3= K :^L!՚?i"T=c6.Fek H ֳ];[vWKX%[-\˖b΃4aGMu~T.eÎ;gc)iiބ0=k4E@zH X6%n2wcfVJ )#)A=8ovUVu_ep|IpW ay'!Tkժ%yT֭['! ۺujT)#yI$Jꗾ% 1L%#瑖#G6/6K [1!0iҤXDƍSF  Oz:2ky@T KFϧkR3\"D W`:´uu [t(ʓ0)qQW~}~zdy"i":8QѤZ !>)>@|GS KJPS wѣGFJpcIن!`TFJ$R[f-d۶msM6N- f;0FF3Lzb"up;AC Ag]F=O v;vmw=U'˼YL>y)H@\R 8ԅ`7㤷s 0OM' w:ʓt/(nBu,)n[`]AH9ߟOҧ{r٨ ߷з!!xW~0($aMS)(@\R{yjlұst`vn"@G/wu[lK!C]IWJ;H My{Lyψ;z%%.#7OH ϴ͛7"%1j… '&8Ĺʕ+`|uE 9БXO`YJTgFJ$qfD.2# J <  TXt`Ub'HhbYf"%YJ@~9q- %%_gxX-r{z0״v^a#@Qoj#tb@--F8';sF[^ʒO 8YH{_HuGOJp>~dQ7OJ#LR 1HV9{a 焗qIN+*8Q4 IDATI ))0R3<\cǎO1**y“" Fu޲q(sNeʿF4K bJ*Qϣ2)T-#pT 1/B ̽LJOJJ/m5$?CD )dZ|t4#?\&ĞA7f͒iQL$<#n[(0#Gш~tN9J(Y:٦dʓp{,4՗Uy@/C?XHRlp|R"|<"GvKJ JH^S0)A{0.FJ$) 'NY s$Qz4ՑpAaǩ" = ?p^+f2EGiY'Y1rϻ;<z).4U e9F#LAШ_[b!`Ak2#QJ:E%ߤt,0l%[=kō;quoO>;d5Ӄ4eq#R陶7W\qOx@:O~i+nsu)Oupѩ,ex/~Xΰ;qFŧ ALk L[nC:FEt\ J>KJ֏2?.yO3l(0P>tl!atn@/Yw|Em72cG C0 C0 C0 FHYJti3 C0 CH~fΜY$oai@)aDȮ`!`!`!`#%(G$!`!`!`@)aDȮ`!`!`!`H;o$?m.ˑiQILԩ#ބjAD}zpFz bekذa08W̙#x_'`jrwߝJ\(*O4yƏ|q/o7ؿk.} FӱiyuHK4ރ/DQLjnGb\رcAHPŧAnr?}ohE#T%2!PN(!K B}BwJ#F>BfiX0H\iM0ABGkgɓ'mJhwyG={H2U_~e駟J;X퓆Ic&a!/Gn\D (`Zvt7xCҿYg^N( :th7m$BFsJsڵr!@j0!Qv2ƍ]z$<8zKN$%8G'%%;a\l7O^2,׬Y3;}u)+Ꮇ<ޟp3f9%X.>W]u۲ewg'zOӔ}se˖ek@flF ۷ow7orb$CM DdB6!6zh7f^fk߾}p_XfB>)u̙3\I2k壞ǿ1CJaiٳǦSCvk t@Q8zjQruQ21 aÆsǏe)ѨQ#Gһwo7m4̸3&E@TԩS/w.\裏d;ի8qbpn)0}OH,G|lWYdرlG'ʓ0)M}|$)iݺuݹs>MJpӧO AyI\I yP\߾}#裏u/.)AGOeG:a+}[@"`D YJhAQa"o99'wXaRM/ѴiSr= 6F6a?3">BgV\7,Q wc(i(4tum[`cǎ1نbU¼YLmݺ5sP$liG .)AGGBdF|Hz0Nz;pt°Ё|<{:m7ӋvpߺO@7zз.E剒L{|_ZI&R@_P, jժ$dQ˕` u/.)ѿb҄ފ)((ARw q1P9e:˪Ĥ+{Fa yyf.464b` S,%|Fp;;jJ?H[ې L?tlW PTPb)- %%pǨ,8wNWIo.t4r]we˖d^q?*O92dڵkt#ۄW)N'JJ`ȳ.#7OH ϴM!%bK:F08ul…^ Re]p}ʕzZKJp:"Hylo,%*3 B$% zb}c֠J*7N)vs. "2<D?WTtP}at'est~a+dJgNT 5t hHT %hp;B"D/y4M,3,% i|'O% #% [KJpСC`ٕՃ :t00@pF0thKY2iG*N'>)’E%./)?kgJ0>zNx[J4,Hu`- K&%o~#N!iO~AC#GʜSNfD@d3dرc<am[BCE%O" Fu޲ ۹sNԸeʿF4̼;CURz=XH GjJ|"a%>>|RPz)̷t C LD:imXG-ҠQG~L=!Ճq[/n g͚%7ljԨ!) ψidV+ h=JYґ8-TXʫeʓpA!,4շVy@/C?|HRp|R"|<"GvKJ_JH^S0)A:0.o? ysQgH4e8.a;!=o7au`KY s$Qz4#D0!Kq^+f2EGiYag1kwߝd{}bXF ~όCc4DsFO(+VhR[@&:md#Q!N\M0l%[=kō;quoO>;Y5Ӄ4xq#RO+u)HTQR)N)Oupѩ,ex/~Xΰ;Fŧ ALk L[nC:FEt\ J>KJ֏2?.> @ 511 K@/YO½L^!`!`!`@!`(ާD+{C0 C0 Ct3gFC!`FJ)QZ%0 C0 C0 Ch0RH)!`!`!`FJV1 C0 C0 C((QR'/~I$l'72w U^'Hm۶OIi<a޼y) Oτ+$BC  p%I2W]t='2zy遈 Æ Z5AxWuWeTht ^h|)OHP?}-[&5|C(ARSNB> /Ķ@RX$~X=C&db ѣݘ1c$ٚ5k\^덅 ǎ9";9DqH'4r?Sއ#:e,f͚R|6l(Ǐe)ѨQ#!k{ } 5k-_1RgD6x3gX~>{!NjҤ\:Lt\1b($^z{:u!M@)~嗻 .`QGv٫W/7qܤDzO<7oq'j)}|$)iݺu&%xӧO AyI\I 9׷o_#,CJL:UPQt;/NÇlhB#,o9&!pu~ +G:\~'Ot /R͟S DH׻K'%`]NoJYDzJ*sҮC,Y"iUYxYJ( \Fu!!dD:| B~bvv)$a#By-8*@_C'C͓LLQ2I b`I𱸤䣏>*տ=A4=zDP6R{ V4 GHɌ'%Pd`oQ|Nss;wXѴiSQICaن&02|߿a)uÂSNx@p1SNc_P4&xgݲN:hg E#.)BhCJfd$ ;v6UVJnڭ[N6WZ nݚv޴Z2Q 7pLk~t/(n$h&tnݺփEJ0U8}i'H m:$~_[ˁwO%uPZ5BرLGv,WR `/qI b”&VH EŖ@q `D 4$+V&%fΜ)P:4aFX! #}4XJ0_ϟ??R 3È/EcMXq,2Ex@訁MJF?X8de +"} !P~Uyc:Ӣi3B)H$sIG/8^lKC q]v-w`)Iַͅ>b ͓(R3MlͱH ;.5mOEc>5!5s( rJ=-X%%8_Ѹ$u2bDE%DJ0FeeƜuF^|a$%̙3)fJ+ #0:?s|`M6aHRCDRJ]A~6&<\Gk: !EFز?X* #%бcID:6)RUVi}`E2#%d.CO/ iF5j w٦|F9k2J`"3]y gCTh[g7ˏod#%CC424ƒ.,%r!%/)5BOJq=t?gJ0>zNx[Jb"n\:(!R/FaK { $W$R8*4TT't`!4gͽ'@:By&XHWqudd%M>*otȆ?k;qtIkёpABo =4,&|r3B~Zⅳa.xSFh UBQ LAaZly6^U2 3 t,hA9eXT-b "%ۥeη2‹c|D,d Jm&DN,}Qn)#um%h fGנme飖tԳ\N.I)Oup#,e ~wc"@pNS+,EAgQHhѢ@ߢtBqI 7`M CH FJ)ni!`!`!P`)aDI{C0 C0 C0 `I)!`!`!` "%֯_/֍\ox&Kmr*„04?o޼t23A19O܋xo&&;Bؼ.,d?)JM:C }p%I2Wd^t# 79(ӧOHDL6l< NHJ0pʢ$*O4yƏ|q/ohxwj!vhUW]%ޗeyB㮷lٲ 6qa@ p)eBVL>fʯ~+( DA5q}~($V*0 䦛nJiGƌ#֬Yڷo[papȑ#|CtB#1idŊkϞ=[7f > 6t=?~|,KF Y/ۻwoMȯf͚˗K2R"bv)>+)!8[j<ԩSTS)-Z[#!`\:|HJDX̙3ȼ{*y_ΝJ{OZeɒ%2Jq$Uv5\# jѠA8 ShqwzKOMYݻWFP}aTd͒>+?mO4))ѸqQA95 -;/ BQ}Db'N/^lI(@1 %#SwihgwޝrN6Xr-k&%=*LE͓LD۶meMJ OJ%%'}Q%%( ]# B'lEbKC\H BlݺUFWpWa"MMCGaن&02|߿_S0SJ|@(rE#@cHƈh`M3ewf?ycŶ#.)A3Xfd$ ;v6fWZU*=غukn:\j(N[sZ2Q b 85Rk9/|nf&Mݺu +`3p7O2 :tHpqH 'xVXku1JJQ>l%.)ѿb`Jz+bbKC80R"!矗(tQl2 FXqgF<K T+XJaFpׯ7nQ5aűGquץP1C~7%-s }=.)KFPR|X[1#%d.h1宻r˖-w:#@TzȐ!k׮eҕ,%2LCQɅ>b{'Q+8Q t[Cǣ1VZǏ/=mʕzZKJpZqHdHV[1#%DJ`"n[08dIRC#^DRJ]A~6>އSRk5K51r~Y˸/[t!-VQ`)K"HtZJ; .:-X)H$s4: Йf@-nQF~m:gQN5%&/?if!*O|R 7 K|$)a3z|]r_Rj4@S9{a 焗qI)Qi,ctJ%,tMP4li\ZH)A@匣!>YK;VL?K?Th O Bh0ϚCA㯑@ i`A*_DAISOc:uSrB!%))OFu&(`y1R"*/Pe*qSO=%M'%dPQgW%뤽;$;~p6aqNMr%!(+jEw9rI/Jci2I͈=[+U$);W#}D] <"GvKJi5.3bm5&LJ~t;21 K !%(bQ zr.sLO8!f(=ZJiՑpA8qgĜ׊ΔU~i0 ^zr]LY!YCFPt46ޣ!&8FgFR+~ xVɔ'4`F4:TӱB9eXT-b "%Eifη2‹c|Do͆}mIGB&B冺2[GV6i~t :/zV& A'ar%%2IN7.:}y W\!?tTAFUGlCę 6K,a::P6AgaR?ƹ-DwtBqI 7OOa ҂A+bt_ ه^lbp_2{[HDe!`!`!`FJ$Rʝ=!`!`FGXn͜93@0RHdx{[C0 C0 C0 AH #% 0ڃ!`!`!`$ #%HV5 C0 C0 C((ARsxUֈZl<oʸ$LLm۶eSO iM0ABA٧&'Ovm۶Lݳg$/,B)S1 BmٲEM6-%<> zI yGݬY3+u£Op<СC].]!Κ' i .)A}kD RU:tԨ7m$QS:k׮ct&1s=nFCh>7zh~ZƍE;vСC(R7DI Br#NV%`,׬Y3;}u)+SݧK?'g̘!9u,R|AyJt=/%p)&!P)QBuXC^p!Bw8{&NG.#X"/;c|o,3ITI 1p7O2|uuΝǮhRbu)o>)>+)!۷{GtSqI QG2ïJl0 _GGuN<(_\J?_9veTa2uΞ=+f:(˾`?w\Wu{rɒ%2JƍWڵF{AجYkL-o7da[n2Q20իW`揜OCR/8P F 3Ix  *_u!တ`N6JaIg)\T$))OB%O~G~G:HK2H 01ZFRU9=S$q|RNu/.X'!񫄳 6N_ XhFH-ʊZQ`#Gt#m$::f`BN0̓N}6ɔ'N7UZI6Rw@?XHR:FEt\-/J,@gF'}X燥B E–@a"KVSroQ۽ C0 C0 C0 BH(reb!`! ϞTUC0FJ${!`!`!`FJ)Q`E0 C0 C0 C ))aDRʺ!`!`!`@!`DO>xaK/$ƍWL+4xC<m6puBpu{9~޼y) Oτ{؜Oo\iӦ0Yak=cAD#Hē1cIjsA\={0hu)%5F1g/-Խ3I&ի*7A4 ”!7oL;xx'] T&R&ueҤIHƍJ 4ȩf)+Rv^R@!cz"Ei޼y`hAӧGZ, U ORBH2 y%#S;ctvޝrN6rÀXZ X4_'aR=~ 7O2 0MDЊ&%b`I𱸤$k|KJhZ,A¤V[oU0 #%>'3@ƒXR!\ǎe:Jclw=I Bbb%DJАXBJZ&Rbƍ"{QFu떓 s*! 0?~f_='>h8üNxe"%PGaq=.)K,Ek IDATFJAMtVT0fvں[i Ç3RBHC_ ԗ-[uF}ǁ<>օ]v-w d@<3gR5R"*O`ij1 ͓(RCl޼9)~GW+ ΋:>5!5s( rJ=-XV4)Aram[@a!`D Qq1g;2Wg@(3qy9qڅK˒t@ {1 +w xVɔ'a2{ 7G5K!}A{2E$quC2YJp Ƿ2é%YLU o!aLթyzܖ; u=eķdm!t_=0]OƟQyk ~V2IN7.:Uy aR*q|J0BKX$L<0m],i-Z$qt0*>2.)BNK5bUl"aKC0%+)EߨL^!`!`!`@!`DZJPG1 C0 C!#|0폩&!`$#%HJY4 C0 C0 C(00R=!`!`!`@R0RHu{OC0 C0 C0 CH$%=bhA#\8F.x".%~۶m9aBpux7o^J:BРsO^z%iG8}xXӦMaTH'rQn]=ZѳgO VN2@`o~Šzd_&!Ν;arIoDHCB2'ݤ^zʋyҥ0D=L>=8W#=1nذaO> 'i%JP8hQ']]0@CZqD&!:Fc?:ǎGiРuC_Û/Z3CC(^(!R"4>)Ax9ҦM7a UY@M&O,!Py gI/,d !L">!l"ǦM_bQHzjYGx.䍉!`dFaÆsǏEJNl֬[|cHSGHSN5 WĢ>pW^nĉIY5Yf'x5o8ָqF~РANMdR qDÌX-X@i=S%}cBGef)`IbQ')30#X2H?>0D55+&t Jt *@ed{yh۶L&%B "I𱸤䣏>R,K0)7. 90U!`&FJ|Nf4)bBw^iQ!? tji(,0SNZiT XjFՃA ӱ޿Xa@Jpݰ`JYV<+kѩUL߈Y# L?E1 ر#xt1߅իZYnl2֭݅[˘wr- %%*N2R"~>K(Rw`Ё`,nh<>`| Et &tnݺEJ0U}i'H m:$v8x`n@2'EZ;  l= zi375jp`G`ڪ5:tia=)RUVi眃J,*FJ(4#IFJϧbI љf@-}F%l`!i˓&Q7Ĥ=&}ٌ0EOJpfaɢod#%@D5,bFKCJ,_@S9{$`G /#bۆ@r0R"!Ν;eB(8لcǎO1w4ICE/8Da#H 4<'. -LR^kN83gF|$L ,- #8}nSۙH GjJH}O"X@ XB0 5Qԑ(lu_%SKYKg5's©NbGR\sG=N٢#h+G[ȴF?`@ǔ\R0~\ILỳN)o`)F~u,y|B#pv@) 6K&`uEqEd:.zFGnKJDԁBuH(L}|Jm7*q!`!`!`!P@)QBTQ C0 C0">TUC0FJ${!`!`!`FJ)Q`E0 C0 C0 C ))aDRʺ!`!`!`@!`DO>xaF.,ڎbbZgB<Dmضm[$rfx7͛Pfxz&=椗^zIEZ76N>-ަ9ִiSG,pr0}\@ڻaÆO>$8(]CqW_}Uw]FF />sQ!D`]OǺy  !-8x"j\F# qcǎIP4h? И@"`D Ǐe ]p((qM6n„ :7L<ٵm֡8{dLNSLc| شiR{Z*DA ,:tҥ#o! h1"%_#%D3_JWܯ_?Sz衜u}IH!/:tM6;^;t8׮]+Lb(]C aoV!o* 0lp 0 Bgf͚AQSV44GIx3fH8yG˅t;އz i +Ohbŋ%HJtmܸQ'-3bG!2zP1L&7tSʨѣݘ1c$ٚ5k\^wu . 9rD/v9sFetR~}XFۃC{#QD+sKgϞ<=D˝w鮿zפI(h(J1b($^z#<\02#@}հaCcDz(oD٬Y3|rȹ󧘏DNr_~pBX~GpM8187)+Qb0k,O͛,z_FZJ()A:t$MJpQ,T.'%r%%As}u}*տ$LJ 1}r aC(L(zR) 0aRń`޽@C8D3~QX`N43T XjFՃA ӱ޿Xa@Jpݰ`JYV<+kѩUL[߈Y#BMc}!.yQv;vmwaV*!n֭['Lw#uִ4PliG .)QuXRDcqE#Ih^D2iM֭[`AYQS%ߗVy`СC⇋CJ/Cjz]cǎ2%۱\I N;'fT4)A^S\aMkKC)QB Ɋ+ԄIpQbk<|304ӈb)AcÜJ|E`^<K `_~ ߱&>Ԕt%Nz!,J\tUM![j׮e=$/,(.zH E–@|Q')?%(:u]nٲe.YgwSΰ.ڵktKL5*QyO`(Z0r|$LOؼys,RB(!c QǘzOz-H )>J\wunʕzZhR}I2 ۶4 BH"%GƜuFi{2EKFWH y!9y-ZZ!5t hxT %hT`iNpɼAQG`ڪ5:tialFD;:]YX'H &Q {W'% 9TΞ=+z(؄sK#%ˆض!(!R/aK;w Q!AD l|űcNJ';QNJf0ChD? Ɵg{VSrŐqI*JLHLaH4#E)N(@JTCI"SD)gRB*(C`r~kﳭg?{繟置ak{}Ǔi$LR0ajĉA#> e ,- )$XGp,:õJ1 w2G8vDD;-7BRN xby,6iDZO4rȧNHB샬ǯΦ͛Ix^vHfn`F 扊L~w׈=ʹ,FͷVeI.R~3ȧb3-PHJZ6mx "%@.!PK)QKH )51Ŕ-WZv7 }i#qABũ" =s+-,]g43%3gx(|crY2bdR#E8KDI/e)o:x_`Z`CO}BPY ĐM|-(dNF!g|$Z0Ѯ5N{vm RQhǑ-:o%21l1e1}M0|Igt3bG*LY#?_YvW`j͒~cEl3EF"NJy{#O`T>bBI I6aXnI7 -@D KnZrmEؚZB@! B@! "%jD51UE! B 9SU"%j>B@! B! RBD5IUG! B@! j "%DJY} ! B@! @5C@D "%FyeE0ayoxE^|e. D{N=T{gh&-[a>۷oL,ό%Ľ»U~HD`߫jE߰Hv- t IDAT\clٿ_Dq ?G80Qի}Pc4m֝da"yQ""%j)v(O>q.j.~>s+|ȿo>T$\tsr1ןx Or@@ёJJt.] 8)q׻Ν;;Bh_~/^TlRWDɉ*\o߾ ͠A\nw} hiy+js@ KFQC^ϟoP 6r9w\BXSCf50֭J'`J2eW#)A`¡W~P%|yBsa}l-ðwoE8yRV^>~vm7G(04<"mڴ)ME9B@T5:sO$1|Pe%+ca̟zꩬ|l<1&c=-Zȳ!)A=IJ<;ݬY#FqR>tI 65!AJ@SI&ERsGgߪU+B:QB6;}qE֤$ d;uo;hy+V7z&qR Fዪ3I#%K5lmڴ_ؤnذ[HJ%%|Es{=zmeEH?K5X`ϋ%pC\%! Pgڵ<[r›):uRZ| RE׫W/")}g% UVbz|h'O\'%0 Ie˖y > 4pGu{Sz`n_gv:0͛{ :hx0]CL"2zf#7J}8=|A0}tYJDhhE "% QQ:3v0t\G KպukoHG 0 0Duƍ @xLM[~!$AbZ} .Aٳg&,Y$NJбmҤ{|}&i֡ DI ,%/1 D Ix^1cƔPRJ?AH . ")S!m!) QcH F`~g>Z}28R9 6|SBŇjɒ%x3h:j*_իwF`QKٜR⥗^ۘիWf̓e,g{r䋔0Դ#P()"A67~.+$Bձ,a 4df(n=)W_2кKNIXe=#%@{X3I#%\R{7, ~{o=/˗?`/ DPR_~~&$B4)QI뮻ø(ҙ) #,0{=D0̙wqR>sǻ3gƋ`Tz|l-b0a!ZIš .[["% -@JJpQ=ڪ;+\9rhSNwuFzCǁM0$UHHJ1 #xӼu+^zޢ ,*y“%'%^~e?O!$AG,:Dő#~Xv?R&s=I%_mYJPOe*1K51wy,PtjyU!PH#%HbS9rd{'aKFolb}v)!LmV7~ذa(m4k%UTUJ($YHY֣LJ7#La:-$)AJ1I N" D<G\~xǤ8>~g_Lp! $/Č`@ k7+ўI B3sy:DJ0RQixb*TH:.+WB,Nd#qAb<`u#P\Bv) m *>Ñ$2=J9GП0'IXe7{;?cA@ O2$@!%fM6:olMu-! B@! 5Rȕ!B@!?Ϟ8qbƙB@,DJYB@! B@A@HVUT! B@! j"%DJ,! B@! @ RDIn)Jsΰ뮻:^{1oxlN :!؀gb3f̈/r<½x(V7Uw5(N:) DݻYHD+Ӣc4a!`Fn"s={7a„X@dp؄<)q5П{챇;S%lGj߾L,ό%ĽτDD }jTma8XsO<%ARX٬<[@(rV^5|6mNga6""X%@)Qba{IM0RGu3GI_ YgW+s1+#MH>RV=[Ox"$%8TRStRQ^_:wg쳏#N5A) RWDօ@n7ܷo_߾^xᅹOG 4u}wM{rY( `ПpΝ;Z쒸* 9oӟCZR'|1PSL2)A`>_\8KGaGn">_w)> >q?_pape*LniӦ6Å("%J>3|#NJxc`x~߼,VZHqHK`̟z)L]'1-ZYPad')=vf'%C`É]1Y4iRt[(5mw}]V\3 @GC'ϙ3wlrdQB ׻f͚v.+RE^ ?"NsQZ)i?[n޼9Ƿz+" g>}ܸqckJV_wv7;hiŊԶY$NJp܈#|Q}&i} M6 6xy Ix^sޚwѶ!%j,yX`ϋ%pC\%! Pgڵ<[r›):uRZ| RIN8͞=;:>L4;ȢZڸq0Z~9F{i۟T LBRbٲe m o!XBRFʺ[|QعgKVSfͽ,BΫK>(0LW#q@:C"H: g}?s?|X>},% -@? R5%8p3|/o=OU`][vX2ґ22L Qݸqc?Ss5_ sAìKPl{ K3tl4i6K3I#%e@'MJ`)A!RHJ %%O3Ɨk?Tz\ׄЉ[X]B`" R2I k0Geg_{vna?J-:?|M: qR%K8>xt;(L$kUW];#;x)BJK#`]Xzy,ygy"% 5-@JJMz7; IvsulF K(aXFbq1X7 A`FH`N]t<Bp %%pǨ myBo 9WVoP4mPjکS'wwfd7t#g8޽{t|M\"%x7L7"@3bĎ. PfH}&Yc DJߑY!hc #W޴iӼEjXƚ5k9j{ 녒o3)>IABd)QHaÆIvf(3a #06?C|SVG2|3L3H %: _#+ƑI|<`;TF9;ZSZ_>,}Ȭ+LBRtRWeI.RPD*J#NT%ep x}Ngy8>vL|Y()jS %%aa4}ÐRT="%j)D#d>48<ıc@K',*y“%'%h?#D#OIQFyԏk׮OVd' ikU4K #RQ%ff sw!)A:aW_4R",({wnȑBZ/mMivOf>$3mYQ 0 X&,yŸsR5a@|FₔUO͏ :*z1W]tsQ#ՂD Q=-EcΥ>r8h_JagnF0Yzuiڴi#^M- Z,BHT1B@T59J"#B1㮸 :xЄ+q= O'%[Ox@8k?:e[W:g|[xqۨ7۷' !%ۭ[7GC{q$@!v-3o񆟇5K!ID!hڀK/*ۘò׫WϷ CMA>sEJjsEJJsXn IDATAѾ{`| o7P k[}{'}K=zEJ0U2e]ULo}wtX.V)Ad>X ~{o߯](%%w2Ud,L} 4o%0THHJ\wue&"2sÑq0 ǝ>>[RkZz2K|ʈ9 kPRG"Y;wd ck(=t~-16О{i紡J ]t'bD\h BwuFzCǁ<>ݻw/w^M߁DH3e5!%9d}&Y3Mg-0#-CkfA~V޴i\+%ƚ5k9l޼{CzǛ5@r&DJx'J5#wjG'#X!0EJЙy̛fMPKUVy+ieV^>uOe]6W %%ÕO<`FD U H#%,%ڵk3r,,0$j,é%Af,n ÇCm3-d0{챇W`y$Bǃ>h[f=D|Y`b$)"aa3v88%!)σO>̓$`ǎ/ %%Om0kz,tBɇF+T()A8$j 8d=W^A"zHMIiD3`IǖQП9L8V"%h#FJ 2GLw+twyw8!)Aۇ=Sڎwmh12`޼yTG1fhBV̊"T6l'*jv+Q viq"K I{&q{'0Tg_F 򩘤uǷE Ix\'D ; %%R_5.3槂5pN'%uT:=RB)QH FUSHOrwi(s% 0 }k9F9t,Yy~T͏³% 8\ YUWnÎgBm|[J{&|ytfQ;%t蜅ΰ]9t؎[h]H F:t;ʹ=Vj.dzƖh!hL={eiYK@Cnh둑:b̙~I55P_=DX>4r/_R"$)ݐ 6}τ>}(~Xv?*B!! ) +%8`} ڳ<ν-/s"XPR ͌;ַ8y &aѯDVB[&\>Z>Q7LEؚOIB@! B@!)Q,%X! B@"".ƪ$-)Q[d])B@!  &%y/JDG?o…~SN_(ʯP%! B@! B` Pդ>%p</~8 8? g:! B@! bTGR2Q $pˏhDM7ݔIsxUN2 bc>Ox31^1c)$U6lNBaY˪E(xKؒ\tIQ|n{@k/G BRSH:V*ź3B)Ny;yB2Ϙ1# 79?&LH|h CFԢ¦UРD` dv`x\Q- ?ӷo_2bSAnݺy 9 jTQU@M@`K"%jT`5 o) ܹs} ! `m lD:X*@0=#֮}Joy*KZEJ0ܺuku )洆)T!7n+cjQ|?cгgHy׭[XXrkҤ{<ԕ}&iDΝ4#CMJ ֥JJП3f/ %% L^0dDP.RkBĭH.Z !uN|)}<lm)|:ЉClJ ' mâ|IN~>PRK>v~2V7$ck/RtlcNhvzf˄d}'15 RIEJjsEJJsXAѾ _P|YD#;,>!%XѣGd=X,R`ߠA7e^3I#%i\R|<-"A,R&DJNJ5|جiӦ)ak֬)y{ 녒ohAL)aП(#);J}#E)AgGU7͜|S\j)NieV^>uE(kڵVZW()x4"%HrU@)d)Ѯ]D>`!fI!Q;fN/ 4fqc >*l-m '( %PIL]c=;&:0MKY$$%8_XX3EJ`R|)x+ kW%!E- Ixw4X$OȽ|IgtC>,e_DzcǎQ4 aPO VX,%ʼnנ=oOݢ2'I %%8=ϐpرc}KbKaf}bHd%! .5kµ x!|e)5%B@! B )QbU #B@! @x}s9ܒiJB@ڂDU,%Bk ,pv+61%N:m*֖BP?|ɀd@2  H$,<{*thBui'Tp))ñB Y 3ɀd@2  H$ ̟?H?a Z^ !ߟRm1R0Q9:p0 H$ɀd@2 ˀHUDE"%~om3g3g<_*d@2  H$@eH )ŦkYNAJƁ)k\&- H$ɀd@2ud@D*"(Yt1!RB5Sȷ~x׻w?/~TX ɀd@2  H$b}pYJH1cW<$š.nye) 1Iom޼Hx?s..ɀd@2  H$"%N  CHҋ@NzyEJAJq EJC mK&$ɀd@2  l)!8T^FJ"%~!$77gΜr9K+Oq.E2  H$ɀd .KUDE$8ImaYJBL_ꫯq… ]r VJ2  H$@"%ReBEba"%&`=x=k=kɀd@2  HJAPhyn:de@/BE"K B~~!0^K2  H$ɀd I>H?a b^,K_( OJn]vqGu[jU]~}ʷ|I+kԩ+W֭[W}!mQ!;֝|eΡAe)kժU;S~8|{:w]b{_,׵V~E-!wOu ]FM7䕭p7o(qR -hrk%\eɎ9r 9f1Xĉv2Xd6oFjc;06mooɒ; c_W>b}w5ư9g-l|?Y裏z"(lӲùi?aݣG2} 7ʒ-<묳9X @q r](m4ՏvK/ע_63 Ν7m'-ڡ#8?O<($t-Koa#l$GJs\16>|i]w;3v0epBoR駟m:t úN;y/kw5jT*)>iRiİa3霤}7n_; (sx,qQ :df@„ryNv eavm!G٦l:lPbF٭,[__N4q送aBL'M[N2+Z^:#(v'%C&93**wE-!OǘΡ#v 4cP%ώ0Fgy1Kf)AFΛ7YyĒ+:tӧxtyV}V0/>&GF|N:[og;%b}Ŋ^g<}Im,rA9(44جo3yk97ݺ{=i92ww~fXU$lt|EG 2 `:tz8PA3A'0,+Nz)E@y˥ߏH$>Щ?鯍1spN`6)p4NJ|nmRvmWf.) QcuH[B0zct!?2aylah$D)Ō8nEaLx;4%z8mȒ;V|W(!=Ǖ)5mKT,%{eεf( (!%曽DA&x4RNvH^C8&@$ЖZnXVK;)i!!8.j &SHylݷՍ%K`r;|^EJ&u9"]òwլiXBŧ $)c?6 N_/8+%;m#l_2E <ֱ4 itiD>\#u DZFz檏"%eU|o,]KXMq] XG2<,\> fs29Żt_ٗ@>Zy5nUdyrK5+?DH $$ &㚠ZqIg 8:ID1m =s:r6z:"xFYBYe~ٹ%mgLäk)֛k3X6(`dЮ˔pmLCe}g_R%: oX3#:tZfꫯv'tRxtr)?ieBh0ňSp-F8QPs:CĄDz^Yc:zmׄSF,/MPQLcs-9>tAU@@ d$ SP!NᫀWds}Pvm"D>|ȇ=M#%|C(aۘv-!cx_f(0a;a;z/|p1 (]={=|?iD>%"'Oþ4RB뎐IH2+#$eɧ9Yˬ-ʠnO[&JYn]oχ>d.H z0A{ŷ)IXc%-_p<ڌp 941=%[+ԛH>Izvͬp}eow\>*Lzhguv]~d2'%LF6RbbZ84Kʐb(O GADt:'(Sz)h >lkRiaұqǗ3&T3ZB>#jZt4V̢xF!bby,:tx>I#Ṭ3B=x^|;>R/Pᢳn Qz1m4? z"$L|[+rFJX#bϗ2eV\Kp I ,%Dέ)aBH2]ٲD lH &;ܧPXX7 vN2>4gXNe4lcֳ4G}䯓R"YTq4向rr.6˶|H3m% ʒHq%|;!%xXYih5K {&v\2F^Rk2-jAHBT%)dDh`xL#% y8>|+npl׎cbSC6 <{~f%[YDR;p,R fXֳ2I=ۗuMI H  ߓ8)jjEJЙ'RRQ3H F("떶FJ;IÝ~;^>(\!)RbŬ,P`dɼc:BIxkaUa)LWX)+ʖ)gt͇`{QiʷʐtPJ')=JJTVxIJ)#bB'Yj3}f*גl.rUx^)l0"N3<ΧCo~FpiAXn $Yxn|xF6R!dN=!%m‡)BW{!DyEش2ʹ?s(Ʀ]R}!yVA$>ٻrmFL Ahh xyY a[ C8˗Ab G>n_?>Ȍ>s՗eĘ1cԲB䑲h*Ceb4${Iyioml!7_sڤDQoPEI M/A'_}!U{IK7a偵6s.`CR"MH RЮAL`mw)A+$[lMAh F'" (ftcBIGGZYקgcS϶C̏$@psk:v.`:l&6Үϒ8 8?R\th)V.%r{ÿE8 1Ru8 dNٵ%D%m!m=-?Ei#4}5·(BG1 C(7F9`odw|om ΅O$|:%蒐|#g,5LsgCy `X7G`~!iceė8p FJԳfPI?Puݺߠb]!ab}}W'ز2 R- bF̈t8gUxVBcjS ̇F"`ZJ>4t_5{SJyT#mKܡfe[ d@2  SHï7|6o_SԩӶNS[ -v*&X*K/d@2  H$ɀd@2  H'\|GG~ .t)! AV#RFDX Kɀd@2  H$ɀd2!?;"oXk)gn'`7DvLqcHW IŶG9sσP`9s}<͛74RaBl˗fR=O d@2  H$ɀd@2  T7)-%6x2qqxvHJ}ꩧK/4'A/)a$7CRހm-ՠH$ɀd@2  H$@)ˀHOJ`pA#<͙3\بx{ϟ ȗyCR<^U§ H$ɀd@2  H$[Jg#/-5}x;0p@Ǵ %P~}끺|駕&%`S3gaGCpK$ɀd@2  H$.%CJ@:$YR>׾}{w뭷G/FL8煖V۷Xl!:2ADѥ5j;㌔K#p&L_Kj$ɀd@2  H$ɀddH81(DX~}طh“{<s= I .<1[nrJ_Ho)ACKʉK tMS#$ H$ɀd@2  H$ %GJ1!'%x M6uW]u۰aw(;: l6VX ,̊"$%>c7h ?#C O?' >`O<\wuޯF~&:t |5Bɀd@2  H$ɀd@2Pe$I Ȇ|S||7uԩS> Y5 %ի ?8BwOھ]FH۶mhܸ["RBVguߟt SC# H$ɀd@2  H$&(Q H$ɀd@2  H$*"%pT}I%ɀd@2  H$@"%򚂢zz>z>ɀd@2  H$@)ʀH "%$ɀd@2  H$ɀd@2P%2PI|b8! B@! B`" Rx>hÆ }N+0o~c„ UcL=S̜9|&+XSꫯB@! B@!P)Qg)q5ה)~D_n]vqj㊱O? %IÇ'aEEJhh]! B@!PZ(J"%>c6۸͛7GWx뭷ܗ_~YqynsǏ+ӧO/cp1Ǹ'xnQFѹ XgޮM6nQ:uw_׺uk`(V87tP̹'| ~[~}t7wkv{Q;~߻-Z= K!ZB@! B@"%H %_2dcZ k FWܠAv.Rbȑ]vѹ2gwQG٦'#6m!nr' uv^<(wn{+ ,HH$TO! B@!Ps)QgFJE曎)SN_+VD``#sX"px4i )q}`9a?,%$&g]wuL rذaI&~ -$R"DCB@! B@H"<$RzL8n42!n)1zhw㙖ѽ{n!", )!nȈv;C*EJwyK ,HiѢE?ڵk,ZB@! B@HDJ&򊏶n:תU+wwm 9@CR<>tcCǎ"% D0wBۻ)Susp}w:5vX̹"%>SSb͔K]v XM>l7`7Dy"B@! B@C@DY)ALG S521nܸj(}d` a (Dҥ$ …4K ~i >-hWgOV$9H vB X c ]pj* GH-[iӦEy~ˎ;5kM)u! B@! @i! R+(B*""%rl! B@! @5F@DH"X"DJT8&B@! (C`FÆ }""2<-R"(e ! B@! 9"%RB@! B@TDJ'B@! B@!P)Q4}L-.D(V:CS"->2@}?fn5:;>d)aUhѢNogo(쪝ܺuk?dw$B@! C@D$!ѥ y[n޼Wݞ{ZO?^y\D0,i/-]4{osO<]ݺu "%: 7qD?;ӓ,?c{+]Ν_AnرHrhB@! ExH .Q~{_k3<ᆴe˖o,S+qN.]?#{x77u;C![Jp޴iܱ7o>qnӦM]fٳ9sxG;찃/kb@Bg{wO8t{QY87tP̹2d?vۼI7:,BQF^yM,!,X7bw%D۶"RR! B@!?DJA!%g 毈ڣGoڴX7VX oO2=~ߓюH8)y0h uܯ~+ esK,q;s陖of9RE$˖-vme:me!ħ~ի~-Zpw_R~}~(;pݺumE!B@! B($KJ > 0_7]3f}ڵk2/\ +CJ#?׿G@JY&vEH B<Ȩ HHOtR%V !% \s'c # mf̘̪c֬YZ#~H8"B@! B# R/)t 3ǒa7v!߿T~+w1o2-5l믿"iƵm֓EEHI&SN9.8kBJ:a}ʜTBI Ү];,^>#9;{=[nDNtB@! B9E|H |J4i=C~ +ljo R'O+ʒqKFJ,ѪUlWH(O>d<=:ڶYJZ ! B@! H"HB.R+ /;{(={>%؇_~橧rj̐ذaW恢vqvN:bl IDAT~(` @bg0$B@! B@E,R"[\2 q߾}Kn@Bao޽{{ )RH~S;A"bJ3?o݂|糞FJ@uQc>}v¢sp o}D߀ 1S >d"? k9j(_SNnx;wσ\ K/"娣r8 ])R#L581<]wfG"%"("B@! ( $E(VE0DJ԰B@! B4Ή(!B@! (|0D2dHA[k (ۗQkAэ ! B@! oHB@! B@*B@U.+B@! B# R@$vءL 1co|i??m۶^z>2^{U&L)D իSp>,eƍS ɘ8q}~t4m@4 JTgtbcJ( .G{s?CΔ:矷,>}9d,X3@wM7yE,gjQX\ e㤓N!z#kZzYyiiB@! B(rH H o(\$)-/rW^T`Jw>&"p(85Eݖ?3<3%i<utSN9ō1"~ߎs 5K,L qAS¢B<-Z xƜKXՅ Gyė7rQO<1Q裏|HY|XHX;1zYyVB@! B@b" Rhc)a0`@ y}qƍ˫6wZ[n9Kɓ'{w?~WB?x;s /<:?oSgKW^y;]w6xEA~ ,?e˖B¬L;vl<߯_?:uÚR>\_C_|-9B ;=S;M0!:>\9sիW+qs|IORd͚5>kgre%Çwmڴ9>Y2+ꤥB@! B+WtM4q=bHJ{<`Ľ8}e~ޢr@Oشi3}N8͞=;:>S4G} \/$b8X%$%W|IY)FpHz%~ƍEO6͟n[nc/^uݻ a94,^FM`,SP 9i+X8X]v-YZQd){ˍ-ZDV9Y2+/~]m ! B@! HJ)y;4hL] RX `зo_GI 4t!:uH(kH6oc%5jTtȑ#PY2+/*L+B@! B@"" R`27L:urw}wE^L[ZJ?k-;e`ǐe[JX^!Kiwb9pu׹~;ru扉_mMRzBLIbP|:~a8/oѣGK k 욶ϖX#L"܇~8 -c8>0+>$4YfuӺB@! BXZb_}cH]t)!$% 8)z"pX Rbذa/]v6NaTn!o+[EX<4jW%>oXF<szXGI&EA`IAi%^8)cHJiD$zYy|! B@! @E)QԂs"D@Nތ?NJ,[O)$%@!d_!)NJz"NJgP2r@8#I%4)>p駟z0aښJzΝ$wW)>LyXaҥKOks)K/SlX8X"LрŸ)EԠ-ԵkE |o2rB, jE! B@!PDDJT̗_~[ `jOO|/pxg||]ʇ#?NZ AJ"OGͺ#C]X瞾] &H}>BXښ㟃p͚5>R>(={( Yr.)xXà r| o B,1-+1DA~,1@ |6RD\3AxLe,ʳB@! B@ BR! B@! ! R tB@! B@!P,DJ I#B@! B@HB@! B@! @)Q,$UB@! B@!P"% 󎏞=݌3DG!CDQ+,奭[owqeۺիWo1|z:"~Vuȷ>D z]w{\:uhDɃ4tP1#LE`}Ა]wիVGʤ~a~=XנA/wD|0YŋMnܸq9kGynݺ~ SzP6Ä>sL EڔVZɓ'G~W3-[t7xcg+;Cl32_ *>s#G"_|xNB{ޓ(lCvx≮{[ #n*Xf͚5hmr:>O.JqZ ! B@! *HJ X)rJפIc+{7n-ZWm};)x`_@O})%=zp?۴ik֬{W7zhro*(>?y睽eV|냅Os]ukg>:}>F@:rHH1@F21{l2×͛77SL ߭mHW%R((=ƍΆ(~ڵ9uWM`0qF{8wu˽ m>W7xD8s뭷[fժU<nk8D B3ϟo*Wl)Ğ={l?Ξ='u]gre.Nьhu_D@D@D@D@D@R% Q"[lF7o]CԒx?=%HسgOUX&JpuDe˖y-!J0v9.<Dnݺy& ȑÆd$w>kao8!$9Qd()$;p%ڶmP3/I U HH:^;#Wŋ\B }<$J_, {g}6Mz{gVjxH% L/> ĝ!q I%<WQ?ȡ~&b%" " " " " (,WB3j(;… mA7!jݻ%8x W|Ľɓiছn2a"mI 1(xП+V2 $çtex <8G:ulՌ⃈@G"#H&X"K&Ml_pיIM # pqKq)Rzp:t5;.|p"%Rʀf͚vϔsε-ɓt@I< 5f(P)\a͜9z(O< xS6 . w8Y&:ǐ ;ԯ_lڴ6Q|lzp> X~+ c$ E$bFSX_tIvJP Jtrm^kf:+BD%Lꊀ$C@D2cD!Cuk$Մ^-ŽMꋀ$C@D2chڴ5kV^"JOyg5{ox^h?%ǑYF΃>YyD@D@D@D@D@.b%.~mE˺DHu1bIH 3D@D@D@D@D@D Hhj_D@D@D@D@D@D@D "h@F(a30SozSgsx衇LYdI/mꫯVtN3'#\3#;2yd3v،eL%[^=rJSNٙR!y9WҥKojRG˗YHHng=%v/JPFAj KtNx cDE p표7έ ܜ%\= ͳh#%j׮8p(@1`ڴi6Wk0p9rľUgd ;ؙ4xȏX#+bϠr. ?= O:?$`Ŝ(:oDE w{={A֭Q/1ƅ*,9|WȿwFXqYDq9U~RE@D@D@D@D D`>1Kɉ0j(;" yXc$-W{JA}9uB"vMs7yy)D DrU8{%% ɡ{k n0J[ ][!Lۈ !J%dY ) g #(p'6a,\xh3hǰiҤ2d= E2M?ޖE <"HFyW}OIE OE .4H"x]RHz,)" K^B8 ѺD' Q":J'>a q;w=.\`'o'dVyuB)`gD ʙ` %(`)A|eyP(pm7-=XA W_}~퀞 ^(Z+ fWpvm0FL`JNw[uA1x ijl% #F243(g箻? L܇BP;  IDATc@x"Y%ʅ^P&g.]XAgM˱x "R'M@DlF ?p#J0]8AL9 @l%b3R,Fx:ڵ+ʆǽ'dx 4e#F J"M@DlFA*!v!פmcG!6PfChLD@D@D@D@D >%Z" " " " " " " L@D:Us" " " " " " " ( 0@vG 9H?~xDlifHHuaY.q!vD9rĺ$tƴbjgspu]%r?"]vݧxȔA#x0cAfD7eT-=E !\0xO훮R.YNIco1.1}g=z/\SOAi~';gi!:*Ν;<`t~۴i9xWHtx ;ugog j#=o% ,h%Jh]D@D@D@D@.n(1o޼@6R# _{5ӢE SBsyg4h`ʗ/on6ӪU+ 3_|f„ ^7}Yup.gK.5N:Ɖ+wﶂW^qpO92٢cǎk5xb0[c=8`:Mݺum;-Fv!S'ĦÇ2qoezI2'A?< }[/ e³ׂ M90`@) ,SE@D@D@D@D& Qd 7EP#zjil۶nWZ,[+k½gw36m5j̩S%\bo+DhXD :te̙c )}Ŋш qGpFsϕ+W9Gza<(PY-@ |򉿪]Gxb{ua3g4M4qEVcY9<>S׌8p'l;ɃCzY4Qp"+Q"h$JOHc"@H JTRv%̆ \͵%pvZSL9zhX.\k&JDǥ(1rH+p0x/^)9`@[o%BI/mHĉ~z)`I #4PH"/ S9M6Tf@|pxBTG8]we XhjBRpPrʙs4$J n1$d8dEpO v\tGoDz"OD@D@D@D@.>%!|#r O{.A=E %P{>pą}dD Buf$Ҍ'Oرc7l7|IU+H69*0 IGp&(_B"SE W3$*JQreSсL`0唠!o'QpN*Å #J$z} !<%Դ." " " " /O$ Q59N;`87t3HEt`7UfRشiӼ6;c˃CDH( ?ԃdfe` ŴI.7&^YEȍ".ׄB:KP2% fҤIvB%ǁtsرB;Nx 7"yJP>j(ldɒ]3fY !a&?1$DpC?:ud;GH>'vO9%0rfW#ZM% |z 8"RȒ#@M\ :JD@D@D@D@D@D 3\Df@No=b_^=^YE@D@D@D@D@D|& Q<;%Û.;T C@Dݶ?ƍ+Zj˗[n5y1G>|S|y??\vehѢ=%.]j<ԩckE5js6kڵ+c0 6̫>i$s 7xZdHH %ݥKӧO[nݺf̘1vŋ[M<ٖOv̠AmV6mڔF߭X<(~m[10uv^z%Ӳeː6:dJ*e>䓐Ax <ث2k,+x;"" " " " " " I(4wH$Qo߾{&gΜӮ 68~ɟ?ٱcWH̙3M&Mz bAÆ 'O\rNa){1/[w@2$J$CD%:wlo9rX/|:u=}޼yw,-(1zhӶmv{ر&_|!,\p$SL2vfĈ6|$b'SD@D@D@D@D@D@$ Q"NP(W!1$ufUVfĉiNI _EFxJP̙3fΜ9<alȑvZ@R$J$%|ݻM0 40ƍso `'ppرߨHb]N3fX!ąq+V+[v֭Ec=$q b^}ĉ}Y!*$JD%.ARI^4oΪ~cЂ ;w=zxog߾}G}d' a&y8#e͚5mK7+Of =7 DRD@D@D@D@D@D@! Q"j:FD@D@D@D@D@D@D e%RFD@D@D@D@D@D@D@! Q"j:FD@D@D@D@D@D@D e%RFD@D@D@D@D@D@D@! Q"j:FD@D@D@D@D@D@D e%R@?mtbJ.mիWO ėlٲǏu>*O Yfǩ@! Q"{(cS\93E[5kbsAEժUz,Qm۶ СNƘM6%fΜijժeo+:TV-(RJYq#|%@." " " " " )xD |YqygC`^jvjN:G[᜗\rDnݺol޼9Z#=z0O>wӈ^a+%CD@D@D@D@D@D (ڲeɝ;wH 76SL]SuSѣgϞM6!mE@x(RHHO?S |O>cЏɓ'k#(Ѿ}{3zhБ#GJhhED@D@D@D@D@D D :1R8qWŋn 4ȫ%؉ؐ7o^< 8s挭<%6mf+JRh3u*g~Hopַo_" " " " " " $J]XB3j(FpB?~Xuolܸцjݻ6%H$bD9mUrDDǎe %n&B&ؽ{wSzD f ^yI>S"p7%" " " " " "uHH^XŠP0ܹsm;I,x<ɓ*Tȴj0[F,#W\ag@\Yjwؐ!C0ТE ꫯfpFI f߸+k@T-۷Ϝ^ʕ+Qo߾I|~k֬hk=/<�u5/Y"2sG@D E ҥӧm` ŋ,۔ڵ3 u?6mJ#JVpoB u&l5|Wlߩ=z۷c.\h*T`87IÇC)K`yZqc߾}^V8`7pٻwA}nW .,HAh;w^<)F6k,{Oq>:t\qoD v!9۲eɟ?' D,b#G4bxl6dWl:t萴Dɒ%CD9ECX[ǎӈ^_}U5k]D@D@D@D@D sHHs$Q7ݻw7 siN>흁w<0oرn"yoҤb <1 6N,4v&˖-3\swhxD =Bc 40ڍuY'W<|8Gu/h|t> 8vXTyPOG}4^$Q"bڪUl;kg=k֬ #J;4LZسgɝ;u$DխHsBn<3]:u\r%V )І@(Hn#(ȑz1S3!X[$QbѦm۶U6 epcYpḓo7Ԯ]z[<6 AbD.a#$~w-O4ێfxX}?~曽f(Gu/i0;ʢ:rM_t_zN!3=m)g,RG v(ꫯM+Bo'3ܸĉgs$Rx. md)%!(W;w07…D: pʭZ2'NLS%(QF^]\Ǜl<n^WH's'Ă,ڠ{۶m9!Dxf8Wyfԥ%"`+$xc8san.Xg@XAu/1;&%R$Cld<%ڵk@.\k&%¹%=Ƌ&ƚA*;B@X@2o)Sƺ33?gi(@ Nf ZgF&::c\2Ĵi\2dja}`kz駭?!!/w\J(qA}}mFE <'DaЍ€ df%7{HpI ebwĎrWٽ>\{:u$%DekOoH%қh淇 %b﷉g񨐉; DχvxK8w9D V 1b͡H'nVL62sK@Ĺ寳EK@E{u" "Et IDAT " " " " "pn HH}#[lPBo4ɓﱙ]oΝ2}Ν;{3n$znf`6o6͡LOٴi4c D[nż⋑OD@D@D@D@D@)&'JoA<`6dxo߾YF`LD'#:u$-J0&C$#m(d_|aSY)R$_mFD@D@D@D@D@D +(]4sYSR%3}tիUW]eZj 6xg{M޼yͥ^jHx{e ){ٲe?앹9s0'9~sYX1k-Z *{;رcaÆye3g4 4y}|WmeͶO>iԨQa쩧x r2?-ZX+C<O+Oy4}Y{(ڵזFOժU͠Al8/ȑ-Z^gǎ]Q%9.\8,{"s;z믿>f۶m9x19Aܒ@V$ Q".JT.]L>}lo]9ri֬Y:tSbv ~IXPdI_BR x 1W^:oEN8ayvpQ;_p `ٳYfv>{]Q#0Xǻa 3wq9s9t萹+ʕ+=z0۷oe6"~6mm!bvk2Uh6~x;a )皹fXjUH945kLꫯE WOK8 HHD%xkݽ{w*ӧOu|[4QUVfʔ)CZG{ 駮 80ؙ(3gN"Çy,YbpRY|oG}nϘ1Ê}׶HΖ-[f(/_>Wdlbo1VIF{CKms7ƍkݺYpԩSVhA@V# Q";I U8ׯo֭kWnC&J0ԩ!5aoФ%;+m߻"3fӮ];o;YQ"<BO?dm8w !x§D[n^qƙڵk x`ҥK{ZaP6KD Qc?D [xqw߅ӆd%RKʕ3s5G#^`@oD m۶M4pQ"S'0{(-x1K(n\ 䪈d(A!\A6d޼y^KkOD$*'" " " " "HH.A6o m:byx7<ڠ=.ڵk *ǎ?,XO41۷b+BZjp \+\pp',iY((B < `H+VC&Nh3%x+WlsJPoTGXCgjA.hbXKB|ȣ5k>0VFTOD@D@D@D@D|" Q"D $lٲٱc"!gF<%3w9  .IILɌA9'3FPBZ1j(MĻPrQL6-<CŀlMbFgAb 3g682A9}g?ܓݻw%;x{2v3$6T_D@D@D@D@D@Ep^D8%hF1;QP" " " " " "Q$JdY&\BGB8?i~C>={d9u"HO%ғ !.RxngPo6S[o5GΔs$" " " " " "$JdU)" " " " " " "DTAD@D@D@D@D@D@D #HH* y iiӦ7ߴ{f˖͖S/z!;$\s5&W\vf4E6#G4S0 *dWnq$C aߘ}cǎv:Tf4hL0ήt>U>|vڙG"Y;l޼+۹sՃ7كSqpvZStFߘAY;믿vZD! Q" xv#JoNS9i$(eL :uTa>gϞF`);g̘a w}Lɴ L#J0(2*3[aѢEPeZn'N§~j˖,Ybʔ)c~{-b}Q[sQ1=\5kC8?VF 3rH{ӆr~7 )mD@D@D@D@D@D@# Q"1^!%8gԩ:thH6͛gWR\}V;+ٻvAktҦB EUGy;& -ZK.$D8p8ydfk 1p4ٳͦM5j~?WVZe!ܾ7xtMvskl߾݊ Ie޼y "uŋ#wܞA9իmΝ;_@$J *RDD;vrʙ6%nj߼C"#ĉ  6AXºuK/@¸qlٶm a ,FkϿ?pQ?63={ DowqST)rJ@A'  A8G$C@vrÃPӧO{Bx~` AĐ('4Q  4(~rOpԩJ*!Cm֬YaÆ.<[_r;?;ٳ^TWE Sri~me9D x㍦K.^@@8G!FTV^xm!6 ,0wyglyZA# wO28}UCqD%Re;7Ѹqc3e/%/¾'y3&hӦ+\㏦CH"[; ;p3aSxvS- %(P!ߢRW;S$ @;#F0P6J¿uN/ŸJ=z!C문DbM^{MmLD@D@D@D@D@D D`>:Yg' yNu!3HE =#"s0<#HׄKd_(O?ٜ3Q"6 H׆ ;s]wѣGM+JPI' e}gV Z~}H1o?|A0`,\pz)`%,E 7o2F%6nhC5\ȅ_O>!3AD;1I4ibSq:QСC6$ ~[n^Swۂ F!X(Ax5Y+^yNo!(ADF"fk_-k<"L&_~!DҥKË 7`{1+ܐh.Igڵ̙3m |$DHARD@D@D@D@D@D -i$gŊf͚6l\sεLJ$x.ɓ&lժ 998ԩcCqP,ɝsAKY&:?n3I<C@Xn޼ٞ!^ 3C FRPW%!.2G)YMtA q&Ol_:Sb{BŅPW_Y\S:xfdhzPGFK& Q"JE@D@D@D@D@D@D@2D fE@D@D@D@D@D@D@ HR " Q"Y`%TD@D@D@D@D@D@D HH?`g7ѴiSozS2.NyY[;^V^g.ڶm]3q0e(30ǏJ+fJ(2ӡvNJ٠AXa Rx.\w}Ӷ;7nw6D _z&[lk׮ԩS^yXDJ̐!C޽{C8pə3_믿 {\Ap`?\ pe.bXn64\-[XѢhѢ6ßS>cn׍7htC;wq^ɝ;-d )`fP#L%$_|a;=%=jziڴi㚉%JѡC@իm[;ȑzd/ᅬz( @6,wߵM# aCz&;BD 1b<%OxE@D@D@D@D@D@2D#8@BEgsXxq(AY֭CrE #m۶:1-0ǁRpO?uf!V#qaFFB*@osD/B ;i׮N;̬~=}1>,D:d)fOEܓT1Vk 7 pa~Q eǎc`p_p|'JPAƅP}vѿ;grUWYQղeK+VXo *n[x/*T&$"#g $|W 9 >7ԩcCG\Ӓ6oNIRɱcǺ"+DyL9R}_~eSL+@Ə?؊W\q8O {L)?/+ě"?x-E@D@D@D@D@D@2Dᬳ(D" " " " " " " C@DpYD@D@D@D@D@D@D@HM! Q"s8," " " " " " " a$JId~giڴJ;|fpo͛Na/W_af!3r$H:_N:Y6`7ztj9f:wl6mGyě]%j,^pq;-3җU:nJX?^d˖[2`o+m٢ElsFӧO{߭/ܛRwr馛̯jefan;dϞN{ld.+g b5Hصkپ} n^+W eEOSΈ6\U3(ѣGS1HC (P D@ %݂ ߽"EO?Զ:zi۶뮻lYwh.R SlGVj9n'>؉wV?(#K.{mfvbKV̓O>i^k}Y! Ν;Ϳ/_ю?r8\@,Lʕ@%{=+DkPy>ݚ9s[;w7/Jr|kvrD,>\C #|0q}gxgW^/b%d0"p>fs;{ɒ%m꺷wq6m?<_DP ƠwŊwxFxKqFhԨQ&wجY3@-f Žݿ(A]=sϿ; wPڊf͛77SLVq?#'G/H#Jj{ǵg{駭@ߧu $Jp%j;{voՉd9dw4UD `-" " " I@D ~a-^81?0/Ŝ E%pm!1>%xL\A;/KP7Tdp$xshOgoHFp g@4C !ᓌ()y(p"DzxF>2ҥ}L^p.lE p~IDv/iQŸ['Sss9(A.HXi ^7Y ?>|;H|8%bb-@D gA>}E$J[wysuKr3g[$F(yܽbuLT@ϧD GCK D"tސ[gĊ?yC qOhg_,_&M ~QddsW-"}Oo nﮁrşnXp !H .J0doN ڂ;Bs3gÀ-% B:&JÇv~) xq ]|~F Q3@ (ޚ_O%KM>0$|'p{we %C>+<.Eг{i;oCM(o\DE ~8q|gI?KAJ$0ms<xno&37HڠA]&|| ~g-" " "UHHN1`aPśFbݛHLGK5߆uč8jyoy;{CN93| VY38Zqw8+epx|fM3$£-Nxq}Jf %$ 'jVs"kDxl9⒛q@G3c9s?Ɖ3%J6{G?#Kar]|:uu'MS"^F%8Pd5pPF‚<%7w't q^FLKA 81p (b*ʠ,u{olnsc238mg0bjP~_y>=q؃g&$K.*r 2.͇{:4<D7/'nj~g".\K8_ H8_!jE2^2|5ĝxoqaE$$o,b^#@Ƀ> }[0#%g|>%]Ζ-5ѮQ"QXUyȨ{O ?Ej_EHnB!\xOV ُ#F#ǐgP%d" " " " HȊwM} D &D@D@D@D@D@D@D +xLtL>L>}qsQg`xw͍7uvcƌ}v%I@ D >E F'„DE s(N:CƸ]őDE5jط͚53v-?SihTP]!%6j&~;]weƍggΜi/oƖ!J V "IP^=w^l%D}:u,{A1۲e)\1 VXa>  UTz+MD fOxGmSn߾rϗo${,qo7l6,A@D A,uo8O;vrʙV[n5e˖5k֬7s=\\[K/-[z &n3;#(Lcdqϗ/_ȴ&L)q饗zDxm;$J\yoPBΝ5ϔ~L,Z_ci۶qB^:tm\A~ذajQF/㣆Ƞ;I&" " " " " YDV<(Q<g@O]ژD.J̚5N {\ %V^튭¶m6y>#?^΢ ӼysWӾ}{o;J(7 團8q[m 'h}ŧ[nVxqy\{e(ᯗ:wߥ9)-Inˑ#L')F/J}" t~xaW7.s۸qc3e%$( 8{J=zӴiǵ %xcЁׅ@ Q!}r˗7_+}W'Do{ FY %pF'ƃHCKڄhD,>ۧORL3ydv=#D%KXa 2L% 3%ɦ9DDg <${6p !W_}uj*ˆg{0&F ()AڝL~ƌjH[3f DfZ(йD@D@D@D@D@DB QBp [ ؿ Is#n$+J0S$O2MIӁ" " " " " "D4H.$!y$_~3fL@ z衸I Z/W_mk2R"4sW=.TX3gNtmf8kwߍ͠tƃs^paSZ5f͚6]HiWifPYܾ񑖎>/ʊ߿ygiIYfM&??:s&J櫯2zVȈdEf͚(Ϳ/ײ* 9w6]v%/+œ8q"(^xp D˛cǎ6x G6YF 3uTۗ7*R%za:wlN>4}; *5j=l̘11_~ɗ/j<]{駟m~@QGزe}<$x+ɇAe^"TwEG nF"+Vlذ!{AǏMh+_8`r"B)S&ftTXמl(ZD駟l$ϒ%K8#9"-~v0ӱcGK/2aÆE%Ξ=knjE_52ܬ #VcȠ)ڂɓK.1/e@XٳCp}x{yeVL'x`saUT1fͲϞ={܇cc * :.F#8\#N:t"hݺuVqh/;t'"Ϙ1D6#}6mTNH SNĺfDY$1 䏨^jl\qPYPHF%>!CMÛaKaAh"N|^l ol a +]r᪏n O< Z|+t1ޜA"APYʂڌVF{A><IZref#>DHV<{s:í3Cn զ;6wڕ/ * oǿt\Ph둞~nIO,#[o;oZ_/;4ۇz(}F%=$܅_E@D@D@.ND``;m4l2k.;H&T #ʜityK9UwS<%ʂE J|3xzdsadD`D" e  $( qaI !  K!'fa(,3cNNn~;置[uSUnWwgю:Kv 01t=mڴ8< |k&UeȐ!%@v-젧^ {" ^ɖ?x?+)T{NOד{-oMeRv) 4(eiNC/HYI( " " "hS;,gk3)QjZp7*}fXm6L_NK[tM,tztKR^x5FZVLWgIs)λ%\It''Ǽ:y" " " hSϕ2^reSdpGɊ+1ݻw4y䄥OVy9ϝ;7~Iu_N:բtA3'e>}?>/F$oz_xqۈ#{*xo_-',!x≘5$ :qت^j}pJpGV1LW˰}Q#}-SF^V^~mvto| _:i鋮im=Hy2Kߖ/}KٻWx둾3#yy-tÓX>U"=]̳,q摎GU'LdZ2'9^UWxlU(L}-{$yzW+z1ӮYJʯβk]K'iGGۅW^"(?h89%΁V7T" D%L$" " " " " " "hrJ4T" D%L$" " " " " " "hrJ4t:|OU'" " " "\-sJSrKOfq_}Uҥ 2 +/~+|=_HXxs  IDAT1?>>{9Uwq .,L<[lZA]ٯot,'yTG>dy=龗Γ:SrH"ƮF%8o/eu5y×Ux2ӓe ) \~zCwJx/;,/`3&lϒ3vi ];>Fe?~Ygʼnf|# ni;~;Kz~SUOWG\wqGL>eʔ馛fPN/?Og{ׇx2^:Ol^~%ƍ<0|m#jOD@D@D@:8%-[n O믇+:3z6N /9 ^}7|Am @9sfg> ߣ>ŭ`2>v)| _/btO~b+4c#&F磌Z)#n2{\m}p M7t~ȑ1M.EʽtHy2K_y"]vK,n_x8Qz `ѣ+eQ-yS\v#gJٝaO'ʉ+ # oOKɚOX׿5|q u@Kmeoo9<2Q'L%K y3M{#k|ХKCݻyo[x<a?\sM裏3,^m)ꋏY~cǎ"眙XTUX;3pTRKɊt1/'E[/'+eǼtүvɒ%q% Hُv{=v?!>1o<n䵗Bedy=龗Γ:QC#Wד׫tiA裏/86sVD@D@D`!wY֜,}Ƹdm)Q5]{뭷Y{#qyUk1]kx饗 ܦN ^df?&z!RwJe6/H> {ǘI.#zw1#y:=+ +HX)'&+JVڵk|ԏUu=PʩFD@D@D@E%N < V\dp̜ yDt޶e;N8xgg\eq@~`w}6_=蠃¯먆 $Փ_>}ϗwQla1YZt>{<ٺ;#8"w(<>d{2—9KkK^{֪# Ï)zj^-dy=_V^4kx(G9%"xWpNڎ1lT#" " " &2 K'$cZ2~t`OV:[-$;SlIl()A~w J (Kqe??ɳ{29:+xQ`s2;餓bYRW^:Oד{xIrܹL!,SPyU!/'(Fxg8É'htV-sJ9ZqV-" " " " " " U~b@g' DgA_D@D@D@D@D@D@:)9%:iũ" " " " " " " )ꫯ? <8~sΜ97<-C O^)]hV;.\p=YQ7HUϾΓc^:Of鋶^:OVˎy`tt:%puYy'ač3&lߏI:B_k6O/;q1ʔ)S¦nuD Gydxw;~merטw~̫('C%Ne˖n-sB0ـs“o„ aaÆ )w9\q/oR“y͇rl>Ȣ~^~Y ^:OV*;dΓy^{,V[mpNԛ_Y=#|x\c cf+%j̡2d=\i銶U3=Y^ /~}Ϟ=/#<hӓ˖{z,ϱ?ψdE;pwXv:ma/'E[/'+eǼtmtHy2Kߞ{Od<ģ"z'-O<9laťtz2Oqt5f\d>vT" " " " /9%$=:޹eļy2GC-mdwȐ!䒿SƴrH9%Sm&q=zĻ͵de͉Z׮]=3=?/zuV~Y̓C:}Ѱ>/naРAYz9餓3d]v_2tNO樬k;?ODVDgYZ͔լk_D" " " " " " "^Sbf}XD%E5$E@D@D@D@D@D@D`#2_Ҹw̙}}Ó-Z( sbL/o˪s8ç>'|2KɲH/'+PyLAAK Tet,SPyU!/'t@UCNO)͞=;r!Sk5j*y:=Y dC^:O)(x ~h,\yU!/'t@UvK2/'+PyLAAK TU:dr/k6;m6L<9/ x:=Y.;dhdE옗ΓYΓc^:Of@KϏ^`#<W@x2ƤIUӥ:{? =u- 80]KχS1o޼بҧ\a^{˗.]z(t=?[L: gcR1m6,K, ;s>Ef˯|vK,}KɊt1/'E[/'+ըc^;YusHUa@Kj>|x\c cf+%jRg+%pfq_z:tW~ûzON8ᄸR“Ig1'+^:O.\Kwe0`;n:dR/'b3OV1GymƎo])_;\X?)ʃ[o5@0'#¬YZ2VVN Kƞ4~>U[o)og>U,\yU!/'t@UvK2/'+PyLAAK TU:dtztHE?O'{ᢋ/ {OioUjjjjjjjjjjj@˝p_wt?3mY}ʲ  bƒh٫h'B=m6:]n!nᤓN ,ʸdɒpLJ78|3 /2K/;AmfQ3<|/9law̎[yòe˲tm&'̎[LoѶGO0NsɎ}C9$|S kz8o7#ii:̙ؖk9vuxnkr{5Z^g'˶xv…s~w=3&{`9_C5.{; E[ٳgge-WX)A?Ī*i;"N˜/bȩ *,â_[9J}. / r;&@X`~tD!S G!1Z\HljP V=Ǝ 3aoօŢ|[Sepgxq6mZ,|;YX`0x衇C#9͜93:\l:nܸ>\s5N OwYnhp̘1#̛7/A83(tJ`codpJl6 y!C8uJpHEN §?6btw}a֬YѸ1C=jݙuY];=ɓㄨ[nu_kSCs7W_}ut☥)1a„oJ8t8X;3~s'?Y82dH\}Ȥڈm#')O{0'=3ɩSG;O?W7pۿ[:W5SkSGfegG򗿌N~wuW̃jáF~{ܸg>87sRfZnS³_*:hMFmBϞ=cG;,N=N2id?}'v>}FZzrJsY /|yaO?:Xx/}'%aŽ#g_Y;p##>Rd8DoLl,e]ˀǜd. wy_Lɦ魵% eŭŃ;/s3’?3d]tQgrƒQ`z"'v윙0aMgE=azǚ*\`2)XWOÇNμk۲:cHʧz0lĄ==>lذ՜՝ ݇vXK|p0P7D[İNaa0}ʋt%q4ROcC ­hMHU S8K?[lofwu,իW~%g38#zY^޵nilKC|5wPq0ɡ?҈Ns~??YC|_];V09^{&v^vLKwX`u}UWFamNFDŽm )A!}*i'hr3QsM8%z}C5^{mA??X~;t2,)5[i=;E}NYRoAxDC 7.0paلʸ̘݅JAq ;>>)}|P{vRzfh:ƙDRHNK[fvT=7z |Vl0zlUnцci?n*}g ]#(#m)+sciEd9 7z&^ZRlLEq5DtL\sU|d#ix|ޏ/ s/(̾0ǙU_0?_/O s oBbq5E#x3ph 0gc1 >m/q3?~|ԋ~2yNC/t)d }YE8ӉO98`&ܹAVÍu8#Ɋ7QGJ sw|8coLle'a4hP0>1hXYXN Vѷ>GMlufv2QN N`lgBB?nv6A[t17E()Aewl ;VZ:J `W{e8m E}sތ'=}{>%[n69%7gsP68YMA^pʸń--كwg 6\{SYHH׳x"cԎaKM=legC.tn`?cvJS«kisNNb.!ebUe7;.-[G)R 8X#_'{azY /uZu;_{{;Lzab00hЍqĈw' ])Aah㕤Qbd311h> πF6.:L8hBkiٲùR;1UVYJ _>EN{'c\g);XqtN9?R i< r3-8>N 817' wm,2<u;Kp4Eqǝ4+M\|ř*d/ĠZSm?(iϹfxwF>sҧzؚzae!Fwh{aܥI_@ӎ8(̄1.Jl`}lt|cE_k-=[l8Li>jE&N3rvy}PHG8?V`+!yz a߳s}zf\PW99W>#N lC&)H_:cCa]tǤeexmĈqg\$څ[9gS+]`g{|tF}l {_6n0%ǣӳM)a:Trqnqq]e`G31~%[y6e:e\+\S"O_\۝*9%x|ѳΈ+!xDZFEK#q 3=+vR7ϟ _.qbt¨Qca@5ǀ`_\8!MI"5JSN;&bI>;?[h,cPT$ 㥍1lqgP޼ScceVOt0v1yN&c=878#W/$궖a sv}'+u]A_AR7Vzb"L] ḍ5s7wSv)c'LarHzY~%l "̪We+fEvD>u9ųG4^Iw-mg:#7ڀ]õ\ȫ[Ґg2vW*`UuJIyR"Wcϭ<U^]eU)Q/NQ˜蒗Wh Ü^s8$f|YE8%a֬a裏 ={52̞JQE:T/^)$/u&|pJOtVH VJT'1|9N spb0ig ?bܳ;5=[攰O>vƩgDϳsgƝv}|5,Z8',}cQXlIXd^XpVXxnEx7[Q|;V#uJ`d21V9pG H&S2t&V1V8D0JWOfԗιh i+K gC1'/xFҲh%/>F "NROwYYIۄ 7HgɊsسIAgE};Ha{=1&/+vnCІ) .a˝P/|g"CXUTwv,5`9kp.x-MWJPG-lly\kZg% T`e/.%_ ~I'-9I6RyuA_{+W1€;%DjX-2HwGgŸ6~$LG_B?:p*rw4,f:iO(7q_iOE8']w8EJh(;XụgL)?eE\op}Va{oBPI}rXxn:uR2LSL)SsFpHrq:XJ :Q:2&uLkqxx>r&tbFi V5frH^||/:%HPΝNGGOr*N &,ׄ wRر ' eUO8h?H21ɜw~Gc]&l܍1H<;fIOwY(F6wb92EE(#= IDATD6eQQs왼6uJcF2Y̝@[D`|O-ƿգmAZڀ:ey>&X`1,1sQ/#bꔠ yl!R_-rn8(ASlF}lr{vx̀%89sJ+BS &=7[&+K=uF[OCN-3~")NJ2;D;cbdұ&_o:g&cz-]z8m1ifl‘OMnhֲɗBc0bGι)3*oy4aΑǘL  ك1rV\Wl0WuJ׫JWyDY}8W'Fb6P % fpe]ywи]Gذ,)5Pֶ<ߦVVz[׮q39%:[h Xt5LZl\^b/no-6, *JcKj[׎d}b];u|x죨ye֣͢i9!ˌ;Wk3,gi=vjkԤcuר>ɬJPonke㖜&4tԗ)QnT*.d +`唐SkKW9 =m ]S%,rJ 4)OkE 6E?6^ IENDB`././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/porting-drivers-from-opencorsairlink.md0000644000175000017500000002162614562144457025417 0ustar00jonasjonas# Porting drivers from OpenCorsairLink _Originally posted as a [comment in issue #129](https://github.com/liquidctl/liquidctl/issues/129#issuecomment-640258429)._ In essence, writing a new liquidctl driver means implementing all (suitable) methods of a [`liquidctl.base.BaseDriver`](https://github.com/liquidctl/liquidctl/blob/main/liquidctl/driver/base.py#L9). Note that you shouldn't _directly_ subclass the BaseDriver; instead you'll inherit from a bus-specific base driver like `liquidctl.usb.UsbDriver` or `liquidctl.usb.UsbHidDevice`, which will already include default implementations for many methods and properties. And for the new driver to work out-of-the-box it's sufficient to import its module in [`liquidctl/driver/__init__.py`](https://github.com/liquidctl/liquidctl/blob/main/liquidctl/driver/__init__.py#L23). Next, in order to port a driver from OCL, the first step is to check the `corsair_device_info` struct that matches the device, which defines the low-level and driver (protocol) functions used for it in OCL, besides a few other important parameters. ```c { .vendor_id = 0x1b1c, .product_id = 0x0c04, .device_id = 0x3b, .name = "H80i", .read_endpoint = 0x01 | LIBUSB_ENDPOINT_IN, .write_endpoint = 0x00 | LIBUSB_ENDPOINT_OUT, .driver = &corsairlink_driver_coolit, .lowlevel = &corsairlink_lowlevel_coolit, .led_control_count = 1, .fan_control_count = 4, .pump_index = 5, }, ``` —_[in `device.c`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/device.c#L107-L119)_ ## The low-level functions Starting with the low-level functions specified by [`corsairlink_lowlevel_coolit`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/drivers/coolit.c#L27-L32), and implemented in [`lowlevel/coolit.c`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/lowlevel/coolit.c): the equivalence between these and the methods in a liquidctl driver is: - `init` -> `connect` (in some cases and/or `initialize`) - `deinit` -> `disconnect` - `read`/`write` -> `self.device.read`/`self.device.write` (see next paragraphs) This is a HID device, so the liquidctl driver should inherit [`liquidctl.usb.UsbHidDriver`](https://github.com/liquidctl/liquidctl/blob/c9f2244200a552ce8af3d64b937d3b01cebdb126/liquidctl/driver/usb.py), meaning that in the driver `self.device` will be a `liquidctl.usb.HidapiDevice`. Additionally, liquidctl already automatically handles how to write to a HID, but does so mimicking hidapi; `HidapiDevice.write` follows the specification: > The first byte of data[] must contain the Report ID. For devices which only support a single report, this must be set to 0x0. The remaining bytes contain the report data. Since the Report ID is mandatory, calls to hid_write() will always contain one more byte than the report contains. >—_from [`hidapi/hidapi.h`](https://github.com/libusb/hidapi/blob/24a822c80f95ae1b46a7a3c16008858dc4d8aec8/hidapi/hidapi.h#L185-L213)_ Practically, it means that you only need to implement `init` and `deinit`, and that in the translated driver, when OCL would call `corsairlink_coolit_write` with `[byte1, byte2, byte3, ...]`, you'll instead call `self.device.write` with `[0x00, byte1, byte2, byte3, ...]` (note the prepended 0x00 byte) ## Higher-level functionality The remaining `get_status`, `set_fixed_speed`, `set_speed_profile` and `set_color` methods (required by BaseDriver) will encapsulate the functionality specified by [`corsairlink_driver_coolit`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/drivers/coolit.c#L34) (implemented in `protocol/coolit/*.c`), and are for the most part what users will access through the CLI. Data that is read from the cooler, like the pump speed, will generally go into `get_status`. The firmware version is an exception in this case: it's read with a specific command (instead of being part of other replies), and so it belongs in the output of `initialize`. _(You can fetch the firmware version directly in `initialize` or, if you need to use it anywhere else, you read it and cache it in `connect`, and only return the cached value in `initialize`.)_ The other three methods are self-explanatory and should be fairly straightforward to implement, apart from the special considerations that I go into next. ## Protocols with _interdependent_ messages A big aspect in the design of the liquidctl CLI was not requiring the user to configure different aspects of the cooler in a single command: you should be able to set the pump speed without resetting the fan speed or the LED colors. For most devices there's a clear mapping between the CLI and the implementation: the CLI command `set speed ` implemented with `set_fixed_speed` won't depend on other BaseDriver methods (apart from `connect` and `disconnect`). There are however "complicated" devices where, at the protocol level, functionality is grouped (all channels must be set at once) or even completely consolidated into a single "state" (everything must be reset when changing a single parameter). Messages can also be required to follow an arbitrary order. So, besides looking at how each individual parameter is configured, you also need to check the "logic" part of OCL, in this case implemented in [`hydro_coolit_settings`](https://github.com/audiohacked/OpenCorsairLink/blob/testing/logic/settings/hydro_coolit.c#L32). This doesn't mean that all OCL devices will fall into the "complicated" category, or that you'll necessarily need to match that order exactly. In fact, in the case of the H80i (or other devices using the same protocol) I think that the different aspects of the cooler can indeed be configured independently, at least for the most part. This is mostly due to the empty implementations of `init` and `deinit`: in more complex cases these functions usually involve some type of opening and closing of a "transaction", but there's nothing of the sort here. The ordering in `hydro_coolit_settings` also seems to be strictly due to natural requirements (you need to know how many sensors there are before reading them), instead of being totally arbitrary. But I could be wrong... Anyway, the main concern I have right now is the [`CommandId`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/include/protocol/coolit.h#L93) byte that's sent in every message. It starts at 0x81 and is continually incremented. On one hand it clearly doesn't need to be a perfect sequence number (as OCL doesn't guarantee that in multiple invocations), but on the other the shorter message chains in liquidctl (due to only a few parameters being read or changed at a time) could cause the cooler to complain. I'd start following OCL: initialize a similar variable to 0x81 every time the driver is instantiated, and increment it every time it's used. But if that somehow doesn't work, you can use the internal [`keyval`](https://github.com/liquidctl/liquidctl/blob/main/liquidctl/keyval.py#L1) API ([example usage](https://github.com/liquidctl/liquidctl/blob/4e649bead665bf692d7df9b8bc1a9a79791d356d/liquidctl/driver/asetek.py#L281)) to temporarily persist it to disk, allowing you to implement a true (wrapping) sequence number _across_ liquidctl invocations. No matter what, just don't forget to explicitly wrap `CommandId` it at 255, you'll probably be using a normal Python integer instead of a `u8`. ## Advanced driver binding liquidctl driver don't normally need to check anything super special to know whether or not they are compatible with a particular device. As long as `_MATCHES` lists the compatible USB vendor and product IDs, besides any additional parameters required by `__init__`, the bus-specific base driver will do the rest. This wont be the case with the H80i: it shares a common vendor and product ID with other devices, and is only differentiated by a "device ID", that has to be explicitly read. Reading of this device ID is implemented in OCL by [`corsairlink_coolit_device_id`](https://github.com/audiohacked/OpenCorsairLink/blob/61d336a61b85705a5e128762430dc136460b110e/protocol/coolit/core.c#L32). There are two ways of handling this in liquidctl. One way is to override `probe` (implemented in `UsbHidDriver`) to fetch the device ID, filter out any unknown IDs, and (only) yield driver instances that have as field a know ID; each instance should also map that ID to the corresponding parameters for that device (`description`, fan count, pump index, etc.). Another way is to have a generic driver that only fetches the ID and customizes itself accordingly at `connect` time, meaning that before that it identifies itself as something like "Undetermined Corsair device". Because having the driver instance in an undetermined state will cause some issues, both for us and for the user, I think you should try the `probe` method first. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/process.md0000644000175000017500000001343014562144457017570 0ustar00jonasjonas# Development process ## Branches and tags The main branch is `main`, and it tracks the changes that will be included in the next release. This branch is kept functional (barring the occasional bug), and its history is never rewritten. Pull requests and patches should generally be developed for it (i.e. using it as base). Releases are tagged as `v` (e.g. `v1.6.0`). Updates to past minor releases are managed in branches following the naming scheme `..x-branch` (e.g. `1.5.x-branch`). Other branches and tags are generally for internal use, and may be deleted or rewritten at any time. ## Release cycle and pre-release freeze periods Besides unscheduled patch releases, a new minor release is expected once every 13 weeks. In the four weeks before a scheduled release, major changes, like new buses or large refactorings, stop being merged into the main branch. This period is referred to as the _major change freeze._ New drivers on existing buses can still be merged during the major change freeze. Then, in the two weeks before the release, only bug fixes and documentation improvements are merged into the main branch. This period is referred to as the _minor change freeze._ Occasionally, scheduled releases may be anticipated (if the activity is low and the freeze periods can be retroactively respected), downgraded (if it only contains bug fixes and documentation improvements) or skipped (if there are no changes). ## Stability and backward compatibility This project adheres to Semantic Versioning, version 2.0, and there are no plans for a new major version release of the project. Because of this, liquidctl releases (minor or patch) retain backward compatibility with previous versions. The stability guarantee that we try to uphold can be better defined by: 1. No breaking changes to the *effect* of CLI commands *on the device,* 2. and no breaking changes to *documented* behavior of *public* APIs, 3. except when required by a bug fix. In particular: 4. The output from CLI commands is *not* guaranteed to be stable, 5. and neither are the items returned from `get_status()` or `initialize()`. Additionally: 6. We occasionally provide backward compatibility for undocumented behavior or private APIs, on a (use) case by case basis; 7. Even when a change is allowed by the policy above, we still try to weight in the possible disruption versus their benefit (e.g. when changing an existing item in `get_status()`); 8. If a tree falls in the forest and no one is around to hear it, it does not make a sound: breaking changes that will not observed by any real users (at the time of the change ) may be ok, on the condition that they be reversed should one of those users come forward; 9. Some APIs are documented to be *unstable* and, thus, are exempt from the stability guarantee. The use of deprecated features is discouraged, but deprecation is not directly correlated with (eventual) removal. Feature removal depends on whether the feature is no longer useful and whether its removal is allowed by the stability guarantee. ## Communicating changes to users The project communicates relevant changes to its users through the [CHANGELOG], the minimum recommended liquidctl versions (MRLVs, listed in the README for each device family), and the "new/changed in" notes in the documentation. The CHANGELOG and MRLVs are both owned by the project maintainers, and updated periodically. Contributors _should not_ attempt to update them as part of their normal patches. However, either can still be the subject of a patch if a correction is necessary. ### New/changed in notes On the other hand, contributors _should_ add "new/changed in" notes to the relevant documentation, when applicable. These should be kept in blocks, and look something like: ```markdown _New in 1.8.0._
_Changed in 1.9.0: the firmware version is now reported during initialization._
_Changed in git: modern firmware versions are now reported in simplified form._
``` "New in" notes are generally applicable to the corresponding subsection of a document, and do not usually go into more detail. "Changed in" notes can appear at the top of the subsection, when the change is significant, or just after the corresponding feature is mentioned, when the change is minor. When adding "new/changed in" notes, contributors should use "git" as the placeholder version. The project maintainers will be replace them by the appropriate version number as part of their release checklist. ## License and copyright Liquidctl is licensed under the GPL, either version 3 or, at the option of those receiving the program, any later version. Changes must be licensed under the same license, but their authors retain the copyright of their individual changes. Each module contains the copyright notice and a [short SPDX license identifier] to unambiguously yet concisely indicate the applicable license. For example: ``` Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later ``` The copyright notice should explicitly list the most important contributors to the module, and then end with "and contributors". Contributors are encouraged to update these notices when submitting major changes to modules. The project's copyright notice explicitly lists the major contributors the project – in this context, a major contributor is someone who has authored at least one non-trivial module of the `liquidctl` package (e.g. a driver) – and then end with "and contributors". Only project maintainers are allowed to update the project's copyright notice; contributors should contact the maintainers if they want to ask to be included among the explicitly listed names. [short SPDX license identifier]: https://spdx.github.io/spdx-spec/v2.3/using-SPDX-short-identifiers-in-source-files/ [CHANGELOG]: ../../CHANGELOG.md ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744525883.718715 liquidctl-1.15.0/docs/developer/protocol/0000755000175000017500000000000014776655074017441 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/protocol/aquacomputer.md0000644000175000017500000006123114562144457022463 0ustar00jonasjonas# Aquacomputer protocols Aquacomputer devices share the same HID report philosophy: * A sensor report with ID `0x01` is sent every second to the host, detailing current sensor readings * A HID feature report with ID `0x03` contains the device settings which can be requested, modified and written back to the device * A control/configuration report that can be requested and sent back to the device, controlling its settings and mode of operation. Contains a CRC-16/USB checksum in the last two bytes * A save report, which is always constant and is sent after a configuration report (the devices seem to work fine without it, but the official software always sends it) These devices also share some substructures in their reports. All listed values are two bytes long and in big endian, unless noted otherwise. ### Sensor report details & substructures There's one important substructure that keeps recurring in sensor reports, and it concerns fan info. The definition of fan here also includes pumps, not only 3/4 pin fans in the literal sense. Here's what it's known to contain: | What | Where (relative offset) | | ------------------ | ----------------------- | | Fan speed (0-100%) | 0x00 | | Fan voltage | 0x02 | | Fan current | 0x04 | | Fan power | 0x06 | | Fan speed (RPM) | 0x08 | Temperature sensors, if not connected, will report `0x7FFF` as their value. ### Control report details & substructures The control report can be requested from and sent to the device using `GET_FEATURE_REPORT` and `SET_FEATURE_REPORT`, respectively. What it contains depends on the device, but it always carries a two byte CRC-16/USB checksum in the last two places. The checksum is calculated from the data between the starting `0x03` report ID at the very beginning and the (existing) checksum at the end. Fan speed control subgroups can be found in the control report, and it's currently known that they look like this: | What | Where (relative offset) | |------------------|-------------------------| | Speed curve type | 0x00 | | Speed (0-100%) | 0x01 | The `Speed curve type` above understands these values (list may be incomplete): | Value | Meaning | |-------|-----------------------------------------------| | 0 | Manual mode (directly honor percentage value) | | 1 | PID control mode | | 2 | Fan curve mode | The liquidctl driver currently supports only the manual mode. ## D5 Next pump The D5 Next pump can, aside from itself, control and monitor an optionally connected fan. ### Sensor report An example sensor report of the D5 Next looks like this: ``` 01 00 03 0B B8 4E 20 00 01 00 00 00 64 03 FB 00 00 00 51 00 00 00 0E A4 00 00 00 45 00 10 42 C4 00 00 00 00 30 C9 00 00 00 A8 9C C2 B9 00 00 01 49 18 87 A1 3C 5C AB 04 BA 01 F8 00 00 00 52 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 00 00 00 00 00 00 00 00 0A 31 7F FF 00 00 7F FF 00 00 00 00 00 00 00 00 00 00 00 00 00 1B DC 04 B9 00 C5 00 EE 0B A8 00 00 00 1B DC 09 AA 08 4C 09 A9 08 4A 00 03 00 06 00 00 00 00 00 00 00 00 00 00 00 00 01 08 E5 00 E7 27 10 27 10 7F 7F ``` Its ID is `0x01` and its length is `0x9e`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |------------------------------------|--------------------------| | Serial number (first part) | 0x03 | | Serial number (second part) | 0x05 | | Firmware version | 0xD | | Number of power cycles *[4 bytes]* | 0x18 | | Liquid (water) temperature | 0x57 | | Pump info substructure | 0x74 | | Fan info substructure | 0x67 | | +5V voltage | 0x39 | | +12V voltage | 0x37 | | Virtual temp sensor 1 | 0x3F | | Virtual temp sensor 2 | 0x41 | | Virtual temp sensor 3 | 0x43 | | Virtual temp sensor 4 | 0x45 | | Virtual temp sensor 5 | 0x47 | | Virtual temp sensor 6 | 0x49 | | Virtual temp sensor 7 | 0x4B | | Virtual temp sensor 8 | 0x4D | ### Control report An example control report of the D5 Next looks like this: ``` 03 00 03 1E 00 00 00 00 00 0A C0 00 7F FF 00 00 00 00 02 02 0E 10 0B B8 00 00 00 00 0A 00 01 00 0A 00 06 00 0A 00 0C 00 0A 00 00 00 00 00 00 01 01 F4 27 10 27 10 07 D0 00 00 00 27 10 27 10 13 88 02 07 D2 00 00 0C 80 01 F4 01 2C 00 00 00 64 00 1E 00 01 0A F0 0A 8C 0A FD 0B 4C 0B 9D 0B E9 0C 46 0C 9F 0C F3 0D 3C 0D A2 0D E5 0E 42 0E 8A 0E E6 0F 35 0F 70 00 00 00 00 00 00 02 D6 04 D6 06 D6 09 81 0A 01 0D AC 12 02 16 2D 17 AD 19 D8 1E AE 22 2E 23 2E 02 12 D3 00 00 0D 48 01 F4 01 2C 00 00 00 64 00 1E 00 01 0A F0 0A 8C 0A FA 0B 4C 0B A4 0C 00 0C 4F 0C A3 0D 11 0D 51 0D A6 0D FD 0E 56 0E 9E 0E EE 0F 20 10 82 00 00 00 8C 00 00 00 00 00 00 00 00 00 00 01 00 01 80 03 54 07 81 0A 81 0B 01 0C 81 0D D7 0E AC 03 E8 FF 00 00 00 00 0F 03 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 32 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 0F 0F 08 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 28 00 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 E7 FF FF 00 FE FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 1E 0F 0B 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 1E 00 28 00 01 00 06 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 FF 02 FF 01 FB FF FF 05 25 FF FF 00 C5 FF FF 03 F5 FF FF 05 F3 FF FF 00 2D 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 FF FF 01 FD FF FF 03 FF FF FF 00 FA FF FF 01 CE 10 FF 00 3C 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 FA FF FF 05 DC FF FF 01 C2 FF FF 00 00 FF FF 07 D0 10 FF 00 4B 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 E8 FF FF 01 C2 FF FF 00 00 FF FF 00 64 FF FF 03 20 10 FF 01 00 06 03 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 1E 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 00 06 00 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 64 00 1E 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF C0 04 01 C2 0F A0 01 10 FB ``` Its ID is `0x03` and its length is `0x329`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |------------------------------------|--------------------------| | Pump speed control subgroup | 0x96 | | Fan speed control subgroup | 0x41 | ## Farbwerk 360 RGB controller The Farbwerk 360 exposes four temperature sensors through its sensor report. ### Sensor report An example sensor report of the Farbwerk 360 looks like this: ``` 01 00 01 41 BB DE 92 03 E8 00 00 00 64 03 FE 00 00 00 11 00 00 00 09 D3 00 00 00 5E 00 08 A4 DD 00 00 00 24 BF E6 C0 34 A2 B4 FF D7 FF D5 FF D6 5A EC 0A 1F 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 FA 01 FB 00 07 00 00 00 03 00 00 00 0B 00 00 00 00 00 15 20 1A 00 00 00 00 00 00 00 00 27 10 27 10 27 10 27 10 27 10 03 E8 00 00 03 E8 00 00 03 E8 00 00 03 E8 00 00 00 00 00 00 00 00 00 00 00 06 00 06 00 05 00 06 01 17 00 06 ``` Its ID is `0x01` and its length is `0xb6`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |------------------------|--------------------------| | Temp sensor 1 | 0x32 | | Temp sensor 2 | 0x34 | | Temp sensor 3 | 0x36 | | Temp sensor 4 | 0x38 | | Virtual temp sensor 1 | 0x3A | | Virtual temp sensor 2 | 0x3C | | Virtual temp sensor 3 | 0x3E | | Virtual temp sensor 4 | 0x40 | | Virtual temp sensor 5 | 0x42 | | Virtual temp sensor 6 | 0x44 | | Virtual temp sensor 7 | 0x46 | | Virtual temp sensor 8 | 0x48 | | Virtual temp sensor 9 | 0x4A | | Virtual temp sensor 10 | 0x4C | | Virtual temp sensor 11 | 0x4E | | Virtual temp sensor 12 | 0x50 | | Virtual temp sensor 13 | 0x52 | | Virtual temp sensor 14 | 0x54 | | Virtual temp sensor 15 | 0x56 | | Virtual temp sensor 16 | 0x58 | ## Octo The Octo exposes four physical and sixteen virtual temperature sensors and eight groups of fan sensor data (outlined in the preamble) through its sensor report. ### Sensor report An example sensor report of the Octo looks like this: ``` 01 00 02 3A 92 C9 EA 03 E8 00 01 00 65 03 FB 00 00 00 01 00 00 00 48 8E 00 00 00 C2 00 3A DF 11 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 04 9D 84 FF DB FF DC FF DD A7 B0 5B FC 10 17 7F FF 7F FF 7F FF 0F 00 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 04 B9 00 02 00 02 00 00 05 5D 04 B9 00 00 00 00 00 00 00 00 08 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 21 3B 04 B9 00 02 00 02 00 00 00 00 08 00 00 00 00 03 E8 05 5D 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 21 3B 00 00 00 00 03 E8 27 10 00 00 00 00 03 E8 27 10 00 00 00 00 00 00 00 00 15 B3 15 0A 27 10 27 10 FF FF ``` Its ID is `0x01` and its length is `0x147`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |------------------------------------|--------------------------| | Serial number (first part) | 0x03 | | Serial number (second part) | 0x05 | | Firmware version | 0xD | | Number of power cycles *[4 bytes]* | 0x18 | | Temp sensor 1 | 0x3D | | Temp sensor 2 | 0x3F | | Temp sensor 3 | 0x41 | | Temp sensor 4 | 0x43 | | Fan 1 substructure | 0x7D | | Fan 2 substructure | 0x8A | | Fan 3 substructure | 0x97 | | Fan 4 substructure | 0xA4 | | Fan 5 substructure | 0xB1 | | Fan 6 substructure | 0xBE | | Fan 7 substructure | 0xCB | | Fan 8 substructure | 0xD8 | | Virtual temp sensor 1 | 0x45 | | Virtual temp sensor 2 | 0x47 | | Virtual temp sensor 3 | 0x49 | | Virtual temp sensor 4 | 0x4B | | Virtual temp sensor 5 | 0x4D | | Virtual temp sensor 6 | 0x4F | | Virtual temp sensor 7 | 0x51 | | Virtual temp sensor 8 | 0x53 | | Virtual temp sensor 9 | 0x55 | | Virtual temp sensor 10 | 0x57 | | Virtual temp sensor 11 | 0x59 | | Virtual temp sensor 12 | 0x5B | | Virtual temp sensor 13 | 0x5D | | Virtual temp sensor 14 | 0x5F | | Virtual temp sensor 15 | 0x61 | | Virtual temp sensor 16 | 0x63 | ### Control report An example control report of the Octo looks like this: ``` 03 00 02 28 00 00 00 A9 00 00 05 14 02 BC 00 00 00 00 00 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 02 01 F4 27 10 27 10 07 D0 00 01 F4 27 10 27 10 07 D0 00 05 5D FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 21 3B FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 00 FF 00 00 00 00 0F 03 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 32 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 0F 0F 08 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 28 00 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 E7 FF FF 00 FE FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 1E 0F 0B 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 1E 00 28 00 01 00 06 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 FF 02 FF 01 FB FF FF 05 25 FF FF 00 C5 FF FF 03 F5 FF FF 05 F3 FF FF 00 2D 0F 13 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 0A 00 05 00 05 00 19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 02 00 FF 78 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 3C 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 FF FF 01 FD FF FF 03 FF FF FF 00 FA FF FF 01 CE 10 FF 00 4B 0F 0F 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 04 00 1E 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 00 78 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 00 0F 03 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 32 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 0F 0F 08 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 28 00 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 E7 FF FF 00 FE FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 1E 0F 0B 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 1E 00 28 00 01 00 06 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 FF 02 FF 01 FB FF FF 05 25 FF FF 00 C5 FF FF 03 F5 FF FF 05 F3 FF FF 01 2D 0F 13 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 0A 00 05 00 05 00 19 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF 02 00 FF 78 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 3C 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 FF FF 01 FD FF FF 03 FF FF FF 00 FA FF FF 01 CE 10 FF 01 4B 0F 0F 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 04 00 1E 00 1E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 78 00 78 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 01 00 00 13 88 13 88 13 88 13 88 01 5E 01 AB 59 ``` Its ID is `0x03` and its length is `0x65F`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |-------------------------|--------------------------| | Fan 1 ctrl substructure | 0x5A | | Fan 2 ctrl substructure | 0xAF | | Fan 3 ctrl substructure | 0x104 | | Fan 4 ctrl substructure | 0x159 | | Fan 5 ctrl substructure | 0x1AE | | Fan 6 ctrl substructure | 0x203 | | Fan 7 ctrl substructure | 0x258 | | Fan 8 ctrl substructure | 0x2AD | ## Quadro The Quadro exposes four physical and sixteen virtual temperature sensors, and four groups of fan sensor data (outlined in the preamble) through its sensor report. ### Sensor report An example sensor report of the Quadro looks like this: ``` 01 00 03 5B 72 FF 40 00 01 00 00 00 65 04 08 00 00 00 01 00 00 00 13 C5 00 00 00 91 00 32 CB B0 00 00 00 00 00 00 00 00 FF D5 FF D6 9B 54 FF D8 A6 FD 5B 97 7F FF 7F FF 06 51 7F FF 09 59 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 7F FF 13 88 7F FF 7F FF 7F FF 03 00 00 00 00 00 00 00 00 00 00 00 03 00 00 00 04 B9 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 27 10 04 B9 00 00 00 00 00 00 00 00 08 05 BB 04 B9 00 00 00 00 01 64 00 00 00 15 E0 04 B9 00 00 00 00 00 00 00 00 08 00 00 00 00 03 E8 00 00 00 00 00 00 03 E8 27 10 00 00 00 00 03 E8 05 BB 00 00 00 00 03 E8 15 E0 00 00 00 00 03 E8 27 10 00 0A 00 00 00 0E 00 00 00 00 27 10 FF 00 00 01 ``` Its ID is `0x01` and its length is `0xDC`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |------------------------------------|--------------------------| | Serial number (first part) | 0x03 | | Serial number (second part) | 0x05 | | Firmware version | 0xD | | Number of power cycles *[4 bytes]* | 0x18 | | Temp sensor 1 | 0x34 | | Temp sensor 2 | 0x36 | | Temp sensor 3 | 0x38 | | Temp sensor 4 | 0x3A | | Fan 1 substructure | 0x70 | | Fan 2 substructure | 0x7D | | Fan 3 substructure | 0x8A | | Fan 4 substructure | 0x97 | | Flow sensor | 0x6E | | Virtual temp sensor 1 | 0x3C | | Virtual temp sensor 2 | 0x3E | | Virtual temp sensor 3 | 0x40 | | Virtual temp sensor 4 | 0x42 | | Virtual temp sensor 5 | 0x44 | | Virtual temp sensor 6 | 0x46 | | Virtual temp sensor 7 | 0x48 | | Virtual temp sensor 8 | 0x4A | | Virtual temp sensor 9 | 0x4C | | Virtual temp sensor 10 | 0x4E | | Virtual temp sensor 11 | 0x50 | | Virtual temp sensor 12 | 0x52 | | Virtual temp sensor 13 | 0x54 | | Virtual temp sensor 14 | 0x56 | | Virtual temp sensor 15 | 0x58 | | Virtual temp sensor 16 | 0x5A | ### Control report An example control report of the Quadro looks like this: ``` 03 00 03 1C 00 00 00 A9 00 00 02 58 05 14 FA EC 05 DC 00 01 F4 27 10 27 10 07 D0 00 01 F4 27 10 27 10 07 D0 00 01 F4 27 10 27 10 07 D0 00 01 F4 27 10 27 10 07 D0 00 00 00 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 4C D0 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 05 BB 00 03 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 00 15 E0 FF FF 0D AC 05 78 04 B0 00 00 00 28 00 14 00 01 0A F0 0A 8C 0A FA 0B 4A 0B A4 0B F4 0C 4E 0C 9D 0C F8 0D 48 0D A2 0D F2 0E 4C 0E 9C 0E F5 0F 46 0F A0 00 00 00 8C 01 18 01 F4 03 20 04 B0 06 90 08 D4 0B 68 0E 4C 11 94 15 2C 19 28 1D 74 22 10 27 10 FF 00 02 00 00 0F 03 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 32 00 64 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 0F 0F 08 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 19 00 28 00 14 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 E7 FF FF 00 FE FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 1E 0F 0B 00 00 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 1E 00 28 00 01 00 06 00 50 00 00 00 00 00 00 00 00 00 00 00 00 00 00 02 FF 02 FF 01 FB FF FF 05 25 FF FF 00 C5 FF FF 03 F5 FF FF 05 F3 FF FF 00 2D 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 FF FF 01 FD FF FF 03 FF FF FF 00 FA FF FF 01 CE 10 FF 00 3C 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 03 FF FF FF 07 D0 FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 4B 0F 04 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 FF 00 28 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 01 CE FF FF 03 FF FF FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 2D 0F 00 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 64 00 28 00 02 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 FA FF FF 01 CE 10 FF 00 00 FF FF 00 00 FF FF 00 00 FF FF 00 2D 0F 00 00 06 FF FF 0F 19 00 00 03 E8 01 64 00 00 03 E8 01 64 00 28 00 05 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0F 00 00 FF FF 01 FD FF FF 03 FF FF FF 00 FA FF FF 01 CE 10 FF 01 00 E0 A8``` ``` Its ID is `0x03` and its length is `0x3C1`. Here is what it's currently known to contain: | What | Where/starts at (offset) | |-------------------------|--------------------------| | Fan 1 ctrl substructure | 0x36 | | Fan 2 ctrl substructure | 0x8B | | Fan 3 ctrl substructure | 0xE0 | | Fan 4 ctrl substructure | 0x135 |././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/developer/protocol/asus_ryujin.md0000644000175000017500000000360614613511524022320 0ustar00jonasjonas# ASUS Ryujin II liquid cooler protocol The data of all usb packets is 65 bytes long, prefixed with `0xEC`. ## Generic Operations ### Get firmware info - Request: - Header: `0xEC 0x82` - Response: - Header: `0xEC 0x02` - Data: - Byte 4-18: Firmware version (ascii) ## Cooling Operations ### Get cooling info - Request: - Header: `0xEC 0x99` - Response: - Header: `0xEC 0x19` - Data: - Byte 4: Liquid temperature (integer digits) - Byte 5: Liquid temperature (decimal digit) - Byte 6-7: Pump rpm (little endian) - Byte 8-9: Embedded Micro Fan rpm (little endian) ### Get duties of pump and of embedded micro fan - Request: - Header: `0xEC 0x9A` - Response: - Header: `0xEC 0x1A` - Data: - Byte 5: Pump duty % from 0x00 to 0x64 - Byte 6: Embedded Micro Fan duty % from 0x00 to 0x64 ### Get fan speed of AIO fan controller - Request: - Header: `0xEC 0xA0` - Response: - Header: `0xEC 0x20` - Data: - Byte 4-5: Fan 4 rpm (little endian) - Byte 6-7: Fan 1 rpm (little endian) - Byte 8-9: Fan 2 rpm (little endian) - Byte 10-11: Fan 3 rpm (little endian) ### Get duty of AIO fan controller - Request: - Header: `0xEC 0xA1` - Response: - Header: `0xEC 0x21` - Data: - Byte 5: AIO fan controller duty from 0x00 to 0xFF ### Set duties of pump and of embedded micro fan - Request: - Header: `0xEC 0x1A` - Data: - Byte 4: Pump duty % from 0x00 to 0x64 - Byte 5: Embedded Micro Fan duty % from 0x00 to 0x64 - Response: - Header: `0xEC 0x1A` ### Set duty of AIO fan controller - Request: - Header: `0xEC 0x21` - Data: - Byte 5: AIO fan controller duty from 0x00 to 0xFF - Response: - Header: `0xEC 0x21` ## Unknown - Request: - Header: `0xEC 0xAF` - Response: - Header: `0xEC 0x2F` - Byte 4-17: ? ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/docs/developer/protocol/commander_core.md0000644000175000017500000002230514617346724022733 0ustar00jonasjonas# Corsair Commander Core Protocol ## Compatible devices | Device Name | USB ID | LED channels | Fan channels | Temperature channels | AIO | |:-----------:|:------:|:------------:|:------------:|:--------------------:|:---:| | Commander Core | `1b1c:0c1c` | 7 | 6 | 2 | Yes | | Commander Core XT | `1b1c:0c2a` | 7 | 6 | 2 | No | | Commander ST | `1b1c:0c32` | 7 | 6 | 2 | No | The Commander Core is typically shipped with the Corsair iCUE Elite Capellix AIOs. The first two LED and temperature channels go to the EXT port, typically in use by the AIO. Newer releases of these AIOs may instead come with the Commander ST. The Commander Core XT is a standalone product that shares the same protocol, but does not include support for an AIO, and as a result channel numbers are offset down by 1. ## Command formats The Commander Core works in different modes, so ensure the proper mode has been sent for each command. Unless stated otherwise all multi-byte numbers used little endian. Host -> Device: 96 bytes for firmware v2.x.x | 1024 bytes for firmware v1.x.x | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | Command | | 0x02 | Channel | | 0x03-... | Data | Device -> Host: 96 bytes for firmware v2.x.x | 1024 bytes for firmware v1.x.x | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | Command | | 0x02 | 0x00 | | 0x03-... | Data | ## Global Commands Global commands should work in any mode. ### `0x01` - Wake up/Sleep Wakeup needs to be run every time the device has not been sent any data for a predefined number of seconds. Sleep should be run when the device should return to hardware mode and no more data will be sent. Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x01 | | 0x02 | 0x03 | | 0x03 | 0x00 | | 0x04 | 0x01 for sleep or 0x02 for wake up | ### `0x02` - Get Firmware Version Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x02 | | 0x02 | 0x13 | Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | 0x00 | | 0x02 | Major | | 0x03 | Minor | | 0x04 | Patch | Note: the `0x01` Init/Wakeup command is exceptionally not necessary before this command. ### `0x0d` - Open Endpoint Sets the mode for the channel to use. Needs to be run **before** a read or write operation. `0x05` - Init/Wakeup will likely need to be run first. Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x0d | | 0x02 | Channel | | 0x03 | New mode | ### `0x05` - Close Endpoint Needs to be run **after** a read or write operation to close a previously opened endpoint. Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x05 | | 0x02 | 0x01 | | 0x03 | Channel to close | ## Mode Commands These are the commands that are used in each of the modes. ### `0x06` - Write Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x06 | | 0x02 | Channel | | 0x03, 0x04 | Data length | | 0x05, 0x06 | 00:00 Unknown (Before data length starts) | | 0x07, 0x08 | Data Type (Included in data length) | | 0x09-... | Data | ### `0x07` - Write More For writing data that does not fit in a single 96 byte Host->Device packet. Can be sent multiple times consecutively until data is completely written. Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x07 | | 0x02 | Channel | | 0x03-... | Data | ### `0x08` - Read Initial/More/Final Read More and Read Final are used to read data larger than the 96 byte device->host packet size. Byte index 0x03 must incremented as each piece of the data is read (i.e. 0x01 first, 0x02 second, and 0x03 third). Command: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x08 | | 0x01 | 0x08 | | 0x02 | 0x00 | | 0x03 | 0x01 for Read Initial or 0x02 for Read More or 0x03 for Read Final | Read Initial Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | 0x08 | | 0x02 | 0x00 | | 0x03, 0x04 | Data type | | 0x05-... | Data | Read More/Final Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | 0x08 | | 0x02 | 0x00 | | 0x03-... | Data | ## Modes: ### `0x17` - Current Speeds of Pump and Fans Data Type: `0x06 0x00` | Byte index | Description (Core) | Description (Core XT) | | ---------- | ------------------ | --------------------- | | 0x00 | Number of speed items | Number of speed items | | 0x01, 0x02 | Speed of AIO/EXT port | Speed of Fan 1 | | 0x03, 0x04 | Speed of Fan 1 | Speed of Fan 2 | | 0x05, 0x06 | Speed of Fan 2 | Speed of Fan 3 | | 0x07, 0x08 | Speed of Fan 3 | Speed of Fan 4 | | 0x09, 0x0a | Speed of Fan 4 | Speed of Fan 5 | | 0x0b, 0x0c | Speed of Fan 5 | Speed of Fan 6 | | 0x0d, 0x0e | Speed of Fan 6 | | ### `0x1a` - Connected Speed Devices Data Type: `0x09 0x00` Connection State: 0x07 if connected or 0x01 if not connected | Byte index | Description (Core) | Description (Core XT) | | ---------- | ------------------ | --------------------- | | 0x00 | Number of Ports | Number of Ports | | 0x01 | AIO/EXT Connection State | Fan 1 Connection State | | 0x02 | Fan 1 Connection State | Fan 2 Connection State | | 0x03 | Fan 2 Connection State | Fan 3 Connection State | | 0x04 | Fan 3 Connection State | Fan 4 Connection State | | 0x05 | Fan 4 Connection State | Fan 5 Connection State | | 0x06 | Fan 5 Connection State | Fan 6 Connection State | | 0x07 | Fan 6 Connection State | | ### `0x20` - Connected LEDs Data Type: `0x0f 0x00` | Byte index | Description | | ---------- | ----------- | | 0x00 | Number of RGB channels | | 0x01, 0x02 | EXT RGB mode | | 0x03, 0x04 | EXT LED count | | 0x05, 0x06 | RGB Port 1 mode | | 0x07, 0x08 | RGB Port 1 LED count | | 0x09, 0x0a | RGB Port 2 mode | | 0x0b, 0x0c | RGB Port 2 LED count | | 0x0d, 0x0e | RGB Port 3 mode | | 0x0f, 0x10 | RGB Port 3 LED count | | 0x11, 0x12 | RGB Port 4 mode | | 0x13, 0x14 | RGB Port 4 LED count | | 0x15, 0x16 | RGB Port 5 mode | | 0x17, 0x18 | RGB Port 5 LED count | | 0x19, 0x1a | RGB Port 6 mode | | 0x1b, 0x1c | RGB Port 6 LED count | RGB Mode: | Fan Mode | Value | | ------------ | ----- | | Connected | 0x02 | | Disconnected | 0x03 | ### `0x21` - Get Temperatures Data Type: `0x10 0x00` | Byte index | Description | | ---------- | ----------- | | 0x00 | Number of temperature sensors | | 0x01 | 0x00 if connected or 0x01 if not connected | | 0x02, 0x03 | Temperature in Celsius with 10ths decimal (3b:01 = 315 = 31.5C) | | 0x04 | 0x00 if connected or 0x01 if not connected | | 0x05, 0x06 | Temperature in Celsius with 10ths decimal (3b:01 = 315 = 31.5C) | ### `0x60 0x6d` - Hardware Speed Device Mode Data Type: `0x03 0x00` | Byte index | Description (Core) | Description (Core XT) | | ---------- | ------------------ | --------------------- | | 0x00 | Number of Ports | Number of Ports | | 0x01 | AIO/EXT Speed Mode | Fan 1 Speed Mode | | 0x02 | Fan 1 Speed Mode | Fan 2 Speed Mode | | 0x03 | Fan 2 Speed Mode | Fan 3 Speed Mode | | 0x04 | Fan 3 Speed Mode | Fan 4 Speed Mode | | 0x05 | Fan 4 Speed Mode | Fan 5 Speed Mode | | 0x06 | Fan 5 Speed Mode | Fan 6 Speed Mode | | 0x07 | Fan 6 Speed Mode | | Speed Modes: | Mode | Description | | ---- | ----------- | | 0x00 | Fixed percentage | | 0x02 | Fan percentage fan curve | Note: This list is not complete and currently only contains what has been confirmed so far ### `0x61 0x6d` - Hardware Fixed Speed (Percentage) Data Type: `0x04 0x00` | Byte index | Description (Core) | Description (Core XT) | | ---------- | ------------------ | --------------------- | | 0x00 | Number of Ports | Number of Ports | | 0x01, 0x02 | Speed as percentage for AIO/EXT port | Speed as percentage for Fan 1 | | 0x03, 0x04 | Speed as percentage for Fan 1 | Speed as percentage for Fan 2 | | 0x05, 0x06 | Speed as percentage for Fan 2 | Speed as percentage for Fan 3 | | 0x07, 0x08 | Speed as percentage for Fan 3 | Speed as percentage for Fan 4 | | 0x09, 0x0a | Speed as percentage for Fan 4 | Speed as percentage for Fan 5 | | 0x0b, 0x0c | Speed as percentage for Fan 5 | Speed as percentage for Fan 6 | | 0x0d, 0x0e | Speed as percentage for Fan 6 | | ### `0x62 0x6d` - Hardware Speed Curve (Percentage) Data Type: `0x05 0x00` | Byte index | Description (Core) | | ---------- | ----------- | | 0x00 | Number of Ports | | 0x01-0x1e | Speed Curve for AIO/EXT | | 0x1f-0x3c | Speed Curve for Fan 1 | | 0x3d-0x5a | Speed Curve for Fan 2 | | 0x5b-0x78 | Speed Curve for Fan 3 | | 0x79-0x96 | Speed Curve for Fan 4 | | 0x97-0xb4 | Speed Curve for Fan 5 | | 0xb5-0xd2 | Speed Curve for Fan 6 | Speed Curve: | Byte index | Description (Core) | | ---------- | ----------- | | 0x00 | Temperature sensor to use 0x00 for water 0x01 for plugged in sensor | | 0x01 | Number of speed curve points | | 0x02-0x05 | Speed curve point 1 | | 0x06-0x09 | Speed curve point 2 | | 0x0a-0x0d | Speed curve point 3 | | 0x0e-0x11 | Speed curve point 4 | | 0x12-0x15 | Speed curve point 5 | | 0x16-0x19 | Speed curve point 6 | | 0x1a-0x1d | Speed curve point 7 | Speed Point: | Byte index | Description (Core) | | ---------- | ----------- | | 0x00, 0x01 | Temperature in Celsius with 10ths decimal (3b:01 = 315 = 31.5C) | | 0x02, 0x03 | Point speed (%) | ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/docs/developer/protocol/coolit.md0000644000175000017500000000550614617346724021253 0ustar00jonasjonas# Coolit Protocol ## Compatible devices | Device Name | USB ID | LED channels | Fan channels | Temperature channels | AIO | |:-----------:|:------:|:------------:|:------------:|:--------------------:|:---:| | Corsair Hydro H110i GT | `1b1c:0c04` | 0 | 2 + pump | 1 (internal) | Yes | This device also features a pump channel, controlled with the appropriate command (see below). ## Command format Unless stated otherwise all multi-byte numbers used little endian. The device expect packets in the following order/format: 1. Request ID byte (usually a counter that rolls over) 2. Opcode byte 3. Command byte 4. Optional arguments See the `_build_data_package` function for data package construction. Multiple "data packages" can be merged into a single command, i.e. one single TX to the device. To do so, simply create a list with data packages, and send the list altogether with `_send_commands`. See `get_status` for a usage example. ## Opcodes The device can be read or written to, in 1/2/3 byte(s). |Opcode|Description|Name in driver| |-|-|-| |0x06|Write 1 byte|`_OP_CODE_WRITE_ONE_BYTE`| |0x08|Write 2 byte|`_OP_CODE_WRITE_TWO_BYTES`| |0x0A|Write 3 byte|`_OP_CODE_WRITE_THREE_BYTES`| |0x07|Read 1 byte|`_OP_CODE_READ_ONE_BYTE`| |0x09|Read 2 byte|`_OP_CODE_READ_TWO_BYTES`| |0x0B|Read 3 byte|`_OP_CODE_READ_THREE_BYTES`| ## Modes and speeds Fan modes: - Fixed duty = 0x02 - Fixed RPM = 0x04 - Custom curve = 0x0E Pump modes: - Quiet = 0x2E - Extreme = 0x0C Pump speeds: - Quiet = 0x2E09 - Extreme = 0x860B ## Commands |Command|Name in driver|Description|Opcode to use|Arguments| |-|-|-|-|-| |0x01|`_COMMAND_FIRMWARE_ID`|Get firmware ID|`_OP_CODE_READ_TWO_BYTES`|n/a| |0x0E|`_COMMAND_TEMP_READ`|Read temperature|`_OP_CODE_READ_TWO_BYTES`|n/a| |0x10|`_COMMAND_FAN_SELECT`|Select fan channel|`_OP_CODE_WRITE_ONE_BYTE`|0x0 to 0x2 (fan1/fan2/pump)| |0x12|`_COMMAND_FAN_MODE`|Set fan mode|`_OP_CODE_WRITE_ONE_BYTE`|See fan/pump modes| |0x13|`_COMMAND_FAN_FIXED_PWM`|Set fan channel duty|`_OP_CODE_WRITE_ONE_BYTE`|duty as RPM| |0x14|`_COMMAND_FAN_FIXED_RPM`|Set fan channel RPM|`_OP_CODE_WRITE_TWO_BYTES`|pump speed| |0x16|`_COMMAND_FAN_READ_RPM`|Get fan channel RPM|`_OP_CODE_READ_TWO_BYTES`|n/a| |0x17|`_COMMAND_FAN_MAX_RPM`|Get max fan channel RPM|`_OP_CODE_READ_TWO_BYTES`|n/a| |0x19|`_COMMAND_FAN_RPM_TABLE`|Set fan channel RPM curve|`_OP_CODE_WRITE_THREE_BYTES`|duty encoded as RPM| |0x1A|`_COMMAND_FAN_TEMP_TABLE`|Set fan channel temperature curve|`_OP_CODE_WRITE_THREE_BYTES`|temperature data| ## Custom curves Fan curves are written in the following order: 1. Fan select 2. Fan mode set 3. Fan RPM curve set 4. Fan temperature curve set Temperature is encoded as (0x00, temperature) pairs. RPM is encoded as (A, B) pairs, where: - A = rpm % 255 - B = rpm - (rpm % 255) >> 8 - rpm = duty * max_rpm / 100 `max_rpm` is read from the device for each channel.././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/developer/protocol/coreliquid.md0000644000175000017500000002377514613511524022116 0ustar00jonasjonas# MSI coreliquid AIO protocol ### Compatible devices | Device Name | USB ID | LED channels | Fan channels | | ----------- | ------ | ------------ | ------------ | | MPG Coreliquid K360 | `ODB0:B130` | 1 | 5 | ### Command formats Most of the communication with the K360 uses 64 byte HID reports. Lighting effect control uses a 185 byte feature report. Incoming and outgoing reports generally share the same size. Write commands start with a prefix of `0xD0`, and multi-byte numbers are little-endian, unless stated otherwise. | Feature report number | Description | | ---------- | ----------- | | 0x52 | Get or set board data, notably lighting control | | 0xD0 | "get all hardware monitor data" (currently unused) | It can be noted that the feature report for the board data seems to be designed to include information about all the leds connected to the motherboard. The driver only needs to set a small subset of the data in order to control the cpu cooler. ## Get General Information ### Get APROM Firmware version - `0xB0` Request: | Byte index | Value | | ---------- | ----------- | | 0x00 | 0x01 | | 0x01 | 0xB0 | | Fill | 0xCC | Response: | Byte index | Description | | ---------- | ----------- | | 0x02 | X | Firmware version is `(X >> 4).(X & 0x0F)` ### Get LDROM Firmware version - `0xB6` Request: | Byte index | Value | | ---------- | ----------- | | 0x00 | 0x01 | | 0x01 | 0xB6 | | Fill | 0xCC | Response: | Byte index | Description | | ---------- | ----------- | | 0x02 | X | Firmware version is `(X >> 4).(X & 0x0F)` ### Get screen Firmware version - `0xF1` Response: | Byte index | Description | | ---------- | ----------- | | 0x02 | Version number | ### Get device model index - `0xB1` Request: | Byte index | Value | | ---------- | ----------- | | Fill | 0xCC | Response: | Byte index | Description | | ---------- | ----------- | | 0x02 | Version number | ## Sending system information for fan control Fan profiles are **NOT** controlled by any internal device temperature measurement. Instead, the device expects periodic reports of the CPU temperature, which it uses to interpolate fan speeds and to show on the screen. ### Set CPU status - `0x85` | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x85 | | 0x02-0x03 | cpu frequency (int, MHz) | | 0x01 | cpu temperature (int, C) | ### Set GPU status - `0x86` | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x86 | | 0x02-0x03 | gpu memory frequency (int, MHz) | | 0x01 | gpu usage (int, %) | ## Lighting effects ### Get all board data (lighting) - Feature report `0x52` Response: | Byte index | Description | |--------------------------|---------------------------------------------------------| | 0x1F | **Lighting mode** | | 0x20-0x22 | **RGB values for color1 in JRainbow1 area** | | 0x23 | **Bits 0-1: Speed (LOW, MEDIUM, HIGH)** | | 0x24 | **Bits 2-6: Brightness level (0-10)** | | 0x24-0x26 | **RGB values for color2 in JRainbow1 area** | | 0x27 | **Bit 7: Color selection (0: Rainbow, 1: User-defined)**| | 0x29 | Number of LEDs in JRainbow1 area | | 0x34 | Number of LEDs in JRainbow2 area | | 0x3D | Bit 0: Stripe (0) or Fan (1) selection | | 0x3D | Bits 1-3: Fan type (SP, HD, LL) | | 0x3E | Bits 2-7: Corsair device quantity (0-63) | | 0x3F | Number of LEDs in JCorsair area | | 0x48 | Bit 0: LL120 outer individual mode (0 or 1) | | 0x4E | Bit 7: Combined JRGB (True or False) | | 0x52 | Bit 0: Onboard sync (True or False) | | 0x52 | Bit 1: Combine JRainbow1 | | 0x52 | Bit 2: Combined JRainbow2 | | 0x52 | Bit 3: Combined JCorsair | | 0x52 | Bit 4: Combined JPipe1 | | 0x52 | Bit 5: Combined JPipe2 | | 0xB8 | **Save to device (0 or 1)** | Ligthing effects are sent to the device by sending the feature report `0x52` with the desired data in the above format. | Byte Value | Lighting Effect Name | |------------|---------------------------| | 0 | DISABLE | | 1 | NO_ANIMATION | | 2 | BREATHING | | 3 | FLASHING | | 4 | DOUBLE_FLASHING | | 5 | LIGHTNING | | 6 | MSI_MARQUEE | | 7 | METEOR | | 8 | WATER_DROP | | 9 | MSI_RAINBOW | | 10 | POP | | 11 | JAZZ | | 12 | PLAY | | 13 | MOVIE | | 14 | MARQUEE | | 15 | COLOR_RING | | 16 | PLANETARY | | 17 | DOUBLE_METEOR | | 18 | ENERGY | | 19 | BLINK | | 20 | CLOCK | | 21 | COLOR_PULSE | | 22 | COLOR_SHIFT | | 23 | COLOR_WAVE | | 24 | VISOR | | 25 | RAINBOW | | 26 | RAINBOW_WAVE | | 27 | VISOR | | 28 | JRAINBOW | | 29 | RAINBOW_FLASHING | | 30 | RAINBOW_DOUBLE_FLASHING | | 31 | RANDOM | | 32 | FAN_CONTROL | | 33 | DISABLE2 | | 34 | COLOR_RING_FLASHING | | 35 | COLOR_RING_DOUBLE_FLASHING| | 36 | STACK | | 37 | CORSAIR_IQUE | | 38 | FIRE | | 39 | LAVA | | 40 | END | ## Fan control ### Fan temperature config - `0x33` (Get) or `0x41` (Set) Format: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0xD0 | | 0x01 | 0x32/0x41 (response/write)| | 0x02-0x09 | Radiator fan 1 config | | 0x0A-0x11 | Radiator fan 2 config | | 0x12-0x19 | Radiator fan 3 config | | 0x01A-0x21 | Pump speed config | | 0x22-0x29 | Waterblock fan config | A fan temperature config consists of 8 bit integer values: - Mode index - 7 temperature points in Celsius. ### Fan speed config - `0x32` (Get) or `0x40` (Set) Format: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0xD0 | | 0x01 | 0x32/0x40 (response/write) | | 0x02-0x09 | Radiator fan 1 config | | 0x0A-0x11 | Radiator fan 2 config | | 0x12-0x19 | Radiator fan 3 config | | 0x01A-0x21 | Pump speed config | | 0x22-0x29 | Waterblock fan config | A fan config consists of 8 bit integer values: - Mode index - 7 duty cycle percentage values. ### Get current fan status - `0x31` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0xD0 | | 0x01 | 0x31 | | 0x02-0x03 | Radiator fan 1 rpm | | 0x04-0x05 | Radiator fan 2 rpm | | 0x06-0x07 | Radiator fan 3 rpm | | 0x08-0x09 | Pump speed rpm | | 0x0A-0x0B | Waterblock fan rpm | | 0x16-0x17 | Radiator fan 1 duty % | | 0x18-0x19 | Radiator fan 2 duty % | | 0x1A-0x1B | Radiator fan 3 duty % | | 0x1C-0x1D | Pump speed duty % | | 0x1E-0x01F | Waterblock fan duty % | ## Display control ### Show Hardware Monitor - `0x71` The device is capable of displaying a maximum of 3 different parameters, which will cycle on the display. | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x71 | | 0x02 | Show CPU frequency (0 or 1) | | 0x03 | Show CPU temperature (0 or 1) | | 0x04 | Show GPU memory frequency (0 or 1) | | 0x05 | Show GPU usage (0 or 1) | | 0x06 | Show pump (0 or 1)| | 0x07 | Show radiator fan (0 or 1) | | 0x08 | Show waterblock fan (0 or 1) | | 0x09 | How many radiator fan speeds to show separately (1 or 3) | ### Set User Message - `0x93` | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x93 | | 0x02-0x3E | Message bytes (ASCII) | | 0x3F | 0x20 | ### Set Clock Display - `0x7A` Sets the clock display style on the OLED screen. `clock_style` determines the visual style of the clock. | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x7A | | 0x02 | `clock_style` (0, 1 or 2) | ### Set Brightness and Direction - `0x7E` | Byte index | Description | | ---------- | ----------- | | 0x01 | 0x7E | | 0x02 | brightness (0-100) | | 0x03 | direction (0-3) | ## Image Upload Commands ### Upload Image - `0xC0` (GIF) or `0xD0` (Banner) File uploads are initiated by a single report, after which the data is transferred in chunks of 60 bytes. Uploaded images must be 240x320 px size, and in the standard 24-bit color BMP format. A short sleep (2 seconds has proven safe) should be placed between the transfer initiation and start of the data transfer to make sure the device is ready. **Transfer initiation report** | Byte index | Description | | ---------- | ----------- | | 0x01 | 0xC0/0xD0 (GIF/Banner) | | 0x02-0x05 | file size to be transferred in bytes (uint32) | | 0x06 | Slot where the image is saved | **Bulk transfer report** | Byte index | Description | | ---------- | ----------- | | 0x01 | 0xC1/0xD1 (GIF/Banner) | | 0x02-0x3D | data chunk | | 0x3E-0x3F | 0x00 | ### Get Image Checksum - `0xC2` (GIF) or `0xD2` (Banner) Response: | Byte index | Description | | ---------- | ----------- | | 0x02-0x03 | checksum value | ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/developer/protocol/lighting_node_rgb.md0000644000175000017500000001170614062723141023407 0ustar00jonasjonas# Corsair Commander Pro and Lighting Node Pro protocol ### Compatible devices | Device Name | USB ID | LED channels | Fan channels | | ----------- | ------ | ------------ | ------------ | | Commander Pro | `1B1C:0C10` | 2 | 6 | | Lighting Node Pro | `1B1C:0C0B` | 2 | 0 | ### Command formats Host -> Device: 16 bytes Device -> Host: 64 bytes ## Get Information commands ### Get Firmware version - `0x02` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | X | | 0x02 | Y | | 0x03 | Z | Firmware version is `X.Y.Z` ### Get Bootloader version - `0x06` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | X | | 0x02 | Y | Bootloader version is `X.Y` ### Get temperature sensor configuration - `0x10` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | `0x01` temp sensor 1 connected, otherwise `0x00` | | 0x02 | `0x01` temp sensor 2 connected, otherwise `0x00` | | 0x03 | `0x01` temp sensor 3 connected, otherwise `0x00` | | 0x04 | `0x01` temp sensor 4 connected, otherwise `0x00` | ### Get temperature value - `0x11` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x11 | | 0x01 | temp sensor number (0x00 - 0x03) | Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | temp MSB | | 0x02 | temp LSB | Divide the temperature value by 100 to get the value is degrees celsius. ### Get bus voltage value - `0x12` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x12 | | 0x01 | rail number (0x00 - 0x02) | rail 0 = 12v rail 1 = 5v rail 2 = 3.3v Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | voltage MSB | | 0x02 | voltage LSB | Divide the value by 1000 to get the actual voltage. ### Get fan configuration - `0x20` Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | Fan 1 mode | | 0x02 | Fan 2 mode | | 0x03 | Fan 3 mode | | 0x04 | Fan 4 mode | | 0x05 | Fan 5 mode | | 0x06 | Fan 6 mode | Fan modes: | Fan Mode | Value | | ------------ | ----- | | Disconnected | 0x00 | | DC (3 pin) | 0x01 | | PWM (4 pin) | 0x02 | ### Get fan RPM value - `0x21` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x21 | | 0x01 | fan number (0x00 - 0x05) | Response: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x00 | | 0x01 | rpm MSB | | 0x02 | rpm LSB | ## Set commands ### Set fixed % - `0x23` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x23 | | 0x01 | fan number (0x00 - 0x05) | | 0x02 | percentage | ### Set fan curve % - `0x25` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x25 | | 0x01 | fan number (0x00 - 0x05) | | 0x02 | temp sensor to use (0x00 - 0x03) 0xFF to use external sensor | | 0x03, 0x04 | temp point 1 MSB | | 0x05, 0x06 | temp point 2 MSB | | 0x07, 0x08 | temp point 3 MSB | | 0x09, 0x0A | temp point 4 MSB | | 0x0B, 0x0C | temp point 5 MSB | | 0x0D, 0x0E | temp point 6 MSB | | 0x0F, 0x10 | rpm point 1 MSB | | 0x11, 0x12 | rpm point 2 MSB | | 0x13, 0x14 | rpm point 3 MSB | | 0x15, 0x16 | rpm point 4 MSB | | 0x17, 0x18 | rpm point 5 MSB | | 0x19, 0x1A | rpm point 6 MSB | ### Hardware LED commands - Send reset channel - send start LED effect - set channel to hardware mode - send effect (one or more messages) - send commit ### Reset channel - `0x37` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x37 | | 0x01 | channel number (0x00 or 0x01) | ### Start channel LED effect - `0x34` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x34 | | 0x01 | channel number (0x00 or 0x01) | ### Set channel state - `0x38` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x38 | | 0x01 | channel number (0x00 or 0x01) | | 0x02 | 0x01 hardware control, 0x02 software control | ### Set LED effect - `0x35` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x35 | | 0x01 | channel number (0x00 or 0x01) | | 0x02 | start LED number | | 0x03 | number of LEDs | | 0x04 | mode | | 0x05 | speed | | 0x06 | direction | | 0x07 | random colors | | 0x08 | 0xFF | | mode | value | Num Colors | | ---- | ----- | --------- | | rainbow | `0x00` | 0 | | color_shift | `0x01` | 2 | | color_pulse | `0x02` | 2 | | color_wave | `0x03` | 2 | | fixed | `0x04` | 1 | | visor | `0x06` | 2 | | marquee | `0x07` | 1 | | blink | `0x08` | 2 | | sequential | `0x09` | 1 | | rainbow2 | `0x0A` | 0 | | speed | value | | ----- | ----- | | slow | `0x02` | | medium | `0x01` | | fast | `0x00` | | direction | value | | ----- | ----- | | forward | `0x01` | | backward | `0x00` | ### Commit hardware settings - `0x33` Request: | Byte index | Description | | ---------- | ----------- | | 0x00 | 0x33 | | 0x01 | 0xFF | ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722818575.0 liquidctl-1.15.0/docs/developer/protocol/nzxt_x3-z3-2023.md0000644000175000017500000001123214654020017022267 0ustar00jonasjonas# Fourth-generation NZXT liquid coolers protocol ## Generic Operations ### Get firmware info - Request: - Header: `0x10 0x01` - Response: - Header: `0x11 0x01` - Data: - Byte 3: Firmware major version - Byte 4: Firmware minor version - Byte 5: Firmware patch version ## RGB Operations ### Get RGB info - Request: - Header: `0x20 0x01` - Response: - Header: `0x21 0x01` - Data: - Byte 14: Number of rgb channels - Byte 21: `0x10` IF device has ring rgb (Kraken X3 Only) - Byte 27: `0x11` IF device has logo rgb (Kraken X3 Only) ## Cooling Operations ### Get cooling info - Request: - Header: `0x74 0x01` - Response: - Header: `0x75 0x01` - Data: - Byte 15: Liquid temperature (integer digits) - Byte 16: Liquid temperature (decimal digits) - Byte 17-18: Pump rpm (little endian) - Byte 19: Pump duty % - Byte 23-24: Fan rpm (little endian) - Byte 25: Fan duty % ### Set speed profile - Request: - Header: `0x72` - Data: - Byte 2-4: Channel to configure. Possible values are: - `0x01 0x00 0x00`: Pump control (Kraken X3 and Z3) - `0x01 0x01 0x00`: Pump control (Kraken 2023) - `0x02 0x00 0x00`: Fan control (Kraken X3 and Z3) - `0x02 0x01 0x01`: Fan control (Kraken 2023) - Byte 5-44 (40 bytes): the representation of a cooling curve where every byte is a value 0-100 setting the speed of the channel at the specific temperature (temperature 20 to 60degree). This is almost always set as a flat curve with all identical values. - Response: - Header: `0x73` ## LCD Operations ### Get lcd info - Request: - Header: `0x30 0x01` - Response: - Header: `0x31 0x01` - Data: - Byte 18: lcd brightness % - Byte 26: lcd orientation (as index of [0, 90, 180, 270]) ### Set lcd brightness/orientation - Request: - Header: `0x30 0x02` - Data: - Byte 3: `0x01` - Byte 4: lcd brightness % - Byte 7: `0x01` - Byte 8: lcd orientation (as index of [0, 90, 180, 270]) - Response: - Header: `0x31 0x02` ### Set lcd mode - Request: - Header: `0x38 0x01` - Data: - Byte 3: Lcd mode. Valid modes are: - `0x02`: show liquid temperature - `0x04`: show image/gif stored in selected bucket - `0x05`: fast rendering (only Kraken 2023 elite) - Byte 4: selected bucket (only needed for mode 4 ) - Response: - Header: `0x39 0x01` - Data: - Byte 14: `0x01` if the operation has been execute successfully ### Get memory bucket info - Request: - Header: `0x30 0x04` - Data: - Byte 3: Bucket index (0 to 15) - Response: - Header: `0x31 0x04` - Data: - Byte 15: Bucket index (0 to 15) > The following are all 0 if the bucket does not exist - Byte 16: asset index (typically bucket index + 1) - Byte 17: `0x02` - Byte 18-19: starting memory address in kilobytes (little endian) - Byte 20-21: memory size in kilobytes (little endian) ### Create memory bucket - Request: - Header: `0x32 0x01` - Data: - Byte 3: Bucket index (0 to 15) - Byte 4: Asset index (typically bucket index + 1) - Byte 5-6: starting memory address in kilobytes (little endian) - Byte 7-8: memory size in kilobytes (little endian) - `0x01` - Response: - Header: `0x33 0x02` - Data: - Byte 14: `0x01` if the operation has been execute successfully ### Delete memory bucket - Request: - Header: `0x32 0x02` - Data: - Byte 3: Bucket index (0 to 15) - Response: - Header: `0x33 0x02` - Data: - Byte 14: `0x01` if the operation has been execute successfully ### Start data transfer - Request: - Header: `0x36 0x01` - Data: - Byte 3: Bucket index to write to (0 to 15) - Response: - Header: `0x37 0x01` - Data: - Byte 14: `0x01` if the operation has been execute successfully ### End data transfer - Request: - Header: `0x36 0x02` - Data: - Byte 3: Bucket index to write to (0 to 15) - Response: - Header: `0x37 0x02` - Data: - Byte 14: `0x01` if the operation has been execute successfully ### Cancel all data transfers - Request: - Header: `0x36 0x03` - Response: - Header: `0x37 0x03` ### Transfer data > the following data is sent to the bulk_transfer endpoint of the device - Transfer 1: - Header: `0x12 0xFA 0x01 0xE8 0xAB 0xCD 0xEF 0x98 0x76 0x54 0x32 0x10` - Data: - Byte 13: data type. Possible values are: - `0x01`: GIF data - `0x02`: Static RGBA data - `0x06`: Q565 Image data (only kraken 2023 with firmware >= v2.0.0) - `0x08`: Q565 Image data (only kraken 2023 elit with firmware > v2.0.0) - Transfer 2: - Data: The binary data of the image you want to send to the device././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/docs/developer/protocol/vengeance_rgb.md0000644000175000017500000000670314677373312022546 0ustar00jonasjonas# Corsair Vengeance RGB DDR4 UDIMMs Unbuffered DDR4 modules with a 4 Kbit SPD EEPROM, a temperature sensor and non-addressable RGB LEDs. The SPD EEPROM does *not* advertise the presence of the temperature sensor. These are I²C devices, connected to the host's SMBus. Each memory module module exposes three I²C devices, using a 4-bit Device Type Identifier Code (DTIC) and a 3-bit Select Address (SA) to generate each I²C Bus Slave Address. The Select Address is set by the host through the dedicated SA0, SA1 and SA2 pins on the DDR4 288-pin connector, and is shared by all I²C devices on the module. Because CorsairLink and iCue acquire kernel locks on the relevant I²C devices, capturing the traffic from/to those devices with software tools is severely limited. Fortunately, connecting a logic analyzer to the SCL and SDA pins of a memory slot (directly or through the use of some dummy module) is a cheap and effective alternative. ## Device 0x50 | SA: SPD EEPROM EE1004-compatible SPD EEPROM. See [JEDEC 21-C 4.1.6] and [JEDEC 21-C 4.1.2.L-5]. ## Device 0x18 | SA: temperature sensor Appears to support the same registers and features as in a TSE10004 SPD EEPROM with temperature sensor, *except for I²C block reads.* Instead, the registers should be read as words, with the caveat that the temperature sensor register values are supposed to be read in big endianess (MSB first, then LSB), but SMBus Read Word Data assumes the data is returned in little endianess (LSB first, then MSB). For the register map, see [JEDEC 21-C 4.1.6]. Note: the SPD EEPROM does not advertise the presence of the temperature sensor. ## Device 0x58 | SA: RGB controller Register map: | Register | Size in bytes | Purpose | | :-- | :-: | :-- | | `0xa4` | 1 | Timing parameter 1 | | `0xa5` | 1 | Timing parameter 2 | | `0xa6` | 1 | Lighting mode | | `0xa7` | 1 | Number of colors | | `0xb0–0xc4` | 1 | Red, green and blue components (up to 7 colors) | Lighting modes: | Value | Animation | | :-- | :-- | | `0x00` | Static or breathing with a *single* color | | `0x01` | Fading with 2–7 colors | | `0x02` | Breathing with 2–7 colors | Timing parameter 1 (TP1): influences the total time in the transition states: increasing *and* decreasing brightness, or fading through from one color to the next. The valid range for animations appears to be from 1 to at least 63, but a consistent conversion to seconds cannot be inferred; the special value 0 disables the animation. Timing parameter 2 (TP2): influences the total time in the stable minimum *and* maximum brightness states. The valid range appears to be from 0 to at least 63, but a consistent conversion to seconds cannot be inferred. Note: CorsairLink always sets both T2 and T1 equally. ## References ([JEDEC 21-C 4.1.6]) Definitions of the EE1004-v 4 Kbit Serial Presence Detect (SPD) EEPROM and TSE2004av 4 Kbit SPD EEPROM with Temperature Sensor (TS) for Memory Module Applications. [JEDEC 21-C 4.1.6]: https://www.jedec.org/standards-documents/docs/spd416 ([JEDEC 21-C 4.1.2.L-5]) Annex L: Serial Presence Detect (SPD) for DDR4 SDRAM Modules. [JEDEC 21-C 4.1.2.L-5]: https://www.jedec.org/standards-documents/docs/spd412l-5 [SMBus captures] in liquidctl/collected-device-data. [SMBus captures]: https://github.com/liquidctl/collected-device-data/tree/master/Corsair%20Vengeance%20RGB [OpenRGB wiki entry] for Corsair Vengeance RGB modules. [OpenRGB wiki entry]: https://gitlab.com/CalcProgrammer1/OpenRGB/-/wikis/Corsair-Vengeance-RGB ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735733058.0 liquidctl-1.15.0/docs/developer/release-checklist.md0000644000175000017500000000510614735227502021475 0ustar00jonasjonas# Release checklist ## Prepare system - [ ] Ensure publishing dependencies are installed: twine - [ ] Export helper environment variable: `export VERSION=` ## Prepare repository - [ ] Update last update date in the man page - [ ] Update the CHANGELOG - [ ] Review and update MRLVs in the table of supported devices (merging lines when appropriate) - [ ] Set the version for "new/changed in git" notes - [ ] Update version in pip install liquidctl==version examples - [ ] Regenerate the udev rules: `(cd extra/linux && python generate-uaccess-udev-rules.py > 71-liquidctl.rules)` - [ ] Commit: `git commit -m "release: prepare for v$VERSION"` ## Test locally - [ ] Run unit and doc tests: `python -m pytest` Then, optionally, install locally and: - [ ] Run my personal setup scripts: `liquidcfg && liquiddyncfg` - [ ] Test yoda.py: `extra/yoda.py --match kraken control pump with '(20,50),(50,100)' on coretemp.package_id_0 and fan with '(20,25),(34,100)' on _internal.liquid --verbose` - [ ] Test liquiddump.py: `extra/liquiddump.py | jq -c .` - [ ] Test krakenduty-poc.py: `extra/krakenduty-poc.py train && extra/krakenduty-poc.py status` ## Test in CI - [ ] Push HEAD: `git push origin HEAD` - [ ] Check all CI job statuses ## Build source distribution and wheel - [ ] Stash any subsequent changes (e.g. to this file) - [ ] Tag HEAD with changelog and PGP signature: `git tag -as "v$VERSION"` - [ ] Build the source distribution and wheel: `python -m build` - [ ] Check that all necessary files are in the `dist/liquidctl-$VERSION.tar.gz` sdist - [ ] Check the contents of the `dist/liquidctl-$VERSION-py3-none-any.whl` wheel - [ ] Sign both sdist and wheel: `gpg --detach-sign -a "dist/liquidctl-$VERSION.tar.gz"` `gpg --detach-sign -a "dist/liquidctl-$VERSION-py3-none-any.whl"` ## Release - [ ] Push vVERSION tag: `git push origin "v$VERSION"` - [ ] Upload sdist and wheel to PyPI: `twine upload dist/liquidctl-$VERSION{.tar.gz,-py3-none-any.whl}` - [ ] Generate SHA256 checksums for the release files: `b3sum dist/liquidctl-$VERSION{.tar.gz,-py3-none-any.whl} | tee "dist/liquidctl-$VERSION.B3SUMS"` - [ ] Upgrade the vVERSION tag on GitHub to a release (with sdist, wheel, and corresponding GPG signatures) - [ ] Update the HEAD changelog with the BLAKE3 checksums ## Post release - [ ] Merge the release branch into the main branch (if appropriate) - [ ] Update the HEAD release-checklist with this checklist - [ ] If necessary, update ArchLinux `liquidctl-git` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/style-guide.md0000644000175000017500000001607414562144457020354 0ustar00jonasjonas# Style guide This is not the code; this is just a tribute. ## General guidelines This section has yet to be written, but for a start... Read [PEP 8], then immediately watch Raymond Hettinger's [Beyond PEP 8] talk. Write code somewhere between those lines. In this repository, newer drivers are usually better examples than older ones. Experience with the domain helps to write better code. Try to keep lines around 100-ish columns wide. Be consistent within a given module; *try* to be consistent between similar modules. We are starting to adopt [psf/black], and new files should be formatted with it. They should also have the following top-level comment after the module docstring: ```py # uses the psf/black style ``` Old files should be maintained in their current style until a comprehensive conversion is performed; black has already been configured to ignore them. [PEP 8]: https://pep8.org/ [Beyond PEP 8]: https://www.youtube.com/watch?v=wf-BqAjZb8M [psf/black]: https://github.com/psf/black [#321]: https://github.com/liquidctl/liquidctl/issues/321 ### Nits for old files **For new files, use the [black code style], with 100-column lines.** For *old* files: - indent with 4 spaces; - prefer to continue lines vertically, aligned with the opening delimiter; - prefer single quotes for string literals, unless double quotes will avoid some escaping; - use f-strings whenever applicable, except for `logging` messages (more about those bellow) which wont necessarily be shown; - use lowercase hexadecimal literals. [black code style]: https://black.readthedocs.io/en/stable/the_black_code_style/index.html ### Grouping and sorting of import statements In normal modules, import statements should be grouped into standard library modules, modules from third-party libraries, and local modules. In test modules, an additional group for test scaffolding modules (inclunding `pytest` and `_testutils`) should come before the standard library modules. Within each group, `import` statements should come before `from <...> import` ones. After that, they should be sorted ascending order. ## Dependencies When adding new dependencies, their benefit must be carefully weighted. Additionally, the new dependency should: - have a small number of transitive dependencies; - support Linux: * be available on ArchLinux; * be available on Fedora; * be available on Debian; * be expected to be packaged in other distributions; - support Windows: * be easily installed (by being pure Python or have binary wheels); - support Mac OS; - be compatible for use in a GPL v3 program (including being distributed under the GPL in the case of the Windows executable). ## Driver behavior ### Fixing or raising on user errors Drivers should fix as most user errors as possible, without making actual choices for the user. For example, it is fine to clamp an integer, like a fan duty cycle, to its minimum and maximum allowed values, since values bellow or above the possible range can safely be interpreted as requests for, respectively, the "minimum" and "maximum" values themselves. On the other hand, if a device has two channels, and the user specifies a third one, the driver should raise a suitable error (`ValueError`, in this case) so the user can check the documentation and decide for *themselves* which channel to use. ### Case sensitivity of string constants Channel and mode names, as well as some other values, must be specified as strings. While this is more explicit then using magic numbers, it introduces the problem of whether comparisons are case sensitive. In fact, from the point of view of a CLI user, the comparisons are case *insensitive.* As of this version of the style guide, ensuring case-insensitive comparisons is a shared responsibility of both CLI and drivers; but whenever possible this should be kept in the CLI code, making the drivers simpler and the behavior more consistent. On the other hand, the liquidctl APIs are **not** specified to ignore casing issues; when that happens it is usually just to keep the implementation simpler. ## Writing messages Liquidctl generates a substantial amount of human readable messages, either intended for direct consumption by CLI users, or for _a posteriori_ debugging by developers. While discussing the format of these messages deviates from the usual tab × spaces wars we have learned to love [citation needed], it is very important to keep the messages clear, objective and consistent. ### Errors Raise exceptions, preferably `liquidctl.error.*` or Python's [built-in exceptions]. Error messages should start with a lowercase letter. [built-in exceptions]: https://docs.python.org/3/library/exceptions.html ### Warnings [warnings]: #warnings Warnings are used to convey information that are deemed always relevant. They should be used to alert of unexpected conditions, degraded states, and other high-importance issues. By default the CLI outputs these to `stderr` for all users. Since the messages are visible to non-developers, the should follow the simplified guidelines bellow: - start with a lowercase letter; - string values that the user is expected to type, or that absolutely need to delimited: use single quotes; - other values: no quotes or backticks; - command line commands or options: no quotes or backticks. Deprecated guidelines for warnings: - outputting values as `key=value` pairs. ### Info Information messages are normally hidden, but can be enabled with the `--verbose` or `--debug` flags. They should be used to display complementary information that may interest any user, like additional `status` items or computed `temp × rpm` curves. Since these messages are also targeted to non-developers, they should follow the same simplified guidelines used for [warnings]. ### Debug Debug messages are only intended for developers or users interested in dealing with the internals of the program. In the CLI these are normally disabled and require the `--debug` flag. Since they are intended for internal use, they follow relaxed guidelines: - start with a lowercase letter; - use `key=value`, `` `expression` ``, `'string'`, or `value`, whichever is clearer; ### A quick refresher on `logging` Get a logger at the start of the module. ```py import logging ... _LOGGER = logging.getLogger(__name__) ``` Prefer old-style %-formatting for logging messages, since this is evaluated lazyly by `logging`, and the message will only be formated if the logging level is enabled. ```py _LOGGER.warning('value %d, expected %d', current, expected) ``` _(While `%d` and `%i` are equivalent, and both print integers, prefer the former over the latter)._ _(The rest of the time `f-strings` are preferred, following the `PEP 498` guideline)._ When writing informational or debug messages, pay attention to the cost of computing each value. A classic example is hex formatting some bytes, which can be expensive; this case can be solved by using `liquidctl.util.LazyHexRepr`, and other similar wrapper types can be constructed for other scenarios. ```py from liquidctl.util import LazyHexRepr ... _LOGGER.debug('buffer: %r', LazyHexRepr(some_bytes)) ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/developer/techniques-for-analyzing-usb-protocols.md0000644000175000017500000002774414562144457025666 0ustar00jonasjonas# Techniques for analyzing USB protocols _Originally posted as a [comment in issue #142](https://github.com/liquidctl/liquidctl/issues/142#issuecomment-650568291)._ ## USB transfers At a basic level you can view USB traffic as a collection of transfers. Transfers can be of a few different types, but for the most part we're only interested in control transfers, interrupt transfers and, very occasionally, bulk transfers. I'll skip over the purposes of each type since we'll merely use whichever ones the protocol mandates. But our devices at not necessarily always manipulated as USB devices. In fact, it's rather common that we need to work on a layer further up the abstraction chain, on a Human Interface Device (HID). USB HIDs are a special type of USB device specified by a corresponding "interface class".¹ Working with HID protocols is almost identical to working with other classes of USB devices: they use control transfers and interrupt transfers, and we capture them with Wireshark. The distinction does matter in a few places, most notability around the "report ID", but I'll get to that later... ## Wireshark, part one So, you have captured some USB traffic to your device with Wireshark. How do you approach that data? First, I would filter the packets to only those coming from or being sent to the device you're interested at. If you know the device address in the bus you can filter with `usb.device_address ==

`, and in Linux it's easy to find the device address with `lsusb`. I usually save these results into a new file, and only use it from that point forward. Another way to find the device address, which can be useful when dealing with old captures or captures made in another OS or machine, it to look in the various `GET DESCRIPTOR DEVICE` responses for the one that matches the `idVendor` you're interested in. You should know that the address on the bus can change, from OS to OS, from boot to boot or if the device is reconnected. Next, I would add a few custom columns: `usb.data_fragment` for data sent in control transfers, and `usb.capdata` for data exchanged in the other types of transfers. _Update: the latest versions of Wireshark have improved HID decoding capabilities, and HID data may also appear in `usbhid.data`._ Wireshark actually works one level of abstraction bellow what I called a transfer, with USB request blocks (URBs), so there's a lot of uninteresting entries in the captured data. You can reduce this by ignoring URBs without any data_fragment or capdata, since only in a few cases these are useful in understanding the protocol. Most of the protocol we need to implement lies in these two Wireshark fields. In interrupt or bulk transfers all data is this `capdata`, the rest is just USB metadata. Control transfers do need to be inspected more carefully, but their use outside of HIDs is very rare (Asetek 690LC coolers being one example). With HIDs is common to see control transfers, particularly if the device has no OUT endpoint (an endpoint is something you write to xor read from). In this case then all writes will be sent as control transfers (instead of interrupt transfers), usually as `SET_REPORT` requests; and this is also where the report numbers I mentioned before become important. HIDs don't just send raw or opaque packets of bytes. They have the concept of a report, which is supposed to structure the data and make the device capabilities self describing. A HID can support a single unnumbered report, or one or more numbered reports. Knowing the correct report ID (or its absence) is part of understanding the protocol, and is especially important when `SET_REPORT` transfers are involved. ![wireshark-hid-set-report2](https://user-images.githubusercontent.com/1832496/85924521-3fd19c80-b869-11ea-9bd1-43f5db6fe6ce.png) The report ID can be decoded from `wValue` argument of the transfer: the most significant byte (MSB) is the report type (0x01 for input, 0x02 for output, 0x3 for feature) and the LSB is the report ID, or zero if the device doesn't use report IDs. Both values are import when implementing the protocol. Some protocols also read data from HIDs with a `GET_REPORT` request, instead of directly from the incoming endpoint. In those cases the report type and ID will also be in `wValue`. ## Groups of transfers Decoding the protocol involves figuring out, for each action of interest, which transfers are involved, what parameters are sent in each transfer, and how they are encoded. The devices we're working with have two very separate sets of actions: - reading data (usually fan/pump/temperature monitoring, but sometimes also less variable device information such as firmware version or accessories) - writing new device configuration (fan or pump speeds, lighting animations and colors, etc.) Sometimes reading monitoring data requires a previous write or the use of an explicit `GET_REPORT` request. Besides obvious `GET_REPORT` requests, you may spot other protocol-specific write transfers that appear to serve no purpose other than to request data. Other times reads will simply timeout if not preceded by a write, which you'll only discover with some experimentation. Mapping between actions and transfers is usually simple, based on what fields you can identify in the data of each transfer. For example, the presence of color parameters is almost always very easy to spot, and clearly indicates some type of color-related configuration. ## Common fields and field encodings ### Fan and/or pump speed (read) Usually in u16le or u16be (16-bit unsigned integer of either endianess). In the case of power suplies, could also be encoded in LINEAR11/LINEAR16, as defined by the PMBus specification ([`liquidctl.pmbus.linear_to_float`]). [`liquidctl.pmbus.linear_to_float`]: https://github.com/liquidctl/liquidctl/blob/d1b8d2424948c564e218e2f0cf5ffb86f21b1445/liquidctl/pmbus.py#L104 ### Fan and/or pump duty values (read/write) Usually a single byte, either as a fraction of 100 (0–100) or 255 (0–255). ### Temperature (read) Usually some custom type of fixed point decimal, taking two bytes. One of the bytes is almost always `floor(temperature)`; the other is used for the remainder, encoded as a fraction of 10 (0–10) or 255 (0–255). "Endianess" varies. ### Temperature (write) When temperatures are sent to the device, either as part of a speed profile or to trigger visual alerts, they are almost always simple single-byte integer values (context will dictate whether should use round, floor or ceil). ### LED colors (write) Almost universally sent as 24-bit RGB. However, endianess varies, and some devices may also use custom orderings. In summary, any order of 16-bit red, green and blue values for each color. ### CRC checksums (read/write) Some devices end all messages (received and sent) with checksum (sometimes known as a PEC byte). They usually, but not always, follow the SMBus specification and use the 8-bit `x⁸ + x² + x¹ + x⁰` polynomial. The desired CRC function can be constructed with `liquidctl.util.mkCrcFun` (which efficiently wraps the crcmod package); for an example, see `liquidctl.pmbus.compute_pec`. ### Action type (read/write) This indicates to the cooloer how it should interpret and act on the rest of the message. Usually a "command" byte, but may also be the report ID. ### Sequence numbers (read/write) Sometimes received and sent on every transfer. May also be (shifted and) OR'ed with a "command" byte or other indicator. You can spot this in byte offsets where the value follows a pattern that repeats every n transfers. May or may not be required for correct operation of the device. ## Techniques for identifying fields Spotting fields in transfers is critical. They tell you not only where to place things, but also what each message is used for. The first technique is simply to watch the transfers and compare, in real time, with values shown/entered in the software you're using to interact with the cooler. This works well if the protocol is simple, and if the rate of transfers is small. In other scenarios it doesn't work that well, but may still be required to decode specific parts of the protocol. A related technique, particurlary when you think you're already close to understanding the protocol, is to try to read and/or write some packets yourself (make sure your writes at least resemble the real packets before trying this; sending arbitrary data to the device is a bad idea). Next you can start to analyze the data in batches, that is, looking at a set of transactions at once. In many cases some fields will become immediately obvious this way: you can see that there's a field, and the value will tell you what it's related to. Taking things one step further, you can compute some basic statistics for every byte offset: min, max, average, median, .... Even if there's a lot of noise (either from too many messages, too many unknown fields, or both) this will usually make the interesting fields more visible. If you see a byte more or less uniformely distributed in the 0–255 range, it's likely a LSB of a two-byte field; if you see bytes with some variance but that are restricted to a specific range, just try to decode them as a fraction of 10 or 255; finally, the second byte of a two-byte field is usually (but not always) just before or just after the first. A similar idea can be applied to bitfields: you count how many times the bit in each position changes, and this will usually tell you where the LSBs are (they are the ones that change the most). This technique could also be applied to the entire transfer, but this isn't really necessary in the protocols we're dealing with. While doing all/any of the above, you still want to pay some attention to the parts of the message you don't understand. They can be "random noise" (either in the true sense or not), but there may be important things in there as well. If you spot patterns, particularly those _not_ of a simple constant, it's worth trying to make sense of what that value could represent. It could be a required aspect of the protocol, or an additional monitoring variable not shown in the official GUI (for example: voltage and current for a fan channel). In the end, it comes down to spotting patterns, and besides knowing more or less what you're looking for and how it's usually encoded, you get better with experience. But it's not very hard, it may just take more than a couple of tries in the really tricky cases. ## Wireshark, part two, and other tools _this section is incomplete; there are many tools, and I'm not particularly good with any of them_ You should know you can export the Wireshark capture to JSON. In fact, it's the only way I know of of getting `usb.capdata` and `usb.data_fragment` off of Wireshark into a standard data manipulation format _without truncation._ This can be done from within the Wireshark UI, or with `tshark`. tshark -r .pcapng -T json > .json You can preprocess this data with `jq`, and then further manipulate it in any tool you're familiar with. Heck, sometimes I even use spreadsheets (not very elegant, I know). You also easily write a custom script to do some analyses or test hypothesis on these JSON captures. For an example, check the [script I used when working the Platinum coolers]. [script I used when working the Platinum coolers]: https://github.com/liquidctl/collected-device-data/blob/master/Corsair%20H115i%20RGB%20Platinum/analyze.py Alternatively you can use: tshark -r .pcapng -2 -e "frame.number" -e "usb.data_fragment" -e "usb.capdata" -Tfields > dump.txt To pull out only the frame number, data_fragment, and capdata fields and output them in to a txt file. The frame number field is really usefull for if you have a seperate text description file that had a description of what commands got set to the device and arround what frame number the message corresponds to. (you can write down the number of one of the bottom messages shown in the wireshark console while the command is being sent) ## Notes _¹ There are Bluetooth HIDs, but these obviously aren't very relevant here._ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/gigabyte-rgb-fusion2-guide.md0000644000175000017500000001045714562144457021154 0ustar00jonasjonas# Gigabyte RGB Fusion 2.0 lighting controllers _Driver API and source code available in [`liquidctl.driver.rgb_fusion2`](../liquidctl/driver/rgb_fusion2.py)._ RGB Fusion 2.0 is a lighting system that supports 12V non-addressable RGB and 5V addressable ARGB lighting accessories, alongside RGB/ARGB memory modules and other elements on the motherboard itself. It is built into motherboards that contain the RGB Fusion 2.0 logo, typically from Gigabyte. These motherboards use one of many possible ITE Tech controller chips, which are connected to the host via SMBus or USB, depending on the motherboard/chip model. A couple of USB controllers are currently supported: - ITE 5702: found in Gigabyte Z490 Vision D - ITE 8297: found in Gigabyte X570 Aorus Elite ## Initialization RGB Fusion 2.0 controllers must be initialized after the system boots or resumes from a suspended state. ``` # liquidctl initialize Gigabyte RGB Fusion 2.0 5702 Controller ├── Hardware name IT5702-GIGABYTE V1.0.10.0 └── Firmware version 1.0.10.0 ``` ## Lighting The controllers have built-in support for six color modes: `off`, `fixed`, `pulse`, `flash`, `double-flash` and `color-cycle`. The `color-cycle` mode fades between all color hues, but the extra [`extra/contrib/fusion_rgb_cycle.py`] script can be used to fade between specific colors. As much as we prefer to use descriptive channel names, currently it is not practical to do so, since the correspondence between the hardware channels and the corresponding features on the motherboard is not stable. Hence, lighting channels are given generic names: `led1`, `led2`, etc.; at this time, eight are defined. In addition to these, it is also possible to use the `sync` pseudo-channel to apply a setting to all lighting channels. ``` # liquidctl set sync color off # liquidctl set led1 color fixed 350017 # liquidctl set led2 color pulse ff2608 # liquidctl set led3 color flash 350017 # liquidctl set led4 color double-flash 350017 # liquidctl set led5 color color-cycle --speed slower ``` For color modes `pulse`, `flash`, `double-flash` and `color-cycle`, the animation speed is governed by the optional `--speed` parameter, with one of six possible values: `slowest`, `slower`, `normal` (the default), `faster`, `fastest` or `ludicrous`. Some more elaborate color/animation schemes supported by the motherboard on the addressable headers are not currently supported. ## Correspondence between lighting channels and physical locations Each user may need to create a table that associates generic channel names to specific areas or headers on their motherboard. For example, a map for the Gigabyte Z490 Vision D might look like: - led1: the LED next to the IO panel; - led2: one of two 12V RGB headers; - led3: the LED on the PCH chip ("Designare" on Vision D); - led4: an array of LEDs behind the PCI slots on *back side* of motherboard; - led5: second 12V RGB header; - led6: one of two 5V addressable RGB headers; - led7: second 5V addressable RGB header; - led8: not in use. ## More on resuming from sleep states On wake-from-sleep, the ITE controller will be reset and all color modes will revert to fixed blue. To work around this, the methods used to [automate the configuration at boot time] should be adapted to also handle resuming from sleep states. On macOS it is also possible to use the _sleepwatcher_ utility, installed via Homebrew, along with a script to run on wake that issues the necessary liquidctl commands and restores desired lighting effects. [automate the configuration at boot time]: ../README.md#automation-and-running-at-boot ## Fading between colors The [`extra/contrib/fusion_rgb_cycle.py`] script can be used to have one of these controllers cycle through a list of colors of the user's choice. It uses the `fixed` mode but updates the controller continuously. It can be started with: ``` # extra/contrib/fusion_rgb_cycle.py 350017 ff2608 ``` If more than two colors are specified, they will be cycled in turn. The [color space] for cycling can be specified with `--space `, channels can be specified with `--channel ` and the time in seconds for each color transition can be set with, eg `--speed `. [`extra/contrib/fusion_rgb_cycle.py`]: ../extra/contrib/fusion_rgb_cycle.py [color space]: https://facelessuser.github.io/coloraide/colors/ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/kraken-x2-m2-guide.md0000644000175000017500000001667114562144457017350 0ustar00jonasjonas# Third-generation NZXT liquid coolers _Driver API and source code available in [`liquidctl.driver.kraken2`](../liquidctl/driver/kraken2.py)._ ## NZXT Kraken X42, X52, X62, X72 The Kraken X42, X52, X62 and X72 are the third generation of Kraken X liquid coolers by NZXT. These devices are manufactured by Asetek and house fifth generation Asetek pumps, plus secondary PCBs specially designed by NZXT for enhanced control and lighting. They incorporate customizable fan and pump speed control with PWM, a liquid temperature probe in the block and addressable RGB lighting. The coolers are powered directly from the power supply unit. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The cooler also reports fan and pump speed and liquid temperature via USB; pump speed can also be sent to the motherboard (or other device) via the sense pin of a standard fan connector. All capabilities available at the hardware level are supported, but other features offered by CAM, like presets based on CPU or GPU temperatures, have not been implemented. Pump and fan control based on liquid temperature is supported on units running firmware versions 4 or above. Monitoring and/or configuring the coolers is not possible with CAM running, otherwise you'll get errors such as `OSError('read error')`. ## NZXT Kraken M22 This driver also supports the NZXT Kraken M22. However, this device has no pump or fan control, nor reports liquid temperatures. ## Initialization [Initialization]: #initialization _Changed in 1.9.0: the firmware version is now reported after initialization._
_Changed in 1.10.0: modern firmware versions are now reported in simplified form, to match CAM._
All devices should be (re)initialized after the system boots or resumes from a suspended state, or if there have been hardware changes. ``` # liquidctl initialize NZXT Kraken X (X42, X52, X62 or X72) └── Firmware version 6.2 ``` With this generation of Kraken X coolers, it is especially important that (re)initialization happens before fan or pump speeds are adjusted. The device should also be reconfigured, as previous settings may have been totally or partially cleared while device was off or by the `initialize` command itself. ## Monitoring _Changed in 1.9.0: the firmware version is no longer reported (see [Initialization])._
The cooler can report the fan and pump speed, as well as the liquid temperature. ``` # liquidctl status NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 29.9 °C ├── Fan speed 853 rpm └── Pump speed 1948 rpm ``` ## Fan and pump speeds First, some important notes... *You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.* *Additionally, the liquid temperature should never reach 60°C, as at that point the pump and tubes might fail or quickly degrade. You must monitor this during your tests and make any necessary adjustments. As a safety measure, fan and pump speeds will forcibly be programmed to 100% for liquid temperatures of 60°C and above.* *You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.* With those out of the way, each channel can be independently configured to a fixed duty value or with a profile dependent on the liquid temperature. Fixed speeds can be set by specifying the desired channel – `fan` or `pump` – and duty. ``` # liquidctl set pump speed 90 ``` | Channel | Minimum duty | Maximum duty | | --- | --- | --- | | fan | 25% | 100% | | pump | 50% | 100% | *Another important note: pump speeds between 50% and 60% are not currently exposed in CAM. Presumably, there might be some scenarios when these lower speeds are not suitable.* For profiles, one or more temperature–duty pairs must be supplied. liquidctl will normalize and optimize this profile before pushing it to the Kraken. Adding `--verbose` will trace the final profile that is being applied. ``` # liquidctl set fan speed 20 30 30 50 34 80 40 90 50 100 ``` ## RGB lighting For lighting, the user can control a total of nine LEDs: one behind the NZXT logo and eight forming the ring that surrounds it. These are separated into two channels, independently accessed through `logo` and `ring`, or synchronized with `sync`. ``` # liquidctl set sync color fixed af5a2f # liquidctl set ring color fading 350017 ff2608 # liquidctl set logo color pulse ffffff # liquidctl set ring color marquee-5 2f6017 --direction backward --speed slower ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | `ring` | `logo` | `sync` | Mode | Colors | Notes | | --- | --- | --- | --- | --- | --- | | ✓ | ✓ | ✓ | `off` | None | | ✓ | ✓ | ✓ | `fixed` | One | | ✓ | ✓ | ✓ | `super-fixed` | Up to 9 (logo + each ring LED) | | ✓ | ✓ | ✓ | `fading` | Between 2 and 8, one for each step | | ✓ | ✓ | ✓ | `spectrum-wave` | None | | ✓ | | | `super-wave` | Up to 8 | | ✓ | | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | ✓ | | | `covering-marquee` | Up to 8, one for each step | | ✓ | | | `alternating` | Two | | ✓ | | | `moving-alternating` | Two | | ✓ | ✓ | ✓ | `breathing` | Up to 8, one for each step | | ✓ | ✓ | ✓ | `super-breathing` | Up to 9 (logo + each ring LED) | Only one step | | ✓ | ✓ | ✓ | `pulse` | Up to 8, one for each pulse | | ✓ | | | `tai-chi` | Two | | ✓ | | | `water-cooler` | None | | ✓ | | | `loading` | One | | ✓ | | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backward compatibility. | `ring` | `logo` | `sync` | Mode | Colors | Notes | | --- | --- | --- | --- | --- | --- | | ✓ | ✓ | ✓ | `backwards-spectrum-wave` | None | | ✓ | | | `backwards-super-wave` | Up to 8 | | ✓ | | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | ✓ | | | `covering-backwards-marquee` | Up to 8, one for each step | | ✓ | | | `backwards-moving-alternating` | Two | ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
Kraken X2 devices are supported by the mainline Linux kernel with its [`nzxt-kraken2`] driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`nzxt-kraken2`]: https://www.kernel.org/doc/html/latest/hwmon/nzxt-kraken2.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739674900.0 liquidctl-1.15.0/docs/kraken-x3-z3-guide.md0000644000175000017500000002214014754252424017350 0ustar00jonasjonas# Fourth-generation NZXT liquid coolers _Driver API and source code available in [`liquidctl.driver.kraken3`](../liquidctl/driver/kraken3.py)._ The fourth-generation of NZXT Kraken coolers is composed by X models—featuring the familiar infinity mirror—and Z models—replacing the infinity mirror with an LCD screen. Both X and Z models house seventh-generation Asetek pump designs, plus secondary PCBs from NZXT for enhanced control and visual customization. The coolers are powered directly from the power supply unit. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The coolers also report relevant data via USB, including pump and/or fan speeds and liquid temperature. The pump speed can be sent to the motherboard (or other device) via the sense pin of a standard fan connector. Monitoring and/or configuring the coolers is not possible with CAM running, otherwise you'll get errors such as `OSError('read error')`. ## NZXT Kraken X53, X63, X73 The X models incorporate customizable pump speed control, a liquid temperature probe in the block and addressable RGB lighting. In comparison with the previous generation of X42/X52/X62/X72 coolers, fan control is no longer provided. All capabilities available at the hardware level are supported, but other features offered by CAM, like presets based on CPU or GPU temperatures, are not part of the scope of the liquidctl CLI. ## NZXT Kraken Z53, Z63, Z73 The most notable difference between Kraken X and Kraken Z models is the replacement of the infinity mirror by a LCD screen. In addition to this, Kraken Z coolers restore the embedded fan controller that is missing from the current Kraken X models. ## NZXT Kraken 2023 Standard, Elite Kraken 2023 AIOs use the same pump and as their Z3 predecessor but the integrated led controller has been removed. The LCD resolution is 240x240 for the standard version and 640x640 for the elite one. ## NZXT Kraken 2024 Elite RGB The functionality of the 2024 RGB AIO is identical to the 2023 model, retaining the 640x640 LCD. There's now a light ring on the pump housing. As of the 1.2.1 firmware, it's still able to use GIF and static modes. ## Initialization Devices must be initialized being read or written to. This is necessary after powering on from Mechanical Off, or if there has been hardware changes. Only then monitoring, proper fan control and all lighting effects will be available. The firmware version and all connected LED accessories are reported during the device initialization. ``` # liquidctl initialize NZXT Kraken X (X53, X63 or X73) ├── Firmware version 1.8.0 ├── LED accessory 1 HUE 2 LED Strip 300 mm ├── LED accessory 1 AER RGB 2 140 mm ├── LED accessory 2 AER RGB 2 140 mm ├── Pump Logo LEDs detected └── Pump Ring LEDs detected ``` ## Monitoring The cooler can report the pump speed and liquid temperature. ``` # liquidctl status NZXT Kraken X (X53, X63 or X73) ├── Liquid temperature 24.1 °C ├── Pump speed 1869 rpm └── Pump duty 60 % ``` ## Fan and pump speeds First, some important notes... *You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.* *The X models do not provide a way to control your fan speeds. You must set those fan curves wherever you plugged your fans in (e.g. motherboard).* *Additionally, the liquid temperature should never reach 60°C, as at that point the pump and tubes might fail or quickly degrade. You must monitor this during your tests and make any necessary adjustments. As a safety measure, pump speed will forcibly be programmed to 100% for liquid temperatures of 60°C and above.* *You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.* With those out of the way, the pump speed can be configured to a fixed duty value or with a profile dependent on the liquid temperature. Fixed speeds can be set by specifying the desired channel and duty value. ``` # liquidctl set pump speed 90 ``` | Channel | Minimum duty | Maximum duty | X models | Z models | | --- | -- | --- | :---: | :---: | | `pump` | 20% | 100% | ✓ | ✓ | | `fan` | 0% | 100% | | ✓ | For profiles, one or more temperature–duty pairs are supplied instead of single value. ``` # liquidctl set pump speed 20 30 30 50 34 80 40 90 50 100 ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) ``` liquidctl will normalize and optimize this profile before pushing it to the Kraken. Adding `--verbose` will trace the final profile that is being applied. _New in 1.14.0._
Adds support for NZXT Kraken 2023 Standard, Elite Adds support for NZXT Kraken 2024 Elite RGB ## RGB lighting with LEDs One or more LED channels are provided, depending on the model. | Channel | Type | LED count | X models | Z models | | --- | --- | --- | :---: | :---: | | `external` | HUE 2/HUE+ accessories | up to 40 | ✓ | ✓ | | `ring` | Infinity mirror: ring | 8 | ✓ | | | `logo` | Infinity mirror: logo | 1 | ✓ | | | `sync` | Synchronize all channels | up to 40 | ✓ | | Color modes can be set independently for each lighting channel, but the specified color mode will then apply to all devices daisy chained on that channel. ``` # liquidctl set sync color fixed af5a2f # liquidctl set ring color fading 350017 ff2608 # liquidctl set logo color pulse ffffff # liquidctl set external color marquee-5 2f6017 --direction backward --speed slower ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Variable speed | | --- | --- | :---: | | `off` | None | | | `fixed` | One | | | `fading` | Between 1 and 8 | ✓ | | | `super-fixed` | Between 1 and 40 | | | `spectrum-wave` | None | ✓ | | `marquee-`, 3 ≤ length ≤ 6 | One | ✓ | | `covering-marquee` | Between 1 and 8 | ✓ | | `alternating-` | Between 1 and 2 | ✓ | | `moving-alternating-`, 3 ≤ length ≤ 6 | Between 1 and 2 | ✓ | | `pulse` | Between 1 and 8 | ✓ | | `breathing` | Between 1 and 8 | ✓ | | `super-breathing` | Between 1 and 40 | ✓ | | `candle` | One | | | `starry-night` | One | ✓ | | `rainbow-flow` | None | ✓ | | `super-rainbow` | None | ✓ | | `rainbow-pulse` | None | ✓ | | `loading` | One | | | `tai-chi` | Between 1 and 2 | ✓ | | `water-cooler` | Two | ✓ | | `wings` | One | ✓ | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backward compatibility. | Mode | Colors | Variable speed | | --- | --- | :---: | | `backwards-spectrum-wave` | None | ✓ | | `backwards-marquee-`, 3 ≤ length ≤ 6 | One | ✓ | | `covering-backwards-marquee` | Between 1 and 8 | ✓ | | `backwards-moving-alternating-`, 3 ≤ length ≤ 6 | Between 1 and 2 | ✓ | | `backwards-rainbow-flow` | None | ✓ | | `backwards-super-rainbow` | None | ✓ | | `backwards-rainbow-pulse` | None | ✓ | ## The LCD screen (only Z, 2023, 2024 models) _New in 1.11.0._
The LCD screen can be configured in a few different modes. ``` # liquidctl [options] set lcd screen liquid # liquidctl [options] set lcd screen brightness # liquidctl [options] set lcd screen orientation (0|90|180|270) # liquidctl [options] set lcd screen (static|gif) ``` Images and GiFs are automatically resized and rotated to match the device orientation. *Note that, on the 2023 models (Standard and Elite), the GIF screen mode is not currently supported on firmware versions 2.X (see [#631][`issue-631`]).* ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
_Changed in 1.12.0: expanded support for reading and writing through hwmon._
Kraken X3 and Z3 devices feature support by the [liquidtux] `nzxt-kraken3` driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [liquidtux]: https://github.com/liquidctl/liquidtux [`issue-631`]: https://github.com/liquidctl/liquidctl/issues/631 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7189336 liquidctl-1.15.0/docs/linux/0000755000175000017500000000000014776655074014752 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/docs/linux/making-systemd-units-wait-for-devices.md0000644000175000017500000000604214062723141024513 0ustar00jonasjonas# Making systemd units wait for devices When using a systemd service to configure devices at boot time, as suggested in [Automation and running at boot/Set up Linux using system](../../README.md#set-up-linux-using-systemd), it can sometimes happen that the hardware is not ready when the service tries to start. A blunt solution to this is to add a small delay, but a more robust alternative is to make the service unit depend on the corresponding hardware being available at the OS level. ## Systemd device units For this it is first necessary to set up systemd to create device units with known names. This is done with udev rules, specifically with `TAG+="systemd"` (to create a device unit) and a memorable `SYMLINK+=""` name. ``` # /etc/udev/rules.d/99-liquidctl-custom.rules # Example udev rules to create device units for some specific liquidctl devices. # create a dev-kraken.device for this third-generation Kraken X ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", ATTRS{serial}=="", SYMLINK+="kraken", TAG+="systemd" # create a dev-clc120.device for this EVGA CLC ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="2433", ATTRS{idProduct}=="b200", SYMLINK+="clc120", TAG+="systemd" ``` Setting a custom name with `SYMLINK` is optional: just with `TAG+="systemd"` alone a device unit will be made available as `dev-bus-usb--.device`, where the `` and the `` numbers can be found with the `lsusb` command. ## Setting the dependencies The new device units can then be added as dependencies to the service unit. ``` # /etc/systemd/system/liquidcfg.service [Unit] Description=AIO startup service Requires=dev-kraken.device Requires=dev-clc120.device After=dev-kraken.device After=dev-clc120.device ... ``` With these changes in place, and after rebooting the system, the service should begin to wait for the devices before trying to starting. Notes: - the `SUBSYSTEM` value must match how liquidctl connects to the device; devices listed by liquidctl as on a `hid` bus should use the value `hidraw`, while the remaining should use `usb` - when possible it is good to include the serial number in the match, to account for the possibility of multiple units of the same model - on the service unit file `Requires=` is used instead of `Wants=` because we want a [strong dependency](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#%5BUnit%5D%20Section%20Options) - rebooting the system is not technically necessary, but triggering the new udev rules without a reboot is outside the scope of this document - some devices may still not be able to response just after being discovered by udev, in which case a delay is really necessary ## Alternative approach An alternative approach is to have systemd start the configuration service when the device is found by udev, by making the device depend on the service: ``` ACTION=="add", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", ATTRS{serial}=="" ENV{SYSTEMD_WANTS}="liquidcfg.service" ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1735729471.0 liquidctl-1.15.0/docs/msi-mpg-coreliquid-guide.md0000644000175000017500000002566114735220477020737 0ustar00jonasjonas# MSI MPG Coreliquid AIO coolers _Driver API and source code available in [`liquidctl.driver.msi`](../liquidctl/driver/msi.py)._ _Currently, only the K360 model is experimentally supported as more testing and feedback is needed._ This driver is for the MSI MPG coreliquid series of AIO coolers, of which currently only the coreliquid K360 has been tested and verified to be working. The usage of speed profiles for this model requires external periodic updates of the current cpu temperature. As a result, to use variable fan speeds you must be careful to make sure that the current cpu temperature gets sent to the device. An example method to accomplish this is the `--use-device-controller` option in [`yoda.py`](../extra/yoda.py). Device configuration, including lighting and fan profiles persist until a new configuration is sent to the device, but saved settings are lost after power is lost (power state S5). Uploaded display images are saved onto the device, and so they can be accessed by their uploaded type and index even after loss of power, preventing the need to repeatedly upload the same files. The lcd display resets to the default animation when the system is suspended (S3). LED lighting is controlled via preset modes, which are sent once to the device as a configuration, after which the device then independently commands the LEDs until a new configuration is received. The K360 model includes an LCD screen capable of displaying various preset animations, hardware status, ASCII banners with a preset or custom background image, and preset or custom images. ## Initialization Controlling the device does not always require initialization, but some features, such as changing the display settings may not function before initialization. Initialization on its own will set default fan curves and LCD screen settings. ``` # liquidctl initialize --pump-mode smart MSI MPG Coreliquid K360 ├── Display firmware version 2 ├── APROM firmware version 0 ├── LDROM firmware version 256 ├── Serial number A02020123456 └── Pump mode smart ``` ## Monitoring The AIO unit is able to report fan speeds, pump speed, water block speed, and duties. ``` # liquidctl status MSI MPG Coreliquid K360 ├── Fan 1 speed 1546 rpm ├── Fan 1 duty 60 % ├── Fan 2 speed 1562 rpm ├── Fan 2 duty 60 % ├── Fan 3 speed 1530 rpm ├── Fan 3 duty 60 % ├── Water block speed 2400 rpm ├── Water block duty 50 % ├── Pump speed 2777 rpm ├── Pump duty 100 % ``` ## Fan and pump speeds First, some important notes... *You must carefully consider what pump and fan speeds to run. Heat output, case airflow, radiator size, installed fans and ambient temperature are some of the factors to take into account. Test your settings under different scenarios, and make sure that they are appropriate, correctly applied and persistent.* *The device has no internal temperature measurement to control the fan speeds, and simply running a liquidctl command to set a speed profile will not persistently provide this necessary data to the device. You can use [`yoda.py`](../extra/yoda.py) to communicate with the cooler, or create your own service to keep the device updated on the current temperature.* *You should also consider monitoring your hardware temperatures and setting alerts for overheating components or pump failures.* With those out of the way, the pump speed can be configured to a fixed duty value or with a profile dependent on a (temperature) signal that MUST be periodically sent to the device. Fixed speeds can be set by specifying the desired channel and duty value. ``` # liquidctl set pump speed 90 ``` | Channel | Minimum duty | Maximum duty | | --- | -- | --- | | `pump` | 60% | 100% | | radiator fans (`fans`, `fan1`, `fan2`, `fan3`) | 20% | 100% | | | `waterblock-fan` | 0% | 100% | | For profiles, one or more temperature–duty pairs are supplied instead of single value. ``` # liquidctl set pump speed 20 30 30 50 34 80 40 90 50 100 ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) ``` liquidctl will normalize and optimize this profile before pushing it to the device. Adding `--verbose` will trace the final profile that is being applied. The device also has preset pump/fan curves that can be applied independently for each channel with [`yoda.py`](../extra/yoda.py). Perhaps most notable is the "smart" mode, which enables fan-stop for two of the three radiator fans. Fan-stop is locked by the device for custom fan profiles, likely to prevent the liquid from overheating. The preset device profiles are: - Silent - Balance - Game - Default - Smart The preset, named modes are supported in the driver and they currently have experimental support in [`yoda.py`](../extra/yoda.py), support in the liquidctl cli is on the way. ## RGB lighting with LEDs LEDs on the device are always synced with the same effect, so the only supported channel argument is "sync". Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)). Each animation mode supports zero to two colors, and some animation modes include an additional "rainbow" mode. Lighting effect speed can be controlled with the `--speed` parameter, which can vary between 0 and 3. ``` # liquidctl set sync color clock aa00aa 00aaaa --speed 0 # liquidctl set sync color steady ffb6c1 ``` Some lighting modes are intended to react to the sounds currently playing on the system. These modes do not currently function as intended with this driver. | Mode | Colors | Rainbow option | Notes | | --- | --- | --- | --- | | `disable` | None | None | | | `steady` | One | No | | | `blink` | ? | ? | Not working as intended | | `breathing` | One | No | Yes | | `clock` | Two | Yes | | | `color pulse` | ? | ? | Not working as intended | | `color ring` | None | None | | | `color ring double flashing` | None | None | | | `color ring flashing` | None | None | | | `color shift` | None | None | Not working as intended | | `color wave` | Two | Yes | | | `corsair ique` | ? | ? | Unclear | | `disable2` | None | None | | | `double flashing` | One | Yes | | | `double meteor` | None | None | | | `energy` | None | None | | | `fan control` | ? | ? | Not working as intended | | `fire` | Two | No | | | `flashing` | One | Yes | | | `jazz` | ? | ? | Not working as intended | | `jrainbow` | ? | ? | Unclear | | `lava` | ? | ? | Not working as intended | | `lightning` | One | No | | | `marquee` | One | No | | | `meteor` | One | Yes | | | `movie` | ? | ? | Not working as intended | | `msi marquee` | One | Yes | | | `msi rainbow` | ? | ? | Not working as intended | | `planetary` | None | None | | | `play` | ? | ? | Not working as intended | | `pop` | ? | ? | Not working as intended | | `rainbow` | ? | ? | Very jittery and slow, rainbow wave is recommended instead | | `rainbow double flashing` | None | None | | | `rainbow flashing` | None | None | | | `rainbow wave` | None | None | | | `random` | None | None | | | `rap` | ? | ? | Not working as intended | | `stack` | One | Yes | | | `visor` | Two | Yes | | | `water drop` | One | Yes | | Support for the rainbow option is not yet exposed by the liquidctl cli, but it is included in the driver as the color_selection flag. ``` >>> from liquidctl.driver import find_liquidctl_devices >>> coreliquid_device = next(find_liquidctl_devices()) >>> coreliquid_device.connect() >>> coreliquid_device.set_colors("sync", "clock", [], color_selection=0) ``` ## The LCD screen The screen resolution is 320 x 240 px, and custom images uploaded with this driver are resized to fit this requirement. The screen orientation and brightness (0-100) can also be controlled. The only channel available for the K360 model is "lcd". Uploading and choosing which images or banner backgrounds to display is experimentally supported, sometimes an uploaded file may be viewable from a different index on the device than the one it was originally uploaded to. ``` # liquidctl set lcd screen settings '80;0' # liquidctl set lcd screen hardware 'cpu_temp;cpu_freq;fan_radiator' ``` ``` # liquidctl set lcd screen banner '1;4;A cool message in 62 characters!;/home/username/Pictures/pic.jpg' # liquidctl set lcd screen banner '0;1;Hello, MSI?' # liquidctl set lcd screen banner '1;4;I can also show a previously uploaded background!' ``` ``` # liquidctl set lcd screen disable ``` Maximum length of the displayed banner mesages is 62 ASCII characters. hardware status display functionality is limited, as the displayed data must be communicated to the device. This functionality is implemented in the driver, but currently its usage is limited to [`yoda.py`](../extra/yoda.py), which is gpu-unaware so the gpu_freq and gpu_usage parameters will not display correct information without custom update services. | mode name | action | options | | --- | --- | --- | | hardware | set the screen to display hardware info | up to 3 semicolon delimited keys from the available sensors | | image | set the screen to display a custom or preset image | \;\[;\] | | banner | set the screen to display a message with custom or preset image as background | \;\;\[;\] | clock | set the screen to display system time (requires control service to send the time to the device) | integer between 0 and 2 to specify the style of the clock display | | settings | set the screen brightness and orientation | \;\ | | disable | disables the lcd screen | | The index field of image and banner modes denotes which of the device-stored images to show on the display, or to which save slot to store an uploaded image in. Use type=0 to display one of the preset animations, and type=1 to upload or display a custom image. | Display orientation | value | | --- | --- | | Default (up) | 0 | | Right | 1 | | Down | 2 | | Left | 3 | | Displayed sensor data | Notes | | --- | --- | | cpu_freq | | | cpu_temp | this is the sensor value that controls set profile fan duties | | gpu_freq | Used by the manufacturer to display gpu memory frequency | | gpu_usage | | | fan_pump | | | fan_radiator | | | fan_cpumos | waterblock fan speed | ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/nvidia-guide.md0000644000175000017500000001753514562144457016504 0ustar00jonasjonas# NVIDIA graphics cards _Driver API and source code available in [`liquidctl.driver.nvidia`](../liquidctl/driver/nvidia.py)._ Support for these cards in only available on Linux. Other requirements must also be met: - `i2c-dev` kernel module has been loaded - r/w permissions to card-specific `/dev/i2c-*` devices - specific unsafe features have been opted in Jump to the appropriate section for a supported card: * _Series 10/Pascal:_ - [ASUS Strix GTX 1050 OC][asus-gtx-rtx] - [ASUS Strix GTX 1050 Ti OC][asus-gtx-rtx] - [ASUS Strix GTX 1060 6GB][asus-gtx-rtx] - [ASUS Strix GTX 1060 OC 6GB][asus-gtx-rtx] - [ASUS Strix GTX 1070 OC][asus-gtx-rtx] - [ASUS Strix GTX 1070 Ti Advanced][asus-gtx-rtx] - [ASUS Strix GTX 1070 Ti][asus-gtx-rtx] - [ASUS Strix GTX 1070][asus-gtx-rtx] - [ASUS Strix GTX 1080 Advanced][asus-gtx-rtx] - [ASUS Strix GTX 1080 OC][asus-gtx-rtx] - [ASUS Strix GTX 1080 Ti OC][asus-gtx-rtx] - [ASUS Strix GTX 1080 Ti][asus-gtx-rtx] - [ASUS Strix GTX 1080][asus-gtx-rtx] - [EVGA GTX 1070 FTW DT Gaming][evga-gp104] - [EVGA GTX 1070 FTW Hybrid][evga-gp104] - [EVGA GTX 1070 FTW][evga-gp104] - [EVGA GTX 1070 Ti FTW2][evga-gp104] - [EVGA GTX 1080 FTW][evga-gp104] * _Series 16/Turing:_ - [ASUS Strix GTX 1650 Super OC][asus-gtx-rtx] - [ASUS Strix GTX 1660 Super OC][asus-gtx-rtx] - [ASUS Strix GTX 1660 Ti OC][asus-gtx-rtx] * _Series 20/Turing:_ - [ASUS Strix RTX 2060 Evo OC][asus-gtx-rtx] - [ASUS Strix RTX 2060 Evo][asus-gtx-rtx] - [ASUS Strix RTX 2060 OC][asus-gtx-rtx] - [ASUS Strix RTX 2060 Super Advanced][asus-gtx-rtx] - [ASUS Strix RTX 2060 Super Evo Advanced][asus-gtx-rtx] - [ASUS Strix RTX 2060 Super OC][asus-gtx-rtx] - [ASUS Strix RTX 2060 Super][asus-gtx-rtx] - [ASUS Strix RTX 2070 Advanced][asus-gtx-rtx] - [ASUS Strix RTX 2070 OC][asus-gtx-rtx] - [ASUS Strix RTX 2070 Super Advanced][asus-gtx-rtx] - [ASUS Strix RTX 2070 Super OC][asus-gtx-rtx] - [ASUS Strix RTX 2070][asus-gtx-rtx] - [ASUS Strix RTX 2080 OC][asus-gtx-rtx] - [ASUS Strix RTX 2080 Super Advanced][asus-gtx-rtx] - [ASUS Strix RTX 2080 Super OC][asus-gtx-rtx] - [ASUS Strix RTX 2080 Ti OC][asus-gtx-rtx] - [ASUS Strix RTX 2080 Ti][asus-gtx-rtx] * _Series 30/Ampere:_ - [ASUS TUF RTX 3060 Ti OC][asus-gtx-rtx] * _[Inherent unsafeness of I²C]_ ## EVGA GTX 1070, 1070 Ti and 1080 [evga-gp104]: #evga-gtx-1070-1070-ti-and-1080 Only RGB lighting supported. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C] - `experimental_evga_gpu`: enable new/experimental devices ### Initialization Not required for this device. ### Retrieving the current RGB lighting mode and color In verbose mode `status` reports the current RGB lighting settings. ``` # liquidctl status --verbose --unsafe=smbus EVGA GTX 1080 FTW ├── Mode Fixed └── Color 2aff00 ``` ### Controlling the LED This GPU only has one led that can be set. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | ---------- | ----------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `breathing` | 1 | | `led` | `rainbow` | 0 | ``` # liquidctl set led color off --unsafe=smbus # liquidctl set led color rainbow --unsafe=smbus # liquidctl set led color fixed ff8000 --unsafe=smbus # liquidctl set led color breathing "hsv(90,85,70)" --unsafe=smbus ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ channel mode color unsafe features ``` The LED color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `--non-volatile`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. ``` # liquidctl set led color fixed 00ff00 --non-volatile --unsafe=smbus ``` ## ASUS Strix GTX and RTX [asus-gtx-rtx]: #asus-strix-gtx-and-rtx Only RGB lighting supported. Unsafe features: - `smbus`: see [Inherent unsafeness of I²C] - `experimental_asus_gpu`: enable new/experimental devices ### Initialization Not required for this device. ### Retrieving the current color mode and LED color In verbose mode `status` reports the current RGB lighting settings. ``` # liquidctl status --verbose --unsafe=smbus ASUS Strix RTX 2080 Ti OC ├── Mode Fixed └── Color ff0000 ``` ### Controlling the LED This GPU only has one led that can be set. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | Colors | | ---------- | ------------- | -----: | | `led` | `off` | 0 | | `led` | `fixed` | 1 | | `led` | `flash` | 1 | | `led` | `breathing` | 1 | | `led` | `rainbow` | 0 | ``` # liquidctl set led color off --unsafe=smbus # liquidctl set led color rainbow --unsafe=smbus # liquidctl set led color fixed ff8000 --unsafe=smbus # liquidctl set led color flash ff8000 --unsafe=smbus # liquidctl set led color breathing "hsv(90,85,70)" --unsafe=smbus ^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^ channel mode color unsafe features ``` The LED color can be specified using any of the [supported formats](../README.md#supported-color-specification-formats). The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `--non-volatile`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. ``` # liquidctl set led color fixed 00ff00 --non-volatile --unsafe=smbus ``` Note: The `off` mode is simply an alias for `fixed 000000`. ## Inherent unsafeness of I2C [Inherent unsafeness of I²C]: #inherent-unsafeness-of-i2c Reading and writing to I²C buses is inherently more risky than dealing with, for example, USB devices. On typical desktop and workstation systems many important chips are connected to these buses, and they may not tolerate writes or reads they do not expect. It is necessary to rely on certain devices being know to use a specific address, or being documented/specified to do so; but there is always some risk that another, unexpected, device is using that same address. On top of this, accessing I²C buses concurrently, from multiple threads or processes, may also result in undesirable or unpredictable behavior. Unsurprisingly, users or programs dealing with I²C devices have occasionally crashed systems and even bricked boards or peripherals. In some cases this is reversible, but not always. For all of these reasons liquidctl requires users to *opt into* accessing I²C and SMBus devices, both of which can be done by enabling the `smbus` unsafe feature. Other unsafe features may also be required for the use of specific devices, based on other *know* risks specific to a particular device. Note that a feature not being labeled unsafe, or a device not requiring the use of additional unsafe features, does in no way assure that it is safe. This is especially true when dealing with I²C/SMBus devices. Finally, liquidctl may list some I²C/SMBus devices even if `smbus` has not been enabled, but only if it is able to discover them without communicating with the bus or the devices. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/nzxt-e-series-psu-guide.md0000644000175000017500000000322314562144457020541 0ustar00jonasjonas# NZXT E-series PSUs _Driver API and source code available in [`liquidctl.driver.nzxt_epsu`](../liquidctl/driver/nzxt_epsu.py)._ ## Initialization It is necessary to initialize the device once it has been powered on. ``` # liquidctl initialize ``` _Note: at the moment initialize is a no-op, but this is likely to change once more features are added to the driver._ ## Monitoring The PSU is able to report monitoring data about its own hardware and the output rails. ``` # liquidctl status NZXT E500 ├── Temperature 45.0 °C ├── Fan speed 505 rpm ├── Firmware version A017/40983 ├── +12V peripherals output voltage 11.89 V ├── +12V peripherals output current 7.75 A ├── +12V peripherals output power 14.48 W ├── +12V EPS/ATX12V output voltage 11.95 V ├── +12V EPS/ATX12V output current 0.00 A ├── +12V EPS/ATX12V output power 0.00 W ├── +12V motherboard/PCI-e output voltage 11.96 V ├── +12V motherboard/PCI-e output current 1.00 A ├── +12V motherboard/PCI-e output power 11.95 W ├── +5V combined output voltage 4.90 V ├── +5V combined output current 0.02 A ├── +5V combined output power 0.11 W ├── +3.3V combined output voltage 3.23 V ├── +3.3V combined output current 0.01 A └── +3.3V combined output power 0.02 W ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/docs/nzxt-hue2-guide.md0000644000175000017500000001674414613511524017065 0ustar00jonasjonas# NZXT HUE 2 and Smart Device V2 controllers _Driver API and source code available in [`liquidctl.driver.smart_device`](../liquidctl/driver/smart_device.py)._ The NZXT HUE 2 lighting system is a refresh of the original HUE+. The main improvement is the ability to mix Aer RGB 2 fans and HUE 2 lighting accessories (e.g. HUE 2 LED strip, HUE 2 Underglow, HUE 2 Cable Comb) in a channel. HUE+ devices, including the original Aer RGB fans, are also supported, but HUE 2 components cannot be mixed with HUE+ components in the same channel. Each channel supports up to 6 accessories and a total of 40 LEDs, and the firmware exposes several color presets, most of them common to other NZXT products. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. Most capabilities available at the hardware level are supported, but other features offered by CAM, like noise level optimization and presets based on CPU/GPU temperatures, have not been implemented. ## NZXT HUE 2 The NZXT HUE 2 controller features four lighting channels. ## NZXT HUE 2 Ambient The NZXT HUE 2 Ambient controller features two lighting channels. ## NZXT Smart Device V2 The NZXT Smart Device V2 is a HUE 2 variant of the original Smart Device fan and LED controller, that ships with NZXT's cases released in mid-2019 including the H510 Elite, H510i, H710i, and H210i. It provides two HUE 2 lighting channels and three independent fan channels with standard 4-pin connectors. Both PWM and DC fan control is supported, and the device automatically chooses the appropriate mode for each channel; the device also reports the state of each fan channel, as well as speed and duty (from 0% to 100%). A microphone is still present onboard for noise level optimization through CAM and AI. ## NZXT H1 V2 _New in 1.10.0._
The second revision of the NZXT H1 case, labeled H1 V2, ships with a variant of the NZXT Smart Device V2 that handles both the internal fans and the AIO pump. Two fan and one pump channels are available, where the formers can be controlled via PWM or DC. The pump speed is not user controllable. The device reports the state, speed and duty of each fan channel, as well as the pump speed. There are no lighting channels available nor an onboard microphone. ## NZXT RGB & Fan Controller The NZXT RGB & Fan Controller is a retail version of the NZXT Smart Device V2. ## NZXT RGB & Fan Controller (3+6 channels) _New in 1.12.1._
In 2022 NZXT released a new version of the RGB & Fan Controller with 3 fan speed channels and 6 lighting channels. For now, this new version is only partially supported by liquidctl: only monitoring and fan control are available. ## Initialization After powering on from Mechanical Off, or if there have been hardware changes, the device must first be initialized. Only then monitoring, proper fan control and all lighting effects will be available. The firmware version and the connected LED accessories are also reported during device initialization. ``` # liquidctl initialize NZXT Smart Device V2 ├── Firmware version 1.5.0 ├── LED 1 accessory 1 HUE 2 LED Strip 300 mm ├── LED 1 accessory 2 HUE 2 Underglow 200 mm ├── LED 1 accessory 3 HUE 2 Underglow 200 mm ├── LED 2 accessory 1 AER RGB 2 140 mm ├── LED 2 accessory 2 AER RGB 2 140 mm ├── LED 2 accessory 3 AER RGB 2 140 mm └── LED 2 accessory 4 AER RGB 2 120 mm ``` ## Monitoring _Changed in 1.9.0: the noise level is not available when data is read from [Linux hwmon]._
The device can report fan information for each channel and the noise level at the on-board sensor. ``` # liquidctl status NZXT Smart Device V2 ├── Fan 2 control mode PWM ├── Fan 2 duty 42 % ├── Fan 2 speed 934 rpm └── Noise level 62 dB ``` ## Fan speeds _Only Smart Device V2, RGB & Fan Controller and H1 V2_ Fan speeds can only be set to fixed duty values. ``` # liquidctl set fan2 speed 90 ``` | Channel | Minimum duty | Maximum duty | Note | | --- | --- | --- | --- | | fan1 | 0% | 100% | H1 V2: GPU exhaust fan | | fan2 | 0% | 100% | H1 V2: AIO fan | | fan3 | 0% | 100% | H1 V2: not available | | sync | 0% | 100% | all available channels | *Always check that the settings are appropriate for the use case, and that they correctly apply and persist.* ## RGB lighting LED channels are numbered sequentially: `led1`, `led2`, etc., up to the number of channels supported by the device. Color modes can be set independently for each lighting channel, but the specified color mode will then apply to all devices daisy chained on that channel. There is also a `sync` channel. ``` # liquidctl set led1 color fixed af5a2f # liquidctl set led2 color fading 350017 ff2608 --speed slower # liquidctl set led3 color pulse ffffff # liquidctl set led4 color marquee-5 2f6017 --direction backward --speed slowest # liquidctl set sync color spectrum-wave ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Notes | | --- | --- | --- | | `off` | None | | `fixed` | One | | `super-fixed` | Up to 40, one for each LED | | `fading` | Between 2 and 8, one for each step | | `spectrum-wave` | None | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-marquee` | Up to 8, one for each step | | `alternating-` | Two | 3 ≤ `length` ≤ 6 | | `moving-alternating-` | Two | 3 ≤ `length` ≤ 6 | | `pulse` | Up to 8, one for each pulse | | `breathing` | Up to 8, one for each step | | `super-breathing` | Up to 40, one for each LED | Only one step | | `candle` | One | | `starry-night` | One | | `rainbow-flow` | None | | `super-rainbow` | None | | `rainbow-pulse` | None | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backward compatibility. | Mode | Colors | Notes | | --- | --- | --- | | `backwards-spectrum-wave` | None | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-backwards-marquee` | Up to 8, one for each step | | `backwards-moving-alternating-` | Two | 3 ≤ `length` ≤ 6 | | `backwards-rainbow-flow` | None | | `backwards-super-rainbow` | None | | `backwards-rainbow-pulse` | None | ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
Smart Device V2 controllers are supported by the mainline Linux kernel with its [`nzxt-smart2`] driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [`nzxt-smart2`]: https://www.kernel.org/doc/html/latest/hwmon/nzxt-smart2.html ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/docs/nzxt-smart-device-v1-guide.md0000644000175000017500000001403114617346724021130 0ustar00jonasjonas# NZXT Smart Device (V1) and Grid+ V3 _Driver API and source code available in [`liquidctl.driver.smart_device`](../liquidctl/driver/smart_device.py)._ The Smart Device is a fan and LED controller that ships with the H200i, H400i, H500i and H700i cases. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode. Additionally, up to four chained HUE+ LED strips or five chained Aer RGB fans can be driven from a single RGB channel. The firmware installed on the device exposes several presets, most of them familiar to other NZXT products. A microphone is also present onboard for noise level optimization through CAM and AI. This driver also supports the NZXT Grid+ V3 fan controller, which has six fan speed channels but no LED support or microphone. All configuration is done through USB, and persists as long as the device still gets power, even if the system has gone to Soft Off (S5) state. The device also reports the state of each fan channel, as well as speed, voltage and current. All capabilities available at the hardware level are supported, but other features offered by CAM, like noise level optimization and presets based on CPU/GPU temperatures, have not been implemented. ## Initialization [Initialization]: #initialization _Changed in 1.9.0: the firmware version and the connected accessories are now reported after initialization._
_Changed in 1.10.0: modern firmware versions are now reported in simplified form, to match CAM._
After powering on from Mechanical Off, or if there have been hardware changes, the device must first be initialized. This takes a few seconds and should detect all connected fans and LED accessories. Only then monitoring, proper fan control and all lighting effects will be available. ``` # liquidctl initialize NZXT Smart Device (V1) ├── Firmware version 1.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip └── LED count (total) 20 ``` ## Monitoring _Changed in 1.9.0: the firmware version and the connected accessories are no longer reported (see [Initialization])._
_Changed in 1.9.0: the noise level is not available when data is read from [Linux hwmon]._
The device can report fan information for each channel, the noise level at the onboard sensor, as well as the type of the connected LED accessories. ``` # liquidctl status NZXT Smart Device (V1) ├── Fan 1 speed 1492 rpm ├── Fan 1 voltage 11.91 V ├── Fan 1 current 0.02 A ├── Fan 1 control mode PWM ├── Fan 2 speed 1368 rpm ├── Fan 2 voltage 11.91 V ├── Fan 2 current 0.02 A ├── Fan 2 control mode PWM ├── Fan 3 speed 1665 rpm ├── Fan 3 voltage 11.91 V ├── Fan 3 current 0.06 A ├── Fan 3 control mode PWM └── Noise level 59 dB ``` ## Fan speeds Fan speeds can only be set to fixed duty values. ``` # liquidctl set fan2 speed 90 ``` | Channel | Minimum duty | Maximum duty | Note | | --- | --- | --- | - | | fan1 | 0% | 100% || | fan2 | 0% | 100% || | fan3 | 0% | 100% || | fan4 | 0% | 100% | Grid+ V3 only | | fan5 | 0% | 100% | Grid+ V3 only | | fan6 | 0% | 100% | Grid+ V3 only | | sync | 0% | 100% | all available channels | *Always check that the settings are appropriate for the use case, and that they correctly apply and persist.* ## RGB lighting _Only NZXT Smart Device (V1)_ For lighting, the user can control up to 40 LEDs, if all four strips or five fans are connected. They are chained in a single channel: `led`. ``` # liquidctl set led color fixed af5a2f # liquidctl set led color fading 350017 ff2608 --speed slower # liquidctl set led color pulse ffffff # liquidctl set led color marquee-5 2f6017 --direction backward --speed slowest ``` Colors can be specified in RGB, HSV or HSL (see [Supported color specification formats](../README.md#supported-color-specification-formats)), and each animation mode supports different number of colors. The animation speed can be customized with the `--speed `, and five relative values are accepted by the device: `slowest`, `slower`, `normal`, `faster` and `fastest`. Some of the color animations can be in either the `forward` or `backward` direction. This can be specified by using the `--direction` flag. | Mode | Colors | Notes | | --- | --- | --- | | `off` | None | | `fixed` | One | | `super-fixed` | Up to 40, one for each LED | | `fading` | Between 2 and 8, one for each step | | `spectrum-wave` | None | | `super-wave` | Up to 40 | | `marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-marquee` | Up to 8, one for each step | | `alternating` | Two | | `moving-alternating` | Two | | `breathing` | Up to 8, one for each step | | `super-breathing` | Up to 40, one for each LED | Only one step | | `pulse` | Up to 8, one for each pulse | | `candle` | One | | `wings` | One | #### Deprecated modes The following modes are now deprecated and the use of the `--direction backward` is preferred, they will be removed in a future version and are kept for now for backward compatibility. | Mode | Colors | Notes | | --- | --- | --- | | `backwards-spectrum-wave` | None | | `backwards-super-wave` | Up to 40 | | `backwards-marquee-` | One | 3 ≤ `length` ≤ 6 | | `covering-backwards-marquee` | Up to 8, one for each step | | `backwards-moving-alternating` | Two | ## Interaction with Linux hwmon drivers [Linux hwmon]: #interaction-with-linux-hwmon-drivers _New in 1.9.0._
These devices are supported by the [liquidtux] `nzxt-grid3` driver, and status data is provided through a standard hwmon sysfs interface. Starting with version 1.9.0, liquidctl automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with `--direct-access`. [liquidtux]: https://github.com/liquidctl/liquidtux ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7191358 liquidctl-1.15.0/docs/windows/0000755000175000017500000000000014776655074015305 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/docs/windows/running-your-first-command-line-program.md0000644000175000017500000000661514562144457025435 0ustar00jonasjonas# Running your first command-line program The command line is very straightforward: it was a precursor to the graphical user interfaces (GUIs) we are so used to, so it is much more simple and explicit than a GUI. A shell/terminal like Windows Command Prompt or Powershell is just some place were you write what you want some program to do. And they come with a few build-in "special programs" like `cd` (change directory). Unlike GUIs, command-line programs simply output their results to the terminal they were called from. I will get to "how to install" in a bit, but for now let us assume liquidctl has already been set up. If you want to list all devices, just type and hit enter: liquidctl list And this will result in an output that looks similar to: Device ID 0: NZXT Smart Device (V1) Device ID 1: NZXT Kraken X (X42, X52, X62 or X72) If you want to list all devices showing a bit more information: liquidctl list --verbose If you want to initialize all devices (which you should!): liquidctl initialize all If you want to show the status information: liquidctl status To change say the pump speed to 42%: liquidctl set pump speed 42 This last command will not show any output. This is normal: command-line programs tend to follow a convention that simplifies chaining programs and automating things with scripts: (unless explicitly requested otherwise), only output useful information or error messages. Some liquidctl commands can get slightly less English-looking than what was showed above, but they should still be readable. For example, to set the fans to follow the profile defined by three points (25°C -> 10%), (30°C -> 50%), (40°C -> 100%), execute: liquidctl set fan speed 25 10 30 50 40 100 While in isolation these numbers are not very self explanatory, they are simply the pairs of temperature and corresponding duty values: liquidctl set fan speed 25 10 30 50 40 100 ^^^^^ ^^^^^ ^^^^^^ pairs of temperature (°C) -> duty (%) _(The profiles run on the device, and therefore can only refer to the internal liquid temperature sensor)._ Each device family has a guide that can be found in the [list of _Supported Devices_], and that lists all features and attributes that are supported by those devices, as well as examples. [list of _Supported Devices_]: ../../README.md#supported-devices ## Setting up liquidctl There is currently no installer for Windows, due to a number of reasons (that are not very important right now). But the README has an extensive section on [how to manually install] liquidctl, and you can join our [Discord server] and ask for assistance. [how to manually install]: ../../README.md#manual-installation [Discord server]: https://discord.gg/GyCBjQhqCd ## Final words While you should be able to use liquidctl with just these tips, I still recommend you take a look at the rest of the [README], the documents in [docs] and, also, the output of: liquidctl --help [README]: ../../README.md [docs]: .. ## Notes ### About this document This document was originally a response to a direct message: > Hi. How are you? Hope you're staying safe and well. I just wanted to know of > there is a windows gui for liquidctl? > I have zero experience with command line stuff and I don't entirely understand > it... also most of the guides are from late 2018 or early 2019. > And i just bought a x53 kraken. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7199228 liquidctl-1.15.0/extra/0000755000175000017500000000000014776655074014006 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744525883.720135 liquidctl-1.15.0/extra/completions/0000755000175000017500000000000014776655074016342 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/extra/completions/liquidctl.bash0000644000175000017500000002356414562144457021174 0ustar00jonasjonas#!/usr/bin/env bash # Bash completions for liquidctl. # # Requires bash-completion.[1] # # Users can place this file in the `completions` subdir of # $BASH_COMPLETION_USER_DIR (defaults to `$XDG_DATA_HOME/bash-completion` or # `~/.local/share/bash-completion` if $XDG_DATA_HOME is not set). # # Distros should instead use the directory returned by # pkg-config --variable=completionsdir bash-completion # # See [1] for more information. # # [1] https://github.com/scop/bash-completion # # Copyright Marshall Asch and contributors # SPDX-License-Identifier: GPL-3.0-or-later # logging method _e() { echo "$1" >> log; } _list_bus_options () { liquidctl list -v | grep 'Bus:' | cut -d ':' -f 2 | sort -u } _list_vendor_options () { liquidctl list -v | grep 'Vendor ID:' | cut -d ':' -f 2 | cut -c4-6 | sort -u } _list_product_options () { liquidctl list -v | grep 'Product ID:' | cut -d ':' -f 2 | cut -c4-6 | sort -u } _list_device_options () { liquidctl list | cut -d ' ' -f 3 | cut -d ':' -f 1 | sort -u } _list_match_options () { liquidctl list | cut -d ':' -f 2 | sort -u | awk '{gsub(/\(|\)/,"",$0); print tolower($0)}' } _list_pick_options () { num=$(liquidctl list | wc -l) seq $num } _list_release_options () { liquidctl list -v | grep 'Release number:' | cut -d ':' -f 2 | sort -u } _list_address_options () { liquidctl list -v | grep 'Address:' | cut -d ':' -f 2 | sort -u } _list_port_options () { liquidctl list -v | grep 'Port:' | cut -d ':' -f 2 | sort -u } _list_serial_options () { liquidctl list -v | grep 'Serial number:' | cut -d ':' -f 2 | sort -u } _liquidctl_main() { local commands=" set initialize list status " local boolean_options=" --verbose -v --debug -g --json --version --help --single-12v-ocp --legacy-690lc --non-volatile --direct-access " local options_with_args=" --match -m --pick -n --vendor --product --release --serial --bus --address --usb-port --device -d --speed --time-per-color --time-off --alert-threshold --alert-color --pump-mode --unsafe --direction --start-led --maximum-leds --temperature-sensor --fan-mode " # generate options list and remove any flag that has already been given # note this will note remove the short and long versions options=($options_with_args $boolean_options) for i in "${!options[@]}"; do if [[ "${COMP_WORDS[@]}" =~ "${options[i]}" ]]; then unset 'options[i]' fi done; options=$(echo "${options[@]}") # This part will check if it is currently completing a flag local previous=$3 local cur="${COMP_WORDS[COMP_CWORD]}" case "$previous" in --vendor) COMPREPLY=($(compgen -W "$(_list_vendor_options)" -- "$cur")) return ;; --product) COMPREPLY=($(compgen -W "$(_list_product_options)" -- "$cur")) return ;; --bus) COMPREPLY=($(compgen -W "$(_list_bus_options)" -- "$cur")) return ;; --address) COMPREPLY=($(compgen -W "$(_list_port_options)" -- "$cur")) return ;; --match | -m ) COMPREPLY=($(compgen -W "$(_list_match_options)" -- "$cur")) return ;; --pick | -n ) COMPREPLY=($(compgen -W "$(_list_pick_options)" -- "$cur")) return ;; --device | -d ) COMPREPLY=($(compgen -W "$(_list_device_options)" -- "$cur")) return ;; --release) COMPREPLY=($(compgen -W "$(_list_release_options)" -- "$cur")) return ;; --serial) COMPREPLY=($(compgen -W "$(_list_serial_options)" -- "$cur")) return ;; --usb-port) COMPREPLY=($(compgen -W "$(_list_port_options)" -- "$cur")) return ;; --pump-mode) COMPREPLY=($(compgen -W "balanced quiet extreme" -- "$cur")) return ;; --* | -[a-z]*1) COMPREPLY=() return ;; esac # This will handle auto completing arguments even if they are given at the end of the command case "$cur" in -*) COMPREPLY=($(compgen -W "$options" -- "$cur")) return ;; esac local i=1 cmd # find the subcommand - first word after the flags while [[ "$i" -lt "$COMP_CWORD" ]] do local s="${COMP_WORDS[i]}" case "$s" in --help | --version) COMPREPLY=() return ;; -*) ;; initialize | list | status | set ) cmd="$s" break ;; esac (( i++ )) done if [[ "$i" -eq "$COMP_CWORD" ]] then COMPREPLY=($(compgen -W "$commands $options" -- "$cur")) return # return early if we're still completing the 'current' command fi # we've completed the 'current' command and now need to call the next completion function # subcommands have their own completion functions case "$cmd" in list) COMPREPLY="" ;; initialize) _liquidctl_initialize_command ;; status) COMPREPLY="" ;; set) _liquidctl_set_command ;; *) ;; esac } _liquidctl_initialize_command () { local i=1 subcommand_index # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" case "$s" in all) subcommand_index=$i break ;; esac (( i++ )) done if [[ "$i" -eq "$COMP_CWORD" ]] then local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "all" -- "$cur")) return # return early if we're still completing the 'current' command fi local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=() #($(compgen -W "all" -- "$cur")) } _liquidctl_set_command () { local i=1 subcommand_index handled=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" case "$s" in fan[0-9] | fan | pump) subcommand_index=$i _liquidctl_set_fan handled=1 break ;; led[0-9] | led | sync | ring | logo | external) subcommand_index=$i _liquidctl_set_led handled=1 break ;; lcd) subcommand_index=$i _liquidctl_set_lcd handled=1 break ;; esac (( i++ )) done # check if it is a fan or an LED that is being set if [[ "$handled" -eq "0" ]] then # no trailing space here so that the fan number can be appended compopt -o nospace # possibly use some command here to get a list of all the possible channels from liquidctl local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "fan fan1 fan2 fan3 fan4 fan5 fan6 led led1 led2 led3 led4 led5 led6 pump sync ring logo external lcd" -- "$cur")) fi } _liquidctl_set_fan () { local i=1 found=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" if [[ "$s" = "speed" ]]; then found=1 break fi (( i++ )) done # check if it is a fan or an LED that is being set if [[ $found = 1 ]]; then COMPREPLY="" else COMPREPLY="speed" fi } _liquidctl_set_led () { local i=1 found=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" if [[ "$s" = "color" ]]; then found=1 break fi (( i++ )) done # check if it is a fan or an LED that is being set if [[ $found = 1 ]]; then COMPREPLY="" else COMPREPLY="color" fi } _liquidctl_set_lcd () { local i=1 found=0 # find the sub command while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" if [[ "$s" = "screen" ]]; then found=1 break fi (( i++ )) done # check if it is a fan or an LED that is being set if [[ $found = 1 ]]; then _liquidctl_set_screen else COMPREPLY="screen" fi } _liquidctl_set_screen () { local i=1 subcommand_index handled=0 # find the sub command (either a fan or an led to set) while [[ $i -lt $COMP_CWORD ]]; do local s="${COMP_WORDS[i]}" case "$s" in liquid) subcommand_index=$i COMPREPLY="" handled=1 break ;; brightness) subcommand_index=$i COMPREPLY="" handled=1 break ;; orientation) subcommand_index=$i local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "0 90 180 270" -- "$cur")) handled=1 break ;; static | gif) subcommand_index=$i local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=( $(compgen -f -- ${cur}) ) handled=1 break ;; esac (( i++ )) done # check if it is a fan or an LED that is being set if [[ "$handled" -eq "0" ]] then # no trailing space here so that the fan number can be appended compopt -o nospace # possibly use some command here to get a list of all the possible channels from liquidctl local cur="${COMP_WORDS[COMP_CWORD]}" COMPREPLY=($(compgen -W "liquid brightness orientation static gif" -- "$cur")) fi } complete -F _liquidctl_main liquidctl ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744525883.720323 liquidctl-1.15.0/extra/contrib/0000755000175000017500000000000014776655074015446 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/extra/contrib/fusion_rgb_cycle.py0000755000175000017500000000527014613511524021320 0ustar00jonasjonas#!/usr/bin/env python3 # Copyright (C) 2022–2022 Peter Eckersley # SPDX-License-Identifier: GPL-3.0-or-later import argparse import sys import time import liquidctl.cli as lc import liquidctl try: import coloraide as c except ImportError: print("The python coloraide package is not installed...") print("Try installing it with: pip install coloraide") sys.exit(1) teal = c.Color("#2de544") bluer = c.Color("#2d8a8d") deep_blue = c.Color("#001030") purple = c.Color("#c000c0") red = c.Color("#d00000") # set up argparse parser = argparse.ArgumentParser( description="Cycle through colors on an Gigabyte RGB Fusion 2.0 device." ) parser.add_argument( "--space", type=str, default="srgb", help="Color space to use; see https://facelessuser.github.io/coloraide/colors/", ) # you can provide 0 or more colors to cycle through parser.add_argument("colors", nargs="*", help="Colors to cycle through") parser.add_argument( "--steps", default=30, type=int, help="Number of steps per color fade (default: 30)" ) parser.add_argument("--debug", action="store_true", help="Print debug messages") parser.add_argument( "--channel", type=str, default="led6", help="Channel to use; see https://github.com/liquidctl/liquidctl/blob/main/docs/gigabyte-rgb-fusion2-guide.md", ) parser.add_argument( "--speed", type=float, default=1.0, help="Speed (seconds per color transition)" ) args = parser.parse_args() colors = [c.Color("#" + x) for x in args.colors] if len(colors) < 2: colors.append(teal) if len(colors) < 2: colors.append(bluer) devs = liquidctl.driver.rgb_fusion2.RgbFusion2.find_supported_devices() if not devs: print("No Gigabyte RGB Fusion 2.0 device found.") sys.exit(1) elif len(devs) > 1: print("Multiple Gigabyte RGB Fusion 2.0 devices found, using first one.") dev = devs[0] lookup = [] def to_int(color): string = color.to_string() # "rgb(255 75 0)" string = string.partition("(")[2][:-1] # "255 75 0" string = string.split() # ["255", "75", "0"] return [int(float(x)) for x in string] colors.append(colors[0]) # add first color at the end, so we can loop for i in range(len(colors) - 1): gradient = colors[i].interpolate(colors[i + 1], space=args.space) cols = [to_int(gradient(x / args.steps)) for x in range(args.steps)] lookup += cols if args.debug: print("Cycling through", len(lookup), "colors:\n", lookup) while True: # reconnect occasionally, just in case these connections die and we can bring them back with dev.connect(): for c in lookup: dev.set_color(channel=args.channel, mode="fixed", colors=[c]) time.sleep(args.speed * 2.0 / args.steps) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7206984 liquidctl-1.15.0/extra/contrib/liquidctlfan/0000755000175000017500000000000014776655074020125 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/contrib/liquidctlfan/README.md0000644000175000017500000000741614062723141021367 0ustar00jonasjonas# liquidctlfan a wrapper script for liquidctl to control your fans When I built my first water-cooled PC a few months ago, using Linux, I thought it would be easy to control or regulate it. I quickly found liquidctl on Github and after a few weeks I could control my NZXT X73 under Linux. Thanks a lot for that! Unfortunately I found out that I couldn't control the fans in relation to the CPU temperature. (see https://github.com/liquidctl/liquidctl/issues/118) ## Components liquidctlfan - Wrapper script as a small demon systemd unit file - Unit file for integration into systemd ## Prerequisites The tools bc, sensors, liquidctl and logger must be installed. Sensors must receive a basic configuration before it can be used. Please use sensors-detect for this. The setup takes some time depending on the system. For example, after installation, a Ryzen 9 looks like this: ``` k10temp-pci-00c3 Adapter: PCI adapter Tdie: +42.0°C (high = +70.0°C) Tctl: +42.0°C ``` The script expects a line like Tdie. The script has been tested under Linux Mint 19.3. ## Installation Copy the file liquidctlfan into the directory "/usr/local/bin/" and set the permissions if necessary. ## Configuration and usage ``` Usage: ./liquidctlfan -p | --product is the product id of your fan controller (e.g. 0x1711) [*] -u | --unit Celsius or Fahrenheit (e.g. c|C|Celsius|f|F|Fahrenheit) [*] -ct1| --cputemp1 CPU temperature threshold value lowest (e.g. 50.0) [*] -ct2| --cputemp2 CPU temperature threshold value (e.g. 60.0) [*] -ct3| --cputemp3 CPU temperature threshold value (e.g. 70.0) [*] -ct4| --cputemp4 CPU temperature threshold value highest (e.g. 80.0) [*] -f0|--fan0 Fan setpoint in percent (e.g. 30) [*] -f1|--fan1 Fan setpoint in percent (e.g. 40) [*] -f2|--fan2 Fan setpoint in percent (e.g. 50) [*] -f3|--fan3 Fan setpoint in percent (e.g. 80) [*] -f4|--fan4 Fan setpoint in percent (e.g. 100) [*] -i|--interval CPU temperature check time in seconds (e.g. 10) [*] -l|--log Enable syslog logging (e.g. enable|disable|ENABLE|DISABLE) [*] -a|--about Show about message -h|--help Show this message [*] mandatory parameter ``` A normal call could be ... `./liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l disable` If it is not desired to pass parameters, all parameters can be stored permanently in the script. Just activate the parameters in the configuration area of the script (remove #). ``` ### Enable configuration to disable parameter handling #### ### Product ID of HUE Grid #PRID="0x1711" ##Unit Celsius C or Fahrenheit F #UNIT="C" ### CPU temperature threshold values #CPUT1="50.0" #CPUT2="60.0" #CPUT3="70.0" #CPUT4="80.0" ### FAN setpoints #FAN0="30" #FAN1="40" #FAN2="50" #FAN3="80" #FAN4="100" ###Interval check time #SLTIME="10" ###Enable Syslog #SYSLOG="enable" ########################################################### ``` ## systemd In the directory systemd you will find the unit file. Copy the file with root access liquidctlfan.service into /etc/systemd/system. Please regard if the parameters are to be transferred or if the stored parameters are to be taken over. The appropriate line must be activated. ``` [Unit] Description=liquidctl Fan Control After=liquidcfg.service [Service] ## Fixed configuration #ExecStart=/usr/local/bin/liquidctlfan ## Handover parameters ExecStart=/usr/local/bin/liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l enable Restart=on-failure [Install] WantedBy=multi-user.target ``` Use the following commands to reload the configuration, load the daemon at startup, start or stop the daemon. ``` systemctl daemon reload systemctl enable liquidctlfan.service systemctl start liquidctlfan.service systemctl stop liquidctlfan.service ``` ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1673154816.0 liquidctl-1.15.0/extra/contrib/liquidctlfan/liquidctlfan0000755000175000017500000002035714356450400022515 0ustar00jonasjonas#!/bin/bash # Place this script in /usr/local/bin or you've to adjust systemd unit file. # Please have a look at cputemp. You may have to adjust something for your system. # # Copyright (C) 2020 Martin Burgholte # SPDX-License-Identifier: GPL-3.0-or-later NAME="liquidctl Fan Control" PROCNAME=`basename "$0"` ### Enable configuration to disable parameter handling #### ### Product ID of HUE Grid #PRID="0x1711" ###Unit Celsius C or Fahrenheit F #UNIT="C" ### CPU temperature threshold values #CPUT1="50.0" #CPUT2="60.0" #CPUT3="70.0" #CPUT4="80.0" ### FAN setpoints #FAN0="30" #FAN1="40" #FAN2="50" #FAN3="80" #FAN4="100" ###Interval check time #SLTIME="10" ###Enable Syslog #SYSLOG="enable" ########################################################### programs=(bc sensors liquidctl logger) for program in "${programs[@]}"; do if ! command -v "$program" > /dev/null 2>&1; then echo "The following application: $program is missing. Please check it first." exit 1 fi done while [[ $# -gt 0 ]] do key="$1" case $key in -p|--product) PRID="$2" shift ;; -u|--unit) UNIT="$2" shift ;; -ct1|--cputemp1) CPUT1="$2" shift ;; -ct2|--cputemp2) CPUT2="$2" shift ;; -ct3|--cputemp3) CPUT3="$2" shift ;; -ct4|--cputemp4) CPUT4="$2" shift ;; -f0|--fan0) FAN0="$2" shift ;; -f1|--fan1) FAN1="$2" shift ;; -f2|--fan2) FAN2="$2" shift ;; -f3|--fan3) FAN3="$2" shift ;; -f4|--fan4) FAN4="$2" shift ;; -i|--interval) SLTIME="$2" shift ;; -l|--log) SYSLOG="$2" shift ;; -a|--about) echo -e "$NAME - wrapper script for liquidctl to control your fans\nCopyright (C) 2020 Martin Burgholte\nSPDX-License-Identifier: GPL-3.0-or-later" exit 0 ;; -h|--help) echo -e "Usage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 0 ;; *) echo -e "Usage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 1 ;; esac shift done # Parameter check if [[ -z $PRID ]] || [[ -z $UNIT ]] || [[ -z $CPUT1 ]] || [[ -z $CPUT2 ]] || [[ -z $CPUT3 ]] || [[ -z $CPUT4 ]] || [[ -z $FAN0 ]] || [[ -z $FAN1 ]] || [[ -z $FAN2 ]] || [[ -z $FAN3 ]] || [[ -z $FAN4 ]] || [[ -z $SLTIME ]] || [[ -z $SYSLOG ]] then echo -e "Missing parameter!\nUsage: $0\n\t-p|--product Product id of controller \n\t-u|--unit Celsius or Fahrenheit \n\t-ct1|--cputemp1 CPU temperature threshold value lowest \n\t-ct2|--cputemp2 CPU temperature threshold value\n\t-ct3|--cputemp3 CPU temperature threshold value \n\t-ct4|--cputemp4 CPU temperature threshold value highest \n\t-f0|--fan0 Fan setpoint in percent lowest (idle) \n\t-f1|--fan1 Fan setpoint in percent \n\t-f2|--fan2 Fan setpoint in percent \n\t-f3|--fan3 Fan setpoint in percent \n\t-f4|--fan4 Fan setpoint in percent highest\n\t-i|--interval CPU temperature check time\n\t--l|--log Enable syslog logging \n\t-a|--about Show about message \n\t-h|--help Show this message" exit 1 fi # Unit check case $UNIT in f|F|Fahrenheit) # echo -n "Fahrenheit" UNIT="F" ;; c|C|Celsius) # echo -n "Celsius" UNIT="C" ;; *) echo -e "Wrong unit parameter! Please use for\n\t°C -> c|C|Celsius or\n\t°F -> f|F|Fahrenheit." exit 1 ;; esac # Syslog check case $SYSLOG in enable|ENABLE|true|TRUE) SYSLOG="1" ;; disable|DISABLE|false|FALSE) SYSLOG="0" ;; *) echo -e "Wrong syslog parameter! Please use for\n\tactive -> enable|ENABLE|TRUE|true or\n\tinactive -> disable|DISABLE|FALSE|false" exit 1 ;; esac # CPU temperature threshold value check if (( $(echo "$CPUT1 < $CPUT2" | bc -l) )) then if (( $(echo "$CPUT2 < $CPUT3" | bc -l) )) then if (( $(echo "$CPUT3 < $CPUT4" | bc -l) )) then echo -e "CPU temperature thresholds looking good." else echo -e "CPU temperature threshold error! CPU temperature t3 $CPUT3 is not less than t4 $CPUT4. Please check your threshold." exit 1 fi else echo -e "CPU temperature threshold error! CPU temperature t2 $CPUT2 is not less than t3 $CPUT3. Please check your threshold." exit 1 fi else echo -e "CPU temperature threshold error! CPU temperature t1 $CPUT1 is not less than t2 $CPUT2. Please check your threshold." exit 1 fi # FAN setpoint check if (( $(echo "$FAN0 < $FAN1" | bc -l) )) then if (( $(echo "$FAN1 < $FAN2" | bc -l) )) then if (( $(echo "$FAN2 < $FAN3" | bc -l) )) then if (( $(echo "$FAN3 < $FAN4" | bc -l) )) then echo -e "FAN setpoints looking good." else echo -e "FAN setpoint error! FAN setpoint f3 $FAN3 is not less than f4 $FAN4. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f2 $FAN2 is not less than f3 $FAN3. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f1 $FAN1 is not less than f2 $FAN2. Please check your setpoints." exit 1 fi else echo -e "FAN setpoint error! FAN setpoint f0 $FAN0 is not less than f1 $FAN1. Please check your setpoints." exit 1 fi LIQBIN=`command -v liquidctl` while true do # ATTENTION Check your CPU temperature sensor value. if [ $UNIT == "F" ] then cputemp=`sensors -f | grep Tdie | awk '{ print $2 }' | sed -e s/°$UNIT//g | sed -e s/+//g` else cputemp=`sensors | grep Tdie | awk '{ print $2 }' | sed -e s/°$UNIT//g | sed -e s/+//g` fi if (( $(echo "$cputemp > $CPUT4" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN4 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN4 %\r" fi $LIQBIN --product $PRID set sync speed $FAN4 elif (( $(echo "$cputemp >= $CPUT3" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN3 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN3 %\r" fi $LIQBIN --product $PRID set sync speed $FAN3 elif (( $(echo "$cputemp >= $CPUT2" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN2 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN2 %\r" fi $LIQBIN --product $PRID set sync speed $FAN2 elif (( $(echo "$cputemp >= $CPUT1" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp°$UNIT setting FANs to $FAN1 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN1 %\r" fi $LIQBIN --product $PRID set sync speed $FAN1 elif (( $(echo "$cputemp < $CPUT1" | bc -l) )) then if [ $SYSLOG -eq "1" ] then logger --id=$$ `echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN0 %\r"` else echo -ne "$PROCNAME CPU temperature is $cputemp °$UNIT setting FANs to $FAN0 %\r" fi $LIQBIN --product $PRID set sync speed $FAN0 fi sleep $SLTIME done ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7208898 liquidctl-1.15.0/extra/contrib/liquidctlfan/systemd/0000755000175000017500000000000014776655074021615 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/contrib/liquidctlfan/systemd/liquidctlfan.service0000644000175000017500000000055414062723141025635 0ustar00jonasjonas[Unit] Description=liquidctl Fan Control After=liquidcfg.service [Service] ## Fixed configuration #ExecStart=/usr/local/bin/liquidctlfan ## Handover parameters ExecStart=/usr/local/bin/liquidctlfan -p 0x1711 -u c -ct1 50.0 -ct2 60.0 -ct3 70.0 -ct4 80.0 -f0 30 -f1 40 -f2 50 -f3 80 -f4 100 -i 10 -l enable Restart=on-failure [Install] WantedBy=multi-user.target ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1744525883.72107 liquidctl-1.15.0/extra/dist/0000755000175000017500000000000014776655074014751 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/extra/dist/pypi-readme.md0000644000175000017500000000317214562144457017501 0ustar00jonasjonas# liquidctl – liquid cooler control Cross-platform tool and drivers for liquid coolers and other devices. Go to the [project homepage] for more information. ``` $ liquidctl list Device #0: Corsair Vengeance RGB DIMM2 Device #1: Corsair Vengeance RGB DIMM4 Device #2: NZXT Smart Device (V1) Device #3: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl initialize all NZXT Smart Device (V1) ├── Firmware version 1.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip └── LED count (total) 20 NZXT Kraken X (X42, X52, X62 or X72) └── Firmware version 6.2 # liquidctl status NZXT Smart Device (V1) ├── Fan 1 speed 1499 rpm ├── Fan 1 voltage 11.91 V ├── Fan 1 current 0.05 A ├── Fan 1 control mode PWM ├── Fan 2 [...] ├── Fan 3 [...] └── Noise level 61 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 34.7 °C ├── Fan speed 798 rpm └── Pump speed 2268 rpm # liquidctl status --match vengeance --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 └── Temperature 37.5 °C Corsair Vengeance RGB DIMM4 └── Temperature 37.8 °C # liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 # liquidctl --match kraken set pump speed 70 # liquidctl --match kraken set sync color fixed 0080ff # liquidctl --match "smart device" set led color moving-alternating "hsv(30,98,100)" "hsv(30,98,10)" --speed slower ``` [project homepage]: https://github.com/liquidctl/liquidctl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/extra/krakenduty-poc.py0000755000175000017500000000705014613511524017302 0ustar00jonasjonas#!/usr/bin/env python3 """krakenduty proof of concept – translate Kraken X speeds to duty values This is just a proof of concept. Usage: krakenduty-poc train krakenduty-poc status krakenduty-poc --help krakenduty-poc --version Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import ast from time import sleep from docopt import docopt from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.util import interpolate_profile as interpolate DATAFILE = ".krakenduty-poc" DUTY_STEP = 5 DUTY_SLEEP = 5 DUTY_SAMPLES = 5 def get_speeds(device): status = {k: v for k, v, u in device.get_status()} return (status["Fan speed"], status["Pump speed"]) def find_duty_values(training_data, fan_speed, pump_speed): # for now simply interpolate, but this is terrible because it ignores variance fan_duty = interpolate(sorted([(speed, duty) for duty, speed, _ in training_data]), fan_speed) pump_duty = interpolate(sorted([(speed, duty) for duty, _, speed in training_data]), pump_speed) # don't return values outside the allowed bounds to avoid confusion return (min(max(fan_duty, 25), 100), min(max(pump_duty, 50), 100)) def do_train(device): # read current values fan_speed, pump_speed = get_speeds(device) print("starting values: fan = {} rpm, pump = {} rpm".format(fan_speed, pump_speed)) # train training_data = [] for duty in range(0, 101, DUTY_STEP): # don't worry if duty is off spec, the driver will correct it for channel in ["fan", "pump"]: device.set_fixed_speed(channel, duty) # wait significantly to allow the speed to stabilize sleep(DUTY_SLEEP) # get a few samples because there is some natural variation; # though this might need some delays and, also, depend on the actually # observed variance samples = [get_speeds(device) for i in range(DUTY_SAMPLES)] average = [sum(i) / len(i) for i in zip(*samples)] print("{}% duty: fan = {:.0f} rpm, pump = {:.0f} rpm".format(duty, *average)) training_data.append([duty] + average) with open(DATAFILE, "w") as f: f.write(str(training_data)) # (try to) restore the current values fan_duty, pump_duty = find_duty_values(training_data, fan_speed, pump_speed) print("applying fixed values: fan = {}%, pump = {}%".format(fan_duty, pump_duty)) device.set_fixed_speed("fan", fan_duty) device.set_fixed_speed("pump", pump_duty) def do_status(device): # read training data training_data = [] with open(DATAFILE, "r") as f: training_data = ast.literal_eval(f.read()) # augment status = [] for k, v, u in device.get_status(): status.append((k, v, u)) if k == "Fan speed": fan_duty, _ = find_duty_values(training_data, v, 0) status.append(("Fan duty", fan_duty, "%")) elif k == "Pump speed": _, pump_duty = find_duty_values(training_data, 0, v) status.append(("Pump duty", pump_duty, "%")) # report print("{}".format(device.description)) for k, v, u in status: print("{:<20} {:>6} {:<3}".format(k, v, u)) print("") if __name__ == "__main__": args = docopt(__doc__, version="0.0.2") device = KrakenTwoDriver.find_supported_devices()[0] device.connect() try: if args["train"]: do_train(device) elif args["status"]: do_status(device) else: raise Exception("nothing to do") finally: device.disconnect() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7212903 liquidctl-1.15.0/extra/linux/0000755000175000017500000000000014776655074015145 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741176960.0 liquidctl-1.15.0/extra/linux/71-liquidctl.rules0000644000175000017500000005762414762040200020424 0ustar00jonasjonas# Rules that grant unprivileged access to devices supported by liquidctl. # # Users and distros are encouraged to use these if they want liquidctl to work # without requiring root privileges (e.g. without the use of `sudo`). # # In the case of I²C/SMBus devices, these rules also cause the loading of the # `i2c-dev` kernel module. This module is required for access to I²C/SMBus # devices from userspace, and manually loading kernel modules is in itself a # privileged operation. # # Distros will likely want to place this file in `/usr/lib/udev/rules.d/`, while # users installing this manually SHOULD use `/etc/udev/rules.d/` instead. # # The suggested name for this file is `71-liquidctl.rules`. Whatever name is # used, it MUST lexically appear before 73-seat-late.rules. The suggested name # was chosen so that it is also lexically after systemd-provided 70-uaccess.rules. # # Once installed, reload and trigger the new rules with: # # # udevadm control --reload # # udevadm trigger # # Note that this will not change the mode of `/dev/hidraw*` devices that have # already been created. In practice, this means that HIDs may continue to require # privileged access until, either, they are rebound to their kernel drivers, or # the system is rebooted. # # These rules assume a system with modern versions of systemd/udev, that support # the `uaccess` tag. On older systems the rules can be changed to instead set # GROUP="plugdev" and MODE="0660"; other groups and modes may also be used. # # The use of the `uaccess` mechanism assumes that only physical sessions (or # "seats") need unprivileged access to the devices.[^1][^2] In case headless # sessions are also expected to interactively run liquidctl, GROUP and MODE should # also be set, as a fallback. # # Finally, this file was automatically generated. To update it, from a Linux # shell and the current directory, execute: # # $ python generate-uaccess-udev-rules.py > 71-liquidctl.rules # # [^1]: https://github.com/systemd/systemd/issues/4288 # [^2]: https://wiki.archlinux.org/title/Users_and_groups#Pre-systemd_groups # Section: special cases # Host SMBus on Intel mainstream/HEDT platforms KERNEL=="i2c-*", DRIVERS=="i801_smbus", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # Section: NVIDIA graphics cards # ASUS Strix GTX 1050 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1c81", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85d8", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1050 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1c82", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85cd", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1050 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1c82", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85d1", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1060 6GB KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1c03", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85a4", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1060 OC 6GB KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1c03", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85ac", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1070 KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b81", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8598", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1070 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b81", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8599", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1070 Ti KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b82", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x861d", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1070 Ti Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b82", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x861e", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b80", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8592", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b80", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85aa", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b80", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85f9", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 Ti KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85eb", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 Ti KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85f1", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85ea", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1080 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x85e4", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1650 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x2187", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x874f", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1660 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x21c4", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8752", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix GTX 1660 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x2182", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x86a5", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Evo KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f08", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x86d3", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Evo OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e89", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8775", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f08", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x868e", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Super KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8730", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Super Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x86fc", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Super Evo Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f47", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8703", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2060 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f06", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x86fb", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e84", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8707", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f07", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8671", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1f07", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8670", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Super Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1ec7", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x86ff", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Super Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e84", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8728", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Super Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e84", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8706", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e84", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8727", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2070 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e84", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8729", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2080 OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e87", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x865f", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2080 Super Advanced KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e81", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8712", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2080 Super OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e81", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8711", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2080 Ti KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e04", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x8687", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS Strix RTX 2080 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1e07", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x866a", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # ASUS TUF RTX 3060 Ti OC KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x2489", ATTRS{subsystem_vendor}=="0x1043", \ ATTRS{subsystem_device}=="0x87c6", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # EVGA GTX 1070 FTW KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b81", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6276", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # EVGA GTX 1070 FTW DT Gaming KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b81", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6274", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # EVGA GTX 1070 FTW Hybrid KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b81", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6278", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # EVGA GTX 1070 Ti FTW2 KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b82", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6775", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # EVGA GTX 1080 FTW KERNEL=="i2c-*", ATTR{name}=="NVIDIA i2c adapter 1 *", ATTRS{vendor}=="0x10de", \ ATTRS{device}=="0x1b80", ATTRS{subsystem_vendor}=="0x3842", \ ATTRS{subsystem_device}=="0x6286", DRIVERS=="nvidia", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" # Section: USB devices and USB HIDs # ASUS Aura LED Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="19af", TAG+="uaccess" # ASUS Aura LED Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="1939", TAG+="uaccess" # ASUS Aura LED Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="18f3", TAG+="uaccess" # ASUS Ryujin II 360 SUBSYSTEMS=="usb", ATTRS{idVendor}=="0b05", ATTRS{idProduct}=="1988", TAG+="uaccess" # Aquacomputer D5 Next SUBSYSTEMS=="usb", ATTRS{idVendor}=="0c70", ATTRS{idProduct}=="f00e", TAG+="uaccess" # Aquacomputer Farbwerk 360 SUBSYSTEMS=="usb", ATTRS{idVendor}=="0c70", ATTRS{idProduct}=="f010", TAG+="uaccess" # Aquacomputer Octo SUBSYSTEMS=="usb", ATTRS{idVendor}=="0c70", ATTRS{idProduct}=="f011", TAG+="uaccess" # Aquacomputer Quadro SUBSYSTEMS=="usb", ATTRS{idVendor}=="0c70", ATTRS{idProduct}=="f00d", TAG+="uaccess" # Asetek 690LC (assuming EVGA CLC) # Asetek 690LC (assuming NZXT Kraken X) SUBSYSTEMS=="usb", ATTRS{idVendor}=="2433", ATTRS{idProduct}=="b200", TAG+="uaccess" # Corsair Commander Core (broken) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c1c", TAG+="uaccess" # Corsair Commander Core XT (broken) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c2a", TAG+="uaccess" # Corsair Commander Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c10", TAG+="uaccess" # Corsair Commander ST (broken) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c32", TAG+="uaccess" # Corsair H110i GT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c04", TAG+="uaccess" # Corsair HX1000i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c07", TAG+="uaccess" # Corsair HX1000i (2022) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c1e", TAG+="uaccess" # Corsair HX1200i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c08", TAG+="uaccess" # Corsair HX1200i ATX 3.1 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c23", TAG+="uaccess" # Corsair HX1500i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c1f", TAG+="uaccess" # Corsair HX750i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c05", TAG+="uaccess" # Corsair HX850i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c06", TAG+="uaccess" # Corsair Hydro H100i GTX SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c03", TAG+="uaccess" # Corsair Hydro H100i Platinum SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c18", TAG+="uaccess" # Corsair Hydro H100i Platinum SE SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c19", TAG+="uaccess" # Corsair Hydro H100i Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c15", TAG+="uaccess" # Corsair Hydro H100i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c20", TAG+="uaccess" # Corsair Hydro H100i v2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c09", TAG+="uaccess" # Corsair Hydro H110i GTX SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c07", TAG+="uaccess" # Corsair Hydro H115i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c0a", TAG+="uaccess" # Corsair Hydro H115i Platinum SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c17", TAG+="uaccess" # Corsair Hydro H115i Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c13", TAG+="uaccess" # Corsair Hydro H115i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c21", TAG+="uaccess" # Corsair Hydro H150i Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c12", TAG+="uaccess" # Corsair Hydro H150i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c22", TAG+="uaccess" # Corsair Hydro H60i Pro XT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c29", TAG+="uaccess" # Corsair Hydro H80i GT SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c02", TAG+="uaccess" # Corsair Hydro H80i v2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c08", TAG+="uaccess" # Corsair Lighting Node Core SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c1a", TAG+="uaccess" # Corsair Lighting Node Pro SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c0b", TAG+="uaccess" # Corsair Obsidian 1000D SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1d00", TAG+="uaccess" # Corsair RM1000i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0d", TAG+="uaccess" # Corsair RM650i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0a", TAG+="uaccess" # Corsair RM750i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0b", TAG+="uaccess" # Corsair RM850i SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="1c0c", TAG+="uaccess" # Corsair iCUE H100i Elite RGB SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c35", TAG+="uaccess" # Corsair iCUE H100i Elite RGB (White) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c40", TAG+="uaccess" # Corsair iCUE H115i Elite RGB SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c36", TAG+="uaccess" # Corsair iCUE H150i Elite RGB SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c37", TAG+="uaccess" # Corsair iCUE H150i Elite RGB (White) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1b1c", ATTRS{idProduct}=="0c41", TAG+="uaccess" # Gigabyte RGB Fusion 2.0 5702 Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="5702", TAG+="uaccess" # Gigabyte RGB Fusion 2.0 8297 Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="048d", ATTRS{idProduct}=="8297", TAG+="uaccess" # MSI MPG Coreliquid K360 SUBSYSTEMS=="usb", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="b130", TAG+="uaccess" # NZXT E500 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="5911", TAG+="uaccess" # NZXT E650 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="5912", TAG+="uaccess" # NZXT E850 SUBSYSTEMS=="usb", ATTRS{idVendor}=="7793", ATTRS{idProduct}=="2500", TAG+="uaccess" # NZXT Grid+ V3 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1711", TAG+="uaccess" # NZXT H1 V2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2015", TAG+="uaccess" # NZXT HUE 2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2001", TAG+="uaccess" # NZXT HUE 2 Ambient SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2002", TAG+="uaccess" # NZXT Kraken 2023 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="300e", TAG+="uaccess" # NZXT Kraken 2023 Elite (broken) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="300c", TAG+="uaccess" # NZXT Kraken 2024 Elite RGB SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="3012", TAG+="uaccess" # NZXT Kraken M22 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1715", TAG+="uaccess" # NZXT Kraken X (X42, X52, X62 or X72) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="170e", TAG+="uaccess" # NZXT Kraken X (X53, X63 or X73) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2007", TAG+="uaccess" # NZXT Kraken X (X53, X63 or X73) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2014", TAG+="uaccess" # NZXT Kraken Z (Z53, Z63 or Z73) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="3008", TAG+="uaccess" # NZXT RGB & Fan Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2009", TAG+="uaccess" # NZXT RGB & Fan Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="200e", TAG+="uaccess" # NZXT RGB & Fan Controller SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2010", TAG+="uaccess" # NZXT RGB & Fan Controller (3+6 channels) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2011", TAG+="uaccess" # NZXT RGB & Fan Controller (3+6 channels) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2019", TAG+="uaccess" # NZXT RGB & Fan Controller (3+6 channels) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="201f", TAG+="uaccess" # NZXT RGB & Fan Controller (3+6 channels) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2020", TAG+="uaccess" # NZXT Smart Device (V1) SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="1714", TAG+="uaccess" # NZXT Smart Device V2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="2006", TAG+="uaccess" # NZXT Smart Device V2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="200d", TAG+="uaccess" # NZXT Smart Device V2 SUBSYSTEMS=="usb", ATTRS{idVendor}=="1e71", ATTRS{idProduct}=="200f", TAG+="uaccess" # Suspected MSI MPG Coreliquid SUBSYSTEMS=="usb", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="ca00", TAG+="uaccess" # Suspected MSI MPG Coreliquid SUBSYSTEMS=="usb", ATTRS{idVendor}=="0db0", ATTRS{idProduct}=="ca02", TAG+="uaccess" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/extra/linux/generate-uaccess-udev-rules.py0000755000175000017500000001267514613511524023021 0ustar00jonasjonas# uses the psf/black style import sys from inspect import cleandoc if __name__ == "__main__": # This script is meant to be executed from this directory or the project root. # We use that assumption to make Python pick the local liquidctl modules, # instead other versions that may be installed on the environment/system. sys.path = ["../..", ""] + sys.path # The 71-liquidctl.rules file must always be written in UTF-8. Therefore, # when we detect that stdout has been redirected to a file, make sure that # it is set to UTF-8, regardless of the current locale. if not sys.stdout.isatty() and sys.stdout.encoding != "utf-8": print("liquidctl udev rules files must use UTF-8, reconfiguring stdout", file=sys.stderr) sys.stdout.reconfigure(encoding="utf-8") from liquidctl.driver.base import find_all_subclasses from liquidctl.driver.nvidia import _NvidiaI2CDriver from liquidctl.driver.usb import BaseUsbDriver HEADER = """ # Rules that grant unprivileged access to devices supported by liquidctl. # # Users and distros are encouraged to use these if they want liquidctl to work # without requiring root privileges (e.g. without the use of `sudo`). # # In the case of I²C/SMBus devices, these rules also cause the loading of the # `i2c-dev` kernel module. This module is required for access to I²C/SMBus # devices from userspace, and manually loading kernel modules is in itself a # privileged operation. # # Distros will likely want to place this file in `/usr/lib/udev/rules.d/`, while # users installing this manually SHOULD use `/etc/udev/rules.d/` instead. # # The suggested name for this file is `71-liquidctl.rules`. Whatever name is # used, it MUST lexically appear before 73-seat-late.rules. The suggested name # was chosen so that it is also lexically after systemd-provided 70-uaccess.rules. # # Once installed, reload and trigger the new rules with: # # # udevadm control --reload # # udevadm trigger # # Note that this will not change the mode of `/dev/hidraw*` devices that have # already been created. In practice, this means that HIDs may continue to require # privileged access until, either, they are rebound to their kernel drivers, or # the system is rebooted. # # These rules assume a system with modern versions of systemd/udev, that support # the `uaccess` tag. On older systems the rules can be changed to instead set # GROUP="plugdev" and MODE="0660"; other groups and modes may also be used. # # The use of the `uaccess` mechanism assumes that only physical sessions (or # "seats") need unprivileged access to the devices.[^1][^2] In case headless # sessions are also expected to interactively run liquidctl, GROUP and MODE should # also be set, as a fallback. # # Finally, this file was automatically generated. To update it, from a Linux # shell and the current directory, execute: # # $ python generate-uaccess-udev-rules.py > 71-liquidctl.rules # # [^1]: https://github.com/systemd/systemd/issues/4288 # [^2]: https://wiki.archlinux.org/title/Users_and_groups#Pre-systemd_groups """ MANUAL_RULES = r""" # Section: special cases # Host SMBus on Intel mainstream/HEDT platforms KERNEL=="i2c-*", DRIVERS=="i801_smbus", TAG+="uaccess", \ RUN{builtin}="kmod load i2c-dev" """ print(cleandoc(HEADER)) print() print() print(cleandoc(MANUAL_RULES)) print() print() print(f"# Section: NVIDIA graphics cards") nvidia_devs = {} for driver in find_all_subclasses(_NvidiaI2CDriver): for did, sdid, description in driver._MATCHES: ids = (driver._VENDOR, did, sdid) if ids in nvidia_devs: nvidia_devs[ids].append(description) nvidia_devs[ids].sort() else: nvidia_devs[ids] = [description] nvidia_devs = [ (svid, did, sdid, description) for (svid, did, sdid), description in nvidia_devs.items() ] nvidia_devs.sort(key=lambda x: x[3][0]) for svid, did, sdid, descriptions in nvidia_devs: print() for desc in descriptions: print(f"# {desc}") entry = f""" KERNEL=="i2c-*", ATTR{{name}}=="NVIDIA i2c adapter 1 *", ATTRS{{vendor}}=="0x10de", \\ ATTRS{{device}}=="{did:#06x}", ATTRS{{subsystem_vendor}}=="{svid:#06x}", \\ ATTRS{{subsystem_device}}=="{sdid:#06x}", DRIVERS=="nvidia", TAG+="uaccess", \\ RUN{{builtin}}="kmod load i2c-dev" """ print(cleandoc(entry)) print() print() print(f"# Section: USB devices and USB HIDs") usb_devs = {} for driver in find_all_subclasses(BaseUsbDriver): for vid, pid, description, _ in driver._MATCHES: ids = (vid, pid) if ids in usb_devs: usb_devs[ids].append(description) usb_devs[ids].sort() else: usb_devs[ids] = [description] usb_devs = [(vid, pid, description) for (vid, pid), description in usb_devs.items()] usb_devs.sort(key=lambda x: x[2][0]) for vid, pid, descriptions in usb_devs: print() for desc in descriptions: print(f"# {desc}") print( f'SUBSYSTEMS=="usb", ATTRS{{idVendor}}=="{vid:04x}", ATTRS{{idProduct}}=="{pid:04x}", TAG+="uaccess"' ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/extra/liquiddump.py0000755000175000017500000000576514613511524016532 0ustar00jonasjonas#!/usr/bin/env python3 """liquiddump – continuously dump monitoring data from liquidctl devices. This is a experimental script that continuously dumps the status of all available devices to stdout in newline-delimited JSON. Usage: liquiddump [options] liquiddump --help liquiddump --version Options: --interval Update interval in seconds [default: 2] --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus --pick Pick among many results for a given filter -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Examples: liquiddump liquiddump --product 0xb200 liquiddump --interval 0.5 liquiddump > file.jsonl liquiddump | jq -c . Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import json import logging import sys import time import liquidctl.cli as _borrow import usb from docopt import docopt from liquidctl.driver import * LOGGER = logging.getLogger(__name__) if __name__ == "__main__": args = docopt(__doc__, version="0.1.1") frwd = _borrow._make_opts(args) devs = list(find_liquidctl_devices(**frwd)) update_interval = float(args["--interval"]) if args["--debug"]: args["--verbose"] = True logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(name)s: %(message)s") elif args["--verbose"]: logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") else: logging.basicConfig(level=logging.WARNING, format="%(message)s") sys.tracebacklimit = 0 try: for d in devs: LOGGER.info("initializing %s", d.description) d.connect() status = {} while True: for d in devs: try: status[d.description] = d.get_status() except usb.core.USBError as err: LOGGER.warning("failed to read from the device, possibly serving stale data") LOGGER.debug(err, exc_info=True) print(json.dumps(status), flush=True) time.sleep(update_interval) except KeyboardInterrupt: LOGGER.info("canceled by user") except: LOGGER.exception("unexpected error") sys.exit(1) finally: for d in devs: try: LOGGER.info("disconnecting from %s", d.description) d.disconnect() except: LOGGER.exception("unexpected error when disconnecting") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7220395 liquidctl-1.15.0/extra/old-tests/0000755000175000017500000000000014776655074015724 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/old-tests/asetek_legacy0000755000175000017500000000345414062723141020433 0ustar00jonasjonas#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200 --legacy-690lc" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 0 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 100 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 50 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 50 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 75 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --time-per-color 2 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-off 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 --alert-threshold 0 --alert-color ffff00 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/old-tests/asetek_modern0000755000175000017500000000344614062723141020454 0ustar00jonasjonas#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 0 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 100 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 50 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set fan speed 20 0 40 100 liquidctl $DEVICE $EXTRAOPTIONS set pump speed 75 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --time-per-color 2 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-off 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 sleep 2 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color fading ff8000 00ff80 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blinking 8000ff --time-per-color 2 --time-off 1 --alert-threshold 0 --alert-color ffff00 sleep 6 liquidctl $DEVICE $EXTRAOPTIONS set logo color fixed 00ff00 --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS set logo color blackout --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/old-tests/asetek_modern_rainbow0000755000175000017500000000105414062723141022166 0ustar00jonasjonas#!/bin/bash -xe DEVICE="--vendor 0x2433 --product 0xb200" liquidctl $DEVICE $EXTRAOPTIONS list --verbose liquidctl $DEVICE $EXTRAOPTIONS initialize sleep 2 liquidctl $DEVICE $EXTRAOPTIONS status liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --speed 1 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --speed 6 sleep 4 liquidctl $DEVICE $EXTRAOPTIONS set logo color rainbow --alert-threshold 0 --alert-color ffff00 sleep 3 liquidctl $DEVICE $EXTRAOPTIONS status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/old-tests/kraken_two0000755000175000017500000000614514062723141017777 0ustar00jonasjonas#!/bin/bash -xe DEVICE="--vendor 0x1e71 --product 0x170e" hold() { liquidctl $DEVICE $EXTRAOPTIONS ${@:2} sleep $1 } hold 0 list --verbose hold 0 initialize hold 0 status hold 0 set sync color off hold 0 set fan speed 0 hold 2 set pump speed 100 hold 0 status hold 0 set fan speed 100 hold 2 set pump speed 50 hold 0 status hold 0 set fan speed 20 0 40 100 hold 2 set pump speed 20 50 40 100 hold 0 status hold 2 set sync color off hold 0 set logo color fixed ff8000 hold 2 set ring color fixed 00ff80 hold 2 set sync color fixed 0000ff hold 2 set sync color off hold 2 set sync color super-fixed \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 2 set sync color off hold 0 set logo color fading ff8000 00ff80 --speed slowest hold 5 set ring color fading 00ff80 8000ff hold 5 set sync color fading 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set logo color spectrum-wave --speed slowest hold 5 set ring color spectrum-wave hold 5 set sync color backwards-spectrum-wave --speed fastest hold 2 set sync color off hold 5 set ring color super-wave --speed slowest \ ff8000 00ff80 8000ff 800000 \ 008000 000080 400000 004000 hold 5 set ring color backwards-super-wave --speed fastest \ ff8000 00ff80 8000ff 800000 \ 008000 000080 400000 004000 hold 2 set sync color off hold 3 set ring color marquee-3 ff8000 --speed slower hold 3 set ring color backwards-marquee-3 ff8000 --speed slower hold 3 set ring color marquee-6 00ff80 --speed faster hold 3 set ring color backwards-marquee-6 00ff80 --speed faster hold 2 set sync color off hold 3 set ring color covering-marquee ff8000 00ff80 0080ff --speed slowest hold 3 set ring color covering-backwards-marquee ff8000 00ff80 0080ff --speed fastest hold 2 set sync color off hold 3 set ring color alternating ff8000 00ff80 --speed slowest hold 3 set ring color moving-alternating ff8000 00ff80 hold 3 set ring color backwards-moving-alternating ff8000 00ff80 --speed fastest hold 2 set sync color off hold 0 set logo color breathing ff8000 00ff80 --speed slowest hold 5 set ring color breathing 00ff80 8000ff hold 5 set sync color breathing 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set sync color super-breathing --speed slower \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 5 set sync color super-breathing --speed faster \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff \ ff0000 00ff00 0000ff hold 2 set sync color off hold 0 set logo color pulse ff8000 00ff80 --speed slowest hold 5 set ring color pulse 00ff80 8000ff hold 5 set sync color pulse 0000ff ff0000 00ff00 --speed fastest hold 2 set sync color off hold 5 set ring color tai-chi ff8000 0080ff hold 2 set sync color off hold 3 set ring color water-cooler --speed slower hold 3 set ring color water-cooler --speed faster hold 2 set sync color off hold 3 set ring color loading ff8000 --speed slower hold 3 set ring color loading 00ff80 --speed faster hold 2 set sync color off hold 3 set ring color wings ff8000 --speed slower hold 3 set ring color wings 00ff80 --speed faster hold 2 set sync color off hold 0 status ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/extra/prometheus-liquidctl-exporter.py0000755000175000017500000001404214613511524022372 0ustar00jonasjonas#!/usr/bin/env python3 """prometheus-liquidctl-exporter – host a metrics HTTP endpoint with Prometheus formatted data from liquidctl This is an experimental script that collects stats from liquidctl and exposes them as a http://localhost:8098/metrics endpoint in the Prometheus text format. See: https://prometheus.io/docs/instrumenting/exposition_formats/#text-format-example Example metric with labels: # HELP liquidctl liquidctl exported metrics # TYPE liquidctl gauge liquidctl{device="NZXT Kraken X (X42, X52, X62 or X72)",sensor="liquid_temperature",unit="°C"} 33.6 Usage: prometheus-liquidctl-exporter [options] Options: --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --server-port Port for the HTTP /metrics endpoint -v, --verbose Output additional information -g, --debug Show debug information on stderr --version Display the version number --help Show this message Device selection options (see: list -v): -m, --match Filter devices by description substring -n, --pick Pick among many results for a given filter --vendor Filter devices by hexadecimal vendor ID --product Filter devices by hexadecimal product ID --release Filter devices by hexadecimal release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus Copyright Alex Berryman, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys import time import usb from datetime import timedelta from docopt import docopt from liquidctl.driver import * from prometheus_client import start_http_server from prometheus_client.core import GaugeMetricFamily, REGISTRY, InfoMetricFamily LOGGER = logging.getLogger(__name__) def gauge_name_sanitize(name): return name.replace(" ", "_").lower() class LiquidCollector(object): def __init__(self): self.description = "liquidctl exported metrics" def collect(self): labels = ["device", "sensor", "unit"] g = GaugeMetricFamily("liquidctl", self.description, labels=labels) i = InfoMetricFamily("liquidctl", self.description, labels=["device"]) for d in devs: try: get_status = d.get_status() for metric in get_status: sanitized_name = gauge_name_sanitize(metric[0]) sample_value = metric[1] unit = metric[2] if isinstance(sample_value, timedelta): # cast timedelta into seconds and override the supplied unit sample_value = sample_value.seconds unit = "seconds" if unit != "": # FIXME doesn't handle multiple equal devices well label_values = [d.description, sanitized_name, unit] g.add_metric(label_values, value=sample_value) LOGGER.debug( "%s: %s as GaugeMetric %s labels %s", d.description, metric, sanitized_name, "/".join(label_values), ) else: i.add_metric([d.description], value={sanitized_name: sample_value}) LOGGER.debug( "%s: %s InfoMetric labeled with %s => %s", d.description, metric, sanitized_name, sample_value, ) except usb.core.USBError as err: LOGGER.warning("failed to read from the device, possibly serving stale data") LOGGER.debug(err, exc_info=True) yield g yield i def _make_opts(arguments): options = {} for arg, val in arguments.items(): if val is not None and arg in _PARSE_ARG: opt = arg.replace("--", "").replace("-", "_") options[opt] = _PARSE_ARG[arg](val) return options _PARSE_ARG = { "--legacy-690lc": bool, "--vendor": lambda x: int(x, 16), "--product": lambda x: int(x, 16), "--release": lambda x: int(x, 16), "--serial": str, "--bus": str, "--address": str, "--usb-port": lambda x: tuple(map(int, x.split("."))), "--match": str, "--pick": int, } if __name__ == "__main__": args = docopt(__doc__, version="0.1.1") opts = _make_opts(args) devs = list(find_liquidctl_devices(**opts)) for d in devs: LOGGER.info("initializing %s", d.description) d.connect() if args["--debug"]: args["--verbose"] = True logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(name)s: %(message)s") elif args["--verbose"]: logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") else: logging.basicConfig(level=logging.WARNING, format="%(message)s") sys.tracebacklimit = 0 REGISTRY.register(LiquidCollector()) if args["--server-port"]: server_port = int(args["--server-port"]) else: server_port = 8098 start_http_server(server_port) LOGGER.debug("server started on port %s", server_port) try: while True: # Keep HTTP server alive in a loop time.sleep(2) except KeyboardInterrupt: LOGGER.info("canceled by user") finally: for d in devs: try: LOGGER.info("disconnecting from %s", d.description) d.disconnect() except: LOGGER.exception("unexpected error when disconnecting") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7224143 liquidctl-1.15.0/extra/windows/0000755000175000017500000000000014776655074015500 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/extra/windows/LQiNFO.py0000644000175000017500000001117714562144457017100 0ustar00jonasjonas"""LQiNFO – export monitoring data from liquidctl devices to HWiNFO. This is a experimental script that exports the status of all available devices to HWiNFO. Usage: LQiNFO.py [options] LQiNFO.py --help LQiNFO.py --version Options: --interval Update interval in seconds [default: 2] --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus --pick Pick among many results for a given filter -v, --verbose Output additional information to stderr -g, --debug Show debug information on stderr --version Display the version number --help Show this message Examples: python LQiNFO.py python LQiNFO.py --product 0xb200 python LQiNFO.py --interval 0.5 Changelog: 0.0.2 Fix cleanup of registry keys when exiting. 0.0.1 First proof-of-concept. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys import time import winreg import liquidctl.cli as _borrow import usb from docopt import docopt from liquidctl.driver import * LOGGER = logging.getLogger(__name__) if __name__ == '__main__': args = docopt(__doc__, version='0.0.2') frwd = _borrow._make_opts(args) devs = list(find_liquidctl_devices(**frwd)) update_interval = float(args['--interval']) if args['--debug']: args['--verbose'] = True logging.basicConfig(level=logging.DEBUG, format='[%(levelname)s] %(name)s: %(message)s') elif args['--verbose']: logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') else: logging.basicConfig(level=logging.WARNING, format='%(message)s') sys.tracebacklimit = 0 hwinfo_sensor_types = { '°C': 'Temp', 'V': 'Volt', 'rpm': 'Fan', 'A': 'Current', 'W': 'Power', 'dB': 'Other' } hwinfo_custom = winreg.CreateKey(winreg.HKEY_CURRENT_USER, r'Software\HWiNFO64\Sensors\Custom') infos = [] try: for i, d in enumerate(devs): LOGGER.info('Initializing %s', d.description) d.connect() dev_key = winreg.CreateKey(hwinfo_custom, f'{d.description} (liquidctl#{i})') dev_values = [] for j, (k, v, u) in enumerate(d.get_status()): hwinfo_type = hwinfo_sensor_types.get(u, None) if not hwinfo_type: dev_values.append(None) else: sensor_key = winreg.CreateKey(dev_key, f'{hwinfo_type}{j}') winreg.SetValueEx(sensor_key, 'Name', None, winreg.REG_SZ, k) winreg.SetValueEx(sensor_key, 'Unit', None, winreg.REG_SZ, u) winreg.SetValueEx(sensor_key, 'Value', None, winreg.REG_SZ, str(v)) dev_values.append(sensor_key) infos.append((dev_key, dev_values)) # set up dev infos and registry status = {} while True: for i, d in enumerate(devs): dev_key, dev_values = infos[i] try: for j, (k, v, u) in enumerate(d.get_status()): sensor_key = dev_values[j] if sensor_key: winreg.SetValueEx(sensor_key, 'Value', None, winreg.REG_SZ, str(v)) except usb.core.USBError as err: LOGGER.warning('Failed to read from the device, possibly serving stale data') LOGGER.debug(err, exc_info=True) time.sleep(update_interval) except KeyboardInterrupt: LOGGER.info('Canceled by user') except: LOGGER.exception('Unexpected error') sys.exit(1) finally: for d in devs: try: LOGGER.info('Disconnecting from %s', d.description) d.disconnect() except: LOGGER.exception('Unexpected error when disconnecting') for dev_key, dev_values in infos: try: for sensor_key in dev_values: if sensor_key: winreg.DeleteKey(sensor_key, '') winreg.DeleteKey(dev_key, '') except: LOGGER.exception('Unexpected error when cleaning the registry') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1623959137.0 liquidctl-1.15.0/extra/windows/liquidctl_logo_v1_circle_256.ico0000644000175000017500000007240414062723141023513 0ustar00jonasjonas tPNG  IHDR\rf IDATxy$Y;'"3~o[WwuwUWu%ԋZ-оDc$ a7`33F~A$4Rw%oowόssN%".uU'NDƍ;KL]r `? {'l2 <(0 <^^vt1$&}j8 W* dboZ`0?<,MWLfrgYČ;AƀdEct|3 ,D-nA?X!f0.!  A,{Y'p5-^ufЬ7X(L Ɓ `ٜ"QLx6Ii^\袀'oқ&&?8Z1-[R5;mZ6a>[A&X8$'QGQa$}b` 3O_æK$p3y.-'~#CNk-[֠ niR5wQs:'A:g1(яɧ3`19 9+h|+c7֮"#Yjj|je +@u(gy(}=!, "0kYmf~3cToy0B|0,T\mIЁbE3,xه~9{> =6+3f˼ Z&^S sK2G(HxbP"1ԟg3Or{7# 66!nQ&o~@T~*Kղ27B\*(_`3Տgab<>&zwfMF.sLȡ-˸*;}X"۲9FsZWFR~:gmNǼf BFWZnb*&!Ʈ7"Kǭ0A+v*u z2Mq1Ӽ6hP~%>6 #ж އ/N1?w\MǴoKGp/p!V 5Оȷ}ܱU\xF%6:߲q&= Vv$rOBll7}Qd;I?S!m#@ @d2ﭑweu?vtWl|֎[9$zws^\y$J-k~ d7 J<{}c1\n_N{zM^1f eZa?ƠO?`-4]g Vz$YWK׵.LWSЇ*Z#[4wH%C5uI?H`@Kk[~}Œ8]=8( iN"k-YlΉ՘/üEzÄ\cK㇘zϓ uH^K6]ܺB!HzeWAP G&ɯv7IHPߺ ^ ̴L0v?ef Hm*UpHq7#ӺN=@>u%EQN?LbcYt|]aa,1Es@ кyQ@q݁a<{{Sp?[-@9&eo#UMa-d6_Ov \ۧ&Ư`͞r(O˻yۧ^F%%%` 1w!Fqm_oL 4.o%LG"IAm6? m _K:E 3ŕM9/e7 <Pׁ?c]5֪ + vodn8Ʊ$)|xWZGe}W-&Zᅵ, $ ג[ 9yhnF49ֱzMaLdHCodPVA<:(KEK(_'/ e;@=ޘaz~$w֋ivK2$-$B`z(Q(PeS2Md?J~駋~ҬW[u㫸vs6q~˿Z<|bz@u5#bbS.l˯BJI&rJh łiy:ʹ:sL3,ҝyur)ˢ}sBHkHp^<~?Ƶ EC/61Sz-Ʉî%X1 -ljaQ(] Kb,~aAq8Lqc+dVyU)uҿ"闓 _*ۃ/h!CSSVm:fgCS4Ň?ܗKn>碍p=Kl>MlW5H6 Xh[uʝ(Ib>G9yg;_>RՁ?S'OcJ_/\,cP&u]~5Htݬ}Y%Ӟ]|76Oh28b>\W5ux^  ʦBg[&8>vbF~ k up?sGny3u^b2,w $ o *?=$_%H}xߖ_䕣!P]'ױqv/_kCAv8܄Z{&n;RR{^1~C t;?L:`|֚ҝ:,57'/dkd?Va7[~]Cd-a? Jo;> QQ ;y;#8k @ R_ =<* j: Xj!wφ&@6A-37&bK } 15 $nA-ΡN솜|y܅h-eч4ؚ #\v h!:q׵B=aݢbK,npMji3^6 jA-5 9{_A#Z$zk{q| =<,霘f|~{ X0SߎgɲBA~3@rg;xdIO;٘rJlWv<'Sz2'SMAAҝrA'۱ͺp37| ZZ^F:w܇yaxqz{tUz* b%P +5PԄ9V%?'\ݴx;c;LnH M5PG1OmWex33{ leY]_/:j =s$ =ABF]=Ye+{Ta_Oڢ{Yf]Q_QNuۓW)>f~Lq5;~w2i\r>Oȉ!Zdm72 /™omm/Jp!0pα w2e{tŘ2Wnwj02/lg)'U_*h O&@pZ "J | of1j HOE_p|~I n]S~yh >x>x#]1L`k{tTAz̽ewӫ9j @~α*{ifkۿb[oFDQOM|1ty+_qahWK[ǏBCWi]'~JXuj};x)rb؏~]R4=> νf6aϲ Zc1޳WeWWoiz" p%aϿH`u]- ~ |퉁s4O<Qb׮Pa?/eK.}N6LH&Tߕ싼ӀaOA=~m:sF{}a:N؉UL`^y~gB[n)&cNC/ _;%zBOYQ34?Dg5CKi&$シQuA!DN@Te8-Oe}W!] R$JS w3,}sMC%.VCvUYFn&$Y w#OJu$@)})~ Ox艝tm"4͎~xϏi "@RJ [ GBGSDphDS3{ԛ蹙<5ݹ=rmX8=$ 'Xaj0_z?wMcZM -*$7ӂ.eq@HO D3@q=0K=H@1hOpuGIDnCK$~z,K20 7%N f'1<~&fՖBaJ#^I+lGh\=Β/w9ji.i PR0'-l3g&|^ZmWS?ƈC;”Px*A fZ/KvL@_~zx HHJq`8 =APO oƷwG5aȚ5jdru*@%j-8%4YEPz~oN}|Ń[hoнfN2Է߿Mkh{~er 6ɧ#@FO3˕#=T=de u@!DhmBTAx| QD*2V״̓v['=>5>?@!Y-3$j݁(9Gvѝ"}~_W4qX\?js8Ȯl0vP;O]ζweal7rfed`XH>LϿu] 7 B_'DBKG:>>'s UAnI|E1H y<#BY'C!9|(NKOF $Zo" 2#WS"O~Co% =T,Rw1_X^,C}z7u;Z8O,Uq99|?&6% ŝߊDF:~ôY$KM0JoǠۢ*cpğ z.{t, E)݁YԙGX|nO>ads !0@Wׂ? y⼄!#ȉVXjaOn?)qL#9Q?u,^J,èa'=+.e#> ?l}[%dn(õĿpmI $*=eĤPBϘTB*$]EP?u(--3"[Js 0jTqQ|NޱI/pw$R2(1MTAn;'j L^Cofnᛧ~\p2@*VbIMժz~x?Q@ |ҁ?DPT@,'R:n4jH)q@6yZN|@DP0tqty%!䚩=DU]߭kAOȐC}P22kb^͗F.v?6 IDAT;hɑ Nׄ^q}}0/tQW[ɮMeW:%#H`H^ -n-W?Fg86YCB(Su'T!hui@pjYen}1{Yt]o#]M~ӺlbM ݎ r j,$'Vr ZpZ Bkx',0|= ?J *ɠ\H V$4ct>c1/%rݒD;v)] ȉX hYhU^4NBQxJ2˦)_׫Rp8Vq[k|1 ^%؇`!%Bd T!2o'F*wՅW~8'*Xf$ szP܁<]N-;$sh]RM ޤArkzt|p *ľUZzו{@%ꬩMDea1=D~$)~-A֌bB1#>e,V߯JS .*c?sň"_AFwS=qdd.7")ß5YA֩snEZݫ+w~._6Lp*эv\+ZRӍ $HXFl7`;u(vx@>2_.B[/F i] * AbA0V mWa:嫹0_$^i:B&S݀Kj^vhf_g;6s(^RAH9'F. *{Yԑyxޞy2iA@[0  ZF ~Ź8g:G9V<" zE\:24&$q|_%I'SgDle;,hJl)q\it;Rc% b?4%,Kxq{{@a> u[ުa@L"KgA]/S_(+3БDfzjJwSgyn!oSY:Kr-PM[dAKx:EÉݾpS/Gw  NCPDљ`g"޷At%#!:|8bsݾh@ "`xz\Qj'B#u%Zd= <>|y1x4&l)pE2]Pl^-%ʹ)̑״T,.Dm.߅(v-d{&>\G7ӧ|뜪<7r!8Gftߓ*] ZgeF1ydn$g5ɅFgh#EAV&KN#2ǃ>eWz:*ss>P{~r&ݥ[v:2qkRn}C;bW4WnXoX ꎌT\6% L{ 4H3oHKp(zAFFVE(Pwj^q-xs|ڄ3w-K~/nA+/Zn @_/p"ATɾw.n{}?cvk EP@O-gS C+7mE,р^8M7%2{!V$@]՛NʃVr##4RܴnXY27i;P!@^E(F뛙B`(p_Py>*;<09>;J:=8;7wfE`)B=gгmmGSt9_/0/+7Pgld0WֆۀO4Pof?A[/De/خPǿzT>ze>Q+݀w3]_/U\/ 02*Un,uSS% PSNH a(CQ<8|fô7;:zSFwAdw_-.pnf]ӛi egǗ%''`|/u<@J~{gvvWmN*ڡ( H~"+ G2ϙ헬EKNVXTYoQ5*F .ܺ9&Q5HTGxL7q LYS{9@Lc~953gwքg^Hz ɀ)2ΫS|~%c%c<\ ?x,TN B|!zED1QhDe* URY mCLކgt}F}*bԋ@,M"?HS{IR5Nj^&a>wȿr k_8=>Uu; eyz* !N^'k$x9wQ|mi\[P6E4FI/Qq n"f}h@?ʰ0]o4~]/х/`s0(\}C1)Ћk5z~:P<0DHI\Wd[mswhb`~c⁃FsO8zi7Anzbxـ%TP  z{~?sy:* S>旕HTػOl Мp'褾:P Pn4]|;2LF𧁭9} 3l\X/DZ wQ-,MQB+|ft1<$Z]e%MTWҞQg.W5" 깲 2<@$9}<.?vc=}1M1+K9-;)q}]lϯ಼K.R7px>}dAJZזܥF €o.o2Vw`+ \\[`᱁BP/>믻;Ǽ6hSno^ʚ9W\nggay:. gSUpS6啄' "!043x2)ZFHk[ ^=$3Pj!k !X Z,cLAykL(\63_hM^^$r> Brp%жG)uMt}>ߵ#E>k Oȼ0lL_FSw 7S~+{08dѷqޗeWN! 0;w1~{}O}HuR"xGdh9M!Үid<5Jf=Q>w@ws^]_JKȫ/ w=%.L;{! *)qdhxr]VÁeWM!';f& h3mU`W w>)ZFrxaR_cBa=OS<|7mF[d+;O%QF@/8bԂ"|k|@D }UL2Wm5TIBIL߆Y#`KV>L{%lH@oCI ATh(NN3_)l4ˁcl@B4Lfy@t@!еI=}P[0aSҁ\ 2=,*s퍖tL@vperYvJ3 RZ0:/a^@ۖ#=~$u`BNh#7+\/"[k5)W ~7"p@{6O'& N-٠,)5Jc6wxyaδ3cT |Ga0W;w![x Sp!j8z nDl$_ d,h3!*&sbp%ib4'߷]B~wrDfX-m3o/2}sL03B^>䊡Q;"P5D`[czA(=xp6w6wc~DS؆d ~љ4 0l\mڶ>Wb:*Jah;Pw8쎱]WW[* aÁ$DQ0@ re b~_1@l7Kmr;E@Z"2Cш +M=#A$N6GP6@'OOszfc Yž΁~_5C v'ɻAQؓ,mzBKA&b$FZ> tindGpk iL o|2ʑ6p9 ?57D˵xW!;F?I@dz*n`%G,:(JL{ L"t1lzgî#iP>a IDATEn&kXoSmf:Ya9p8Ywí:o .w?f#AH"!w5`󃮿+7]7|lܦ |\$L`$ b 3{G7#NI @{Z ;"pK@Ms[+`UYyFؤ~DXEBS_V:tz) (&2LuÖ9' y~!L;焆Z0f([{vQx@ Ɖ!Zn%M=ΓSIxY&:HH;gB@?ᴉ-a`Za "bp6:b$)\=_5Yp͖S-K Dgf8䑳|78}j?yԹa^t9>M &#Y9هqlNٜA$ki@{ER;}6mA4YyE`?B^/}vj z+_ډ^d#c4uڤ 0[_mV'rĐ$ܺSLCB0ۣ3-\鹄2+*'^Q.@è|K5wfτuumK_fZGؽ%OTj0WM sh9[LRV \I–$tO⧚3ߝlYׯ Q܄Koo)_7NW-ݷÖaw Qcu`_!MH`}st0ZuMC, ܮj5܏J {Q3xR)ݱ7+*S+"HWI YK|_`9x9Iv] pxjm{i ~p!ӗ?3E]"AP$w[}l=Y6<(dϤH 4/P&sإ) t^ 磣t%JJrcT~ʢJǀЈzps.i_TḬ>Ȁe gi RuR()$e+uavK V ͦ!R8 p_rnY:x)[|4cuPd߲Tn_$4A7rRg)wMp\/~t~#zh@0 `hUZ$A ^0HFQ.`o|W7hPH/bc>8kgKzDDzIOAU|mU _Zn ~웧x| 2_rb#:FXWA02V]L-L92$̺ Ü OAДecBe2_UJ;LĴd,K| ozenfm6] MYnw^wNI>)^CmA[t_p= ,!JCn1%ǒg-FvRZӊ Oi-dY 9- PK^Wfq٠0I<-n_}8 7&g x?k3\z.牮3( L}[y苜/;-^'MNU^(l5J3a{E3|kb@);[ @䖳A\5nn1B,+z(9~*x7/bBf˜W/k,ښ>\7<ěFҹ qz]+ o 7N;RpX hўDs7kDvy*y8t)NowbakgT.inC)]8P59s5lh^KC:1d&>ZςA6e2c+-yJ]"\GQ-JⰇo/c 6BƄfsh ]4'Q!{+_pmūGl~n=z:Y&rA 6}4(JG$V!O%e)b!!5\K>9~>fm4xz]Igi?g^ˑi`:Yi8nR8>ڳmu7'QZ /.4@/%[Zh i2) w8XE\69?tynWjqݏs}y:RQBml)܄cPzY늈f\ّ025Ӆqix@t@&;_[G[T~ƽ@R(_/G/qZ> r>\V˧$zw?D_dVRXS~D\oeVOCü8[ac0ۚÂcw-tg1pW/HM!JX@^!!oqL"zkL/b1l_<)O`w=9!**&KkWgݿs!mh϶{#`Y8!,!kj6ɴ~T -# 7M_0?wɸrto2S%MBQ-AWip`a0*YGxbn}Kr=pҕHv g@QA H]<'&c*wKuCtU^ 65Bn_<Ƿ 2,0GXvD~=[%{e|پ{,॥? 4GEZHTnt-ZVv9㙤 VnD`CM3b>&M_\(=kt D\췟mi[ojsy# .(=TA_Bp@y9r__@N2Xu(NnyPri7J,gT 2n3 -xыG%zQ& :̵+&~(׷zGNa]_ M!l4Brg!84OE|y* +#5V޷XU+i]w!kPDMw 㱯[j]K7H]˄ as [40vo`s4%6fط%clH̭צiczeJ}HmhDƫ]€`yE,u:.wf!;K/Y]uz,lHp 9=^iDeKR<0QUeې@NJ@&u|B p۵*s7|ۇJ o_~]?VmwYi(2=W}ҥ'՗y<|?9ޙGYRs_l= 0l2HAH2XJNNDb'rp%|r u,c[R$$` ɀ0 ,=Kwn{-ݼ_{^}w7>"o2*5Oox|_/Qz [([ޤԼ>{IhJ@!<5:8۪ SJHH9d~45Wɇ[I #=6QPӭ<<㿆~M98I@ ţ85!L;CZbύMspb|"H62zD"DH~LO_|_s:1[}5-"kD v hd6$\@ 8v,hF5-^ f*3dqRH\ se(sj%:z{L|s_>4 n~MsWa543}f_A~ұ\*kدl4FEZן 0ԶO^+\%z\|dz(&i=tz[f&.8ϖygi?J4|g raD'[J>n~Oٿ6QBzKDSA}gш Un3<*̶=ɗƲ{^aj$+1h2s"H3s iWͧ\O O/#e׷5gø.go,BMH,س1 $Ջƒ/y]׿z]w,% ̪B&$M8#BO='<эg+"t[`_<ͥgB"%~شȇzw^ԯi2Y_—u3Wo!Bл^_?jdPQ"| iс/D"L-}I ˯r{\ 9v(J| }zj k}w/wZO﷗cmBzdA4cJJ3E?}-mf@b} |985uYP0l#7^oD26)Ƕ{TTi?!=g1$9@m{D25!"U(CI33):"ΟM ڶYua3灜c{ ` Hϐٯl$ h [WZj-m'Bɟq?ޱe๩(SKse{r!5U!Ϳp3&sek|/=as0#$gBy^fXP hi6H}C 'X  |0Z Jf%D!<$?R [XI-Dey #:J7ʚܗX/;La ɃFSrv @)eazsސ(m3 X&2I$#'%I'8-{I H$s v4s0Aדߚ0k%?7Xn`Βy WVmU)0A"YCB}5#`zm&`+{px->F*!V V՟Fğ`,q;|&"NJU;+{˞HcBhd!4#42-ItMi"ٟ)&=R'\`l2=0 \ %z\140@YI޽q KnۣmM I{NRz=~5ށd1:eMh֔ ЀIIK Z^? ,B@;bfnI)=)foi8 |ydu[ ԁZ>ہfBv-sbƞͥJ7(dO>=C8VSҺPIDATۂn_ sEٷ伽dԼo+ cFлZ-G UR|t5`{:ѕ0^,Ѐ0R|3$9R|q>ifZ?f/7.: qJ<`_-EjRdύ{gh=٫Bu? @\;;&q kxAκRߣgKakMAFZ@5[I@k|4ޡlZ|qJ{@O,3;Po#RXc'ر;/ZǍkXᢞKzM~ꜩT>5P( )u,X^Mmkdu+r^[r>ˎӟ_<:[fI/9bdKdJ;H/Ysx3l,"Уu?oy*'7:P!m)_caZ #M4KO֖ƫu$r(IG*LG* t䫌tpl'΅!Tx'{c}m9a'v LVӸEo@J"[I0杀dx,/Ǫ->ݲPu-*.d(RmKR©+ |.j2kJHR‘,y\I#9c?2>nfY Rho7Z_7#x5gǢm@صDx+HF΅yz\3_vk/v> BW"Gvm/gv E0d]Sc_L"Hpq˼2> ?Y"gTJ6-G0FL>ZcW/Uw16F vC4n5PvwVF*4tOR#U~>iζ[oՀOH`Il) `7p-@6p ZvyK# @!:5*|/\.佾RJHm'3U(t) im3B 7~r$wKc瞰_ǁa?ݖO ~K|펳ٓu('ٌDޘCһ!%Gdl7{.U_' W1y<*F贜Ox p~Z"0dz ]iɮ*|i i -ތ|o$nkۀfoD@Y5yoܥD^ (EHpS#Z  6 Y}зx`~vE:KQqK}f5},qZYvb ˮ r$F6 =ͷHUCv /Tϵz[JC,H.t/+ Jr <|;QT%#"Sgi=:3]XFIzUh_^yF0Qc>۷l]ﳦWK7ZvIL:| ժdlN39QnɌԄ/&߫mVzE@8Nf ЫlS0c ص}k`:?8Tgc\|6hdžU>Ș,#PS 85q\#u3xA21\9cԄ#f =gyxe̻;Kc1DpnMG>e|q=! QTBe t A>c#d'+8ơpm1yv2"lЛ`4.R1aٺL~djrg(a*n𺌤bJҊR:Mfvd á!.@X&W-zCM(T+ ͳk%HԤ+a#U9m s-T-pb֫$J!muR.i-VTfs(ks'^F6Q^k5ӝ_f;;xoܮj|MoMOcV|@Z/ד|20' Ԁ ܉ {є%0=q`O7qԤ/"a,;3(y+&ʑ7 h;$({v3Moו(tE[8bZQ@biF|TIANPbҿH 8$@wXuI~P|~̕eu3utϬA)w dz Y`n@x :u_.LpzsVE1_({*_*ъ]E׀` F]u"@skL±'򠴪]oDVd]55(4O" ɽ;nY<#(v$Ejhx&8u!oVxd`@>8Or G5 @G*՟vfIaZ\:@ZTǡIC3\}}~l(,CM.2X͓'C9}cF4 yE3q6<$9<.$h4OVX`S{"T+uE(!9_3SFg& H.P*-r6{UAQO~Ÿ}bڊә%10ϟX//I +ӰŐg\y \?G Yգ%Y#H >?g{Z=vlG蠘߶N&P$j >NsLJ8qT5֬K.)y)];Χ?Zn܏ۿ(X`j5}(uNٸc>_, dE]o @Cj|2{_1`UDd4n[4< !6yolpr- =@ |ʼG> o*9q3þCFV- an;bm%e G/=m~úb~/tGxlH%c#G" 9qdb,B|˨0+&$&&[e)JVpJ1!*I[wc?c6G_Uh8D`LܵZ PҿqH]5-s/0T1d|([:TQVVMo>p+ Uu[:D&@7d\ѶUS0uKK῔SGn -)3yLwm ہ^vҚR^EnkM7?K@u HWwͲut WRP~>DoUm˻D|KG%a%3ѵYBwnh|J8p-]wm&PP/xK8 Jwߵ%>}n;((w7A jo/FVu&P_U.0n6YuYp5pOR;^͠f{+,I7u_Υ7Lr# >K;Qw/u_@z灟C `t-By(? &ZZk%`3p^=ěJP÷=֮5ٺ>Q n2xJ7\0j with on [and])... yoda --help yoda --version Options: --interval Update interval in seconds [default: 2] -m, --match Filter devices by description substring -n, --pick Pick among many results for a given filter --vendor Filter devices by vendor id --product Filter devices by product id --release Filter devices by release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus --unsafe Comma-separated bleeding-edge features to enable -v, --verbose Output additional information -g, --debug Show debug information on stderr --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --use-device-controller Use the control loop integrated to the device (MPG coreliquid device) --version Display the version number --help Show this message Requirements: all platforms liquidctl, including the Python APIs (pip install liquidctl) Linux/FreeBSD psutil [optional] (pip install psutil) macOS iStats (gem install iStats) Windows none, system sensors not yet supported Changelog: 0.0.6 Add --use-device-controller and make psutil optional. 0.0.5 Document how profiles are specified 0.0.4 Fix casing of log and error messages 0.0.3 Remove duplicate option definition 0.0.2 Add low-pass filter and basic error handling. 0.0.1 Generalization of krakencurve-poc 0.0.2 to multiple devices. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import ast import logging import math import sys import time from datetime import datetime from docopt import docopt import liquidctl.cli as _borrow from liquidctl.util import normalize_profile, interpolate_profile import liquidctl.driver VERSION = "0.0.6" LOGGER = logging.getLogger(__name__) INTERNAL_CHIP_NAME = "_internal" MAX_FAILURES = 3 if sys.platform == "darwin": import re import subprocess elif sys.platform.startswith("linux") or sys.platform.startswith("freebsd"): try: import psutil except ModuleNotFoundError: psutil = None def read_sensors(device, **kwargs): sensors = {} for k, v, u in device.get_status(**kwargs): if u == "°C": sensor_name = k.lower().replace(" ", "_").replace("_temperature", "") sensors[f"{INTERNAL_CHIP_NAME}.{sensor_name}"] = v if sys.platform == "darwin": istats_stdout = subprocess.check_output(["istats"]).decode("utf-8") for line in istats_stdout.split("\n"): if line.startswith("CPU"): cpu_temp = float(re.search(r"\d+\.\d+", line).group(0)) sensors["istats.cpu"] = cpu_temp break elif psutil: for m, li in psutil.sensors_temperatures().items(): for label, current, _, _ in li: sensor_name = label.lower().replace(" ", "_") sensors[f"{m}.{sensor_name}"] = current sensors["cpu_freq"] = psutil.cpu_freq().current return sensors def show_sensors(device, **kwargs): print("{:<60} {:>14}".format("Sensor identifier", "Value")) print("-" * 80) sensors = read_sensors(device, **kwargs) for k, v in sensors.items(): unit = "MHz" if k == "cpu_freq" else "°C" print(f"{k:<60} {v:>14.1f} {unit}") def parse_profile(arg, mintemp=0, maxtemp=100, minduty=0, maxduty=100, str_allowed=False): """Parse, validate and normalize a temperature–duty profile. >>> parse_profile('smart', str_allowed=True) 'smart' >>> parse_profile('(20,30),(30,50),(34,80),(40,90)', 0, 60, 25, 100) [(20, 30), (30, 50), (34, 80), (40, 90), (60, 100)] >>> parse_profile('35', 0, 60, 25, 100) [(0, 35), (59, 35), (60, 100)] The profile is validated in structure and acceptable ranges. Duty is checked against `minduty` and `maxduty`. Temperature must be between `mintemp` and `maxtemp`. >>> parse_profile('(20,30),(50,100', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: profile must be comma-separated (temperature, duty) tuples or supported mode name >>> parse_profile('(20,30),(50,100,2)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: profile must be comma-separated (temperature, duty) tuples >>> parse_profile('(20,30),(50,97.6)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: duty must be integer between 25 and 100 >>> parse_profile('(20,15),(50,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: duty must be integer between 25 and 100 >>> parse_profile('(20,30),(70,100)', 0, 60, 25, 100) Traceback (most recent call last): ... ValueError: temperature must be integer between 0 and 60 """ try: if str_allowed and arg in liquidctl.driver.msi.MpgCooler.BUILTIN_MODES: return arg else: val = ast.literal_eval("[" + arg + "]") if len(val) == 1 and isinstance(val[0], int): # for arg == '' set fixed duty between mintemp and maxtemp - 1 val = [(mintemp, val[0]), (maxtemp - 1, val[0])] except: raise ValueError( "profile must be comma-separated (temperature, duty) tuples or supported mode name" ) for step in val: if not isinstance(step, tuple) or len(step) != 2: raise ValueError("profile must be comma-separated (temperature, duty) tuples") temp, duty = step if not isinstance(temp, int) or temp < mintemp or temp > maxtemp: raise ValueError( "temperature must be integer between {} and {}".format(mintemp, maxtemp) ) if not isinstance(duty, int) or duty < minduty or duty > maxduty: raise ValueError("duty must be integer between {} and {}".format(minduty, maxduty)) return normalize_profile(val, critx=maxtemp) def auto_control(device, channels, profiles, sensors, update_interval, **kwargs): """Communicate sensor data directly with the device. Implemented for use with the MSI coreliquid AIO. Allows compatible devices to utilize their internal control loop to determine appropriate fan speeds for the CPU temperature. """ assert getattr( device, "HAS_AUTOCONTROL", False ), f"No registered control loop capability for device {device}!" device.set_profiles(channels, profiles, **kwargs) assert all( s == sensors[0] for s in sensors ), "Controlling different channels with different sensors not possible with device control" sensor = sensors[0] LOGGER.info("starting...") failures = 0 while True: try: sensor_data = read_sensors(device, **kwargs) temp = sensor_data[sensor] freq = sensor_data.get("cpu_freq", 0) device.set_time(datetime.now(), **kwargs) device.set_hardware_status( temp, cpu_f=freq, gpu_f=sensor_data.get("gpu_freq", 0), gpu_U=sensor_data.get("gpu_usage", 0), **kwargs, ) failures = 0 except Exception as err: failures += 1 LOGGER.error(err) if failures >= MAX_FAILURES: LOGGER.critical("too many failures in a row: %d", failures) raise time.sleep(update_interval) def control(device, channels, profiles, sensors, update_interval, **kwargs): LOGGER.info( "device: %s on bus %s and address %s", device.description, device.bus, device.address ) for channel, profile, sensor in zip(channels, profiles, sensors): LOGGER.info("channel: %s following profile %s on %s", channel, str(profile), sensor) averages = [None] * len(channels) cutoff_freq = 1 / update_interval / 10 alpha = 1 - math.exp(-2 * math.pi * cutoff_freq) LOGGER.info( "update interval: %d s; cutoff frequency (low-pass): %.2f Hz; ema alpha: %.2f", update_interval, cutoff_freq, alpha, ) try: # more efficient and safer API, but only supported by very few devices apply_duty = device.set_instantaneous_speed except AttributeError: apply_duty = device.set_fixed_speed LOGGER.info("starting...") failures = 0 while True: try: sensor_data = read_sensors(device, **kwargs) for i, (channel, profile, sensor) in enumerate(zip(channels, profiles, sensors)): # compute the exponential moving average (ema), used as a low-pass filter (lpf) ema = averages[i] sample = sensor_data[sensor] if ema is None: ema = sample else: ema = alpha * sample + (1 - alpha) * ema averages[i] = ema # interpolate on sensor ema and apply corresponding duty duty = interpolate_profile(profile, ema) LOGGER.info( "%s control: lpf(%s) = lpf(%.1f°C) = %.1f°C => duty := %d%%", channel, sensor, sample, ema, duty, ) apply_duty(channel, duty, **kwargs) if getattr(device, "NEEDS_TIME", False): device.set_time(datetime.now(), **kwargs) if getattr(device, "NEEDS_HWSTATUS", False): device.set_hardware_status( sensor_data[sensors[0]], cpu_f=sensor_data.get("cpu_freq", 0), gpu_f=sensor_data.get("gpu_freq", 0), gpu_U=sensor_data.get("gpu_usage", 0), **kwargs, ) failures = 0 except Exception as err: failures += 1 LOGGER.error(err) if failures >= MAX_FAILURES: LOGGER.critical("too many failures in a row: %d", failures) raise time.sleep(update_interval) if __name__ == "__main__": if len(sys.argv) == 2 and sys.argv[1] == "doctest": import doctest doctest.testmod(verbose=True) sys.exit(0) args = docopt(__doc__, version="yoda v{}".format(VERSION)) if args["--debug"]: args["--verbose"] = True logging.basicConfig(level=logging.DEBUG, format="[%(levelname)s] %(name)s: %(message)s") import liquidctl.version LOGGER.debug("yoda v%s", VERSION) LOGGER.debug("liquidctl v%s", liquidctl.version.__version__) elif args["--verbose"]: logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s") LOGGER.setLevel(logging.INFO) else: logging.basicConfig(level=logging.WARNING, format="%(levelname)s: %(message)s") sys.tracebacklimit = 0 if args["--unsafe"] is not None: unsafe = args["--unsafe"].lower().split(",") else: unsafe = [] if (sys.platform.startswith("linux") or sys.platform.startswith("freebsd")) and not psutil: LOGGER.warning("system sensors are not available, psutil not found") frwd = _borrow._make_opts(args) selected = list(liquidctl.driver.find_liquidctl_devices(**frwd)) if len(selected) > 1: raise SystemExit( "too many devices, filter or select one. See liquidctl --help and yoda --help." ) elif len(selected) == 0: raise SystemExit("no devices matches available drivers and selection criteria") device = selected[0] device.connect(unsafe=unsafe) try: if args["show-sensors"]: show_sensors(device, unsafe=unsafe) elif args["control"]: if args["--use-device-controller"]: auto_control( device, args[""], list(map(lambda p: parse_profile(p, str_allowed=True), args[""])), args[""], update_interval=int(args["--interval"]), unsafe=unsafe, ) else: control( device, args[""], list(map(parse_profile, args[""])), args[""], update_interval=int(args["--interval"]), unsafe=unsafe, ) else: raise Exception("nothing to do") except KeyboardInterrupt: LOGGER.info("stopped by user.") finally: device.disconnect() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744525883.724175 liquidctl-1.15.0/liquidctl/0000755000175000017500000000000014776655074014655 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/__init__.py0000644000175000017500000000505614562144457016763 0ustar00jonasjonas"""Monitor and control liquid coolers and other devices. liquidctl provides facilities for monitoring and controlling liquid coolers and other hardware monitoring or LED controller devices in Python: from liquidctl import find_liquidctl_devices # Find all connected and supported devices. devices = find_liquidctl_devices() for dev in devices: # Connect to the device. In this example we use a context manager, but # the connection can also be manually managed. The context manager # automatically calls `disconnect`; when managing the connection # manually, `disconnect` must eventually be called, even if an # exception is raised. with dev.connect(): print(f'{dev.description} at {dev.bus}:{dev.address}:') # Devices should be initialized after every boot. In this example # we assume that this has not been done before. print('- initialize') init_status = dev.initialize() # Print all data returned by `initialize`. if init_status: for key, value, unit in init_status: print(f'- {key}: {value} {unit}') # Get regular status information from the device. status = dev.get_status() # Print all data returned by `get_status`. print('- get status') for key, value, unit in status: print(f'- {key}: {value} {unit}') # For a particular device, set the pump LEDs to red. if 'Kraken' in dev.description: print('- set pump to radical red') radical_red = [0xff, 0x35, 0x5e] dev.set_color(channel='pump', mode='fixed', colors=[radical_red]) A command-line interface is also available: $ python -m liquidctl --help Once the liquidctl package is installed, a `liquidctl` executable should also be available: $ liquidctl --help Copyright 2018–2023 Jonas Malaco, Marshall Asch, CaseySJ, Tom Frey, Andrew Robertson, ParkerMc, Aleksa Savic, Shady Nawara and contributors Some modules also incorporate or use as reference work by leaty, Ksenija Stanojevic, Alexander Tong, Jens Neumaier, Kristóf Jakab, Sean Nelson, Chris Griffith, notaz, realies and Thomas Pircher. This is mentioned in the module docstring, along with appropriate additional copyright notices. SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style from liquidctl.driver import find_liquidctl_devices from liquidctl.error import * from liquidctl.version import __version__ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/__main__.py0000644000175000017500000000042614562144457016740 0ustar00jonasjonas"""liquidctl – monitor and control liquid coolers and other devices. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style import sys from liquidctl.cli import main if __name__ == "__main__": sys.exit(main()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl/_version.py0000644000175000017500000000100114776655073017042 0ustar00jonasjonas# file generated by setuptools-scm # don't change, don't track in version control __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] TYPE_CHECKING = False if TYPE_CHECKING: from typing import Tuple from typing import Union VERSION_TUPLE = Tuple[Union[int, str], ...] else: VERSION_TUPLE = object version: str __version__: str __version_tuple__: VERSION_TUPLE version_tuple: VERSION_TUPLE __version__ = version = '1.15.0' __version_tuple__ = version_tuple = (1, 15, 0) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726520621.0 liquidctl-1.15.0/liquidctl/cli.py0000644000175000017500000004407314672116455015774 0ustar00jonasjonas"""liquidctl – monitor and control liquid coolers and other devices. Usage: liquidctl [options] list liquidctl [options] initialize [all] liquidctl [options] status liquidctl [options] set speed ( ) ... liquidctl [options] set speed liquidctl [options] set color [] ... liquidctl [options] set screen [] liquidctl --help liquidctl --version Device selection options (see: list -v): -m, --match Filter devices by description substring -n, --pick Pick among many results for a given filter --vendor Filter devices by hexadecimal vendor ID --product Filter devices by hexadecimal product ID --release Filter devices by hexadecimal release number --serial Filter devices by serial number --bus Filter devices by bus --address
Filter devices by address in bus --usb-port Filter devices by USB port in bus Animation options (devices/modes can support zero or more): --speed Abstract animation speed (device/mode specific) --time-per-color Time to wait on each color (seconds) --time-off Time to wait with the LED turned off (seconds) --alert-threshold Threshold temperature for a visual alert (°C) --alert-color Color used by the visual high temperature alert --direction If the pattern should move forward or backward. --start-led The first led to start the effect at --maximum-leds The number of LED's the effect should apply to Other device options: --single-12v-ocp Enable single rail +12V OCP --pump-mode Set the pump mode (certain Corsair coolers) --fan-mode :[,...] Set the mode for each fan (certain Corsair devices) --temperature-sensor The temperature sensor number for the Commander Pro --legacy-690lc Use Asetek 690LC in legacy mode (old Krakens) --non-volatile Store on non-volatile controller memory --direct-access Directly access the device despite kernel drivers --unsafe Comma-separated bleeding-edge features to enable Other interface options: -v, --verbose Output additional information -g, --debug Show debug information on stderr --json JSON output (list/initialization/status) --version Display the version number --help Show this message Deprecated: -d, --device Select device by listing index Copyright 2018–2023 Jonas Malaco, Marshall Asch, CaseySJ, Tom Frey, Andrew Robertson, ParkerMc, Aleksa Savic, Shady Nawara and contributors Some modules also incorporate or use as reference work by leaty, Ksenija Stanojevic, Alexander Tong, Jens Neumaier, Kristóf Jakab, Sean Nelson, Chris Griffith, notaz, realies and Thomas Pircher. This is mentioned in the module docstring, along with appropriate additional copyright notices. SPDX-License-Identifier: GPL-3.0-or-later 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 General Public License for more details. """ import datetime import errno import inspect import json import locale import logging import os import platform import re import sys from importlib.metadata import distribution, version, PackageNotFoundError from numbers import Number from traceback import format_exception import colorlog from docopt import docopt from liquidctl import __version__ from liquidctl.driver import * from liquidctl.error import LiquidctlError from liquidctl.util import color_from_str, fan_mode_parser # conversion from CLI arg to internal option; as options as forwarded to bused # and drivers, they must: # - have no default value in the CLI level (not forwarded unless explicitly set); # - and avoid unintentional conflicts with target function arguments _PARSE_ARG = { '--vendor': lambda x: int(x, 16), '--product': lambda x: int(x, 16), '--release': lambda x: int(x, 16), '--serial': str, '--bus': str, '--address': str, '--usb-port': lambda x: tuple(map(int, x.split('.'))), '--match': str, '--pick': int, '--speed': str.lower, '--time-per-color': int, '--time-off': int, '--alert-threshold': int, '--alert-color': color_from_str, '--temperature-sensor': int, '--direction': str.lower, '--start-led': int, '--maximum-leds': int, '--single-12v-ocp': bool, '--pump-mode': str.lower, '--legacy-690lc': bool, '--non-volatile': bool, '--direct-access': bool, '--fan-mode': lambda x: fan_mode_parser(x), '--unsafe': lambda x: x.lower().split(','), '--verbose': bool, '--debug': bool, } # options that cause liquidctl.driver.find_liquidctl_devices to ommit devices _FILTER_OPTIONS = [ 'vendor', 'product', 'release', 'serial', 'bus', 'address', 'usb-port', 'match', 'pick', # --device generates no option ] # custom number formats for values of select units _VALUE_FORMATS = { '%': '.0f', 'A': '.2f', 'V': '.2f', 'W': '.2f', 'rpm': '.0f', '°C': '.1f', } _LOGGER = logging.getLogger(__name__) def _list_devices_objs(devices): def getattr_or(object, name, default=None): """Call `getattr` and return `default` on exceptions.""" try: return getattr(object, name, default) except Exception: return default return [ { 'description': dev.description, 'vendor_id': dev.vendor_id, 'product_id': dev.product_id, 'release_number': dev.release_number, 'serial_number': getattr_or(dev, 'serial_number', None), 'bus': dev.bus, 'address': dev.address, 'port': dev.port, 'driver': type(dev).__name__, # deprecated: 'experimental': '(experimental)' in dev.description, } for dev in devices ] def _list_devices_human(devices, *, using_filters, device_id, verbose, debug, **opts): for i, dev in enumerate(devices): warnings = [] if not using_filters: print(f'Device #{i}: {dev.description}') elif device_id is not None: print(f'Device #{device_id}: {dev.description}') else: print(f'Result #{i}: {dev.description}') if not verbose: continue if dev.vendor_id: print(f'├── Vendor ID: {dev.vendor_id:#06x}') if dev.product_id: print(f'├── Product ID: {dev.product_id:#06x}') if dev.release_number: print(f'├── Release number: {dev.release_number:#06x}') try: if dev.serial_number: print(f'├── Serial number: {dev.serial_number}') except: msg = 'could not read the serial number' if sys.platform.startswith('linux') and os.geteuid: msg += ' (requires root privileges)' elif sys.platform in ['win32', 'cygwin'] and 'Hid' not in type(dev.device).__name__: msg += ' (device possibly requires a kernel driver)' if debug: _LOGGER.exception(msg.capitalize()) else: warnings.append(msg) print(f'├── Bus: {dev.bus}') print(f'├── Address: {dev.address}') if dev.port: port = '.'.join(map(str, dev.port)) print(f'├── Port: {port}') print(f'└── Driver: {type(dev).__name__}') if debug: driver_hier = (i.__name__ for i in inspect.getmro(type(dev))) _LOGGER.debug('MRO: %s', ', '.join(driver_hier)) for msg in warnings: _LOGGER.warning(msg) print('') assert 'device' not in opts or len(devices) <= 1, 'too many results listed with --device' def _dev_status_obj(dev, status): # don't suppress devices without status data (typically from initialize) if not status: status = [] # convert to types that are more suitable to serialization, when that # cannot be done later (e.g. because it requires adjusting the `unit`) def convert(i): key, val, unit = i if isinstance(val, datetime.timedelta): val = val.total_seconds() unit = 's' return { 'key': key, 'value': val, 'unit': unit } return { 'bus': dev.bus, 'address': dev.address, 'description': dev.description, 'status': [convert(x) for x in status] } def _print_dev_status(dev, status): if not status: return print(dev.description) tmp = [] kcols, vcols = 0, 0 for k, v, u in status: if isinstance(v, datetime.timedelta): v = str(v) elif isinstance(v, bool): v = 'Yes' if v else 'No' elif v is None: v = 'N/A' else: valfmt = _VALUE_FORMATS.get(u, '') v = f'{v:{valfmt}}' kcols = max(kcols, len(k)) vcols = max(vcols, len(v)) tmp.append((k, v, u)) for k, v, u in tmp[:-1]: print(f'├── {k:<{kcols}} {v:>{vcols}} {u}') k, v, u = tmp[-1] print(f'└── {k:<{kcols}} {v:>{vcols}} {u}') print('') def _device_set_color(dev, args, **opts): color = map(color_from_str, args['']) dev.set_color(args[''].lower(), args[''].lower(), color, **opts) def _device_set_screen(dev, args, **opts): dev.set_screen(args[""], args[""], args[""], **opts) def _device_set_speed(dev, args, **opts): if len(args['']) > 0: profile = zip(map(int, args['']), map(int, args[''])) dev.set_speed_profile(args[''].lower(), profile, **opts) else: dev.set_fixed_speed(args[''].lower(), int(args[''][0]), **opts) def _make_opts(args): opts = {} for arg, val in args.items(): if val is not None and arg in _PARSE_ARG: opt = arg.replace('--', '').replace('-', '_') opts[opt] = _PARSE_ARG[arg](val) return opts def _log_env_infos(): _LOGGER.debug('script: %s', sys.argv[0]) _LOGGER.debug('version: %s', __version__) _LOGGER.debug('platform: %s', platform.platform()) _LOGGER.debug('python: %s', sys.version.replace('\n', ' ')) _LOGGER.debug('encoding: %s current, %s preferred, utf8_mode %s', locale.getlocale()[1], locale.getpreferredencoding(), sys.flags.utf8_mode) try: dist = distribution('liquidctl') except PackageNotFoundError: _LOGGER.debug('not installed, package metadata not available') return for req in dist.requires: name = re.search('^[a-zA-Z0-9]([a-zA-Z0-9._-]*)', req).group(0) try: _LOGGER.debug('with %s: %s', name, version(name)) except Exception as err: _LOGGER.debug('with %s: version n/a (%s)', name, err) class _ErrorAcc: __slots__ = ['_errors'] def __init__(self): self._errors = 0 def log(self, msg, *args, err=None, show_err=False): self._errors += 1 if err: # log the err with traceback before reporting it properly, this time # without traceback; this puts error messages are at the bottom of the # output, where most users first look for them _LOGGER.info('detailed error: %s: %r', msg, err, *args, exc_info=True) if show_err and err: _LOGGER.error('%s: %r', msg, err, *args) else: _LOGGER.error(msg, *args) def exit_code(self): return 0 if self.is_empty() else 1 def is_empty(self): return not bool(self._errors) def main(): args = docopt(__doc__) if args['--version']: print(f'liquidctl v{__version__} ({platform.platform()})') sys.exit(0) if args['--debug']: args['--verbose'] = True log_fmt = '%(log_color)s[%(levelname)s] (%(module)s) (%(funcName)s): %(message)s' log_level = logging.DEBUG elif args['--verbose']: log_fmt = '%(log_color)s%(levelname)s: %(message)s' log_level = logging.INFO else: log_fmt = '%(log_color)s%(levelname)s: %(message)s' log_level = logging.WARNING sys.tracebacklimit = 0 if sys.platform == 'win32': log_colors = { 'DEBUG': f'bold_blue', 'INFO': f'bold_purple', 'WARNING': 'yellow,bold', 'ERROR': 'red,bold', 'CRITICAL': 'red,bold,bg_white', } else: log_colors = { 'DEBUG': f'blue', 'INFO': f'purple', 'WARNING': 'yellow,bold', 'ERROR': 'red,bold', 'CRITICAL': 'red,bold,bg_white', } log_fmtter = colorlog.TTYColoredFormatter(fmt=log_fmt, stream=sys.stderr, log_colors=log_colors) log_handler = logging.StreamHandler() log_handler.setFormatter(log_fmtter) logging.basicConfig(level=log_level, handlers=[log_handler]) _log_env_infos() if __name__ == '__main__': _LOGGER.warning('python -m liquidctl.cli is deprecated, prefer python -m liquidctl') errors = _ErrorAcc() # unlike humans, machines want to know everything; imply verbose everywhere # other than when setting default logging level and format (which are # inherently for human consumption) if args['--json']: args['--verbose'] = True opts = _make_opts(args) filter_count = sum(1 for opt in opts if opt in _FILTER_OPTIONS) device_id = None if not args['--device']: selected = list(find_liquidctl_devices(**opts)) else: _LOGGER.warning('-d/--device is deprecated, prefer --match or other selection options') device_id = int(args['--device']) no_filters = {opt: val for opt, val in opts.items() if opt not in _FILTER_OPTIONS} compat = list(find_liquidctl_devices(**no_filters)) if device_id < 0 or device_id >= len(compat): errors.log('device index out of bounds') return errors.exit_code() if filter_count: # check that --device matches other filter criteria matched_devs = [dev.device for dev in find_liquidctl_devices(**opts)] if compat[device_id].device not in matched_devs: errors.log('device index does not match remaining selection criteria') return errors.exit_code() _LOGGER.warning('mixing --device with other filters is not recommended; ' 'to disambiguate between results prefer --pick ') selected = [compat[device_id]] if args['list']: if args['--json']: objs = _list_devices_objs(selected) print(json.dumps(objs, ensure_ascii=(os.getenv('LANG', None) == 'C'))) else: _list_devices_human(selected, using_filters=bool(filter_count), device_id=device_id, json=json, **opts) return if len(selected) > 1 and not (args['status'] or args['all']): errors.log('multiple devices available, use filters to select one (see: liquidctl --help)') return errors.exit_code() elif len(selected) == 0: errors.log('no device matches available drivers and selection criteria') return errors.exit_code() # for json obj_buf = [] for dev in selected: _LOGGER.debug('device: %s', dev.description) try: with dev.connect(**opts): if args['initialize']: status = dev.initialize(**opts) if args['--json']: obj_buf.append(_dev_status_obj(dev, status)) else: _print_dev_status(dev, status) elif args['status']: status = dev.get_status(**opts) if args['--json']: obj_buf.append(_dev_status_obj(dev, status)) else: _print_dev_status(dev, status) elif args['set'] and args['speed']: _device_set_speed(dev, args, **opts) elif args['set'] and args['color']: _device_set_color(dev, args, **opts) elif args['set'] and args['screen']: _device_set_screen(dev, args, **opts) else: assert False, 'unreachable' except LiquidctlError as err: errors.log(f'{dev.description}: {err}', err=err) except OSError as err: # each backend API returns a different subtype of OSError (OSError, # usb.core.USBError or PermissionError) for permission issues if err.errno in [errno.EACCES, errno.EPERM]: errors.log(f'{dev.description}: insufficient permissions', err=err) elif err.args == ('open failed', ): errors.log( f'{dev.description}: could not open, possibly due to insufficient permissions', err=err ) else: errors.log(f'{dev.description}: unexpected OS error', err=err, show_err=True) except Exception as err: errors.log(f'{dev.description}: unexpected error', err=err, show_err=True) if errors.is_empty() and args['--json']: # use __str__ for values that cannot be directly serialized to JSON # (e.g. enums) print(json.dumps(obj_buf, ensure_ascii=(os.getenv('LANG', None) == 'C'), default=lambda x: str(x))) return errors.exit_code() if __name__ == '__main__': sys.exit(main()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7299738 liquidctl-1.15.0/liquidctl/driver/0000755000175000017500000000000014776655074016150 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/liquidctl/driver/__init__.py0000644000175000017500000000535714677373312020263 0ustar00jonasjonas"""liquidctl buses and drivers. The typical use case of generic scripts and interfaces – including the liquidctl CLI – is to instantiate drivers for all known devices found on the system. from liquidctl.driver import * for dev in find_liquidctl_devices(): print(dev.description) Is also possible to find devices compatible with a specific driver. from liquidctl.driver.kraken_two import KrakenTwoDriver for dev in KrakenTwoDriver.find_supported_devices(): print(dev.description) Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import sys from liquidctl.driver.base import BaseBus, find_all_subclasses # automatically enabled drivers from liquidctl.driver import asetek from liquidctl.driver import asetek_pro from liquidctl.driver import asus_ryujin from liquidctl.driver import aura_led from liquidctl.driver import aquacomputer from liquidctl.driver import commander_core from liquidctl.driver import commander_pro from liquidctl.driver import coolit from liquidctl.driver import corsair_hid_psu from liquidctl.driver import hydro_platinum from liquidctl.driver import kraken2 from liquidctl.driver import kraken3 from liquidctl.driver import msi from liquidctl.driver import nzxt_epsu from liquidctl.driver import rgb_fusion2 from liquidctl.driver import smart_device if sys.platform == 'linux': from liquidctl.driver import ddr4 from liquidctl.driver import nvidia def find_liquidctl_devices(pick=None, **kwargs): """Find devices and instantiate corresponding liquidctl drivers. Probes all buses and drivers that have been loaded at the time of the call and yields driver instances. Filter conditions can be passed through to the buses and drivers via `**kwargs`. A driver instance will be yielded for each compatible device that matches the supplied filter conditions. If `pick` is passed, only the driver instance for the `(pick + 1)`-th matched device will be yielded. """ buses = sorted(find_all_subclasses(BaseBus), key=lambda x: (x.__module__, x.__name__)) num = 0 for bus_cls in buses: for dev in bus_cls().find_devices(**kwargs): if pick is not None: if num == pick: yield dev return num += 1 else: yield dev __all__ = [ 'find_liquidctl_devices', ] # allow old driver imports to continue to work by manually placing these into # the module cache, so import liquidctl.driver.foo does not need to # check the filesystem for foo sys.modules['liquidctl.driver.kraken_two'] = kraken2 sys.modules['liquidctl.driver.nzxt_smart_device'] = smart_device sys.modules['liquidctl.driver.seasonic'] = nzxt_epsu ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740977182.0 liquidctl-1.15.0/liquidctl/driver/aquacomputer.py0000644000175000017500000005045414761232036021217 0ustar00jonasjonas"""liquidctl driver for Aquacomputer family of watercooling devices. Aquacomputer D5 Next watercooling pump -------------------------------------- The pump sends a status HID report every second with no initialization being required. The status HID report exposes sensor values such as liquid temperature and two groups of fan sensors, for the pump and the optionally connected fan. These groups provide RPM speed, voltage, current and power readings. The pump additionally exposes +5V and +12V voltage rail readings and eight virtual temperature sensors. The pump and fan can be set to a fixed speed (0-100%). Aquacomputer Farbwerk 360 ------------------------- Farbwerk 360 is an RGB controller and sends a status HID report every second with no initialization being required. The status HID report exposes four physical and sixteen virtual temperature sensor values. Aquacomputer Octo ------------------------- Octo is a fan/RGB controller and sends a status HID report every second with no initialization being required. The status HID report exposes four temperature sensor values and eight groups of fan sensors for optionally connected fans. Octo additionaly exposes sixteen virtual temp sensors through this report. Aquacomputer Quadro ------------------------- Quadro is a fan/RGB controller and sends a status HID report every second with no initialization being required. The status HID report exposes four physical and sixteen virtual temperature sensor values, and four groups of fan sensors for optionally connected fans. Driver ------ Linux has the aquacomputer_d5next driver available since v5.15. Subsequent releases have more functionality and support a wider range of devices (detailed below). If present, it's used instead of reading the status reports directly. Hwmon support: - D5 Next watercooling pump: sensors - 5.15+, direct PWM control - not yet in fully - Farbwerk 360: sensors - 5.18+ - Octo: sensors - 5.19+, direct PWM control - not yet in fully - Quadro: sensors - 6.0+, direct PWM control - not yet in fully Virtual temp sensor reading is supported in 6.0+. Copyright Aleksa Savic and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style import logging, time, errno from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDriver, NotSupportedByDevice from liquidctl.util import u16be_from, clamp, mkCrcFun _LOGGER = logging.getLogger(__name__) _AQC_TEMP_SENSOR_DISCONNECTED = 0x7FFF _AQC_FAN_VOLTAGE_OFFSET = 0x02 _AQC_FAN_CURRENT_OFFSET = 0x04 _AQC_FAN_POWER_OFFSET = 0x06 _AQC_FAN_SPEED_OFFSET = 0x08 _AQC_STATUS_READ_ENDPOINT = 0x01 _AQC_CTRL_REPORT_ID = 0x03 _AQC_FAN_TYPE_OFFSET = 0x00 _AQC_FAN_PERCENT_OFFSET = 0x01 def put_unaligned_be16(value, data, offset): value_be = bytearray(value.to_bytes(2, "big")) data[offset], data[offset + 1] = value_be[0], value_be[1] class Aquacomputer(UsbHidDriver): _DEVICE_D5NEXT = "D5 Next" _DEVICE_FARBWERK360 = "Farbwerk 360" _DEVICE_OCTO = "Octo" _DEVICE_QUADRO = "Quadro" _DEVICE_INFO = { _DEVICE_D5NEXT: { "type": _DEVICE_D5NEXT, "fan_sensors": [0x6C, 0x5F], "temp_sensors": [0x57], "virt_temp_sensors": [0x3F + offset * 2 for offset in range(0, 8)], "plus_5v_voltage": 0x39, "plus_12v_voltage": 0x37, "temp_sensors_label": ["Liquid temperature"], "virt_temp_sensors_label": [f"Soft. Sensor {num}" for num in range(1, 8 + 1)], "fan_speed_label": ["Pump speed", "Fan speed"], "fan_power_label": ["Pump power", "Fan power"], "fan_voltage_label": ["Pump voltage", "Fan voltage"], "fan_current_label": ["Pump current", "Fan current"], "status_report_length": 0x9E, "ctrl_report_length": 0x329, "fan_ctrl": {"pump": 0x96, "fan": 0x41}, "hwmon_ctrl_mapping": {"pump": "pwm1", "fan": "pwm2"}, }, _DEVICE_FARBWERK360: { "type": _DEVICE_FARBWERK360, "temp_sensors": [0x32, 0x34, 0x36, 0x38], "virt_temp_sensors": [0x3A + offset * 2 for offset in range(0, 16)], "temp_sensors_label": ["Sensor 1", "Sensor 2", "Sensor 3", "Sensor 4"], "virt_temp_sensors_label": [f"Soft. Sensor {num}" for num in range(1, 16 + 1)], "status_report_length": 0xB6, }, _DEVICE_OCTO: { "type": _DEVICE_OCTO, "fan_sensors": [0x7D, 0x8A, 0x97, 0xA4, 0xB1, 0xBE, 0xCB, 0xD8], "temp_sensors": [0x3D, 0x3F, 0x41, 0x43], "virt_temp_sensors": [0x45 + offset * 2 for offset in range(0, 16)], "temp_sensors_label": ["Sensor 1", "Sensor 2", "Sensor 3", "Sensor 4"], "virt_temp_sensors_label": [f"Soft. Sensor {num}" for num in range(1, 16 + 1)], "fan_speed_label": [f"Fan {num} speed" for num in range(1, 8 + 1)], "fan_power_label": [f"Fan {num} power" for num in range(1, 8 + 1)], "fan_voltage_label": [f"Fan {num} voltage" for num in range(1, 8 + 1)], "fan_current_label": [f"Fan {num} current" for num in range(1, 8 + 1)], "status_report_length": 0x147, "ctrl_report_length": 0x65F, "fan_ctrl": { name: offset for (name, offset) in zip( [f"fan{i}" for i in range(1, 8 + 1)], [0x5A, 0xAF, 0x104, 0x159, 0x1AE, 0x203, 0x258, 0x2AD], ) }, }, _DEVICE_QUADRO: { "type": _DEVICE_QUADRO, "fan_sensors": [0x70, 0x7D, 0x8A, 0x97], "temp_sensors": [0x34, 0x36, 0x38, 0x3A], "virt_temp_sensors": [0x3C + offset * 2 for offset in range(0, 16)], "temp_sensors_label": ["Sensor 1", "Sensor 2", "Sensor 3", "Sensor 4"], "virt_temp_sensors_label": [f"Soft. Sensor {num}" for num in range(1, 16 + 1)], "fan_speed_label": [f"Fan {num} speed" for num in range(1, 4 + 1)], "fan_power_label": [f"Fan {num} power" for num in range(1, 4 + 1)], "fan_voltage_label": [f"Fan {num} voltage" for num in range(1, 4 + 1)], "fan_current_label": [f"Fan {num} current" for num in range(1, 4 + 1)], "flow_sensor_offset": 0x6E, "status_report_length": 0xDC, "ctrl_report_length": 0x3C1, "fan_ctrl": { name: offset for (name, offset) in zip( [f"fan{i}" for i in range(1, 4 + 1)], [0x36, 0x8B, 0xE0, 0x135], ) }, }, } _MATCHES = [ ( 0x0C70, 0xF00E, "Aquacomputer D5 Next", {"device_info": _DEVICE_INFO[_DEVICE_D5NEXT]}, ), ( 0x0C70, 0xF010, "Aquacomputer Farbwerk 360", {"device_info": _DEVICE_INFO[_DEVICE_FARBWERK360]}, ), ( 0x0C70, 0xF011, "Aquacomputer Octo", {"device_info": _DEVICE_INFO[_DEVICE_OCTO]}, ), ( 0x0C70, 0xF00D, "Aquacomputer Quadro", {"device_info": _DEVICE_INFO[_DEVICE_QUADRO]}, ), ] def __init__(self, device, description, device_info, **kwargs): super().__init__(device, description) # Read when necessary self._firmware_version = None self._serial = None self._device_info = device_info def initialize(self, **kwargs): """Initialize the device and the driver. This method should be called every time the system boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ fw = self.firmware_version serial_number = self._serial_number return [("Firmware version", fw, ""), ("Serial number", serial_number, "")] def _get_status_directly(self): def _read_temp_sensors(offsets_key, labels_key): for idx, temp_sensor_offset in enumerate(self._device_info.get(offsets_key, [])): temp_sensor_value = u16be_from(msg, temp_sensor_offset) if temp_sensor_value != _AQC_TEMP_SENSOR_DISCONNECTED: temp_sensor_reading = ( self._device_info[labels_key][idx], temp_sensor_value * 1e-2, "°C", ) sensor_readings.append(temp_sensor_reading) msg = self._read() sensor_readings = [] # Read temp sensor values _read_temp_sensors("temp_sensors", "temp_sensors_label") # Read virtual temp sensor values _read_temp_sensors("virt_temp_sensors", "virt_temp_sensors_label") # Read fan speed and related values for idx, fan_sensor_offset in enumerate(self._device_info.get("fan_sensors", [])): fan_speed = ( self._device_info["fan_speed_label"][idx], u16be_from(msg, fan_sensor_offset + _AQC_FAN_SPEED_OFFSET), "rpm", ) sensor_readings.append(fan_speed) fan_power = ( self._device_info["fan_power_label"][idx], u16be_from(msg, fan_sensor_offset + _AQC_FAN_POWER_OFFSET) * 1e-2, "W", ) sensor_readings.append(fan_power) fan_voltage = ( self._device_info["fan_voltage_label"][idx], u16be_from(msg, fan_sensor_offset + _AQC_FAN_VOLTAGE_OFFSET) * 1e-2, "V", ) sensor_readings.append(fan_voltage) fan_current = ( self._device_info["fan_current_label"][idx], u16be_from(msg, fan_sensor_offset + _AQC_FAN_CURRENT_OFFSET) * 1e-3, "A", ) sensor_readings.append(fan_current) # Special-case sensor readings if self._device_info["type"] == self._DEVICE_D5NEXT: # Read +5V voltage rail value plus_5v_voltage = ( "+5V voltage", u16be_from(msg, self._device_info["plus_5v_voltage"]) * 1e-2, "V", ) sensor_readings.append(plus_5v_voltage) # Read +12V voltage rail value plus_12v_voltage = ( "+12V voltage", u16be_from(msg, self._device_info["plus_12v_voltage"]) * 1e-2, "V", ) sensor_readings.append(plus_12v_voltage) elif self._device_info["type"] == self._DEVICE_QUADRO: # Read flow sensor value flow_sensor_value = ( "Flow sensor", u16be_from(msg, self._device_info["flow_sensor_offset"]), "dL/h", ) sensor_readings.append(flow_sensor_value) return sensor_readings def _get_status_from_hwmon(self): def _read_temp_sensors(offsets_key, labels_key, idx_add=0): encountered_errors = False for idx, temp_sensor_offset in enumerate(self._device_info.get(offsets_key, [])): try: hwmon_val = self._hwmon.read_int(f"temp{idx + 1 + idx_add}_input") * 1e-3 except OSError as os_error: # For reference, the driver returns ENODATA when a sensor is unset/empty. ENOENT means that the # current driver version does not support virtual sensors, warn the user later if os_error.errno == errno.ENOENT: encountered_errors = True continue temp_sensor_reading = ( self._device_info[labels_key][idx], hwmon_val, "°C", ) sensor_readings.append(temp_sensor_reading) if encountered_errors: _LOGGER.warning( f"some temp sensors cannot be read from %s kernel driver", self._hwmon.driver, ) sensor_readings = [] # Read temp sensor values _read_temp_sensors("temp_sensors", "temp_sensors_label") # Read virtual temp sensor values _read_temp_sensors( "virt_temp_sensors", "virt_temp_sensors_label", len(self._device_info.get("temp_sensors", [])), ) # Read fan speed and related values for idx, fan_sensor_offset in enumerate(self._device_info.get("fan_sensors", [])): fan_speed = ( self._device_info["fan_speed_label"][idx], self._hwmon.read_int(f"fan{idx + 1}_input"), "rpm", ) sensor_readings.append(fan_speed) fan_power = ( self._device_info["fan_power_label"][idx], self._hwmon.read_int(f"power{idx + 1}_input") * 1e-6, "W", ) sensor_readings.append(fan_power) fan_voltage = ( self._device_info["fan_voltage_label"][idx], self._hwmon.read_int(f"in{idx}_input") * 1e-3, "V", ) sensor_readings.append(fan_voltage) fan_current = ( self._device_info["fan_current_label"][idx], self._hwmon.read_int(f"curr{idx + 1}_input") * 1e-3, "A", ) sensor_readings.append(fan_current) # Special-case sensor readings if self._device_info["type"] == self._DEVICE_D5NEXT: # Read +5V voltage rail value plus_5v_voltage = ("+5V voltage", self._hwmon.read_int("in2_input") * 1e-3, "V") sensor_readings.append(plus_5v_voltage) if self._hwmon.has_attribute("in3_input"): # The driver exposes the +12V voltage of the pump (kernel v6.0+), read the value plus_12v_voltage = ("+12V voltage", self._hwmon.read_int("in3_input") * 1e-3, "V") sensor_readings.append(plus_12v_voltage) else: _LOGGER.warning( "+12V voltage cannot be read from %s kernel driver", self._hwmon.driver ) elif self._device_info["type"] == self._DEVICE_QUADRO: # Read flow sensor value flow_sensor_value = ("Flow sensor", self._hwmon.read_int("fan5_input"), "dL/h") sensor_readings.append(flow_sensor_value) return sensor_readings def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self._hwmon and not direct_access: _LOGGER.info("bound to %s kernel driver, reading status from hwmon", self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning( "directly reading the status despite %s kernel driver", self._hwmon.driver ) return self._get_status_directly() def set_speed_profile(self, channel, profile, **kwargs): if ( self._device_info["type"] == self._DEVICE_D5NEXT or self._device_info["type"] == self._DEVICE_OCTO or self._device_info["type"] == self._DEVICE_QUADRO ): # Not yet reverse engineered / implemented raise NotSupportedByDriver() elif self._device_info["type"] == self._DEVICE_FARBWERK360: raise NotSupportedByDevice() def _fan_name_to_hwmon_names(self, channel): if "hwmon_ctrl_mapping" in self._device_info: # Custom fan name to hwmon pwmX translation pwm_name = self._device_info["hwmon_ctrl_mapping"][channel] else: # Otherwise, assume that fanX translates to pwmX pwm_name = f"pwm{channel[3]}" return pwm_name, f"{pwm_name}_enable" def _set_fixed_speed_hwmon(self, channel, duty): hwmon_pwm_name, hwmon_pwm_enable_name = self._fan_name_to_hwmon_names(channel) # Set channel to direct percent mode self._hwmon.write_int(hwmon_pwm_enable_name, 1) # Some devices (Octo, Quadro and Aquaero) can not accept reports in quick succession, so slow down a bit time.sleep(0.2) # Convert duty from percent to PWM range (0-255) pwm_duty = duty * 255 // 100 # Write to hwmon self._hwmon.write_int(hwmon_pwm_name, pwm_duty) def _set_fixed_speed_directly(self, channel, duty): # Request an up to date ctrl report report_length = self._device_info["ctrl_report_length"] ctrl_settings = self.device.get_feature_report(_AQC_CTRL_REPORT_ID, report_length) fan_ctrl_offset = self._device_info["fan_ctrl"][channel] # Set fan to direct percent-value mode ctrl_settings[fan_ctrl_offset + _AQC_FAN_TYPE_OFFSET] = 0 # Write down duty for channel put_unaligned_be16( duty * 100, # Centi-percent ctrl_settings, fan_ctrl_offset + _AQC_FAN_PERCENT_OFFSET, ) # Update checksum value at the end of the report crc16usb_func = mkCrcFun("crc-16-usb") checksum_part = bytes(ctrl_settings[0x01 : report_length - 3 + 1]) checksum_bytes = crc16usb_func(checksum_part) put_unaligned_be16(checksum_bytes, ctrl_settings, report_length - 2) # Some devices (Octo, Quadro and Aquaero) can not accept reports in quick succession, so slow down a bit time.sleep(0.2) self.device.send_feature_report(ctrl_settings) def set_fixed_speed(self, channel, duty, direct_access=False, **kwargs): if self._device_info["type"] == self._DEVICE_FARBWERK360: raise NotSupportedByDevice() # Clamp duty between 0 and 100 duty = clamp(duty, 0, 100) if self._hwmon: hwmon_pwm_name, hwmon_pwm_enable_name = self._fan_name_to_hwmon_names(channel) # pwmX and pwmX_enable attributes are required in order to set the PWM value, # as well as the channel mode to "direct PWM value". Without pwmX_enable, # we can't guarantee that the PWM value will at all be used, as the channel # could be in a different mode (PID, curve, fan follow). if self._hwmon.has_attribute(hwmon_pwm_name) and self._hwmon.has_attribute( hwmon_pwm_enable_name ): # They are, and if we have to use direct access, warn that we are sidestepping the kernel driver if direct_access: _LOGGER.warning( "directly writing fixed speed despite %s kernel driver having support", self._hwmon.driver, ) return self._set_fixed_speed_directly(channel, duty) _LOGGER.info( "bound to %s kernel driver, writing fixed speed to hwmon", self._hwmon.driver ) return self._set_fixed_speed_hwmon(channel, duty) elif not direct_access: _LOGGER.warning( "required PWM functionality is not available in %s kernel driver, falling back to direct access", self._hwmon.driver, ) self._set_fixed_speed_directly(channel, duty) def set_color(self, channel, mode, colors, **kwargs): # Not yet reverse engineered / implemented raise NotSupportedByDriver() def _read_device_statics(self): if self._firmware_version is None or self._serial is None: msg = self._read(clear_first=False) self._firmware_version = u16be_from(msg, 0xD) self._serial = f"{u16be_from(msg, 0x3):05}-{u16be_from(msg, 0x5):05}" @property def firmware_version(self): self._read_device_statics() return self._firmware_version @property def _serial_number(self): self._read_device_statics() return self._serial def _read(self, clear_first=True): if clear_first: self.device.clear_enqueued_reports() msg = self.device.read(self._device_info["status_report_length"]) return msg ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715785037.0 liquidctl-1.15.0/liquidctl/driver/asetek.py0000644000175000017500000004467014621146515017771 0ustar00jonasjonas"""liquidctl drivers for fifth generation Asetek 690LC liquid coolers. Supported devices: - EVGA CLC (120 CL12, 240, 280 or 360); modern generic Asetek 690LC - NZXT Kraken X (X31, X41 or X61); legacy generic Asetek 690LC - NZXT Kraken X (X40 or X60); legacy generic Asetek 690LC - Corsair Hydro H80i GT, H100i GTX or H110i GTX - Corsair Hydro H80i v2, H100i v2 or H115i Copyright Jonas Malaco and contributors Incorporates or uses as reference work by Kristóf Jakab, Sean Nelson and Chris Griffith. SPDX-License-Identifier: GPL-3.0-or-later """ import logging import usb from liquidctl.driver.usb import UsbDriver from liquidctl.error import NotSupportedByDevice from liquidctl.keyval import RuntimeStorage from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _CMD_RUNTIME = 0x10 _CMD_PROFILE = 0x11 _CMD_OVERRIDE = 0x12 _CMD_PUMP_PWM = 0x13 _CMD_LUID = 0x14 _CMD_READ_ONLY_RUNTIME = 0x20 _CMD_STORE_SETTINGS = 0x21 _CMD_EXTERNAL_TEMPERATURE = 0x22 _FIXED_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'pump': (_CMD_PUMP_PWM, 50, 100), # min/max must correspond to _MIN/MAX_PUMP_SPEED_CODE } _VARIABLE_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'fan': (_CMD_PROFILE, 0, 100) } _MAX_PROFILE_POINTS = 6 _CRITICAL_TEMPERATURE = 60 _HIGH_TEMPERATURE = 45 _MIN_PUMP_SPEED_CODE = 0x32 _MAX_PUMP_SPEED_CODE = 0x42 _READ_ENDPOINT = 0x82 _READ_LENGTH = 32 _WRITE_ENDPOINT = 0x2 _LEGACY_FIXED_SPEED_CHANNELS = { # (message type, minimum duty, maximum duty) 'fan': (_CMD_OVERRIDE, 0, 100), 'pump': (_CMD_PUMP_PWM, 50, 100), } # USBXpress specific control parameters; from the USBXpress SDK # (Customization/CP21xx_Customization/AN721SW_Linux/silabs_usb.h) _USBXPRESS_REQUEST = 0x02 _USBXPRESS_FLUSH_BUFFERS = 0x01 _USBXPRESS_CLEAR_TO_SEND = 0x02 _USBXPRESS_NOT_CLEAR_TO_SEND = 0x04 _USBXPRESS_GET_PART_NUM = 0x08 # Unknown control parameters; from Craig's libSiUSBXp and OpenCorsairLink _UNKNOWN_OPEN_REQUEST = 0x00 _UNKNOWN_OPEN_VALUE = 0xffff # Control request type _USBXPRESS = usb.util.CTRL_OUT | usb.util.CTRL_TYPE_VENDOR | usb.util.CTRL_RECIPIENT_DEVICE class _Base690Lc(UsbDriver): """Common methods for Asetek 690LC devices.""" _LEGACY_690LC = False @classmethod def probe(cls, handle, legacy_690lc=False, **kwargs): """Probe `handle` and yield corresponding driver instances.""" if legacy_690lc != cls._LEGACY_690LC: return yield from super().probe(handle, **kwargs) @classmethod def find_supported_devices(cls, **kwargs): """Find devices specifically compatible with this driver. Automatically sets the appropriate value for `legacy_690lc`. """ return super().find_supported_devices(legacy_690lc=cls._LEGACY_690LC, **kwargs) def _configure_flow_control(self, clear_to_send): """Set the software clear-to-send flow control policy for device.""" _LOGGER.debug('set clear to send = %s', clear_to_send) if clear_to_send: self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_CLEAR_TO_SEND) else: self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_NOT_CLEAR_TO_SEND) def _begin_transaction(self): """Begin a new transaction before writing to the device.""" _LOGGER.debug('begin transaction') self.device.claim() self.device.ctrl_transfer(_USBXPRESS, _USBXPRESS_REQUEST, _USBXPRESS_FLUSH_BUFFERS) def _write(self, data): self.device.write(_WRITE_ENDPOINT, data) def _end_transaction_and_read(self): """End the transaction by reading from the device. According to the official documentation, as well as Craig's open-source implementation (libSiUSBXp), it should be necessary to check the queue size and read the data in chunks. However, leviathan and its derivatives seem to work fine without this complexity; we also successfully follow this approach. """ msg = self.device.read(_READ_ENDPOINT, _READ_LENGTH) self.device.release() return msg def _configure_device(self, color1=[0, 0, 0], color2=[0, 0, 0], color3=[255, 0, 0], alert_temp=_HIGH_TEMPERATURE, interval1=0, interval2=0, blackout=False, fading=False, blinking=False, enable_alert=True): self._write([0x10] + color1 + color2 + color3 + [alert_temp, interval1, interval2, not blackout, fading, blinking, enable_alert, 0x00, 0x01]) def _prepare_profile(self, profile, min_duty, max_duty, max_points): opt = list(profile) size = len(opt) if size < 1: raise ValueError('at least one PWM point required') elif size > _MAX_PROFILE_POINTS: raise ValueError(f'too many PWM points ({size}), only {max_points} supported') for i, (temp, duty) in enumerate(opt): opt[i] = (temp, clamp(duty, min_duty, max_duty)) missing = max_points - size if missing: # Some issues were observed when padding with (0°C, 0%), though # they were hard to reproduce. So far it *seems* that in some # instances the device will store the last "valid" profile index # somewhere, and would need another call to initialize() to clear # that up. Padding with (CRIT, 100%) appears to avoid all issues, # at least within the reasonable range of operating temperatures. _LOGGER.info('filling missing %d PWM points with (60°C, 100%%)', missing) opt = opt + [(_CRITICAL_TEMPERATURE, 100)]*missing return opt def connect(self, **kwargs): """Connect to the device. Enables the device to send data to the host. """ ret = super().connect(**kwargs) self._configure_flow_control(clear_to_send=True) return ret def initialize(self, **kwargs): """Initialize the device.""" self._begin_transaction() self._configure_device() self._end_transaction_and_read() def disconnect(self, **kwargs): """Disconnect from the device. Implementation note: unlike SI_Close is supposed to do,¹ do not send _USBXPRESS_NOT_CLEAR_TO_SEND to the device. This allows one program to disconnect without stopping reads from another. Surrounding device.read() with _USBXPRESS_[NOT_]CLEAR_TO_SEND would make more sense, but there seems to be a yet unknown minimum delay necessary for that to work reliably. ¹ https://github.com/craigshelley/SiUSBXp/blob/master/SiUSBXp.c """ super().disconnect(**kwargs) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class _ModernBase690Lc(_Base690Lc): def _persist(self): _LOGGER.debug('instructing device to persist current settings') self._begin_transaction() self._write([_CMD_STORE_SETTINGS]) self._end_transaction_and_read() def initialize(self, non_volatile=False, **kwargs): super().initialize(**kwargs) if non_volatile: self._persist() def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self._begin_transaction() self._write([_CMD_LUID, 0, 0, 0]) msg = self._end_transaction_and_read() firmware = '{}.{}.{}.{}'.format(*tuple(msg[0x17:0x1b])) return [ ('Liquid temperature', msg[10] + msg[14]/10, '°C'), ('Fan speed', msg[0] << 8 | msg[1], 'rpm'), ('Pump speed', msg[8] << 8 | msg[9], 'rpm'), ('Firmware version', firmware, '') ] def set_color(self, channel, mode, colors, time_per_color=1, time_off=None, alert_threshold=_HIGH_TEMPERATURE, alert_color=[255, 0, 0], speed=3, non_volatile=False, **kwargs): """Set the color mode for a specific channel.""" # keyword arguments may have been forwarded from cli args and need parsing colors = list(colors) self._begin_transaction() if mode == 'rainbow': if isinstance(speed, str): speed = int(speed) self._write([0x23, clamp(speed, 1, 6)]) # make sure to clear blinking or... chaos self._configure_device(alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'fading': self._configure_device(fading=True, color1=colors[0], color2=colors[1], interval1=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'blinking': if time_off is None: time_off = time_per_color self._configure_device(blinking=True, color1=colors[0], interval1=clamp(time_off, 1, 255), interval2=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'fixed': self._configure_device(color1=colors[0], alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) self._write([0x23, 0]) elif mode == 'blackout': # stronger than just 'off', suppresses alerts and rainbow self._configure_device(blackout=True, alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) else: raise KeyError(f'unknown lighting mode {mode}') self._end_transaction_and_read() if non_volatile: self._persist() def set_speed_profile(self, channel, profile, non_volatile=False, **kwargs): """Set channel to follow a speed duty profile.""" mtype, dmin, dmax = _VARIABLE_SPEED_CHANNELS[channel] adjusted = self._prepare_profile(profile, dmin, dmax, _MAX_PROFILE_POINTS) for temp, duty in adjusted: _LOGGER.info('setting %s PWM point: (%d°C, %d%%), device interpolated', channel, temp, duty) temps, duties = map(list, zip(*adjusted)) self._begin_transaction() self._write([mtype, 0] + temps + duties) self._end_transaction_and_read() if non_volatile: self._persist() def set_fixed_speed(self, channel, duty, non_volatile=False, **kwargs): """Set channel to a fixed speed duty.""" if channel == 'fan': # While devices seem to recognize a specific channel for fixed fan # speeds (mtype == 0x12), its use can later conflict with custom # profiles. # Note for a future self: the conflict can be cleared with # *another* call to initialize(), i.e. with another # configuration command. _LOGGER.info('using a flat profile to set %s to a fixed duty', channel) self.set_speed_profile(channel, [(0, duty), (_CRITICAL_TEMPERATURE - 1, duty)], non_volatile, **kwargs) return mtype, dmin, dmax = _FIXED_SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) total_levels = _MAX_PUMP_SPEED_CODE - _MIN_PUMP_SPEED_CODE + 1 level = round((duty - dmin)/(dmax - dmin)*total_levels) effective_duty = round(dmin + level*(dmax - dmin)/total_levels) _LOGGER.info('setting %s PWM duty to %d%% (level %d)', channel, effective_duty, level) self._begin_transaction() self._write([mtype, _MIN_PUMP_SPEED_CODE + level]) self._end_transaction_and_read() if non_volatile: self._persist() class Modern690Lc(_ModernBase690Lc): """Modern fifth generation Asetek 690LC cooler.""" _MATCHES = [ (0x2433, 0xb200, 'Asetek 690LC (assuming EVGA CLC)', {}), ] def downgrade_to_legacy(self): """Take the device handle and return a new Legacy690Lc instance for it. This method returns a new instance that takes the device handle from `self`. Because of this, the caller should immediately discard `self`, as it is no longer valid to call any of its methods or access any of its properties. While it is sometimes possible to downgrade a device that has seen modern traffic since it booted, this will generally not work. Additionally, no attempt to disconnect from the device is made while downgrading the instance. Thus, callers are strongly advised to only call this function before connecting to the device from this instance and, in fact, before calling any other methods at all on the device, from any instance. Finally, this method is not yet considered stable and its signature and/or behavior may change. Callers should follow the development of liquidctl and the stabilization of this API. """ legacy = Legacy690Lc(self.device, self._description) self.device = None self._description = None return legacy class Legacy690Lc(_Base690Lc): """Legacy fifth generation Asetek 690LC cooler.""" _MATCHES = [ (0x2433, 0xb200, 'Asetek 690LC (assuming NZXT Kraken X)', {}), ] _LEGACY_690LC = True def __init__(self, device, description, **kwargs): super().__init__(device, description, **kwargs) # --device causes drivers to be instantiated even if they are later # discarded; defer instantiating the data storage until to connect() self._data = None def connect(self, runtime_storage=None, **kwargs): ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' loc = f'bus{self.bus}_address{self.address}' self._data = RuntimeStorage(key_prefixes=[ids, loc, 'legacy']) return ret def _set_all_fixed_speeds(self): self._begin_transaction() for channel in ['pump', 'fan']: mtype, dmin, dmax = _LEGACY_FIXED_SPEED_CHANNELS[channel] duty = clamp(self._data.load(f'{channel}_duty', of_type=int, default=dmax), dmin, dmax) _LOGGER.info('setting %s duty to %d%%', channel, duty) self._write([mtype, duty]) return self._end_transaction_and_read() def initialize(self, **kwargs): super().initialize(**kwargs) self._data.store('pump_duty', None) self._data.store('fan_duty', None) self._set_all_fixed_speeds() _warn_on_unsupported_option(**kwargs) def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ msg = self._set_all_fixed_speeds() firmware = '{}.{}.{}.{}'.format(*tuple(msg[0x17:0x1b])) return [ ('Liquid temperature', msg[10] + msg[14]/10, '°C'), ('Fan speed', msg[0] << 8 | msg[1], 'rpm'), ('Pump speed', msg[8] << 8 | msg[9], 'rpm'), ('Firmware version', firmware, '') ] def set_color(self, channel, mode, colors, time_per_color=None, time_off=None, alert_threshold=_HIGH_TEMPERATURE, alert_color=[255, 0, 0], **kwargs): """Set the color mode for a specific channel.""" # keyword arguments may have been forwarded from cli args and need parsing colors = list(colors) self._begin_transaction() if mode == 'fading': if time_per_color is None: time_per_color = 5 self._configure_device(fading=True, color1=colors[0], color2=colors[1], interval1=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'blinking': if time_per_color is None: time_per_color = 1 if time_off is None: time_off = time_per_color self._configure_device(blinking=True, color1=colors[0], interval1=clamp(time_off, 1, 255), interval2=clamp(time_per_color, 1, 255), alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'fixed': self._configure_device(color1=colors[0], alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) elif mode == 'blackout': # stronger than just 'off', suppresses alerts and rainbow self._configure_device(blackout=True, alert_temp=clamp(alert_threshold, 0, 100), color3=alert_color) else: raise KeyError(f'unsupported lighting mode {mode}') self._end_transaction_and_read() _warn_on_unsupported_option(**kwargs) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" mtype, dmin, dmax = _LEGACY_FIXED_SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) self._data.store(f'{channel}_duty', duty) self._set_all_fixed_speeds() _warn_on_unsupported_option(**kwargs) def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class Hydro690Lc(_ModernBase690Lc): """Corsair-branded fifth generation Asetek 690LC cooler.""" _MATCHES = [ (0x1b1c, 0x0c02, 'Corsair Hydro H80i GT', {}), (0x1b1c, 0x0c03, 'Corsair Hydro H100i GTX', {}), (0x1b1c, 0x0c07, 'Corsair Hydro H110i GTX', {}), (0x1b1c, 0x0c08, 'Corsair Hydro H80i v2', {}), (0x1b1c, 0x0c09, 'Corsair Hydro H100i v2', {}), (0x1b1c, 0x0c0a, 'Corsair Hydro H115i', {}), ] def set_color(self, channel, mode, colors, **kwargs): """Set the color mode for a specific channel.""" if mode == 'rainbow': raise KeyError(f'unsupported lighting mode {mode}') super().set_color(channel, mode, colors, **kwargs) def _warn_on_unsupported_option(**kwargs): if kwargs.get('non_volatile'): _LOGGER.warning('device does not support non-volatile settings, skipping') # deprecated aliases AsetekDriver = Modern690Lc LegacyAsetekDriver = Legacy690Lc CorsairAsetekDriver = Hydro690Lc ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/asetek_pro.py0000644000175000017500000002027214562144457020650 0ustar00jonasjonas"""liquidctl drivers for sixth generation Asetek liquid coolers. Copyright Andrew Robertson, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging from liquidctl.driver.asetek import _Base690Lc from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _READ_ENDPOINT = 0x81 _READ_MAX_LENGTH = 32 _WRITE_ENDPOINT = 0x1 _MAX_PROFILE_POINTS = 7 _CMD_READ_AIO_TEMP = 0xa9 _CMD_READ_FAN_SPEED = 0x41 _CMD_READ_FIRMWARE = 0xaa _CMD_READ_PUMP_MODE = 0x33 _CMD_READ_PUMP_SPEED = 0x31 _CMD_WRITE_COLOR_LIST = 0x56 _CMD_WRITE_COLOR_SPEED = 0x53 _CMD_WRITE_FAN_CURVE = 0x40 _CMD_WRITE_FAN_SPEED = 0x42 _CMD_WRITE_PUMP_MODE = 0x32 _PUMP_MODES = ['quiet', 'balanced', 'performance'] _COLOR_SPEEDS = ['slower', 'normal', 'faster'] _COLOR_SPEEDS_VALUES = { 'shift': [ 0x46, # slower 0x28, # normal 0x0F # faster ], 'pulse':[ 0x50, # slower 0x37, # normal 0x1E # faster ], 'blinking': [ 0x0F, # slower 0x0A, # normal 0x05 # faster ], } _COLOR_CHANGE_MODES = { 'alert': [], 'shift': [0x55, 0x01], 'pulse': [0x52, 0x01], 'blinking': [0x58, 0x01], 'fixed': [0x55, 0x01], } # FIXME unknown required and maximum values _COLOR_COUNT_BOUNDS = { 'alert': (3, 3), 'shift': (2, 4), 'pulse': (1, 4), 'blinking': (1, 4), 'fixed': (1, 1), } def _quoted(*names): return ', '.join(map(repr, names)) # we inherit from _Base690Lc to reuse its implementation of connect # and disconnect, that emulates the stock SiUSBXp driver on Windows class HydroPro(_Base690Lc): """liquidctl driver for Corsair-branded sixth generation Asetek coolers.""" _MATCHES = [ (0x1b1c, 0x0c12, 'Corsair Hydro H150i Pro', {'fan_count': 3}), (0x1b1c, 0x0c13, 'Corsair Hydro H115i Pro', {'fan_count': 2}), (0x1b1c, 0x0c15, 'Corsair Hydro H100i Pro', {'fan_count': 2}) ] def __init__(self, device, description, fan_count, **kwargs): super().__init__(device, description, **kwargs) self._fan_count = fan_count self._data = None def _post(self, data, *, read_length=None): """Write `data` and return response of up to `read_length` bytes.""" assert read_length is not None and read_length <= _READ_MAX_LENGTH self.device.write(_WRITE_ENDPOINT, data) return self.device.read(_READ_ENDPOINT, read_length)[0:read_length] def initialize(self, pump_mode='balanced', **kwargs): """Initialize the device.""" pump_mode = pump_mode.lower() if pump_mode not in _PUMP_MODES: raise ValueError(f'unknown pump mode, should be one of: {_quoted(*_PUMP_MODES)}') self._post([_CMD_WRITE_PUMP_MODE, _PUMP_MODES.index(pump_mode)], read_length=5) msg = self._post([_CMD_READ_FIRMWARE], read_length=7) firmware = '{}.{}.{}.{}'.format(*tuple(msg[3:7])) self.device.release() return [ ('Firmware version', firmware, ''), ] def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ msg = self._post([_CMD_READ_AIO_TEMP], read_length=6) aio_temp = msg[3] + msg[4]/10 fan_speeds = self._get_fan_speeds() msg = self._post([_CMD_READ_PUMP_MODE], read_length=4) pump_mode = _PUMP_MODES[msg[3]] msg = self._post([_CMD_READ_PUMP_SPEED], read_length=5) pump_speed = (msg[3] << 8) + msg[4] self.device.release() status = [('Liquid temperature', aio_temp, '°C')] for fan, speed in fan_speeds: status.append((f'Fan {fan} speed', speed, 'rpm')) return status + [ ('Pump mode', pump_mode, ""), ('Pump speed', pump_speed, 'rpm'), ] def _get_fan_speeds(self): """Read the RPM speed of the fans.""" speeds = [] for i in range(self._fan_count): msg = self._post([_CMD_READ_FAN_SPEED, i], read_length=6) if msg[0] != 0x41 or msg[3] != i: _LOGGER.warning('failed to get current speed of fan %d', i) continue speeds.append((i + 1, (msg[4] << 8) + msg[5])) return speeds def set_color(self, channel, mode, colors, speed='normal', **kwargs): """Set the color mode for a specific channel.""" mode = mode.lower() speed = speed.lower() colors = list(colors) if mode not in _COLOR_CHANGE_MODES: valid = _quoted(*_COLOR_CHANGE_MODES.keys()) raise ValueError(f'unknown lighting mode, should be one of: {valid}') if speed not in _COLOR_SPEEDS: valid = _quoted(*_COLOR_SPEEDS) raise ValueError(f'unknown speed value, should be one of {valid}') if mode == 'alert': # FIXME this mode is far from being completely implemented; for # one, the temperatures are hardcoded; additionally, it may also be # possible to combine it with other modes, but exploring that would # require some experimentation temps = (30, 40, 50) self._post([0x5f, temps[0], 0x00, temps[1], 0x00, temps[2], 0x00] + colors[0] + colors[1] + colors[2], read_length=6) self._post([0x5e, 0x01], read_length=3) self.device.release() return colors = self._check_color_count_bounds(colors, mode) if mode == 'fixed': colors = [colors[0], colors[0]] set_colors = list(itertools.chain(*colors)) self._post([_CMD_WRITE_COLOR_LIST, len(colors)] + set_colors, read_length=3) if mode != 'fixed': magic_value = _COLOR_SPEEDS_VALUES[mode][_COLOR_SPEEDS.index(speed)] self._post([_CMD_WRITE_COLOR_SPEED, magic_value], read_length=3) self._post(_COLOR_CHANGE_MODES[mode], read_length=3) self.device.release() def _check_color_count_bounds(self, color_list, mode_name): requires, maximum = _COLOR_COUNT_BOUNDS[mode_name] if len(color_list) < requires: raise ValueError(f'{mode_name} mode requires {requires} colors') if len(color_list) > maximum: _LOGGER.debug('too many colors, dropping to %d', maximum) color_list = color_list[:maximum] return color_list def set_speed_profile(self, channel, profile, **kwargs): """Set channel to follow a speed duty profile.""" channel = channel.lower() fan_indexes = self._fan_indexes(channel) adjusted = self._prepare_profile(profile, 0, 100, _MAX_PROFILE_POINTS) for temp, duty in adjusted: _LOGGER.info('setting %s PWM point: (%i°C, %i%%), device interpolated', channel, temp, duty) temps, duties = map(list, zip(*adjusted)) for i in fan_indexes: self._post([_CMD_WRITE_FAN_CURVE, i] + temps + duties, read_length=32) self.device.release() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" channel = channel.lower() duty = clamp(duty, 0, 100) for i in self._fan_indexes(channel): _LOGGER.info('setting speed for fan %d to %d', i + 1, duty) self._post([_CMD_WRITE_FAN_SPEED, i, duty], read_length=32) self.device.release() def _fan_indexes(self, channel): if channel.startswith('fan'): if len(channel) > 3: channel_num = int(channel[3:]) - 1 if channel_num >= self._fan_count: raise ValueError(f'unknown channel: {channel}') return [channel_num] return range(self._fan_count) elif channel == 'pump': raise NotSupportedByDevice() else: raise ValueError(f'unknown channel: {channel}') def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() @classmethod def probe(cls, handle, **kwargs): return super().probe(handle, **kwargs) # backward compatibility CorsairAsetekProDriver = HydroPro ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/liquidctl/driver/asus_ryujin.py0000644000175000017500000001262714613511524021062 0ustar00jonasjonas"""liquidctl driver for the ASUS Ryujin II liquid coolers. Copyright Florian Freudiger and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging from typing import List from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import ExpectationNotMet, NotSupportedByDriver from liquidctl.util import clamp, u16le_from, rpadlist, fraction_of_byte _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 65 _PREFIX = 0xEC # Requests and their response headers _REQUEST_GET_FIRMWARE = (0x82, 0x02) _REQUEST_GET_COOLER_STATUS = (0x99, 0x19) _REQUEST_GET_COOLER_DUTY = (0x9A, 0x1A) _REQUEST_GET_CONTROLLER_DUTY = (0xA1, 0x21) _REQUEST_GET_CONTROLLER_SPEED = (0xA0, 0x20) # Command headers that don't need a response _CMD_SET_COOLER_SPEED = 0x1A _CMD_SET_CONTROLLER_SPEED = 0x21 _STATUS_FIRMWARE = "Firmware version" _STATUS_TEMPERATURE = "Liquid temperature" _STATUS_PUMP_SPEED = "Pump speed" _STATUS_PUMP_DUTY = "Pump duty" _STATUS_COOLER_FAN_SPEED = "Pump fan speed" _STATUS_COOLER_FAN_DUTY = "Pump fan duty" _STATUS_CONTROLLER_FAN_SPEED = "External fan {} speed" _STATUS_CONTROLLER_FAN_DUTY = "External fan duty" class AsusRyujin(UsbHidDriver): """ASUS Ryujin II liquid cooler.""" _MATCHES = [ (0x0B05, 0x1988, "ASUS Ryujin II 360", {}), ] def initialize(self, **kwargs): msg = self._request(*_REQUEST_GET_FIRMWARE) return [(_STATUS_FIRMWARE, "".join(map(chr, msg[3:18])), "")] def _get_cooler_duty(self) -> (int, int): """Get current pump and embedded fan duty in %.""" msg = self._request(*_REQUEST_GET_COOLER_DUTY) return msg[4], msg[5] def _get_cooler_status(self) -> (int, int, int): """Get current liquid temperature, pump and embedded fan speed.""" msg = self._request(*_REQUEST_GET_COOLER_STATUS) liquid_temp = msg[3] + msg[4] / 10 pump_speed = u16le_from(msg, 5) fan_speed = u16le_from(msg, 7) return liquid_temp, pump_speed, fan_speed def _get_controller_speeds(self) -> List[int]: """Get AIO controller fan speeds in rpm.""" msg = self._request(*_REQUEST_GET_CONTROLLER_SPEED) speed1 = u16le_from(msg, 5) speed2 = u16le_from(msg, 7) speed3 = u16le_from(msg, 9) speed4 = u16le_from(msg, 3) # For some reason comes first in msg return [speed1, speed2, speed3, speed4] def _get_controller_duty(self) -> int: """Get AIO controller fan duty in %.""" msg = self._request(*_REQUEST_GET_CONTROLLER_DUTY) return round(msg[4] / 0xFF * 100) def get_status(self, **kwargs): pump_duty, fan_duty = self._get_cooler_duty() liquid_temp, pump_speed, fan_speed = self._get_cooler_status() controller_speeds = self._get_controller_speeds() controller_duty = self._get_controller_duty() status = [ (_STATUS_TEMPERATURE, liquid_temp, "°C"), (_STATUS_PUMP_DUTY, pump_duty, "%"), (_STATUS_PUMP_SPEED, pump_speed, "rpm"), (_STATUS_COOLER_FAN_DUTY, fan_duty, "%"), (_STATUS_COOLER_FAN_SPEED, fan_speed, "rpm"), (_STATUS_CONTROLLER_FAN_DUTY, controller_duty, "%"), ] for i, controller_speed in enumerate(controller_speeds): status.append((_STATUS_CONTROLLER_FAN_SPEED.format(i + 1), controller_speed, "rpm")) return status def _set_cooler_duties(self, pump_duty: int, fan_duty: int): self._write([_PREFIX, _CMD_SET_COOLER_SPEED, 0x00, pump_duty, fan_duty]) def _set_cooler_pump_duty(self, duty: int): pump_duty, fan_duty = self._get_cooler_duty() if duty == pump_duty: return self._set_cooler_duties(duty, fan_duty) def _set_cooler_fan_duty(self, duty: int): pump_duty, fan_duty = self._get_cooler_duty() if duty == fan_duty: return self._set_cooler_duties(pump_duty, duty) def _set_controller_duty(self, duty: int): # Controller duty is set between 0x00 and 0xFF duty = fraction_of_byte(percentage=duty) self._write([_PREFIX, _CMD_SET_CONTROLLER_SPEED, 0x00, 0x00, duty]) def set_fixed_speed(self, channel, duty, **kwargs): duty = clamp(duty, 0, 100) if channel == "pump": self._set_cooler_pump_duty(duty) elif channel == "fans": self._set_cooler_fan_duty(duty) self._set_controller_duty(duty) elif channel == "pump-fan": self._set_cooler_fan_duty(duty) elif channel == "external-fans": self._set_controller_duty(duty) else: raise ValueError("invalid channel") def set_screen(self, channel, mode, value, **kwargs): # Not yet reverse engineered / implemented raise NotSupportedByDriver() def _request(self, request_header: int, response_header: int) -> List[int]: self.device.clear_enqueued_reports() self._write([_PREFIX, request_header]) return self._read(response_header) def _read(self, expected_header=None) -> List[int]: msg = self.device.read(_REPORT_LENGTH) if msg[0] != _PREFIX: raise ExpectationNotMet("Unexpected report prefix") if expected_header is not None and msg[1] != expected_header: raise ExpectationNotMet("Unexpected report header") return msg def _write(self, data: List[int]): self.device.write(rpadlist(data, _REPORT_LENGTH, 0)) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740977264.0 liquidctl-1.15.0/liquidctl/driver/aura_led.py0000644000175000017500000003136214761232160020260 0ustar00jonasjonas"""liquidctl driver for ASUS Aura LED USB controllers. Supported controllers: - ASUS Aura LED Controller: idVendor=0x0b05, idProduct=0x19af This controller is found on ASUS ProArt Z690-Creator WiFi and other boards Additional controllers (requires user testing and feedback): - idVendor=0x0B05, idProduct=0x1939 - idVendor=0x0B05, idProduct=0x18F3 These controllers have not been sufficiently tested. Users are asked to test and report issues. Limitations: This controller only supports 'effect' modes that are managed entirely by the controller. The other mode is 'direct' that is managed by software, but this requires the continuous transmission of command codes to control dynamic lighting effects (i.e. all lighting modes in which colors either change or the LED blinks/fades/pulses). In 'effect' mode, the software simply issues a 'fire-and-forget' command to the controller, which subsequently manages the lighting effect on its own. There are some limitations in 'effect' mode, as follows: - Dynamic color modes (spectrum, rainbow, etc.) cannot be applied to individual color channels, but apply to all channels - Static color modes (static) can be applied to individual color channels - Off mode turns all channels off regardless of which channel is selected. However, individual channels can subsequently be enabled, while others will remain off. This applies only to static modes. Acknowledgements: - Aura Addressable Header Controller (for list of color mode names) https://gitlab.com/cneil02/aura-addressable-header-controller - OpenRGB Project (for list of color mode names) https://github.com/CalcProgrammer1/OpenRGB - @dehjomz for discovering color modes 0x10, 0x11, 0x12, 0x13 Aura LED control codes were independently obtained from USB traffic captured using Wireshark on Windows. This Aura LED controller uses very different control codes from previous Aura LED controllers. Copyright CaseySJ and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys from collections import namedtuple from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _READ_LENGTH = 65 _WRITE_LENGTH = 65 _RESET_CHANNEL_ITERATIONS = 5 # Aura Crate software uses 38 _CMD_CODE = 0xEC _FUNCTION_CODE = { "direct": [_CMD_CODE, 0x40], "end_direct": [_CMD_CODE, 0x35, 0x00, 0x00, 0x00, 0x00], "end_effect": [_CMD_CODE, 0x35, 0x00, 0x00, 0x00, 0xFF], "start_seq1": [_CMD_CODE, 0x35], "start_seq2": [_CMD_CODE, 0x36, 0x00], "end_seq1": [_CMD_CODE, 0x35, 0x00, 0x00, 0x01, 0x05], "end_seq2": [_CMD_CODE, 0x3F, 0x55, 0x00, 0x00], "reset_seq1": [_CMD_CODE, 0x38, 0x01, 0x01], "reset_seq2": [_CMD_CODE, 0x38, 0x00, 0x01], "channel_off_pre1": [_CMD_CODE, 0x38, 0x01, 0x00], "channel_off_pre2": [_CMD_CODE, 0x38, 0x00, 0x00], "channel_off_prefix": [_CMD_CODE, 0x35, 0x01, 0x00, 0x00, 0xFF], "firmware": [_CMD_CODE, 0x82], "config": [_CMD_CODE, 0xB0], } # channel_type 0 designates RGB bus # channel_type 1 designates ARGB bus # "effect" mode channel IDs are different from "direct" mode channel IDs _ColorChannel = namedtuple( "_ColorChannel", ["name", "channel_id", "direct_channel_id", "channel_type", "rgb_offset"] ) _COLOR_CHANNELS = { channel.name: channel for channel in [ _ColorChannel("led1", 0x01, 0x00, 0x00, 0x0), # rgb channel _ColorChannel("led2", 0x02, 0x00, 0x01, 0x01), # argb channel _ColorChannel("led3", 0x04, 0x01, 0x01, 0x02), # argb channel _ColorChannel("led4", 0x08, 0x02, 0x01, 0x03), # argb channel ] } _ColorMode = namedtuple("_ColorMode", ["name", "value", "takes_color"]) _COLOR_MODES = { mode.name: mode for mode in [ _ColorMode("off", 0x00, takes_color=False), _ColorMode("static", 0x01, takes_color=True), _ColorMode("breathing", 0x02, takes_color=True), _ColorMode("flashing", 0x03, takes_color=True), _ColorMode("spectrum_cycle", 0x04, takes_color=False), _ColorMode("rainbow", 0x05, takes_color=False), _ColorMode("spectrum_cycle_breathing", 0x06, takes_color=False), _ColorMode("chase_fade", 0x07, takes_color=True), _ColorMode("spectrum_cycle_chase_fade", 0x08, takes_color=False), _ColorMode("chase", 0x09, takes_color=True), _ColorMode("spectrum_cycle_chase", 0x0A, takes_color=False), _ColorMode("spectrum_cycle_wave", 0x0B, takes_color=False), _ColorMode("chase_rainbow_pulse", 0x0C, takes_color=False), _ColorMode("rainbow_flicker", 0x0D, takes_color=False), _ColorMode("gentle_transition", 0x10, takes_color=False), _ColorMode("wave_propagation", 0x11, takes_color=False), _ColorMode("wave_propagation_pause", 0x12, takes_color=False), _ColorMode("red_pulse", 0x13, takes_color=False), ] } class AuraLed(UsbHidDriver): """ liquidctl driver for ASUS Aura LED USB controllers. This driver only supports 'effect' mode, hence no speed/color channels Devices 0x1939 and 0x18F3 are not fully supported at this time; users are asked to experiment with this driver and provide feedback """ _MATCHES = [ (0x0B05, 0x19AF, "ASUS Aura LED Controller", {}), (0x0B05, 0x1939, "ASUS Aura LED Controller", {}), (0x0B05, 0x18F3, "ASUS Aura LED Controller", {}), ] def initialize(self, **kwargs): """Initialize the device. Returns a list of `(property, value, unit)` tuples, containing the firmware version and other useful information provided by the hardware. """ # Get firmware version self._write(_FUNCTION_CODE["firmware"]) # Build reply string status = [] data = self.device.read(_READ_LENGTH) if data[1] == 0x02: status.append(("Firmware version", "".join(map(chr, data[2:17])), "")) else: status.append("Unexpected reply for firmware", "", "") return status # This stops Direct mode if it was previously applied self._write(_FUNCTION_CODE["end_direct"]) """ Extra operations during initialization This is experimental and may not be necessary self._write([0xec, 0x31, 0x0d, 0x00]); self._write([0xec, 0xb1, 0x00, 0x00]); self.device.read(_READ_LENGTH) self._write([0xec, 0x31, 0x0e, 0x00]); self._write([0xec, 0xb1, 0x00, 0x00]); self.device.read(_READ_LENGTH) self._write([0xec, 0x31, 0x0f, 0x00]); self._write([0xec, 0xb1, 0x00, 0x00]); self.device.read(_READ_LENGTH) self._write([0xec, 0x83, 0x00, 0x00]); self.device.read(_READ_LENGTH) """ return status def get_status(self, **kwargs): """Get a status report.""" status = [] # Get config table self._write(_FUNCTION_CODE["config"]) data = self.device.read(_READ_LENGTH) if data[1] == 0x30: start_index = 4 # index of first record argb_channels = data[start_index + 2] rgb_channels = data[start_index + 3] status.append(("ARGB channels: " + str(argb_channels), "", "")) status.append((" RGB channels: " + str(rgb_channels), "", "")) if "debug" in kwargs and kwargs["debug"] == True: num = 6 # number of bytes per record count = 1 while start_index + num < _READ_LENGTH: status.append( ( "Device Config: " + str(count), ", ".join( "0x{:02x}".format(x) for x in data[start_index : start_index + num] ), "", ) ) start_index += num count += 1 else: status.append("Unexpected reply for config", "", "") return status def set_color(self, channel, mode, colors, speed="normal", **kwargs): """Set the color mode for a specific channel. `colors` should be an iterable of zero or one `[red, green, blue]` triples, where each red/green/blue component is a value in the range 0–255. """ colors = iter(colors) if _COLOR_MODES[mode].takes_color: try: r, g, b = next(colors) single_color = (r, g, b) except: raise ValueError(f"one color required for this mode") from None else: single_color = (0, 0, 0) if channel != "sync" and channel not in _COLOR_CHANNELS: message = "valid channels are " for chan in _COLOR_CHANNELS: message += chan + " " raise KeyError(message) from None return """ This is experimental (it's an example of direct mode) if mode == 'off': self.channel_off(channel) self.reset_all_channels() return """ if channel == "sync": selected_channels = _COLOR_CHANNELS.values() else: selected_channels = (_COLOR_CHANNELS[channel],) full_cmd_seq = [] # entire series of commands are added to this list """ Experimental code for treating RGB channel differently from others if channel == "led1": cmd_tuple=self.construct_color_commands(channel, mode, single_color) self._write(cmd_tuple[0]) self._write(cmd_tuple[1]) self._write(_FUNCTION_CODE["end_seq2"]) self.end_color_sequence() self._write(cmd_tuple[0]) self._write(cmd_tuple[1]) self._write(_FUNCTION_CODE["end_seq2"]) else: """ for chan in selected_channels: cmd_tuple = self.construct_color_commands(chan.name, mode, single_color) full_cmd_seq.append(cmd_tuple[0]) full_cmd_seq.append(cmd_tuple[1]) full_cmd_seq.append(_FUNCTION_CODE["end_seq2"]) """ ASUS Aura Crate sends command sequence twice, but our tests show that this may be redundant. Nevertheless, let's keep this code here in case we need to send commands twice as well #full_cmd_seq.append(cmd_tuple[0]) #full_cmd_seq.append(cmd_tuple[1]) #full_cmd_seq.append(_FUNCTION_CODE["end_seq2"]) """ for cmd_seq in full_cmd_seq: self._write(cmd_seq) self.end_color_sequence() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def reset_all_channels(self): """Reset all LED channels.""" for i in range(_RESET_CHANNEL_ITERATIONS): self._write(_FUNCTION_CODE["reset_seq1"]) self._write(_FUNCTION_CODE["reset_seq2"]) def channel_off(self, channel): """ Uses direct mode to disable a specific channel """ self._write(_FUNCTION_CODE["end_effect"]) for i in range(_RESET_CHANNEL_ITERATIONS): self._write(_FUNCTION_CODE["channel_off_pre1"]) self._write(_FUNCTION_CODE["channel_off_pre2"]) self._write(_FUNCTION_CODE["channel_off_prefix"]) # set all LEDs to off, 20 at a time for i in (0, 20, 40, 60, 80, 100): self._write( _FUNCTION_CODE["direct"] + [_COLOR_CHANNELS[channel].direct_channel_id | (0x80 * (i == 100)), i, 20] ) self.end_color_sequence() self._write(_FUNCTION_CODE["end_direct"]) def construct_color_commands(self, channel, mode, single_color): """ Create command strings for specified color channel """ mode = _COLOR_MODES[mode] channel_type = _COLOR_CHANNELS[channel].channel_type # 0=RGB, 1=ARGB channel_id = _COLOR_CHANNELS[channel].channel_id rgb_offset = _COLOR_CHANNELS[channel].rgb_offset data1 = _FUNCTION_CODE["start_seq1"] + [channel_type, 0x00, 0x00, mode.value] data2 = _FUNCTION_CODE["start_seq2"] + [channel_id, 0x00] + [0, 0, 0] * rgb_offset data2 += single_color return (data1, data2) def end_color_sequence(self): self._write(_FUNCTION_CODE["end_seq1"]) self._write(_FUNCTION_CODE["end_seq2"]) def _write(self, data): padding = [0x0] * (_WRITE_LENGTH - len(data)) self.device.write(data + padding) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/base.py0000644000175000017500000001212514562144457017424 0ustar00jonasjonas"""Base bus and driver API. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style class BaseDriver: """Base driver API. All drivers are expected to implement this API for compatibility with the liquidctl CLI or other thirdy party tools. Drivers will automatically implement the context manager protocol, but this should only be used from a call to `connect`. Example: for dev in .find_supported_devices(): with dev.connect(): print(dev.get_status()) if dev.serial_number == '49385027ZP': dev.set_fixed_speed('fan3', 42) """ @classmethod def find_supported_devices(cls, **kwargs): """Find and bind to compatible devices. Returns a list of bound driver instances. """ raise NotImplementedError() def connect(self, **kwargs): """Connect to the device. Returns `self`. ## Notes for driver authors Procedure before any read or write operation can be performed. Typically a handshake between driver and device. """ raise NotImplementedError() def initialize(self, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. ## Notes for driver authors Apart from `connect()`, some devices might require a one-time initialization procedure after powering on, or to detect hardware changes. This should be called *after* connecting to the device. """ raise NotImplementedError() def disconnect(self, **kwargs): """Disconnect from the device. ## Notes for driver authors Procedure before the driver can safely unbind from the device. Typically just cleanup. """ raise NotImplementedError() def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ raise NotImplementedError() def set_color(self, channel, mode, colors, **kwargs): """Set the color mode for a specific channel.""" raise NotImplementedError() def set_screen(self, channel, mode, value, **kwargs): """Set the screen mode and content. Unstable. """ raise NotImplementedError() def set_speed_profile(self, channel, profile, **kwargs): """Set channel to follow a speed duty profile.""" raise NotImplementedError() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" raise NotImplementedError() @property def description(self): """Human readable description of the corresponding device.""" raise NotImplementedError() @property def vendor_id(self): """Numeric vendor identifier, or None if N/A.""" raise NotImplementedError() @property def product_id(self): """Numeric product identifier, or None if N/A.""" raise NotImplementedError() @property def release_number(self): """Device versioning number, or None if N/A. In USB devices this is bcdDevice. """ raise NotImplementedError() @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" raise NotImplementedError() @property def bus(self): """Bus the device is connected to, or None if N/A.""" raise NotImplementedError() @property def address(self): """Address of the device on the corresponding bus, or None if N/A. This typically depends on the bus enumeration order. """ raise NotImplementedError() @property def port(self): """Physical location of the device, or None if N/A. This typically refers to a USB port, which is *not* dependent on bus enumeration order. However, a USB port is hub-specific, and hubs can be chained. Thus, for USB devices, this returns a tuple of port numbers, from the root hub to the parent of the connected device. """ raise NotImplementedError() def __enter__(self): return self def __exit__(self, *args): self.disconnect() class BaseBus: """Base bus API.""" def find_devices(self, **kwargs): """Find compatible devices and yield corresponding driver instances.""" return def find_all_subclasses(cls): """Recursively find loaded subclasses of `cls`. Returns a set of subclasses of `cls`. """ sub = set(cls.__subclasses__()) return sub.union([s for c in cls.__subclasses__() for s in find_all_subclasses(c)]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1716101336.0 liquidctl-1.15.0/liquidctl/driver/commander_core.py0000644000175000017500000003201114622320330021442 0ustar00jonasjonas"""liquidctl drivers for Corsair Commander Core. Supported devices: - Corsair Commander Core Copyright ParkerMc and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging from contextlib import contextmanager from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import ExpectationNotMet, NotSupportedByDriver, NotSupportedByDevice from liquidctl.util import clamp, u16le_from _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 96 _RESPONSE_LENGTH = 96 _INTERFACE_NUMBER = 0 _FAN_COUNT = 6 _CMD_WAKE = (0x01, 0x03, 0x00, 0x02) _CMD_SLEEP = (0x01, 0x03, 0x00, 0x01) _CMD_GET_FIRMWARE = (0x02, 0x13) _CMD_CLOSE_ENDPOINT = (0x05, 0x01, 0x00) _CMD_OPEN_ENDPOINT = (0x0d, 0x00) _CMD_READ_INITIAL = (0x08, 0x00, 0x01) _CMD_READ_MORE = (0x08, 0x00, 0x02) _CMD_READ_FINAL = (0x08, 0x00, 0x03) _CMD_WRITE = (0x06, 0x00) _CMD_WRITE_MORE = (0x07, 0x00) _MODE_LED_COUNT = (0x20,) _MODE_GET_SPEEDS = (0x17,) _MODE_GET_TEMPS = (0x21,) _MODE_CONNECTED_SPEEDS = (0x1a,) _MODE_HW_SPEED_MODE = (0x60, 0x6d) _MODE_HW_FIXED_PERCENT = (0x61, 0x6d) _MODE_HW_CURVE_PERCENT = (0x62, 0x6d) _DATA_TYPE_SPEEDS = (0x06, 0x00) _DATA_TYPE_LED_COUNT = (0x0f, 0x00) _DATA_TYPE_TEMPS = (0x10, 0x00) _DATA_TYPE_CONNECTED_SPEEDS = (0x09, 0x00) _DATA_TYPE_HW_SPEED_MODE = (0x03, 0x00) _DATA_TYPE_HW_FIXED_PERCENT = (0x04, 0x00) _DATA_TYPE_HW_CURVE_PERCENT = (0x05, 0x00) _FAN_MODE_FIXED_PERCENT = 0x00 _FAN_MODE_CURVE_PERCENT = 0x02 class CommanderCore(UsbHidDriver): """Corsair Commander Core""" # For a non-exhaustive list of issues, see: #520, #583, #598, #623, #705 _MATCHES = [ (0x1b1c, 0x0c1c, 'Corsair Commander Core (broken)', {"has_pump": True}), (0x1b1c, 0x0c2a, 'Corsair Commander Core XT (broken)', {"has_pump": False}), (0x1b1c, 0x0c32, 'Corsair Commander ST (broken)', {"has_pump": True}), ] def __init__(self, device, description, has_pump, **kwargs): super().__init__(device, description, **kwargs) self._has_pump = has_pump def initialize(self, **kwargs): """Initialize the device and get the fan modes.""" with self._wake_device_context(): # Get Firmware res = self._send_command(_CMD_GET_FIRMWARE) fw_version = (res[3], res[4], res[5]) status = [('Firmware version', '{}.{}.{}'.format(*fw_version), '')] # Get LEDs per fan res = self._read_data(_MODE_LED_COUNT, _DATA_TYPE_LED_COUNT) num_devices = res[0] led_data = res[1:1 + num_devices * 4] for i in range(0, num_devices): connected = u16le_from(led_data, offset=i * 4) == 2 num_leds = u16le_from(led_data, offset=i * 4 + 2) if self._has_pump: label = 'AIO LED count' if i == 0 else f'RGB port {i} LED count' else: label = f'RGB port {i+1} LED count' status += [(label, num_leds if connected else None, '')] # Get what fans are connected res = self._read_data(_MODE_CONNECTED_SPEEDS, _DATA_TYPE_CONNECTED_SPEEDS) num_devices = res[0] for i in range(0, num_devices): if self._has_pump: label = 'AIO port connected' if i == 0 else f'Fan port {i} connected' else: label = f'Fan port {i+1} connected' status += [(label, res[i + 1] == 0x07, '')] # Get what temp sensors are connected for i, temp in enumerate(self._get_temps()): connected = temp is not None if self._has_pump: label = 'Water temperature sensor' if i == 0 and self._has_pump else f'Temperature sensor {i}' else: label = f'Temperature sensor {i+1}' status += [(label, connected, '')] return status def get_status(self, **kwargs): """Get all the fan speeds and temps""" status = [] with self._wake_device_context(): for i, speed in enumerate(self._get_speeds()): if self._has_pump: label = 'Pump speed' if i == 0 else f'Fan speed {i}' else: label = f'Fan speed {i+1}' status += [(label, speed, 'rpm')] for i, temp in enumerate(self._get_temps()): if temp is None: continue if self._has_pump: label = 'Water temperature' if i == 0 else f'Temperature {i}' else: label = f'Temperature {i}' status += [(label, temp, '°C')] return status def set_color(self, channel, mode, colors, **kwargs): raise NotSupportedByDriver def set_speed_profile(self, channel, profile, **kwargs): channels = self._parse_channels(channel) curve_points = list(profile) if len(curve_points) < 2: ValueError('a minimum of 2 speed curve points must be configured.') if len(curve_points) > 7: ValueError('a maximum of 7 speed curve points may be configured.') with self._wake_device_context(): # Set hardware speed mode res = self._read_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE) device_count = res[0] data = bytearray(res[0:device_count + 1]) for chan in channels: data[chan + 1] = _FAN_MODE_CURVE_PERCENT self._write_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE, data) # Read in data and split by device res = self._read_data(_MODE_HW_CURVE_PERCENT, _DATA_TYPE_HW_CURVE_PERCENT) device_count = res[0] data_by_device = [] i = 1 for _ in range(0, device_count): count = res[i+1] start = i end = i + 4 * count + 2 i = end data_by_device.append(res[start:end]) # Modify data for channels in channels array for chan in channels: new_data = [] # set temperature sensor new_data.append(b"\x00") # set number of curve points new_data.append(int.to_bytes(len(curve_points), length=1, byteorder="big")) # set curve points for (temp, duty) in curve_points: new_data.append(int.to_bytes(temp*10, length=2, byteorder="little", signed=False)) new_data.append(int.to_bytes(clamp(duty, 0, 100), length=2, byteorder="little", signed=False)) # Update device data data_by_device[chan] = b''.join(new_data) out = bytes([device_count]) + b''.join(data_by_device) self._write_data(_MODE_HW_CURVE_PERCENT, _DATA_TYPE_HW_CURVE_PERCENT, out) def set_fixed_speed(self, channel, duty, **kwargs): channels = self._parse_channels(channel) with self._wake_device_context(): # Set hardware speed mode res = self._read_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE) device_count = res[0] data = bytearray(res[0:device_count + 1]) for chan in channels: data[chan + 1] = _FAN_MODE_FIXED_PERCENT self._write_data(_MODE_HW_SPEED_MODE, _DATA_TYPE_HW_SPEED_MODE, data) # Set speed res = self._read_data(_MODE_HW_FIXED_PERCENT, _DATA_TYPE_HW_FIXED_PERCENT) device_count = res[0] data = bytearray(res[0:device_count * 2 + 1]) duty_le = int.to_bytes(clamp(duty, 0, 100), length=2, byteorder="little", signed=False) for chan in channels: i = chan * 2 + 1 data[i: i + 2] = duty_le # Update the device speed self._write_data(_MODE_HW_FIXED_PERCENT, _DATA_TYPE_HW_FIXED_PERCENT, data) @classmethod def probe(cls, handle, **kwargs): """Ensure we get the right interface""" if handle.hidinfo['interface_number'] != _INTERFACE_NUMBER: return yield from super().probe(handle, **kwargs) def _get_speeds(self): speeds = [] res = self._read_data(_MODE_GET_SPEEDS, _DATA_TYPE_SPEEDS) num_speeds = res[0] speeds_data = res[1:1 + num_speeds * 2] for i in range(0, num_speeds): speeds.append(u16le_from(speeds_data, offset=i * 2)) return speeds def _get_temps(self): temps = [] res = self._read_data(_MODE_GET_TEMPS, _DATA_TYPE_TEMPS) num_temps = res[0] temp_data = res[1:1 + num_temps * 3] for i in range(0, num_temps): connected = temp_data[i * 3] == 0x00 if connected: temps.append(u16le_from(temp_data, offset=i * 3 + 1) / 10) else: temps.append(None) return temps def _read_data(self, mode, data_type): self._send_command(_CMD_OPEN_ENDPOINT, mode) raw_data = self._send_command(_CMD_READ_INITIAL) more_raw_data = self._send_command(_CMD_READ_MORE) final_raw_data = self._send_command(_CMD_READ_FINAL) self._send_command(_CMD_CLOSE_ENDPOINT) if tuple(raw_data[3:5]) != data_type: raise ExpectationNotMet('device returned incorrect data type') return raw_data[5:] + more_raw_data[3:] + final_raw_data[3:] def _send_command(self, command, data=()): # self.device.write expects buf[0] to be the report number or 0 if not used buf = bytearray(_REPORT_LENGTH + 1) # buf[1] when going out is always 08 buf[1] = 0x08 # Indexes for the buffer cmd_start = 2 data_start = cmd_start + len(command) data_end = data_start + len(data) # Fill in the buffer buf[cmd_start:data_start] = command buf[data_start:data_end] = data self.device.clear_enqueued_reports() self.device.write(buf) res = self.device.read(_RESPONSE_LENGTH) while res[0] != 0x00: res = self.device.read(_RESPONSE_LENGTH) buf = bytes(res) assert buf[1] == command[0], 'response does not match command' return buf @contextmanager def _wake_device_context(self): try: self._send_command(_CMD_WAKE) yield finally: self._send_command(_CMD_SLEEP) def _write_data(self, mode, data_type, data): self._read_data(mode, data_type) # Will ensure we are writing the correct data type to avoid breakage self._send_command(_CMD_OPEN_ENDPOINT, mode) # Write data data_len = len(data) data_start_index = 0 while (data_start_index < data_len): if (data_start_index == 0): # First 9 bytes are in use packet_data_len = _REPORT_LENGTH - 9 if (data_len < packet_data_len): packet_data_len = data_len # Num Data Length bytes + 0x05 + 0x06 + Num Data Type bytes + Num Data bytes buf = bytearray(2 + 2 + len(data_type) + packet_data_len) # Data Length value (includes data type length) - 0x03 and 0x04 buf[0: 2] = int.to_bytes(data_len + len(data_type), length=2, byteorder="little", signed=False) # Data Type value - 0x07 and 0x08 buf[4: 4 + len(data_type)] = data_type # Data - 0x09 onwards buf[4 + len(data_type):] = data[0:packet_data_len] self._send_command(_CMD_WRITE, buf) data_start_index += packet_data_len else: # First 3 bytes are in use packet_data_len = _REPORT_LENGTH - 3 if data_len - data_start_index < packet_data_len: packet_data_len = data_len - data_start_index self._send_command(_CMD_WRITE_MORE, data[data_start_index:data_start_index + packet_data_len]) data_start_index += packet_data_len self._send_command(_CMD_CLOSE_ENDPOINT) def _fan_to_channel(self, fan): if self._has_pump: return fan else: # On devices without a pump, channel 0 is fan 1 return fan - 1 def _parse_channels(self, channel): if self._has_pump and channel == 'pump': return [0] elif channel == "fans": return [self._fan_to_channel(x) for x in range(1, _FAN_COUNT + 1)] elif channel.startswith("fan") and channel[3:].isnumeric() and 0 < int(channel[3:]) <= _FAN_COUNT: return [self._fan_to_channel(int(channel[3:]))] else: fan_names = ['fan' + str(i) for i in range(1, _FAN_COUNT + 1)] fan_names_part = '", "'.join(fan_names) if self._has_pump: fan_names.insert(0, "pump") raise ValueError(f'unknown channel, should be one of: "{fan_names_part}" or "fans"') def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/commander_pro.py0000644000175000017500000005417014562144457021345 0ustar00jonasjonas"""liquidctl drivers for Corsair Commander Pro devices. Supported devices: - Corsair Commander Pro - Corsair Lighting Node Pro Important notes: - This device currently only has hardware control implemented but it also supports a software control mode. - Software control will be enabled at a future time. Copyright Marshall Asch and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging import re from enum import Enum, unique from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.keyval import RuntimeStorage from liquidctl.util import clamp, fraction_of_byte, u16be_from, u16le_from, \ normalize_profile, check_unsafe, map_direction _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _RESPONSE_LENGTH = 16 _CMD_GET_FIRMWARE = 0x02 _CMD_GET_BOOTLOADER = 0x06 _CMD_GET_TEMP_CONFIG = 0x10 _CMD_GET_TEMP = 0x11 _CMD_GET_VOLTS = 0x12 _CMD_GET_FAN_MODES = 0x20 _CMD_GET_FAN_RPM = 0x21 _CMD_SET_FAN_DUTY = 0x23 _CMD_SET_FAN_PROFILE = 0x25 _CMD_SET_FAN_MODE = 0x28 _CMD_RESET_LED_CHANNEL = 0x37 _CMD_BEGIN_LED_EFFECT = 0x34 _CMD_SET_LED_CHANNEL_STATE = 0x38 _CMD_LED_EFFECT = 0x35 _CMD_LED_COMMIT = 0x33 _LED_PORT_STATE_HARDWARE = 0x01 _LED_PORT_STATE_SOFTWARE = 0x02 _LED_SPEED_FAST = 0x00 _LED_SPEED_MEDIUM = 0x01 _LED_SPEED_SLOW = 0x02 _LED_DIRECTION_FORWARD = 0x01 _LED_DIRECTION_BACKWARD = 0x00 _FAN_MODE_DISCONNECTED = 0x00 _FAN_MODE_DC = 0x01 _FAN_MODE_PWM = 0x02 _PROFILE_LENGTH = 6 _CRITICAL_TEMPERATURE = 60 _CRITICAL_TEMPERATURE_HIGH = 100 _MAX_FAN_RPM = 5000 # I have no idea if this is a good value or not _MAX_LEDS = 204 _MODES = { 'off': 0x04, # this is a special case of fixed 'rainbow': 0x00, 'color_shift': 0x01, 'color_pulse': 0x02, 'color_wave': 0x03, 'fixed': 0x04, # 'temperature': 0x05, # ignore this 'visor': 0x06, 'marquee': 0x07, 'blink': 0x08, 'sequential': 0x09, 'rainbow2': 0x0a, } _FAN_MODES = { 'off': _FAN_MODE_DISCONNECTED, 'dc': _FAN_MODE_DC, 'pwm': _FAN_MODE_PWM, } def _prepare_profile(original, critcalTempature): clamped = ((temp, clamp(duty, 0, _MAX_FAN_RPM)) for temp, duty in original) normal = normalize_profile(clamped, critcalTempature, _MAX_FAN_RPM) missing = _PROFILE_LENGTH - len(normal) if missing < 0: raise ValueError(f'too many points in profile (remove {-missing})') if missing > 0: normal += missing * [(critcalTempature, _MAX_FAN_RPM)] return normal def _quoted(*names): return ', '.join(map(repr, names)) def _fan_mode_desc(mode): """This will convert the fan mode value to a descriptive name. """ if mode == _FAN_MODE_DC: return 'DC' elif mode == _FAN_MODE_PWM: return 'PWM' else: if mode != _FAN_MODE_DISCONNECTED: _LOGGER.warning('unknown fan mode: {mode:#04x}') return None class CommanderPro(UsbHidDriver): """Corsair Commander Pro LED and fan hub""" # support for hwmon: corsair-cpro, Linux 5.9 _MATCHES = [ (0x1b1c, 0x0c10, 'Corsair Commander Pro', {'fan_count': 6, 'temp_probs': 4, 'led_channels': 2}), (0x1b1c, 0x0c0b, 'Corsair Lighting Node Pro', {'fan_count': 0, 'temp_probs': 0, 'led_channels': 2}), (0x1b1c, 0x0c1a, 'Corsair Lighting Node Core', {'fan_count': 0, 'temp_probs': 0, 'led_channels': 1}), (0x1b1c, 0x1d00, 'Corsair Obsidian 1000D', {'fan_count': 6, 'temp_probs': 4, 'led_channels': 2}), ] def __init__(self, device, description, fan_count, temp_probs, led_channels, **kwargs): super().__init__(device, description, **kwargs) # the following fields are only initialized in connect() self._data = None self._fan_names = [f'fan{i+1}' for i in range(fan_count)] if led_channels == 1: self._led_names = ['led'] else: self._led_names = [f'led{i+1}' for i in range(led_channels)] self._temp_probs = temp_probs self._fan_count = fan_count def connect(self, runtime_storage=None, **kwargs): """Connect to the device.""" ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' # must use the HID path because there is no serial number; however, # these can be quite long on Windows and macOS, so only take the # numbers, since they are likely the only parts that vary between two # devices of the same model loc = 'loc' + '_'.join(re.findall(r'\d+', self.address)) self._data = RuntimeStorage(key_prefixes=[ids, loc]) return ret def _initialize_directly(self, set_fan_modes, **kwargs): res = self._send_command(_CMD_GET_FIRMWARE) fw_version = (res[1], res[2], res[3]) res = self._send_command(_CMD_GET_BOOTLOADER) bootloader_version = (res[1], res[2]) # is it possible for there to be a third value? status = [ ('Firmware version', '{}.{}.{}'.format(*fw_version), ''), ('Bootloader version', '{}.{}'.format(*bootloader_version), ''), ] if self._temp_probs > 0: res = self._send_command(_CMD_GET_TEMP_CONFIG) temp_connected = res[1:5] self._data.store('temp_sensors_connected', temp_connected) status += [ (f'Temperature probe {i + 1}', bool(temp_connected[i]), '') for i in range(4) ] if self._fan_count > 0: for i, value in set_fan_modes.items(): fan_num = int(i) - 1 if value not in ['dc', 'pwm', 'off']: raise ValueError(f"invalid fan mode: '{value}'") self._send_command(_CMD_SET_FAN_MODE, [0x02, fan_num, _FAN_MODES.get(value)]) res = self._send_command(_CMD_GET_FAN_MODES) fan_modes = res[1:self._fan_count+1] self._data.store('fan_modes', fan_modes) status += [ (f'Fan {i + 1} control mode', _fan_mode_desc(fan_modes[i]), '') for i in range(6) ] return status def _get_static_info_from_hwmon(self): # firmware and bootloader versions are not available through hwmon, but # we don't want to race with the kernel driver _LOGGER.warning('some attributes cannot be read from %s kernel driver', self._hwmon.driver) status = [] if self._temp_probs > 0: # use ints to mimic how we normally handle the raw data sensors = [int(self._hwmon.has_attribute(f'temp{n}_input')) for n in range(1, 5)] _LOGGER.debug('%r', sensors) self._data.store('temp_sensors_connected', sensors) for n, connected in zip(range(1, 5), sensors): status.append((f'Temperature probe {n}', bool(connected), '')) if self._fan_count > 0: def hwmon_fan_mode(hwmon, n): attr = f'fan{n}_label' if not hwmon.has_attribute(attr): return _FAN_MODE_DISCONNECTED label = hwmon.get_string(attr) if label.endswith('4pin'): return _FAN_MODE_PWM elif label.endswith('3pin'): return _FAN_MODE_DC else: assert label.endswith('other') _LOGGER.warning('hwmon reported the fan mode as other') return None fans = [hwmon_fan_mode(self._hwmon, n) for n in range(1, 7)] _LOGGER.debug('%r', fans) self._data.store('fan_modes', fans) for n, mode in zip(range(1, 7), fans): status.append((f'Fan {n} control mode', _fan_mode_desc(mode), '')) return status def initialize(self, direct_access=False, fan_mode={}, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ # fix #615 but preserve API compatibility with liquidctl <= 1.12.x if not fan_mode and kwargs.get("fan_modes"): _LOGGER.warning("deprecated parameter name `fan_modes`, use `fan_mode`") fan_mode = kwargs["fan_modes"] if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, assuming it is already initialized', self._hwmon.driver) if fan_mode: # say "with" instead of "by" because eventually the driver may start supporting fan # modes, and we're not actually checking for it at runtime _LOGGER.warning('fan mode options not supported with %s kernel driver, ignoring', self._hwmon.driver) return self._get_static_info_from_hwmon() else: if self._hwmon: _LOGGER.warning('forcing re-initialization despite %s kernel driver', self._hwmon.driver) return self._initialize_directly(fan_mode) def _get_status_directly(self): temp_probes = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) status = [] # get the temperature sensor values for i, probe_enabled in enumerate(temp_probes): if probe_enabled: temp = self._get_temp(i) status.append((f'Temperature {i + 1}', temp, '°C')) # get fan RPMs of connected fans for i, fan_mode in enumerate(fan_modes): if fan_mode == _FAN_MODE_DC or fan_mode == _FAN_MODE_PWM: speed = self._get_fan_rpm(i) status.append((f'Fan {i + 1} speed', speed, 'rpm')) # get the real power supply voltages for i, rail in enumerate(["+12V", "+5V", "+3.3V"]): raw = self._send_command(_CMD_GET_VOLTS, [i]) voltage = u16be_from(raw, offset=1) / 1000 status.append((f'{rail} rail', voltage, 'V')) return status def _get_status_from_hwmon(self): temp_probes = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) status = [] # get the temperature sensor values for i, probe_enabled in enumerate(temp_probes): if probe_enabled: n = i + 1 temp = self._hwmon.read_int(f'temp{n}_input') * 1e-3 status.append((f'Temperature {n}', temp, '°C')) # get fan RPMs of connected fans for i, fan_mode in enumerate(fan_modes): if fan_mode == _FAN_MODE_DC or fan_mode == _FAN_MODE_PWM: n = i + 1 speed = self._hwmon.read_int(f'fan{n}_input') status.append((f'Fan {n} speed', speed, 'rpm')) # get the real power supply voltages for i, rail in enumerate(["+12V", "+5V", "+3.3V"]): voltage = self._hwmon.read_int(f'in{i}_input') * 1e-3 status.append((f'{rail} rail', voltage, 'V')) return status def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self._fan_count == 0 or self._temp_probs == 0: _LOGGER.debug('only Commander Pro and Obsidian 1000D report status') return [] if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) return self._get_status_directly() def _get_temp(self, sensor_num): """This will get the temperature in degrees celsius for the specified temp sensor. sensor number MUST be in range of 0-3 """ if self._temp_probs == 0: raise ValueError('this device does not have a temperature sensor') if sensor_num < 0 or sensor_num > 3: raise ValueError(f'sensor_num {sensor_num} invalid, must be between 0 and 3') res = self._send_command(_CMD_GET_TEMP, [sensor_num]) temp = u16be_from(res, offset=1) / 100 return temp def _get_fan_rpm(self, fan_num): """This will get the rpm value of the fan. fan number MUST be in range of 0-5 """ if self._fan_count == 0: raise ValueError('this device does not have any fans') if fan_num < 0 or fan_num > 5: raise ValueError(f'fan_num {fan_num} invalid, must be between 0 and 5') res = self._send_command(_CMD_GET_FAN_RPM, [fan_num]) speed = u16be_from(res, offset=1) return speed def _get_hw_fan_channels(self, channel): """This will get a list of all the fan channels that the command should be sent to It will look up the name of the fan channel given and return a list of the real fan number """ if channel == 'sync' or len(self._fan_names) == 1: return list(range(len(self._fan_names))) if channel in self._fan_names: return [self._fan_names.index(channel)] raise ValueError(f'unknown channel, should be one of: {_quoted("sync", *self._fan_names)}') def _get_hw_led_channels(self, channel): """This will get a list of all the led channels that the command should be sent to It will look up the name of the led channel given and return a list of the real led device number """ if channel == 'sync' or len(self._led_names) == 1: return list(range(len(self._led_names))) if channel in self._led_names: return [self._led_names.index(channel)] raise ValueError(f'unknown channel, should be one of: {_quoted("sync", *self._led_names)}') def set_fixed_speed(self, channel, duty, **kwargs): """Set fan or fans to a fixed speed duty. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. """ if self._fan_count == 0: raise NotSupportedByDevice() duty = clamp(duty, 0, 100) fan_channels = self._get_hw_fan_channels(channel) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) for fan in fan_channels: mode = fan_modes[fan] if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: self._send_command(_CMD_SET_FAN_DUTY, [fan, duty]) def set_speed_profile(self, channel, profile, temperature_sensor=1, **kwargs): """Set fan or fans to follow a speed profile _in rpm._ Unlike corresponding methods in other drivers, this function operates with angular speeds in rpm, not duty values in percentage. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to six (temperature, speed) pairs can be supplied in `profile`, with temperatures in Celsius and speeds in rpm. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be heuristically set to max out at 60°C. """ # send fan num, temp sensor, check to make sure it is actually enabled, and do not let the user send external sensor # 6 2-byte big endian temps (celsius * 100), then 6 2-byte big endian rpms # need to figure out how to find out what the max rpm is for the given fan if self._fan_count == 0: raise NotSupportedByDevice() profile = list(profile) criticalTemp = _CRITICAL_TEMPERATURE_HIGH if check_unsafe('high_temperature', **kwargs) else _CRITICAL_TEMPERATURE profile = _prepare_profile(profile, criticalTemp) # fan_type = kwargs['fan_type'] # need to make sure this is set temp_sensor = clamp(temperature_sensor, 1, self._temp_probs) sensors = self._data.load('temp_sensors_connected', default=[0]*self._temp_probs) if sensors[temp_sensor-1] != 1: raise ValueError('the specified temperature sensor is not connected') buf = bytearray(26) buf[1] = temp_sensor-1 # 0 # use temp sensor 1 for i, entry in enumerate(profile): temp = entry[0]*100 rpm = entry[1] # convert both values to 2 byte big endian values buf[2 + i*2] = temp.to_bytes(2, byteorder='big')[0] buf[3 + i*2] = temp.to_bytes(2, byteorder='big')[1] buf[14 + i*2] = rpm.to_bytes(2, byteorder='big')[0] buf[15 + i*2] = rpm.to_bytes(2, byteorder='big')[1] fan_channels = self._get_hw_fan_channels(channel) fan_modes = self._data.load('fan_modes', default=[0]*self._fan_count) for fan in fan_channels: mode = fan_modes[fan] if mode == _FAN_MODE_DC or mode == _FAN_MODE_PWM: buf[0] = fan self._send_command(_CMD_SET_FAN_PROFILE, buf) def set_color(self, channel, mode, colors, direction='forward', speed='medium', start_led=1, maximum_leds=_MAX_LEDS, **kwargs): """Set the color of each LED. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | Num colors | | -------- | ----------- | ---------- | | led | off | 0 | | led | fixed | 1 | | led | color_shift | 2 | | led | color_pulse | 2 | | led | color_wave | 2 | | led | visor | 2 | | led | blink | 2 | | led | marquee | 1 | | led | sequential | 1 | | led | rainbow | 0 | | led | rainbow2 | 0 | """ # a special mode to clear the current led settings. # this is usefull if the the user wants to use a led mode for multiple devices if mode == 'clear': self._data.store('saved_effects', None) return colors = list(colors) expanded = colors[:3] c = itertools.chain(*((r, g, b) for r, g, b in expanded)) colors = list(c) direction = map_direction(direction, _LED_DIRECTION_FORWARD, _LED_DIRECTION_BACKWARD) speed = _LED_SPEED_SLOW if speed == 'slow' else _LED_SPEED_FAST if speed == 'fast' else _LED_SPEED_MEDIUM start_led = clamp(start_led, 1, _MAX_LEDS) - 1 num_leds = clamp(maximum_leds, 1, _MAX_LEDS - start_led) random_colors = 0x00 if mode == 'off' or len(colors) != 0 else 0x01 mode_val = _MODES.get(mode, -1) if mode_val == -1: raise ValueError(f'mode "{mode}" is not valid') # FIXME clears on 'off', while the docs only mention this behavior for 'clear' saved_effects = [] if mode == 'off' else self._data.load('saved_effects', default=[]) for led_channel in self._get_hw_led_channels(channel): lighting_effect = { 'channel': led_channel, 'start_led': start_led, 'num_leds': num_leds, 'mode': mode_val, 'speed': speed, 'direction': direction, 'random_colors': random_colors, 'colors': colors } saved_effects += [lighting_effect] # check to make sure that too many LED effects are not being sent. # the max seems to be 8 as found here https://github.com/liquidctl/liquidctl/issues/154#issuecomment-762372583 if len(saved_effects) > 8: _LOGGER.warning(f'too many lighting effects. Run `liquidctl set {channel} color clear` to reset the effect') return # start sending the led commands self._send_command(_CMD_RESET_LED_CHANNEL, [led_channel]) self._send_command(_CMD_BEGIN_LED_EFFECT, [led_channel]) self._send_command(_CMD_SET_LED_CHANNEL_STATE, [led_channel, 0x01]) # FIXME clears on 'off', while the docs only mention this behavior for 'clear' self._data.store('saved_effects', None if mode == 'off' else saved_effects) for effect in saved_effects: config = [effect.get('channel'), effect.get('start_led'), effect.get('num_leds'), effect.get('mode'), effect.get('speed'), effect.get('direction'), effect.get('random_colors'), 0xff ] + effect.get('colors') self._send_command(_CMD_LED_EFFECT, config) self._send_command(_CMD_LED_COMMIT, [0xff]) def _send_command(self, command, data=None): # self.device.write expects buf[0] to be the report number or 0 if not used buf = bytearray(_REPORT_LENGTH + 1) buf[1] = command start_at = 2 if data: data = data[:_REPORT_LENGTH-1] buf[start_at: start_at + len(data)] = data self.device.clear_enqueued_reports() self.device.write(buf) buf = bytes(self.device.read(_RESPONSE_LENGTH)) return buf def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/liquidctl/driver/coolit.py0000644000175000017500000003276114617346724020015 0ustar00jonasjonas"""liquidctl driver for Corsair Platinum and PRO XT coolers. Supported devices ----------------- - Corsair H110i GT Supported features ------------------ - general monitoring - pump speed control - fan speed control Copyright Roberto Marques, Serphentas, and other contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import re from enum import Enum, unique from liquidctl.error import NotSupportedByDevice, NotSupportedByDriver from liquidctl.driver.usb import UsbHidDriver from liquidctl.keyval import RuntimeStorage from liquidctl.util import clamp, fraction_of_byte, u16le_from, normalize_profile LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _PUMP_INDEX = 0x02 _COMMAND_FIRMWARE_ID = 0x01 _COMMAND_TEMP_READ = 0x0E _COMMAND_FAN_SELECT = 0x10 _COMMAND_FAN_MODE = 0x12 _COMMAND_FAN_FIXED_PWM = 0x13 _COMMAND_FAN_FIXED_RPM = 0x14 _COMMAND_FAN_READ_RPM = 0x16 _COMMAND_FAN_MAX_RPM = 0x17 _COMMAND_FAN_RPM_TABLE = 0x19 _COMMAND_FAN_TEMP_TABLE = 0x1A _OP_CODE_WRITE_ONE_BYTE = 0x06 _OP_CODE_READ_ONE_BYTE = 0x07 _OP_CODE_WRITE_TWO_BYTES = 0x08 _OP_CODE_READ_TWO_BYTES = 0x09 _OP_CODE_WRITE_THREE_BYTES = 0x0A _OP_CODE_READ_THREE_BYTES = 0x0B _PROFILE_LENGTH = 5 _CRITICAL_TEMPERATURE = 60 _PUMP_DEFAULT_QUIET = [0x2E, 0x09] _PUMP_DEFAULT_EXTREME = [0x86, 0x0B] _SEQUENCE_MIN = 1 _SEQUENCE_MAX = 255 @unique class _FanMode(Enum): FIXED_DUTY = 0x02 FIXED_RPM = 0x04 CUSTOM_PROFILE = 0x0E @classmethod def _missing_(cls, value): LOGGER.debug("falling back to FIXED_DUTY for _FanMode(%s)", value) return _FanMode.FIXED_DUTY @unique class _PumpMode(Enum): QUIET = 0x08 EXTREME = 0x0C @classmethod def _missing_(cls, value): LOGGER.debug("falling back to QUIET for _PumpMode(%s)", value) return _PumpMode.QUIET def _sequence(): """Return a generator that produces valid protocol sequence numbers. Sequence numbers start from 2 to 31, then rolling over to 1 and up again. """ num = 1 while True: yield (num % 31) + 1 num += 1 def _prepare_profile(original): clamped = ((temp, clamp(duty, 0, 100)) for temp, duty in original) normal = normalize_profile(clamped, _CRITICAL_TEMPERATURE) missing = _PROFILE_LENGTH - len(normal) if missing < 0: raise ValueError(f"Too many points in profile (remove {-missing})") if missing > 0: normal += missing * [(_CRITICAL_TEMPERATURE, 100)] return normal def _quoted(*names): return ", ".join(map(repr, names)) class Coolit(UsbHidDriver): """liquidctl driver for Corsair H110i GT cooler""" _MATCHES = [ ( 0x1B1C, 0x0C04, "Corsair H110i GT", {"has_pump": True, "fan_count": 2, "rgb_fans": False}, ), ] def __init__(self, device, description, fan_count, rgb_fans, **kwargs): super().__init__(device, description, **kwargs) self._component_count = 1 + fan_count * rgb_fans self._fan_names = [f"fan{i + 1}" for i in range(fan_count)] # the following fields are only initialized in connect() self._data = None self._sequence = None def connect(self, **kwargs): """Connect to the device.""" ret = super().connect(**kwargs) ids = f"vid{self.vendor_id:04x}_pid{self.product_id:04x}" loc = "loc" + "_".join(re.findall(r"\d+", self.address)) self._data = RuntimeStorage(key_prefixes=[ids, loc]) self._sequence = _sequence() return ret def initialize(self, pump_mode="quiet", **kwargs): """Initialize the device and set the pump mode The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. Valid values for `pump_mode` are "quiet" and "extreme". Unconfigured fan channels may default to 100% duty. Returns a list of `(property, value, unit)` tuples. """ if pump_mode not in ["quiet", "extreme"]: LOGGER.warning('pump mode must be either "quiet" or "extreme", falling back to "quiet"') pump_mode = "quiet" self._data.store("pump_mode", _PumpMode[pump_mode.upper()].value) res = self._send_command( self._build_data_package(_COMMAND_FIRMWARE_ID, _OP_CODE_READ_TWO_BYTES) ) fw_version = (res[3] >> 4, res[3] & 0xF, res[2]) return [("Firmware version", "%d.%d.%d" % fw_version, "")] def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ dataPackages = list() # temperature dataPackages.append(self._build_data_package(_COMMAND_TEMP_READ, _OP_CODE_READ_TWO_BYTES)) # fan 1 dataPackages.append( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([0]) ) ) dataPackages.append( self._build_data_package(_COMMAND_FAN_READ_RPM, _OP_CODE_READ_TWO_BYTES) ) # fan 2 dataPackages.append( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([1]) ) ) dataPackages.append( self._build_data_package(_COMMAND_FAN_READ_RPM, _OP_CODE_READ_TWO_BYTES) ) # pump dataPackages.append( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([2]) ) ) dataPackages.append( self._build_data_package(_COMMAND_FAN_READ_RPM, _OP_CODE_READ_TWO_BYTES) ) res = self._send_commands(dataPackages) temp = res[3] + res[2] / 255 return [ ("Liquid temperature", temp, "°C"), ("Fan 1 speed", u16le_from(res, offset=8), "rpm"), ("Fan 2 speed", u16le_from(res, offset=14), "rpm"), ("Pump speed", u16le_from(res, offset=20), "rpm"), ] def set_fixed_speed(self, channel, duty, **kwargs): """Set fan or fans to a fixed speed duty. Valid channel values are "fanN", where N >= 1 is the fan number, and "fan", to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. """ for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f"{hw_channel}_mode", _FanMode.FIXED_DUTY.value) self._data.store(f"{hw_channel}_duty", duty) LOGGER.info(f"setting {hw_channel} to duty mode") self._send_set_cooling() def set_speed_profile(self, channel, profile, **kwargs): """Set fan or fans to follow a speed duty profile. Valid channel values are "fanN", where N >= 1 is the fan number, and "fan", to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to seven (temperature, duty) pairs can be supplied in `profile`, with temperatures in Celsius and duty values in percentage. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. """ profile = list(profile) for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f"{hw_channel}_mode", _FanMode.CUSTOM_PROFILE.value) self._data.store(f"{hw_channel}_profile", profile) LOGGER.info(f"setting {hw_channel} to profile mode") self._send_set_cooling() def set_color(self, channel, mode, colors, **kwargs): """Not supported by this driver.""" raise NotSupportedByDriver() def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def _get_hw_fan_channels(self, channel): channel = channel.lower() if channel == "fan": return self._fan_names if channel in self._fan_names: return [channel] raise ValueError(f"Unknown channel, should be one of: {_quoted('fan', *self._fan_names)}") def _build_data_package(self, command, opCode, params=None): if params: buf = bytearray(3 + len(params)) buf[3 : 3 + len(params)] = params else: buf = bytearray(3) buf[0] = next(self._sequence) buf[1] = opCode buf[2] = command return buf def _send_commands(self, dataPackages): buf = bytearray(_REPORT_LENGTH) startIndex = 1 for dataPackage in dataPackages: buf[startIndex : startIndex + len(dataPackage)] = dataPackage startIndex += len(dataPackage) buf[0] = startIndex - 1 return self._send_buffer(buf) def _send_command(self, dataPackage): buf = bytearray(_REPORT_LENGTH) buf[0] = len(dataPackage) buf[1:] = dataPackage return self._send_buffer(buf) def _send_buffer(self, buf): self.device.clear_enqueued_reports() self.device.write(buf) buf = bytes(self.device.read(_REPORT_LENGTH)) return buf def _send_set_cooling(self): for fan in self._fan_names: fanIndex = 0 if fan == "fan1" else 1 mode = _FanMode(self._data.load(f"{fan}_mode", of_type=int)) if mode is _FanMode.FIXED_DUTY: stored = self._data.load(f"{fan}_duty", of_type=int, default=100) duty = clamp(stored, 0, 100) self._send_command( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([fanIndex]) ) ) self._send_command( self._build_data_package( _COMMAND_FAN_MODE, _OP_CODE_WRITE_ONE_BYTE, params=bytes([mode.value]) ) ) self._send_command( self._build_data_package( _COMMAND_FAN_FIXED_PWM, _OP_CODE_WRITE_ONE_BYTE, params=bytes([fraction_of_byte(percentage=duty)]), ) ) LOGGER.info("setting %s to %d%% duty cycle", fan, duty) elif mode is _FanMode.CUSTOM_PROFILE: stored = self._data.load(f"{fan}_profile", of_type=list, default=[]) profile = _prepare_profile(stored) # ensures correct len(profile) pairs = ((temp, fraction_of_byte(percentage=duty)) for temp, duty in profile) # "magical" 0x0A in front of curve definition packages fanTemperatureData = [0x0A] fanDutyData = [0x0A] # get max RPM for current fan dataPackages = [] dataPackages.append( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([0]) ) ) dataPackages.append( self._build_data_package(_COMMAND_FAN_MAX_RPM, _OP_CODE_READ_TWO_BYTES) ) max_rpm = u16le_from(self._send_commands(dataPackages), offset=4) for temp, duty in profile: fanTemperatureData.append(0x00) fanTemperatureData.append(temp) rpm = duty * max_rpm / 100 fanDutyData.append(int(rpm % 255)) fanDutyData.append(int(rpm - (rpm % 255)) >> 8) # select fan to customize self._send_command( self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([fanIndex]) ) ) # Change mode to custom Profile self._send_command( self._build_data_package( _COMMAND_FAN_MODE, _OP_CODE_WRITE_ONE_BYTE, params=bytes([mode.value]) ) ) # Send duty cycle Profile self._send_command( self._build_data_package( _COMMAND_FAN_RPM_TABLE, _OP_CODE_WRITE_THREE_BYTES, params=bytes(fanDutyData), ) ) # Send temperature profile self._send_command( self._build_data_package( _COMMAND_FAN_TEMP_TABLE, _OP_CODE_WRITE_THREE_BYTES, params=bytes(fanTemperatureData), ) ) LOGGER.info("setting %s to follow profile %r", fan, profile) else: raise ValueError(f"Unsupported fan {mode}") pump_mode = _PumpMode(self._data.load("pump_mode", of_type=int)) self._send_commands( [ self._build_data_package( _COMMAND_FAN_SELECT, _OP_CODE_WRITE_ONE_BYTE, params=bytes([_PUMP_INDEX]) ), self._build_data_package( _COMMAND_FAN_FIXED_RPM, _OP_CODE_WRITE_TWO_BYTES, params=bytes( _PUMP_DEFAULT_QUIET if pump_mode == _PumpMode.QUIET else _PUMP_DEFAULT_EXTREME ), ), ] ) LOGGER.info("setting pump mode to %s", pump_mode.name.lower()) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1740979118.0 liquidctl-1.15.0/liquidctl/driver/corsair_hid_psu.py0000644000175000017500000003260614761235656021700 0ustar00jonasjonas"""liquidctl drivers for Corsair HXi and RMi series power supply units. Supported devices: - Corsair HXi (HX750i, HX850i, HX1000i and HX1200i) - Corsair RMi (RM650i, RM750i, RM850i and RM1000i) Copyright Jonas Malaco and contributors Port of corsaiRMi by notaz and realies. Copyright (c) notaz, 2016 Incorporates or uses as reference work by Sean Nelson. SPDX-License-Identifier: GPL-3.0-or-later """ import logging from datetime import timedelta from enum import Enum from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.pmbus import CommandCode as CMD from liquidctl.pmbus import WriteBit, linear_to_float from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _SLAVE_ADDRESS = 0x02 _CORSAIR_READ_TOTAL_UPTIME = CMD.MFR_SPECIFIC_D1 _CORSAIR_READ_UPTIME = CMD.MFR_SPECIFIC_D2 _CORSAIR_12V_OCP_MODE = CMD.MFR_SPECIFIC_D8 _CORSAIR_READ_OUTPUT_POWER = CMD.MFR_SPECIFIC_EE _CORSAIR_FAN_CONTROL_MODE = CMD.MFR_SPECIFIC_F0 _RAIL_12V = 0x0 _RAIL_5V = 0x1 _RAIL_3P3V = 0x2 _RAIL_NAMES = {_RAIL_12V: '+12V', _RAIL_5V: '+5V', _RAIL_3P3V: '+3.3V'} _MIN_FAN_DUTY = 30 class OCPMode(Enum): """Overcurrent protection mode.""" SINGLE_RAIL = 0x1 MULTI_RAIL = 0x2 def __str__(self): return self.name.capitalize().replace('_', ' ') class FanControlMode(Enum): """Fan control mode.""" HARDWARE = 0x0 SOFTWARE = 0x1 def __str__(self): return self.name.capitalize() class CorsairHidPsu(UsbHidDriver): """Corsair HXi or RMi series power supply unit.""" # support for hwmon: corsair-psu, Linux 5.11 (5.13 recommended) _MATCHES = [ (0x1b1c, 0x1c05, 'Corsair HX750i', { 'fpowin115': (0.00013153276902318052, 1.0118732314945875, 9.783796618886313), 'fpowin230': ( 9.268856467314546e-05, 1.0183515407387007, 8.279822175342481), }), (0x1b1c, 0x1c06, 'Corsair HX850i', { 'fpowin115': (0.00011552923724840388, 1.0111311876704099, 12.015296651918918), 'fpowin230': ( 8.126644224872423e-05, 1.0176256272095185, 10.290640442373850), }), (0x1b1c, 0x1c07, 'Corsair HX1000i', { # CP-9020074-NA 'fpowin115': (9.48609754417109e-05, 1.0170509865269720, 11.619826520447452), 'fpowin230': (9.649987544008507e-05, 1.0018241767296636, 12.759957859756842), }), (0x1b1c, 0x1c08, 'Corsair HX1200i', { 'fpowin115': (6.244705156199815e-05, 1.0234738310580973, 15.293509559389241), 'fpowin230': (5.9413179794350966e-05, 1.0023670927127724, 15.886126793547152), }), (0x1b1c, 0x1c23, 'Corsair HX1200i ATX 3.1', { # CP-9020281-NA # Certification is source for efficiency numbers https://www.cybenetics.com/evaluations/psus/2510/ # 115v: 10%-.8877, 20%-.91774, 50%-.92095, 100%-.88146 # 230v: 10%-.89881, 20%-.92956, 50%-.93047, 100%-.91292 'fpowin115': (9.930197967499293e-05, 1.003634953854399, 13.956713659543981), 'fpowin230': (4.716701557627399e-05, 1.031689131040792, 8.562560345390088), }), (0x1b1c, 0x1c0a, 'Corsair RM650i', { 'fpowin115': (0.00017323493381072683, 1.0047044721686030, 12.376592422281606), 'fpowin230': (0.00012413136310310370, 1.0284317478987164, 9.465259079360674), }), (0x1b1c, 0x1c0b, 'Corsair RM750i', { 'fpowin115': (0.00015013694263596336, 1.0047044721686027, 14.280683564171110), 'fpowin230': (0.00010460621468919797, 1.0173089573727216, 11.495900706372142), }), (0x1b1c, 0x1c0c, 'Corsair RM850i', { 'fpowin115': (0.00012280002467981107, 1.0159421430340847, 13.555472968718759), 'fpowin230': ( 8.816054254801031e-05, 1.0234738318592156, 10.832902491655597), }), (0x1b1c, 0x1c0d, 'Corsair RM1000i', { 'fpowin115': (0.00010018433053123574, 1.0272313660072225, 14.092187353321624), 'fpowin230': ( 8.600634771656125e-05, 1.0289245073649413, 13.701515390258626), }), (0x1b1c, 0x1c1e, 'Corsair HX1000i (2022)', { # CP-9020214-NA # FIXME does not accurately represent known efficiency curve at 2%–10% loads 'fpowin115': (0.00012038623467957958, 0.9899868099948035, 13.125601514017152), 'fpowin230': ( 8.725695209710315e-05, 1.0017598021499974, 9.789546063300154), }), (0x1b1c, 0x1c1f, 'Corsair HX1500i', { # CP-9020215-NA # FIXME does not accurately represent known efficiency curve at 2%–10% loads 'fpowin115': (6.605968230747892e-05, 1.0125991461405333, 17.96728350708451), 'fpowin230': (4.634428233657273e-05, 1.0183515407387007, 16.559644350684962), }), ] def __init__(self, *args, fpowin115=None, fpowin230=None, **kwargs): assert fpowin115 and fpowin230 super().__init__(*args, **kwargs) self.fpowin115 = fpowin115 self.fpowin230 = fpowin230 def initialize(self, single_12v_ocp=False, direct_access=False, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ # necessary to receive non-zero status responses from the device: # replies before calling this function appear to follow the pattern #
if self._hwmon: if not direct_access: _LOGGER.warning('bound to %s kernel driver, OCP and fan modes not changed', self._hwmon.driver) return else: _LOGGER.warning('forcing re-initialization despite %s kernel driver', self._hwmon.driver) self._write([0xfe, 0x03]) _ = self._read() # don't check current OCP and fan control modes in case we're racing # with a hwmon driver and the returned values, which aren't available # through hwmon, are corrupted by the race mode = OCPMode.SINGLE_RAIL if single_12v_ocp else OCPMode.MULTI_RAIL _LOGGER.info('setting +12V OCP mode to %s', mode) self._exec(WriteBit.WRITE, _CORSAIR_12V_OCP_MODE, [mode.value]) _LOGGER.info('resetting fan control to hardware mode') self._set_fan_control_mode(FanControlMode.HARDWARE) def _get_status_directly(self): self.device.clear_enqueued_reports() ret = self._exec(WriteBit.WRITE, CMD.PAGE, [0]) if ret[1] == 0xfe: _LOGGER.warning('possibly uninitialized device') input_voltage = self._get_float(CMD.READ_VIN) ret = [ ('Current uptime', self._get_timedelta(_CORSAIR_READ_UPTIME), ''), ('Total uptime', self._get_timedelta(_CORSAIR_READ_TOTAL_UPTIME), ''), ('VRM temperature', self._get_float(CMD.READ_TEMPERATURE_1), '°C'), ('Case temperature', self._get_float(CMD.READ_TEMPERATURE_2), '°C'), ('Fan control mode', self._get_fan_control_mode(), ''), ('Fan speed', self._get_float(CMD.READ_FAN_SPEED_1), 'rpm'), ('Input voltage', input_voltage, 'V'), ('+12V OCP mode', self._get_12v_ocp_mode(), ''), ] for rail in [_RAIL_12V, _RAIL_5V, _RAIL_3P3V]: name = _RAIL_NAMES[rail] self._exec(WriteBit.WRITE, CMD.PAGE, [rail]) ret.append((f'{name} output voltage', self._get_float(CMD.READ_VOUT), 'V')) ret.append((f'{name} output current', self._get_float(CMD.READ_IOUT), 'A')) ret.append((f'{name} output power', self._get_float(CMD.READ_POUT), 'W')) output_power = self._get_float(_CORSAIR_READ_OUTPUT_POWER) input_power = round(self._input_power_at(input_voltage, output_power), 0) efficiency = round(output_power / input_power * 100, 0) ret.append(('Total power output', output_power, 'W')) ret.append(('Estimated input power', input_power, 'W')) ret.append(('Estimated efficiency', efficiency, '%')) self._exec(WriteBit.WRITE, CMD.PAGE, [0]) return ret def _get_status_from_hwmon(self): # can't report some values (current and total uptime are only available # on debugfs, and fan and ocp modes are not available at all); still, # with this particular device, it is better to ignore them than to race # with a kernel driver _LOGGER.warning('some attributes cannot be read from %s kernel driver', self._hwmon.driver) input_voltage = self._hwmon.read_int('in0_input') * 1e-3 ret = [ ('VRM temperature', self._hwmon.read_int('temp1_input') * 1e-3, '°C'), ('Case temperature', self._hwmon.read_int('temp2_input') * 1e-3, '°C'), ('Fan speed', self._hwmon.read_int('fan1_input'), 'rpm'), ('Input voltage', input_voltage, 'V'), ] for n, rail in zip(range(2, 5), [_RAIL_12V, _RAIL_5V, _RAIL_3P3V]): i = n - 1 name = _RAIL_NAMES[rail] ret.append((f'{name} output voltage', self._hwmon.read_int(f'in{i}_input') * 1e-3, 'V')) ret.append((f'{name} output current', self._hwmon.read_int(f'curr{n}_input') * 1e-3, 'A')) ret.append((f'{name} output power', self._hwmon.read_int(f'power{n}_input') * 1e-6, 'W')) output_power = self._hwmon.read_int('power1_input') * 1e-6 input_power = round(self._input_power_at(input_voltage, output_power), 0) efficiency = round(output_power / input_power * 100, 0) ret.append(('Total power output', output_power, 'W')) ret.append(('Estimated input power', input_power, 'W')) ret.append(('Estimated efficiency', efficiency, '%')) return ret def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) return self._get_status_directly() def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" duty = clamp(duty, _MIN_FAN_DUTY, 100) _LOGGER.info('ensuring fan control is in software mode') self._set_fan_control_mode(FanControlMode.SOFTWARE) _LOGGER.info('setting fan PWM duty to %d%%', duty) self._exec(WriteBit.WRITE, CMD.FAN_COMMAND_1, [duty]) def set_color(self, channel, mode, colors, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def _input_power_at(self, input_voltage, output_power): def quadratic(params, x): a, b, c = params return a * x**2 + b * x + c for_in115v = quadratic(self.fpowin115, output_power) for_in230v = quadratic(self.fpowin230, output_power) _LOGGER.debug('input power estimates: %.3f W @ 115 V; %.3f W @ 230 V', for_in115v, for_in230v) # linearly interpolate for input_voltage return for_in115v + (for_in230v - for_in115v) / 115 * (input_voltage - 115) def _write(self, data): assert len(data) <= _REPORT_LENGTH packet = bytearray(1 + _REPORT_LENGTH) packet[1: 1 + len(data)] = data # device doesn't use numbered reports self.device.write(packet) def _read(self): return self.device.read(_REPORT_LENGTH) def _exec(self, writebit, command, data=None): out = [_SLAVE_ADDRESS | WriteBit(writebit), CMD(command)] + (data or []) self._write(out) ret = self._read() assert ret[0:2] == out[0:2], f'invalid response (possible conflict with another program)' return ret def _get_12v_ocp_mode(self): """Get +12V single/multi-rail OCP mode.""" return OCPMode(self._exec(WriteBit.READ, _CORSAIR_12V_OCP_MODE)[2]) def _get_fan_control_mode(self): """Get hardware/software fan control mode.""" return FanControlMode(self._exec(WriteBit.READ, _CORSAIR_FAN_CONTROL_MODE)[2]) def _set_fan_control_mode(self, mode): """Set hardware/software fan control mode.""" return self._exec(WriteBit.WRITE, _CORSAIR_FAN_CONTROL_MODE, [mode.value]) def _get_float(self, command): """Get float value with `command`.""" return linear_to_float(self._exec(WriteBit.READ, command)[2:]) def _get_timedelta(self, command): """Get timedelta with `command`.""" secs = int.from_bytes(self._exec(WriteBit.READ, command)[2:6], byteorder='little') return timedelta(seconds=secs) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() # deprecated aliases CorsairHidPsuDriver = CorsairHidPsu ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/liquidctl/driver/ddr4.py0000644000175000017500000003140614677373312017353 0ustar00jonasjonas"""liquidctl drivers for DDR4 memory. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging from collections import namedtuple from enum import Enum, unique from liquidctl.driver.smbus import SmbusDriver from liquidctl.error import ExpectationNotMet, NotSupportedByDevice, NotSupportedByDriver from liquidctl.util import RelaxedNamesEnum, check_unsafe, clamp _LOGGER = logging.getLogger(__name__) class Ddr4Spd: """Partial decoding of DDR4 Serial Presence Detect (SPD) information. Properties will raise on data they are not yet prepared to handle, but what is implemented attempts to comply with JEDEC 21-C 4.1.2 Annex L. """ class DramDeviceType(Enum): """DRAM device type (not exhaustive).""" DDR4_SDRAM = 0x0c LPDDR4_SDRAM = 0x10 LPDDR4X_SDRAM = 0x11 def __str__(self): return self.name.replace('_', ' ') class BaseModuleType(Enum): """Base module type (not exhaustive).""" RDIMM = 0b0001 UDIMM = 0b0010 SO_DIMM = 0b0011 LRDIMM = 0x0100 def __str__(self): return self.name.replace('_', ' ') # Standard Manufacturer's Identification Code from JEDEC JEP106; # (not exhaustive) maps banks and IDs to names: _JEP106[][id] _JEP106 = { 1: { 0x2c: 'Micron', 0xad: 'SK Hynix', 0xce: 'Samsung', }, 2: {0x98: 'Kingston'}, 3: {0x9e: 'Corsair'}, 5: { 0xcd: 'G.SKILL', 0xef: 'Team Group', }, 6: { 0x02: 'Patriot', 0x9b: 'Crucial', }, } def __init__(self, eeprom): self._eeprom = eeprom if self.dram_device_type not in [self.DramDeviceType.DDR4_SDRAM, self.DramDeviceType.LPDDR4_SDRAM, self.DramDeviceType.LPDDR4X_SDRAM]: raise ValueError('not a DDR4 SPD EEPROM') @property def spd_bytes_used(self): nibble = self._eeprom[0x00] & 0x0f assert nibble <= 0b0100, 'reserved' return nibble * 128 @property def spd_bytes_total(self): nibble = (self._eeprom[0x00] >> 4) & 0b111 assert nibble <= 0b010, 'reserved' return nibble * 256 @property def spd_revision(self): enc_level = self._eeprom[0x01] >> 4 add_level = self._eeprom[0x01] & 0x0f return (enc_level, add_level) @property def dram_device_type(self): return self.DramDeviceType(self._eeprom[0x02]) @property def module_type(self): base = self._eeprom[0x03] & 0x0f hybrid = self._eeprom[0x03] >> 4 assert not hybrid return (self.BaseModuleType(base), None) @property def module_thermal_sensor(self): present = self._eeprom[0x0e] >> 7 return bool(present) @property def module_manufacturer(self): bank = 1 + self._eeprom[0x140] & 0x7f mid = self._eeprom[0x141] return self._JEP106[bank][mid] @property def module_part_number(self): return self._eeprom[0x149:0x15d].decode(encoding='ascii').rstrip() @property def dram_manufacturer(self): bank = 1 + self._eeprom[0x15e] & 0x7f mid = self._eeprom[0x15f] return self._JEP106[bank][mid] class Ddr4Temperature(SmbusDriver): """DDR4 module with TSE2004-compatible SPD EEPROM and temperature sensor.""" _SPD_DTIC = 0x50 _TS_DTIC = 0x18 _SA_MASK = 0b111 _REG_CAPABILITIES = 0x00 _REG_TEMPERATURE = 0x05 _UNSAFE = ['smbus', 'ddr4_temperature'] @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): # FIXME support mainstream AMD chipsets on Linux; note that unlike # i801_smbus, piix4_smbus does not enumerate and register the available # SPD EEPROMs with i2c_register_spd _SMBUS_DRIVERS = ['i801_smbus'] if smbus.parent_driver not in _SMBUS_DRIVERS \ or any([vendor, product, release, serial]): # wont match, always None return for dimm in range(cls._SA_MASK + 1): spd_addr = cls._SPD_DTIC | dimm _LOGGER.debug('%s checking address: %x', cls.__name__, spd_addr) eeprom = smbus.load_eeprom(spd_addr) if not eeprom or eeprom.name != 'ee1004': continue try: spd = Ddr4Spd(eeprom.data) if spd.dram_device_type != Ddr4Spd.DramDeviceType.DDR4_SDRAM: continue desc = cls._match(spd) except: continue if not desc: continue desc += f' DIMM{dimm + 1}' if (address and int(address, base=16) != spd_addr) \ or (match and match.lower() not in desc.lower()): continue # set the default device address to a weird value to prevent # accidental attempts of writes to the SPD EEPROM (DDR4 SPD writes # are also disabled by default in many motherboards) dev = cls(smbus, desc, address=(None, None, spd_addr)) _LOGGER.debug('%s identified: %s', cls.__name__, desc) yield dev @classmethod def _match(cls, spd): if not spd.module_thermal_sensor: return None try: manufacturer = spd.module_manufacturer except: return 'DDR4' if spd.module_part_number: return f'{manufacturer} {spd.module_part_number}' else: return manufacturer def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._ts_address = self._TS_DTIC | (self._address[2] & self._SA_MASK) @property def address(self): return f'{self._address[2]:#04x}' def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning("%s: disabled, requires unsafe features '%s'", self.description, ','.join(self._UNSAFE)) return [] treg = self._read_temperature_register() # discard flags bits and interpret remaining bits as 2s complement treg = treg & 0x1fff if treg > 0x0fff: treg -= 0x2000 # should always be supported resolution, bits = (.25, 10) multiplier = treg >> (12 - bits) return [ ('Temperature', resolution * multiplier, '°C'), ] def _read_temperature_register(self): # in theory we should first write 0x05 to the pointer register, but # avoid writing to the device, even if that means occasionally reading # garbage; ideally we would check the currently set pointer, but we # have not found any way to do that # while JEDEC 21-C 4.1.6 uses the term "block read", it has little to # do with the SMBus Block Read protocol; instead, it is closer to the # SMBus Read Word protocol, except in big endianess treg = self._smbus.read_word_data(self._ts_address, self._REG_TEMPERATURE) # swap LSB and MSB before returning: read_word_data reads in little # endianess, but the register must be read in big endianess return ((treg & 0xff) << 8) | (treg >> 8) def initialize(self, **kwargs): """Initialize the device.""" pass def set_color(self, channel, mode, colors, **kwargs): """Not supported by this driver.""" raise NotSupportedByDriver() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class VengeanceRgb(Ddr4Temperature): """Corsair Vengeance RGB DDR4 module.""" _RGB_DTIC = 0x58 _REG_RGB_TIMING1 = 0xa4 _REG_RGB_TIMING2 = 0xa5 _REG_RGB_MODE = 0xa6 _REG_RGB_COLOR_COUNT = 0xa7 _REG_RGB_COLOR_START = 0xb0 _REG_RGB_COLOR_END = 0xc5 _UNSAFE = ['smbus', 'vengeance_rgb'] @unique class Mode(bytes, RelaxedNamesEnum): def __new__(cls, value, min_colors, max_colors): obj = bytes.__new__(cls, [value]) obj._value_ = value obj.min_colors = min_colors obj.max_colors = max_colors return obj FIXED = (0x00, 1, 1) FADING = (0x01, 2, 7) BREATHING = (0x02, 1, 7) OFF = (0xf0, 0, 0) # pseudo mode, equivalent to fixed #000000 def __str__(self): return self.name.lower() @unique class SpeedTimings(RelaxedNamesEnum): SLOWEST = 63 SLOWER = 48 NORMAL = 32 FASTER = 16 FASTEST = 1 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._rgb_address = None @classmethod def _match(cls, spd): if spd.module_type != (Ddr4Spd.BaseModuleType.UDIMM, None) \ or spd.module_manufacturer != 'Corsair' \ or not spd.module_part_number.startswith('CMR'): return None return 'Corsair Vengeance RGB' def set_color(self, channel, mode, colors, speed='normal', transition_ticks=None, stable_ticks=None, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Colors | | -------- | --------- | ------ | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1–7 | | led | fading | 2–7 | The speed of the breathing and fading animations can be adjusted with `speed`; the allowed values are 'slowest', 'slower', 'normal' (default), 'faster' and 'fastest'. It is also possible to override the raw timing parameters through `transition_ticks` and `stable_ticks`; these should be integer values in the range 0–63. """ check_unsafe(*self._UNSAFE, error=True, **kwargs) try: common = self.SpeedTimings[speed].value tp1 = tp2 = common except KeyError: raise ValueError(f'invalid speed preset: {speed!r}') from None if transition_ticks is not None: tp1 = clamp(transition_ticks, 0, 63) if stable_ticks is not None: tp2 = clamp(stable_ticks, 0, 63) colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.min_colors: raise ValueError(f'{mode} mode requires {mode.min_colors} colors') if len(colors) > mode.max_colors: _LOGGER.debug('too many colors, dropping to %d', mode.max_colors) colors = colors[:mode.max_colors] self._compute_rgb_address() if mode == self.Mode.OFF: mode = self.Mode.FIXED colors = [[0x00, 0x00, 0x00]] def rgb_write(register, value): self._smbus.write_byte_data(self._rgb_address, register, value) if mode == self.Mode.FIXED: rgb_write(self._REG_RGB_TIMING1, 0x00) else: rgb_write(self._REG_RGB_TIMING1, tp1) rgb_write(self._REG_RGB_TIMING2, tp2) color_registers = range(self._REG_RGB_COLOR_START, self._REG_RGB_COLOR_END) color_components = itertools.chain(*colors) for register, component in zip(color_registers, color_components): rgb_write(register, component) rgb_write(self._REG_RGB_COLOR_COUNT, len(colors)) if mode == self.Mode.BREATHING and len(colors) == 1: rgb_write(self._REG_RGB_MODE, self.Mode.FIXED.value) else: rgb_write(self._REG_RGB_MODE, mode.value) def _compute_rgb_address(self): if self._rgb_address: return # the dimm's rgb controller is typically at 0x58–0x5f candidate = self._RGB_DTIC | (self._address[2] & self._SA_MASK) # reading from any register should return 0xba if we have the right device if self._smbus.read_byte_data(candidate, self._REG_RGB_MODE) != 0xba: raise ExpectationNotMet(f'{self.bus}:{candidate:#04x} is not the RGB controller') self._rgb_address = candidate ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/hwmon.py0000644000175000017500000000355414562144457017650 0ustar00jonasjonas"""Access to Linux HWMON data. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style import logging import sys from pathlib import Path _LOGGER = logging.getLogger(__name__) _IS_LINUX = sys.platform == "linux" class HwmonDevice: """Unstable API.""" __slots__ = [ "driver", "path", ] def __init__(self, driver, path): self.driver = driver self.path = path @property def name(self): return self.path.name def has_attribute(self, name): return (self.path / name).is_file() def get_string(self, name): value = (self.path / name).read_text().rstrip() _LOGGER.debug("read %s: %s", name, value) return value def read_int(self, name): return int(self.get_string(name)) def write_int(self, name, value): (self.path / name).write_text(str(value)) @classmethod def from_hidraw(cls, path): """Find the `HwmonDevice` for `path`.""" if not _IS_LINUX: return None if not path.startswith(b"/dev/hidraw"): _LOGGER.debug("cannot search hwmon device for %s: unsupported path", path) return None path = path.decode() name = path[5:] class_path = Path("/sys/class/hidraw", name) sys_device = class_path / "device" hwmon_devices = sys_device / "hwmon" if not hwmon_devices.exists(): return None hwmon_paths = hwmon_devices.iterdir() path = next(hwmon_paths) if next(hwmon_paths, None): _LOGGER.debug("cannot pick hwmon device for %s: more than one alternative", path) return None # use resolve() to be compatible with Python < 3.9 driver = (sys_device / "driver").resolve().name return HwmonDevice(driver, path) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728750383.0 liquidctl-1.15.0/liquidctl/driver/hydro_platinum.py0000644000175000017500000004273114702521457021551 0ustar00jonasjonas"""liquidctl drivers for Corsair Hydro Platinum and Pro XT liquid coolers. Supported devices: - Corsair Hydro H100i Platinum - Corsair Hydro H100i Platinum SE - Corsair Hydro H115i Platinum - Corsair Hydro H60i Pro XT - Corsair Hydro H100i Pro XT - Corsair Hydro H115i Pro XT - Corsair Hydro H150i Pro XT - Corsair iCUE H100i Elite RGB - Corsair iCUE H115i Elite RGB - Corsair iCUE H150i Elite RGB Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging import re from enum import Enum, unique from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.keyval import RuntimeStorage from liquidctl.util import RelaxedNamesEnum, clamp, fraction_of_byte, \ u16le_from, normalize_profile, mkCrcFun _LOGGER = logging.getLogger(__name__) _REPORT_LENGTH = 64 _WRITE_PREFIX = 0x3f _FEATURE_COOLING = 0b000 _FEATURE_COOLING2 = 0b011 _CMD_GET_STATUS = 0xff _CMD_SET_COOLING = 0x14 _FEATURE_LIGHTING = None _CMD_SET_LIGHTING1 = 0b100 _CMD_SET_LIGHTING2 = 0b101 _CMD_SET_LIGHTING3 = 0b110 # cooling data starts at offset 3 and ends just before the PEC byte _SET_COOLING_DATA_LENGTH = _REPORT_LENGTH - 4 _SET_COOLING_DATA_PREFIX = [0x0, 0xff, 0x5, 0xff, 0xff, 0xff, 0xff, 0xff] _FAN_MODE_OFFSETS = [0x0b - 3, 0x11 - 3] _FAN_DUTY_OFFSETS = [offset + 5 for offset in _FAN_MODE_OFFSETS] _FAN_PROFILE_OFFSETS = [0x1e - 3, 0x2c - 3] _FAN_OFFSETS = list(zip(_FAN_MODE_OFFSETS, _FAN_DUTY_OFFSETS, _FAN_PROFILE_OFFSETS)) _PUMP_MODE_OFFSET = 0x17 - 3 _PROFILE_LENGTH_OFFSET = 0x1d - 3 _PROFILE_LENGTH = 7 _CRITICAL_TEMPERATURE = 60 @unique class _FanMode(Enum): CUSTOM_PROFILE = 0x0 CUSTOM_PROFILE_WITH_EXTERNAL_SENSOR = 0x1 FIXED_DUTY = 0x2 FIXED_RPM = 0x4 @classmethod def _missing_(cls, value): _LOGGER.debug('falling back to FIXED_DUTY for _FanMode(%s)', value) return _FanMode.FIXED_DUTY @unique class _PumpMode(RelaxedNamesEnum): QUIET = 0x0 BALANCED = 0x1 EXTREME = 0x2 @classmethod def _missing_(cls, value): _LOGGER.debug('falling back to BALANCED for _PumpMode(%s)', value) return _PumpMode.BALANCED def _sequence(storage): """Return a generator that produces valid protocol sequence numbers. Sequence numbers increment across successful invocations of liquidctl, but are not atomic. The sequence is: 1, 2, 3... 29, 30, 31, 1, 2, 3... In the protocol the sequence number is usually shifted left by 3 bits, and a shifted sequence will look like: 8, 16, 24... 232, 240, 248, 8, 16, 24... """ while True: seq = storage.load_store('sequence', lambda x : x % 31 + 1, of_type=int, default=0) yield seq[1] def _prepare_profile(original): clamped = ((temp, clamp(duty, 0, 100)) for temp, duty in original) normal = normalize_profile(clamped, _CRITICAL_TEMPERATURE) missing = _PROFILE_LENGTH - len(normal) if missing < 0: raise ValueError(f'too many points in profile (remove {-missing})') if missing > 0: normal += missing * [(_CRITICAL_TEMPERATURE, 100)] return normal def _quoted(*names): return ', '.join(map(repr, names)) class HydroPlatinum(UsbHidDriver): """Corsair Hydro Platinum or Pro XT liquid cooler.""" _MATCHES = [ (0x1b1c, 0x0c18, 'Corsair Hydro H100i Platinum', {'fan_count': 2, 'fan_leds': 4}), (0x1b1c, 0x0c19, 'Corsair Hydro H100i Platinum SE', {'fan_count': 2, 'fan_leds': 16}), (0x1b1c, 0x0c17, 'Corsair Hydro H115i Platinum', {'fan_count': 2, 'fan_leds': 4}), (0x1b1c, 0x0c29, 'Corsair Hydro H60i Pro XT', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c20, 'Corsair Hydro H100i Pro XT', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c21, 'Corsair Hydro H115i Pro XT', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c22, 'Corsair Hydro H150i Pro XT', {'fan_count': 3, 'fan_leds': 0}), (0x1b1c, 0x0c35, 'Corsair iCUE H100i Elite RGB', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c36, 'Corsair iCUE H115i Elite RGB', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c37, 'Corsair iCUE H150i Elite RGB', {'fan_count': 3, 'fan_leds': 0}), (0x1b1c, 0x0c40, 'Corsair iCUE H100i Elite RGB (White)', {'fan_count': 2, 'fan_leds': 0}), (0x1b1c, 0x0c41, 'Corsair iCUE H150i Elite RGB (White)', {'fan_count': 3, 'fan_leds': 0}) ] @classmethod def probe(cls, handle, vendor=None, product=None, release=None, serial=None, match=None, **kwargs): """Probe `handle` and yield corresponding driver instances.""" # this is modified from BaseUsbDriver.probe to match regardless of # presence of "Hydro", for backward compatibility with 1.5.0 and # previous versions for vid, pid, desc, devargs in cls._MATCHES: if (vendor and vendor != vid) or handle.vendor_id != vid: continue if (product and product != pid) or handle.product_id != pid: continue if release and handle.release_number != release: continue if serial and handle.serial_number != serial: continue if match: match = match.lower() descr = desc.lower() if not (match in descr or match in descr.replace('hydro ', '')): continue consargs = devargs.copy() consargs.update(kwargs) dev = cls(handle, desc, **consargs) _LOGGER.debug('%s identified: %s', cls.__name__, desc) yield dev def __init__(self, device, description, fan_count, fan_leds, **kwargs): super().__init__(device, description, **kwargs) self._led_count = 16 + fan_count * fan_leds self._fan_names = [f'fan{i + 1}' for i in range(fan_count)] self._mincolors = { ('led', 'super-fixed'): 1, ('led', 'fixed'): 1, ('led', 'off'): 0, } self._maxcolors = { ('led', 'super-fixed'): self._led_count, ('led', 'fixed'): 1, ('led', 'off'): 0, } # the following fields are only initialized in connect() self._data = None self._sequence = None def connect(self, runtime_storage=None, **kwargs): """Connect to the device.""" ret = super().connect(**kwargs) if runtime_storage: self._data = runtime_storage else: ids = f'vid{self.vendor_id:04x}_pid{self.product_id:04x}' # must use the HID path because there is no serial number; however, # these can be quite long on Windows and macOS, so only take the # numbers, since they are likely the only parts that vary between two # devices of the same model loc = 'loc' + '_'.join(re.findall(r'\d+', self.address)) self._data = RuntimeStorage(key_prefixes=[ids, loc]) self._sequence = _sequence(self._data) return ret def initialize(self, pump_mode='balanced', **kwargs): """Initialize the device and set the pump mode. The device should be initialized every time it is powered on, including when the system resumes from suspending to memory. Valid values for `pump_mode` are 'quiet', 'balanced' and 'extreme'. Unconfigured fan channels may default to 100% duty. Subsequent calls should leave the fan speeds unaffected. Returns a list of `(property, value, unit)` tuples. """ # set the flag so the LED command will need to be set again self._data.store('leds_enabled', 0) self._data.store('pump_mode', _PumpMode[pump_mode].value) res = self._send_set_cooling() fw_version = (res[2] >> 4, res[2] & 0xf, res[3]) if fw_version < (1, 1, 0): # see: #201 ("Fan settings affects Fan 1 only and disables fan2") _LOGGER.warning('outdated and possibly unsupported firmware version') return [('Firmware version', '{}.{}.{}'.format(*fw_version), '')] def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ res = self._send_command(_FEATURE_COOLING, _CMD_GET_STATUS) info = [ ('Liquid temperature', res[8] + res[7] / 255, '°C'), ] channels = [('Fan 1', 14), ('Fan 2', 21), ('Fan 3', 42)][:len(self._fan_names)] channels.append(('Pump', 28)) for name, base in channels: info.append((f'{name} speed', u16le_from(res, offset=base + 1), 'rpm')) info.append((f'{name} duty', round(res[base] / 255 * 100), '%')) return info def set_fixed_speed(self, channel, duty, **kwargs): """Set fan or fans to a fixed speed duty. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. """ for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f'{hw_channel}_mode', _FanMode.FIXED_DUTY.value) self._data.store(f'{hw_channel}_duty', duty) self._send_set_cooling() def set_speed_profile(self, channel, profile, **kwargs): """Set fan or fans to follow a speed duty profile. Valid channel values are 'fanN', where N >= 1 is the fan number, and 'fan', to simultaneously configure all fans. Unconfigured fan channels may default to 100% duty. Up to seven (temperature, duty) pairs can be supplied in `profile`, with temperatures in Celsius and duty values in percentage. The last point should set the fan to 100% duty cycle, or be omitted; in the latter case the fan will be set to max out at 60°C. """ profile = list(profile) for hw_channel in self._get_hw_fan_channels(channel): self._data.store(f'{hw_channel}_mode', _FanMode.CUSTOM_PROFILE.value) self._data.store(f'{hw_channel}_profile', profile) self._send_set_cooling() def set_color(self, channel, mode, colors, **kwargs): """Set the color of each LED. In reality the device does not have the concept of different channels or modes, but this driver provides a few for convenience. Animations still require successive calls to this API. The 'led' channel can be used to address individual LEDs, and supports the 'super-fixed', 'fixed' and 'off' modes. In 'super-fixed' mode, each color in `colors` is applied to one individual LED, successively. LEDs for which no color has been specified default to off/solid black. This is closest to how the device works. In 'fixed' mode, all LEDs are set to the first color taken from `colors`. The `off` mode is equivalent to calling this function with 'fixed' and a single solid black color in `colors`. The `colors` argument should be an iterable of one or more `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. The table bellow summarizes the available channels, modes, and their associated maximum number of colors for each device family. | Channel | Mode | LEDs | Platinum | Pro XT/Elite RGB | Platinum SE | | -------- | ----------- | ------------ | -------- | ---------------- | ----------- | | led | off | synchronized | 0 | 0 | 0 | | led | fixed | synchronized | 1 | 1 | 1 | | led | super-fixed | independent | 24 | 16 | 48 | """ colors = list(colors) self._check_color_args(channel, mode, colors) if mode == 'off': expanded = [] elif (channel, mode) == ('led', 'super-fixed'): expanded = colors[:self._led_count] elif (channel, mode) == ('led', 'fixed'): expanded = list(itertools.chain(*([color] * self._led_count for color in colors[:1]))) else: assert False, 'assumed unreacheable' if self._data.load('leds_enabled', of_type=int, default=0) == 0: # These hex strings are currently magic values that work but Im not quite sure why. d1 = bytes.fromhex("0101ffffffffffffffffffffffffff7f7f7f7fff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffff00ffffffffffffffffffffffffffffff") d2 = bytes.fromhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f2021222324252627ffffffffffffffffffffffffffffffffffffffffff") d3 = bytes.fromhex("28292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4fffffffffffffffffffffffffffffffffffffffffff") # Send the magic messages to enable setting the LEDs to statuC values self._send_command(None, 0b001, data=d1) self._send_command(None, 0b010, data=d2) self._send_command(None, 0b011, data=d3) self._data.store('leds_enabled', 1) data1 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[0:20]))) data2 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[20:40]))) data3 = bytes(itertools.chain(*((b, g, r) for r, g, b in expanded[40:]))) self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING1, data=data1) if self._led_count > 20: self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING2, data=data2) if self._led_count > 40: self._send_command(_FEATURE_LIGHTING, _CMD_SET_LIGHTING3, data=data3) def _check_color_args(self, channel, mode, colors): try: mincolors = self._mincolors[(channel, mode)] maxcolors = self._maxcolors[(channel, mode)] except KeyError: raise ValueError(f'unsupported (channel, mode) pair, ' f'should be one of: {_quoted(*self._mincolors)}') from None if len(colors) < mincolors: raise ValueError(f'at least {mincolors} required for {_quoted(channel, mode)}') if len(colors) > maxcolors: _LOGGER.warning('too many colors, dropping to %d', maxcolors) return maxcolors return len(colors) def _get_hw_fan_channels(self, channel): if channel == 'fan': return self._fan_names if channel in self._fan_names: return [channel] raise ValueError(f'unknown channel, should be one of: {_quoted("fan", *self._fan_names)}') def _send_command(self, feature, command, data=None): # self.device.write expects buf[0] to be the report number or 0 if not used buf = bytearray(_REPORT_LENGTH + 1) buf[1] = _WRITE_PREFIX buf[2] = next(self._sequence) << 3 if feature is not None: buf[2] |= feature buf[3] = command start_at = 4 else: buf[2] |= command start_at = 3 if data: buf[start_at: start_at + len(data)] = data buf[-1] = mkCrcFun('crc-8')(buf[2:-1]) self.device.clear_enqueued_reports() self.device.write(buf) buf = bytes(self.device.read(_REPORT_LENGTH)) if mkCrcFun('crc-8')(buf[1:]): _LOGGER.warning('response checksum does not match data') return buf def _generate_cooling_payload(self, fan_names): data = bytearray(_SET_COOLING_DATA_LENGTH) data[0: len(_SET_COOLING_DATA_PREFIX)] = _SET_COOLING_DATA_PREFIX data[_PROFILE_LENGTH_OFFSET] = _PROFILE_LENGTH for fan, (imode, iduty, iprofile) in zip(fan_names, _FAN_OFFSETS): mode = _FanMode(self._data.load(f'{fan}_mode', of_type=int)) if mode is _FanMode.FIXED_DUTY: stored = self._data.load(f'{fan}_duty', of_type=int, default=100) duty = clamp(stored, 0, 100) data[iduty] = fraction_of_byte(percentage=duty) _LOGGER.info('setting %s to %d%% duty cycle', fan, duty) elif mode is _FanMode.CUSTOM_PROFILE: stored = self._data.load(f'{fan}_profile', of_type=list, default=[]) profile = _prepare_profile(stored) # ensures correct len(profile) pairs = ((temp, fraction_of_byte(percentage=duty)) for temp, duty in profile) data[iprofile: iprofile + _PROFILE_LENGTH * 2] = itertools.chain(*pairs) _LOGGER.info('setting %s to follow profile %r', fan, profile) else: raise ValueError(f'unsupported fan {mode}') data[imode] = mode.value return data def _send_set_cooling(self): pump_mode = _PumpMode(self._data.load('pump_mode', of_type=int)) data = self._generate_cooling_payload(self._fan_names[0:2]) data[_PUMP_MODE_OFFSET] = pump_mode.value _LOGGER.info('setting pump mode to %s', pump_mode.name.lower()) if len(self._fan_names) == 3: data2 = self._generate_cooling_payload(self._fan_names[2:]) data2[_PUMP_MODE_OFFSET] = 0xff self._send_command(_FEATURE_COOLING2, _CMD_SET_COOLING, data=data2) return self._send_command(_FEATURE_COOLING, _CMD_SET_COOLING, data=data) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/kraken2.py0000644000175000017500000002707014562144457020054 0ustar00jonasjonas"""liquidctl drivers for third generation NZXT Kraken X and M liquid coolers. Kraken X (X42, X52, X62 and X72) -------------------------------- These coolers house 5-th generation Asetek pumps with additional PCBs for advanced control and RGB capabilites. Kraken M22 ---------- The Kraken M22 shares similar RGB funcionality to the X models of the same generation, but has no liquid temperature sensor and no hability to report or set fan or pump speeds. Copyright Jonas Malaco and contributors Incorporates work by leaty, Ksenija Stanojevic, Alexander Tong and Jens Neumaier. SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp, normalize_profile, interpolate_profile, \ map_direction _LOGGER = logging.getLogger(__name__) _SPEED_CHANNELS = { # (base, minimum duty, maximum duty) 'fan': (0x80, 0, 100), 'pump': (0xc0, 0, 100), } _STATUS_TEMPERATURE = 'Liquid temperature' _STATUS_FAN_SPEED = 'Fan speed' _STATUS_PUMP_SPEED = 'Pump speed' _STATUS_FWVERSION = 'Firmware version' # more aggressive than observed 4.0.3 and 6.2 firmware defaults _RESET_FAN_PROFILE = [(20, 25), (30, 50), (50, 90), (60, 100)] _RESET_PUMP_PROFILE = [(20, 50), (30, 60), (40, 90), (50, 100)] _CRITICAL_TEMPERATURE = 60 _COLOR_CHANNELS = { 'sync': 0x0, 'logo': 0x1, 'ring': 0x2, } _COLOR_MODES = { # (byte3/mode, byte2/reverse, byte4/modifier, min colors, max colors, only ring) 'off': (0x00, 0x00, 0x00, 0, 0, False), 'fixed': (0x00, 0x00, 0x00, 1, 1, False), 'super-fixed': (0x00, 0x00, 0x00, 1, 9, False), # independent logo + ring leds 'fading': (0x01, 0x00, 0x00, 2, 8, False), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0, False), 'marquee-3': (0x03, 0x00, 0x00, 1, 1, True), 'marquee-4': (0x03, 0x00, 0x08, 1, 1, True), 'marquee-5': (0x03, 0x00, 0x10, 1, 1, True), 'marquee-6': (0x03, 0x00, 0x18, 1, 1, True), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8, True), 'alternating': (0x05, 0x00, 0x00, 2, 2, True), 'moving-alternating': (0x05, 0x08, 0x00, 2, 2, True), 'breathing': (0x06, 0x00, 0x00, 1, 8, False), # colors for each step 'super-breathing': (0x06, 0x00, 0x00, 1, 9, False), # one step, independent logo + ring leds 'pulse': (0x07, 0x00, 0x00, 1, 8, False), 'tai-chi': (0x08, 0x00, 0x00, 2, 2, True), 'water-cooler': (0x09, 0x00, 0x00, 0, 0, True), 'loading': (0x0a, 0x00, 0x00, 1, 1, True), 'wings': (0x0c, 0x00, 0x00, 1, 1, True), 'super-wave': (0x0d, 0x00, 0x00, 1, 8, True), # independent ring leds } _ANIMATION_SPEEDS = { 'slowest': 0x0, 'slower': 0x1, 'normal': 0x2, 'faster': 0x3, 'fastest': 0x4, } _READ_ENDPOINT = 0x81 _READ_LENGTH = 64 _WRITE_ENDPOINT = 0x1 _WRITE_LENGTH = 65 class Kraken2(UsbHidDriver): """Third generation NZXT Kraken X or M liquid cooler.""" # support for hwmon: nzxt-kraken2, Linux 5.13 DEVICE_KRAKENX = 'Kraken X' DEVICE_KRAKENM = 'Kraken M' _MATCHES = [ (0x1e71, 0x170e, 'NZXT Kraken X (X42, X52, X62 or X72)', { 'device_type': DEVICE_KRAKENX }), (0x1e71, 0x1715, 'NZXT Kraken M22', { 'device_type': DEVICE_KRAKENM }), ] def __init__(self, device, description, device_type=DEVICE_KRAKENX, **kwargs): super().__init__(device, description) self.device_type = device_type self.supports_lighting = True self.supports_cooling = self.device_type != self.DEVICE_KRAKENM self._firmware_version = None # read once necessary def initialize(self, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ if self.supports_cooling_profiles: # due to a firmware limitation the same set of temperatures must be # used on both channels; ensure that is always true, even if the # user later only changes one of them, by resetting the profiles self.set_speed_profile('fan', _RESET_FAN_PROFILE) self.set_speed_profile('pump', _RESET_PUMP_PROFILE) fw = self.firmware_version _LOGGER.debug('raw firmware version: %r', fw) # after the transition to the unified 5.x/6.x firmware, NZXT/CAM # simplified how they report the firmware version from its raw # 4-component form fw_human = f'{fw[0]}.{fw[3]}' if fw[0] >= 5 else f'{fw[0]}.{fw[2]}.{fw[3]}' return [('Firmware version', fw_human, '')] def _get_status_directly(self): msg = self._read() return [ (_STATUS_TEMPERATURE, msg[1] + msg[2]/10, '°C'), (_STATUS_FAN_SPEED, msg[3] << 8 | msg[4], 'rpm'), (_STATUS_PUMP_SPEED, msg[5] << 8 | msg[6], 'rpm'), ] def _get_status_from_hwmon(self): return [ (_STATUS_TEMPERATURE, self._hwmon.read_int('temp1_input') * 1e-3, '°C'), (_STATUS_FAN_SPEED, self._hwmon.read_int('fan1_input'), 'rpm'), (_STATUS_PUMP_SPEED, self._hwmon.read_int('fan2_input'), 'rpm'), ] def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self.device_type == self.DEVICE_KRAKENM: return [] if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) return self._get_status_directly() def set_color(self, channel, mode, colors, speed='normal', direction='forward', **kwargs): """Set the color mode for a specific channel.""" if not self.supports_lighting: raise NotSupportedByDevice() if mode == 'super': _LOGGER.warning('deprecated mode, move to super-fixed, super-breathing or super-wave') mode = 'super-fixed' if 'backwards' in mode: _LOGGER.warning('deprecated mode, move to direction=backward option') mode = mode.replace('backwards-', '') direction = 'backward' mval, mod2, mod4, mincolors, maxcolors, ringonly = _COLOR_MODES[mode] mod2 += map_direction(direction, 0, 0x10) if ringonly and channel != 'ring': _LOGGER.warning('mode=%s unsupported with channel=%s, dropping to ring', mode, channel) channel = 'ring' steps = self._generate_steps(colors, mincolors, maxcolors, mode, ringonly) sval = _ANIMATION_SPEEDS[speed] byte2 = mod2 | _COLOR_CHANNELS[channel] for i, leds in enumerate(steps): seq = i << 5 byte4 = sval | seq | mod4 logo = [leds[0][1], leds[0][0], leds[0][2]] ring = list(itertools.chain(*leds[1:])) self._write([0x2, 0x4c, byte2, mval, byte4] + logo + ring) def _generate_steps(self, colors, mincolors, maxcolors, mode, ringonly): colors = list(colors) if len(colors) < mincolors: raise ValueError(f'not enough colors for mode={mode}, at least {mincolors} required') elif maxcolors == 0: if len(colors) > 0: _LOGGER.warning('too many colors for mode=%s, none needed', mode) colors = [(0, 0, 0)] # discard the input but ensure at least one step elif len(colors) > maxcolors: _LOGGER.warning('too many colors for mode=%s, dropping to %d', mode, maxcolors) colors = colors[:maxcolors] # generate steps from mode and colors: usually each color set by the user generates # one step, where it is specified to all leds and the device handles the animation; # but in super mode there is a single step and each color directly controls a led if 'super' not in mode: steps = [(color,)*9 for color in colors] elif ringonly: steps = [[(0, 0, 0)] + colors] else: steps = [colors] return steps def set_speed_profile(self, channel, profile, **kwargs): """Set channel to follow a speed duty profile.""" if not self.supports_cooling_profiles: raise NotSupportedByDevice() norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) # due to a firmware limitation the same set of temperatures must be # used on both channels; we reduce the number of writes by trimming the # interval and/or resolution to the most useful range stdtemps = list(range(20, 50)) + list(range(50, 60, 2)) + [60] interp = [(t, interpolate_profile(norm, t)) for t in stdtemps] cbase, dmin, dmax = _SPEED_CHANNELS[channel] for i, (temp, duty) in enumerate(interp): duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s PWM duty to %d%% for liquid temperature >= %d°C', channel, duty, temp) self._write([0x2, 0x4d, cbase + i, temp, duty]) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" if not self.supports_cooling: raise NotSupportedByDevice() elif self.supports_cooling_profiles: self.set_speed_profile(channel, [(0, duty), (59, duty), (60, 100), (100, 100)]) else: self.set_instantaneous_speed(channel, duty) def set_instantaneous_speed(self, channel, duty, **kwargs): """Set channel to speed duty, but do not guarantee persistence.""" if not self.supports_cooling: raise NotSupportedByDevice() cbase, dmin, dmax = _SPEED_CHANNELS[channel] duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s PWM duty to %d%%', channel, duty) self._write([0x2, 0x4d, cbase & 0x70, 0, duty]) @property def supports_cooling_profiles(self): return self.supports_cooling and self.firmware_version[0] >= 3 @property def firmware_version(self): if self._firmware_version is None: _ = self._read(clear_first=False) return self._firmware_version def _read(self, clear_first=True): if clear_first: self.device.clear_enqueued_reports() msg = self.device.read(_READ_LENGTH) self._firmware_version = tuple(msg[0xb:0xf]) return msg def _write(self, data): padding = [0x0]*(_WRITE_LENGTH - len(data)) self.device.write(data + padding) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() # deprecated aliases KrakenTwoDriver = Kraken2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1739674900.0 liquidctl-1.15.0/liquidctl/driver/kraken3.py0000644000175000017500000012745314754252424020060 0ustar00jonasjonas"""liquidctl drivers for fourth-generation NZXT Kraken X liquid coolers. Supported devices: - NZXT Kraken X (X53, X63 and X73) - NZXT Kraken Z (Z53, Z63 and Z73) - NZXT Kraken 2023 - NZXT Kraken 2023 Elite Copyright Tom Frey, Jonas Malaco, Shady Nawara and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style import itertools import io import math import logging import sys import time from PIL import Image, ImageSequence if sys.platform == "win32": from winusbcdc import WinUsbPy from liquidctl.driver.usb import PyUsbDevice, UsbHidDriver from liquidctl.error import NotSupportedByDevice, NotSupportedByDriver from liquidctl.util import ( LazyHexRepr, normalize_profile, interpolate_profile, clamp, Hue2Accessory, HUE2_MAX_ACCESSORIES_IN_CHANNEL, map_direction, ) _LOGGER = logging.getLogger(__name__) _READ_LENGTH = 64 _WRITE_LENGTH = 64 _MAX_READ_ATTEMPTS = 12 _LCD_TOTAL_MEMORY = 24320 _STATUS_TEMPERATURE = "Liquid temperature" _STATUS_PUMP_SPEED = "Pump speed" _STATUS_PUMP_DUTY = "Pump duty" _STATUS_FAN_SPEED = "Fan speed" _STATUS_FAN_DUTY = "Fan duty" # Available speed channels for model X coolers # name -> (channel_id, min_duty, max_duty) # TODO adjust min duty value to what the firmware enforces _SPEED_CHANNELS_KRAKENX = { "pump": ([0x1, 0x0, 0x0], 20, 100), } # Available speed channels for model Z coolers # name -> (channel_id, min_duty, max_duty) # TODO adjust min duty values to what the firmware enforces _SPEED_CHANNELS_KRAKENZ = { "pump": ([0x1, 0x0, 0x0], 20, 100), "fan": ([0x2, 0x0, 0x0], 0, 100), } _SPEED_CHANNELS_KRAKEN2023 = { "pump": ([0x1, 0x1, 0x0], 20, 100), "fan": ([0x2, 0x1, 0x1], 0, 100), } _CRITICAL_TEMPERATURE = 59 # Available color channels and IDs for model X coolers _COLOR_CHANNELS_KRAKENX = {"external": 0b001, "ring": 0b010, "logo": 0b100, "sync": 0b111} # Available color channels and IDs for model Z coolers _COLOR_CHANNELS_KRAKENZ = { "external": 0b001, } # Available color channels and IDs for model Z coolers _COLOR_CHANNELS_KRAKEN2023 = {} _HWMON_CTRL_MAPPING_KRAKENX = {"pump": 1} _HWMON_CTRL_MAPPING_KRAKENZ = {"pump": 1, "fan": 2} # Available LED channel modes/animations # name -> (mode, size/variant, speed scale, min colors, max colors) # FIXME any point in a one-color *alternating* or tai-chi animations? # FIXME are all modes really supported by all channels? (this is better because # of synchronization, but it's not how the previous generation worked, so # I would like to double check) _COLOR_MODES = { "off": (0x00, 0x00, 0, 0, 0), "fixed": (0x00, 0x00, 0, 1, 1), "fading": (0x01, 0x00, 1, 1, 8), "super-fixed": (0x01, 0x01, 9, 1, 40), "spectrum-wave": (0x02, 0x00, 2, 0, 0), "marquee-3": (0x03, 0x03, 2, 1, 1), "marquee-4": (0x03, 0x04, 2, 1, 1), "marquee-5": (0x03, 0x05, 2, 1, 1), "marquee-6": (0x03, 0x06, 2, 1, 1), "covering-marquee": (0x04, 0x00, 2, 1, 8), "alternating-3": (0x05, 0x03, 3, 1, 2), "alternating-4": (0x05, 0x04, 3, 1, 2), "alternating-5": (0x05, 0x05, 3, 1, 2), "alternating-6": (0x05, 0x06, 3, 1, 2), "moving-alternating-3": (0x05, 0x03, 4, 1, 2), "moving-alternating-4": (0x05, 0x04, 4, 1, 2), "moving-alternating-5": (0x05, 0x05, 4, 1, 2), "moving-alternating-6": (0x05, 0x06, 4, 1, 2), "pulse": (0x06, 0x00, 5, 1, 8), "breathing": (0x07, 0x00, 6, 1, 8), "super-breathing": (0x03, 0x00, 10, 1, 40), "candle": (0x08, 0x00, 0, 1, 1), "starry-night": (0x09, 0x00, 5, 1, 1), "rainbow-flow": (0x0B, 0x00, 2, 0, 0), "super-rainbow": (0x0C, 0x00, 2, 0, 0), "rainbow-pulse": (0x0D, 0x00, 2, 0, 0), "loading": (0x10, 0x00, 8, 1, 1), "tai-chi": (0x0E, 0x00, 7, 1, 2), "water-cooler": (0x0F, 0x00, 6, 2, 2), "wings": (None, 0x00, 11, 1, 1), # deprecated in favor of direction=backward "backwards-spectrum-wave": (0x02, 0x00, 2, 0, 0), "backwards-marquee-3": (0x03, 0x03, 2, 1, 1), "backwards-marquee-4": (0x03, 0x04, 2, 1, 1), "backwards-marquee-5": (0x03, 0x05, 2, 1, 1), "backwards-marquee-6": (0x03, 0x06, 2, 1, 1), "covering-backwards-marquee": (0x04, 0x00, 2, 1, 8), "backwards-moving-alternating-3": (0x05, 0x03, 4, 1, 2), "backwards-moving-alternating-4": (0x05, 0x04, 4, 1, 2), "backwards-moving-alternating-5": (0x05, 0x05, 4, 1, 2), "backwards-moving-alternating-6": (0x05, 0x06, 4, 1, 2), "backwards-rainbow-flow": (0x0B, 0x00, 2, 0, 0), "backwards-super-rainbow": (0x0C, 0x00, 2, 0, 0), "backwards-rainbow-pulse": (0x0D, 0x00, 2, 0, 0), } # A static value per channel that is somehow related to animation time and # synchronization, although the specific mechanism is not yet understood. # Could require information from `initialize`, but more testing is required. _STATIC_VALUE = { 0b001: 40, # may result in long all-off intervals (FIXME?) 0b010: 8, 0b100: 1, 0b111: 40, # may result in long all-off intervals (FIXME?) } # Speed scale/timing bytes # scale -> (slowest, slower, normal, faster, fastest) _SPEED_VALUE = { 0: ([0x32, 0x00], [0x32, 0x00], [0x32, 0x00], [0x32, 0x00], [0x32, 0x00]), 1: ([0x50, 0x00], [0x3C, 0x00], [0x28, 0x00], [0x14, 0x00], [0x0A, 0x00]), 2: ([0x5E, 0x01], [0x2C, 0x01], [0xFA, 0x00], [0x96, 0x00], [0x50, 0x00]), 3: ([0x40, 0x06], [0x14, 0x05], [0xE8, 0x03], [0x20, 0x03], [0x58, 0x02]), 4: ([0x20, 0x03], [0xBC, 0x02], [0xF4, 0x01], [0x90, 0x01], [0x2C, 0x01]), 5: ([0x19, 0x00], [0x14, 0x00], [0x0F, 0x00], [0x07, 0x00], [0x04, 0x00]), 6: ([0x28, 0x00], [0x1E, 0x00], [0x14, 0x00], [0x0A, 0x00], [0x04, 0x00]), 7: ([0x32, 0x00], [0x28, 0x00], [0x1E, 0x00], [0x14, 0x00], [0x0A, 0x00]), 8: ([0x14, 0x00], [0x14, 0x00], [0x14, 0x00], [0x14, 0x00], [0x14, 0x00]), 9: ([0x00, 0x00], [0x00, 0x00], [0x00, 0x00], [0x00, 0x00], [0x00, 0x00]), 10: ([0x37, 0x00], [0x28, 0x00], [0x19, 0x00], [0x0A, 0x00], [0x00, 0x00]), 11: ([0x6E, 0x00], [0x53, 0x00], [0x39, 0x00], [0x2E, 0x00], [0x20, 0x00]), } _ANIMATION_SPEEDS = { "slowest": 0x0, "slower": 0x1, "normal": 0x2, "faster": 0x3, "fastest": 0x4, } class KrakenX3(UsbHidDriver): """Fourth-generation Kraken X liquid cooler.""" # support for hwmon: nzxt-kraken3, liquidtux # https://github.com/liquidctl/liquidtux/blob/3b80dafead6f/nzxt-kraken3.c _MATCHES = [ ( 0x1E71, 0x2007, "NZXT Kraken X (X53, X63 or X73)", { "speed_channels": _SPEED_CHANNELS_KRAKENX, "color_channels": _COLOR_CHANNELS_KRAKENX, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENX, }, ), ( 0x1E71, 0x2014, "NZXT Kraken X (X53, X63 or X73)", { "speed_channels": _SPEED_CHANNELS_KRAKENX, "color_channels": _COLOR_CHANNELS_KRAKENX, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENX, }, ), ] def __init__( self, device, description, speed_channels, color_channels, hwmon_ctrl_mapping, **kwargs ): super().__init__(device, description) self._speed_channels = speed_channels self._color_channels = color_channels self._hwmon_ctrl_mapping = hwmon_ctrl_mapping self._fw = None def initialize(self, direct_access=False, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ self.device.clear_enqueued_reports() # request static infos self._write([0x10, 0x01]) # firmware info self._write([0x20, 0x03]) # lighting info # initialize if self._hwmon and not direct_access: _LOGGER.info( "bound to %s kernel driver, assuming it is already initialized", self._hwmon.driver ) else: if self._hwmon: _LOGGER.warning( "forcing re-initialization despite %s kernel driver", self._hwmon.driver ) update_interval = (lambda secs: 1 + round((secs - 0.5) / 0.25))(0.5) # see issue #128 self._write([0x70, 0x02, 0x01, 0xB8, update_interval]) self._write([0x70, 0x01]) self._status = [] self._read_until({b"\x11\x01": self.parse_firm_info, b"\x21\x03": self.parse_led_info}) self._status.append(("Firmware version", f"{self._fw[0]}.{self._fw[1]}.{self._fw[2]}", "")) return sorted(self._status) def parse_firm_info(self, msg): self._fw = (msg[0x11], msg[0x12], msg[0x13]) def parse_led_info(self, msg): channel_count = msg[14] assert channel_count == len(self._color_channels) - ( "sync" in self._color_channels ), f"Unexpected number of color channels received: {channel_count}" def find(channel, accessory): offset = 15 # offset of first channel/first accessory acc_id = msg[offset + channel * HUE2_MAX_ACCESSORIES_IN_CHANNEL + accessory] return Hue2Accessory(acc_id) if acc_id else None for i in range(HUE2_MAX_ACCESSORIES_IN_CHANNEL): accessory = find(0, i) if not accessory: break self._status.append((f"LED accessory {i + 1}", accessory, "")) if len(self._color_channels) > 1: found_ring = find(1, 0) == Hue2Accessory.KRAKENX_GEN4_RING found_logo = find(2, 0) == Hue2Accessory.KRAKENX_GEN4_LOGO self._status.append(("Pump Ring LEDs", "detected" if found_ring else "missing", "")) self._status.append(("Pump Logo LEDs", "detected" if found_logo else "missing", "")) assert found_ring and found_logo, "Pump ring and/or logo were not detected" def _get_status_directly(self): self.device.clear_enqueued_reports() msg = self._read() if msg[15:17] == [0xFF, 0xFF]: _LOGGER.warning("unexpected temperature reading, possible firmware fault;") _LOGGER.warning("try resetting the device or updating the firmware") _LOGGER.warning("(see https://github.com/liquidctl/liquidctl/issues/172)") return [ (_STATUS_TEMPERATURE, msg[15] + msg[16] / 10, "°C"), (_STATUS_PUMP_SPEED, msg[18] << 8 | msg[17], "rpm"), (_STATUS_PUMP_DUTY, msg[19], "%"), ] def _get_status_from_hwmon(self): status_readings = [ (_STATUS_TEMPERATURE, self._hwmon.read_int("temp1_input") * 1e-3, "°C"), (_STATUS_PUMP_SPEED, self._hwmon.read_int("fan1_input"), "rpm"), ] if self._hwmon.has_attribute("pwm1"): status_readings.append( (_STATUS_PUMP_DUTY, self._hwmon.read_int("pwm1") * 100.0 / 255, "%") ) else: # An older version of the kernel driver only exposed coolant temp and pump speed _LOGGER.warning("pump duty cannot be read from %s kernel driver", self._hwmon.driver) return status_readings def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self._hwmon and not direct_access: _LOGGER.info("bound to %s kernel driver, reading status from hwmon", self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning( "directly reading the status despite %s kernel driver", self._hwmon.driver ) return self._get_status_directly() def set_color(self, channel, mode, colors, speed="normal", direction="forward", **kwargs): """Set the color mode for a specific channel.""" if len(self._color_channels) == 0: raise NotSupportedByDevice() if "backwards" in mode: _LOGGER.warning("deprecated mode, move to direction=backward option") mode = mode.replace("backwards-", "") direction = "backward" cid = self._color_channels[channel] _, _, _, mincolors, maxcolors = _COLOR_MODES[mode] colors = [[g, r, b] for [r, g, b] in colors] if len(colors) < mincolors: raise ValueError(f"not enough colors for mode={mode}, at least {mincolors} required") elif maxcolors == 0: if colors: _LOGGER.warning("too many colors for mode=%s, none needed", mode) colors = [[0, 0, 0]] elif len(colors) > maxcolors: _LOGGER.warning("too many colors for mode=%s, dropping to %d", mode, maxcolors) colors = colors[:maxcolors] sval = _ANIMATION_SPEEDS[speed] self._write_colors(cid, mode, colors, sval, direction) def _set_speed_profile_hwmon(self, channel, interp): hwmon_ctrl_channel = self._hwmon_ctrl_mapping[channel] # Write duty curve for channel for idx, duty in enumerate(interp): pwm_duty = duty * 255 // 100 self._hwmon.write_int(f"temp{hwmon_ctrl_channel}_auto_point{idx + 1}_pwm", pwm_duty) # The device can get confused when hammered with HID reports, which can happen when # we set all curve points (done above) through the kernel driver, when the device # is in curve mode. In that case, the driver sends a report for each point value change # to update it. We send the whole curve to the device again by setting pwmX_enable to 2, # regardless of what it was, to ensure that the curve is properly applied. Wait just for # a bit to ensure that goes through time.sleep(0.2) # Set channel to curve mode self._hwmon.write_int(f"pwm{hwmon_ctrl_channel}_enable", 2) def set_speed_profile(self, channel, profile, direct_access=False, **kwargs): """Set channel to use a speed duty profile.""" cid, dmin, dmax = self._speed_channels[channel] header = [0x72] + cid norm = normalize_profile(profile, _CRITICAL_TEMPERATURE) stdtemps = list(range(20, _CRITICAL_TEMPERATURE + 1)) interp = [clamp(interpolate_profile(norm, t), dmin, dmax) for t in stdtemps] for temp, duty in zip(stdtemps, interp): _LOGGER.info( "setting %s PWM duty to %d%% for liquid temperature >= %d°C", channel, duty, temp ) if self._hwmon: hwmon_pwm_enable_name = f"pwm{self._hwmon_ctrl_mapping[channel]}_enable" # Check if the required attribute is present if self._hwmon.has_attribute(hwmon_pwm_enable_name): # It is, and if we have to use direct access, warn that we are sidestepping the kernel driver if direct_access: _LOGGER.warning( "directly writing duty curve despite %s kernel driver having support", self._hwmon.driver, ) return self._write(header + interp) _LOGGER.info( "bound to %s kernel driver, writing duty curve to hwmon", self._hwmon.driver ) return self._set_speed_profile_hwmon(channel, interp) elif not direct_access: _LOGGER.warning( "required duty curve functionality is not available in %s kernel driver, falling back to direct access", self._hwmon.driver, ) return self._write(header + interp) def _set_fixed_speed_directly(self, channel, duty): self.set_speed_profile(channel, [(0, duty), (_CRITICAL_TEMPERATURE - 1, duty)], True) def _set_fixed_speed_hwmon(self, channel, duty): hwmon_pwm_name = f"pwm{self._hwmon_ctrl_mapping[channel]}" hwmon_pwm_enable_name = f"{hwmon_pwm_name}_enable" # Convert duty from percent to PWM range (0-255) pwm_duty = duty * 255 // 100 # Write duty to hwmon self._hwmon.write_int(hwmon_pwm_name, pwm_duty) # Set channel to direct percent mode self._hwmon.write_int(hwmon_pwm_enable_name, 1) def set_fixed_speed(self, channel, duty, direct_access=False, **kwargs): """Set channel to a fixed speed duty.""" if self._hwmon: _, dmin, dmax = self._speed_channels[channel] duty = clamp(duty, dmin, dmax) hwmon_pwm_name = f"pwm{self._hwmon_ctrl_mapping[channel]}" # Check if the required attribute is present if self._hwmon.has_attribute(hwmon_pwm_name): # It is, and if we have to use direct access, warn that we are sidestepping the kernel driver if direct_access: _LOGGER.warning( "directly writing fixed speed despite %s kernel driver having support", self._hwmon.driver, ) return self._set_fixed_speed_directly(channel, duty) _LOGGER.info( "bound to %s kernel driver, writing fixed speed to hwmon", self._hwmon.driver ) return self._set_fixed_speed_hwmon(channel, duty) elif not direct_access: _LOGGER.warning( "required PWM functionality is not available in %s kernel driver, falling back to direct access", self._hwmon.driver, ) return self._set_fixed_speed_directly(channel, duty) def _read(self): data = self.device.read(_READ_LENGTH) return data def _read_until(self, parsers): for _ in range(_MAX_READ_ATTEMPTS): msg = self._read() prefix = bytes(msg[0:2]) func = parsers.pop(prefix, None) if func: func(msg) if not parsers: return assert False, f"missing messages (attempts={_MAX_READ_ATTEMPTS}, missing={len(parsers)})" def _write(self, data): padding = [0x0] * (_WRITE_LENGTH - len(data)) self.device.write(data + padding) def _write_colors(self, cid, mode, colors, sval, direction): mval, size_variant, speed_scale, mincolors, maxcolors = _COLOR_MODES[mode] color_count = len(colors) if "super-fixed" == mode or "super-breathing" == mode: color = list(itertools.chain(*colors)) + [0x00, 0x00, 0x00] * (maxcolors - color_count) speed_value = _SPEED_VALUE[speed_scale][sval] self._write([0x22, 0x10, cid, 0x00] + color) self._write([0x22, 0x11, cid, 0x00]) self._write( [0x22, 0xA0, cid, 0x00, mval] + speed_value + [0x08, 0x00, 0x00, 0x80, 0x00, 0x32, 0x00, 0x00, 0x01] ) elif mode == "wings": # wings requires special handling self._write([0x22, 0x10, cid]) # clear out all independent LEDs self._write([0x22, 0x11, cid]) # clear out all independent LEDs color_lists = {} color_lists[0] = colors[0] * 2 color_lists[1] = [int(x // 2.5) for x in color_lists[0]] color_lists[2] = [int(x // 4) for x in color_lists[1]] color_lists[3] = [0x00] * 8 speed_value = _SPEED_VALUE[speed_scale][sval] for i in range(8): # send color scheme first, before enabling wings mode mod = 0x05 if i in [3, 7] else 0x01 alt = [0x04, 0x84] if i // 4 == 0 else [0x84, 0x04] msg = ( [0x22, 0x20, cid, i, 0x04] + speed_value + [mod] + [0x00] * 7 + [0x02] + alt + [0x00] * 10 ) self._write(msg + color_lists[i % 4]) # this actually enables wings mode self._write([0x22, 0x03, cid, 0x08]) else: opcode = [0x2A, 0x04] address = [cid, cid] speed_value = _SPEED_VALUE[speed_scale][sval] header = opcode + address + [mval] + speed_value color = list(itertools.chain(*colors)) + [0, 0, 0] * (16 - color_count) if "marquee" in mode: backward_byte = 0x04 elif mode == "starry-night" or "moving-alternating" in mode: backward_byte = 0x01 else: backward_byte = 0x00 backward_byte += map_direction(direction, 0, 0x02) if mode == "fading" or mode == "pulse" or mode == "breathing": mode_related = 0x08 elif mode == "tai-chi": mode_related = 0x05 elif mode == "water-cooler": mode_related = 0x05 color_count = 0x01 elif mode == "loading": mode_related = 0x04 else: mode_related = 0x00 static_byte = _STATIC_VALUE[cid] led_size = size_variant if mval == 0x03 or mval == 0x05 else 0x03 footer = [backward_byte, color_count, mode_related, static_byte, led_size] self._write(header + color + footer) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class KrakenZ3(KrakenX3): """Fourth-generation Kraken Z liquid cooler.""" _MATCHES = [ ( 0x1E71, 0x3008, "NZXT Kraken Z (Z53, Z63 or Z73)", { "speed_channels": _SPEED_CHANNELS_KRAKENZ, "color_channels": _COLOR_CHANNELS_KRAKENZ, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENZ, "bulk_buffer_size": 512, "lcd_resolution": (320, 320), }, ), ( 0x1E71, 0x300C, "NZXT Kraken 2023 Elite (broken)", { "speed_channels": _SPEED_CHANNELS_KRAKEN2023, "color_channels": _COLOR_CHANNELS_KRAKEN2023, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENZ, "bulk_buffer_size": 1024 * 1024 * 2, # 2 MB "lcd_resolution": (640, 640), }, ), ( 0x1E71, 0x300E, "NZXT Kraken 2023", { "speed_channels": _SPEED_CHANNELS_KRAKEN2023, "color_channels": _COLOR_CHANNELS_KRAKEN2023, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENZ, "bulk_buffer_size": 1024 * 1024 * 2, # 2 MB "lcd_resolution": (240, 240), }, ), ( 0x1E71, 0x3012, "NZXT Kraken 2024 Elite RGB", { "speed_channels": _SPEED_CHANNELS_KRAKEN2023, "color_channels": _COLOR_CHANNELS_KRAKEN2023, "hwmon_ctrl_mapping": _HWMON_CTRL_MAPPING_KRAKENZ, "bulk_buffer_size": 1024 * 1024 * 2, # 2 MB "lcd_resolution": (640, 640), }, ), ] def __init__( self, device, description, speed_channels, color_channels, bulk_buffer_size, lcd_resolution, **kwargs, ): super().__init__(device, description, speed_channels, color_channels, **kwargs) if sys.platform == "win32": self.bulk_device = WinUsbPy() found_device = self._find_winusb_device( device.vendor_id, device.product_id, self.device.serial_number ) if not found_device: self.bulk_device = None else: self.bulk_device = next( ( handle for handle in PyUsbDevice.enumerate(self.vendor_id, self.product_id) if handle.serial_number == self.device.serial_number ), None, ) if self.bulk_device: self.bulk_device.open() self.bulk_buffer_size = bulk_buffer_size self.lcd_resolution = lcd_resolution # 0 = Normal, 1 = +90 degrees, 2 = 180 degrees, 3 = -90(270) degrees self.orientation = 0 self.brightness = 50 # default 50% def _find_winusb_device(self, vid, pid, serial): winusb_devices = self.bulk_device.list_usb_devices( deviceinterface=True, present=True, findparent=True ) for device in winusb_devices: if ( device.path.find("vid_{:x}&pid_{:x}".format(vid, pid)) != -1 and device.parent and device.parent.find(serial) != -1 ): self.bulk_device.init_winusb_device_with_path(device.path) return True return False def _get_fw_version(self, clear_reports=True): if self._fw is not None: return # Already cached if clear_reports: self.device.clear_enqueued_reports() self._write([0x10, 0x01]) # firmware info self._read_until({b"\x11\x01": self.parse_firm_info}) def initialize(self, direct_access=False, **kwargs): """Initialize the device and the driver. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ self.device.clear_enqueued_reports() # initialize if self._hwmon and not direct_access: _LOGGER.info( "bound to %s kernel driver, assuming it is already initialized", self._hwmon.driver ) else: if self._hwmon: _LOGGER.warning( "forcing re-initialization despite %s kernel driver", self._hwmon.driver ) # see issue #128 update_interval = (lambda secs: 1 + round((secs - 0.5) / 0.25))(0.5) self._write([0x70, 0x02, 0x01, 0xB8, update_interval]) self._write([0x70, 0x01]) self._status = [] # request static infos self._get_fw_version(clear_reports=False) self._status.append(("Firmware version", f"{self._fw[0]}.{self._fw[1]}.{self._fw[2]}", "")) self._write([0x30, 0x01]) # lcd info self._read_until({b"\x31\x01": self.parse_lcd_info}) if len(self._color_channels) > 0: self._write([0x20, 0x03]) # lighting info self._read_until({b"\x21\x03": self.parse_led_info}) return sorted(self._status) def parse_lcd_info(self, msg): self.brightness = msg[0x18] self.orientation = msg[0x1A] self._status.append(("LCD Brightness", self.brightness, "%")) self._status.append(("LCD Orientation", self.orientation * 90, "°")) def _get_status_directly(self): self.device.clear_enqueued_reports() self._write([0x74, 0x01]) msg = self._read() if msg[15:17] == [0xFF, 0xFF]: _LOGGER.warning("unexpected temperature reading, possible firmware fault;") _LOGGER.warning("try resetting the device or updating the firmware") return [ (_STATUS_TEMPERATURE, msg[15] + msg[16] / 10, "°C"), (_STATUS_PUMP_SPEED, msg[18] << 8 | msg[17], "rpm"), (_STATUS_PUMP_DUTY, msg[19], "%"), (_STATUS_FAN_SPEED, msg[24] << 8 | msg[23], "rpm"), (_STATUS_FAN_DUTY, msg[25], "%"), ] def _get_status_from_hwmon(self): return [ (_STATUS_TEMPERATURE, self._hwmon.read_int("temp1_input") * 1e-3, "°C"), (_STATUS_PUMP_SPEED, self._hwmon.read_int("fan1_input"), "rpm"), (_STATUS_PUMP_DUTY, self._hwmon.read_int("pwm1") * 100.0 / 255, "%"), (_STATUS_FAN_SPEED, self._hwmon.read_int("fan2_input"), "rpm"), (_STATUS_FAN_DUTY, self._hwmon.read_int("pwm2") * 100.0 / 255, "%"), ] def _read_until_first_match(self, parsers): for _ in range(_MAX_READ_ATTEMPTS): msg = self._read() prefix = bytes(msg[0:2]) func = parsers.pop(prefix, None) if func: return func(msg) if not parsers: return assert False, f"missing messages (attempts={_MAX_READ_ATTEMPTS}, missing={len(parsers)})" def _write_then_read(self, data): self._write(data) return self._read() def _bulk_write(self, data): if sys.platform == "win32": _LOGGER.debug("writing %d bytes: %r", len(data), LazyHexRepr(data)) data = bytes(data) self.bulk_device.write(0x2, data) def set_screen(self, channel, mode, value, **kwargs): """Set the screen mode and content. Unstable. Supported channels, modes and values: | Channel | Mode | Value | | --- | --- | --- | | `lcd` | `liquid` | — | | `lcd` | `brightness` | int between `0` and `100` (%) | | `lcd` | `orientation` | `0`, `90`, `180` or `270` (°) | | `lcd` | `static` | path to image | | `lcd` | `gif` | path to animated GIF | """ assert channel.lower() == "lcd", "Invalid Channel, valid: lcd, provided: " + channel assert mode != None, "No mode specified" if mode != "liquid": assert value != None, f"Mode: {mode} needs a value" # get orientation and brightness self._write([0x30, 0x01]) def parse_lcd_info(msg): self.brightness = msg[0x18] self.orientation = msg[0x1A] def _is_2023_fw_version2(): device_product_id = self.device.product_id if device_product_id == 0x300E: self._get_fw_version() return self._fw[0] == 2 return False self._read_until({b"\x31\x01": parse_lcd_info}) if mode == "brightness": value_int = int(value) assert value_int >= 0 and value_int <= 100, "Invalid brightness value" self._write([0x30, 0x02, 0x01, value_int, 0x0, 0x0, 0x1, self.orientation]) return elif mode == "orientation": value_int = int(value) assert ( value_int == 0 or value_int == 90 or value_int == 180 or value_int == 270 ), "Invalid orientation value" self._write([0x30, 0x02, 0x01, self.brightness, 0x0, 0x0, 0x1, int(value_int / 90)]) return elif mode == "static": if _is_2023_fw_version2(): data = self._prepare_static_file_rgb16(value, self.orientation) self._send_2023_data_fw2( data, [0x06, 0x0, 0x0, 0x0] + list(len(data).to_bytes(4, "little")) ) # sending it twice is only required once after initialization # the same behaviour is observed in manufacturer at init # some soft of framebuffer swapping? self._send_2023_data_fw2( data, [0x06, 0x0, 0x0, 0x0] + list(len(data).to_bytes(4, "little")) ) else: data = self._prepare_static_file(value, self.orientation) self._send_data(data, [0x02, 0x0, 0x0, 0x0] + list(len(data).to_bytes(4, "little"))) return elif mode == "gif": if _is_2023_fw_version2(): raise NotSupportedByDriver( "gif images are not supported on firmware 2.X.Y, please see issue #631" ) data = self._prepare_gif_file(value, self.orientation) assert ( len(data) / 1000 < _LCD_TOTAL_MEMORY ), f"Max file size after resize is 24MB, selected file is {len(data) / 1000000}MB" self._send_data(data, [0x01, 0x0, 0x0, 0x0] + list(len(data).to_bytes(4, "little"))) return elif mode == "liquid": self._switch_bucket(0, 2) return # release device when finished if self.bulk_device and (mode == "static" or mode == "gif"): if sys.platform == "win32": self.bulk_device.close_winusb_device() else: self.bulk_device.release() raise TypeError("Invalid mode") def _prepare_static_file(self, path, rotation): """ path is the path to any image file Rotation is expected as 0 = no rotation, 1 = 90 degrees, 2 = 180 degrees, 3 = 270 degrees """ data = ( Image.open(path) .resize(self.lcd_resolution) .rotate(rotation * -90) .convert("RGB") .getdata() ) result = [] pixelDataIndex = 0 for pixelDataIndex in range(0, len(data)): result.append(data[pixelDataIndex][0]) result.append(data[pixelDataIndex][1]) result.append(data[pixelDataIndex][2]) result.append(0) return result def _prepare_static_file_rgb16(self, path, rotation): """ path is the path to any image file Rotation is expected as 0 = no rotation, 1 = 90 degrees, 2 = 180 degrees, 3 = 270 degrees """ data = ( Image.open(path) .resize(self.lcd_resolution) .rotate(rotation * -90) .convert("RGB") .getdata() ) result = [] pixelDataIndex = 0 for pixelDataIndex in range(0, len(data)): dr = data[pixelDataIndex][0] >> 3 dg = data[pixelDataIndex][1] >> 2 db = data[pixelDataIndex][2] >> 3 result.append((dr << 3) + (dg >> 3)) result.append(((dg & 0x7) << 5) + db) return result def _prepare_gif_file(self, path, rotation): """ path is the path of the gif file Rotation is expected as 0 = no rotation, 1 = 90 degrees, 2 = 180 degrees, 3 = 270 degrees Gifs are resized to LCD resolution and rotated to match the desired orientation result is a bytesIo stream """ img = Image.open(path) frames = ImageSequence.Iterator(img) def prepare_frames(frames): for frame in frames: resized = frame.copy().resize(self.lcd_resolution).rotate(rotation * -90) yield resized frames = prepare_frames(frames) result_img = next(frames) # Handle first frame separately result_img.info = img.info # Copy sequence info result_bytes = io.BytesIO() result_img.save( result_bytes, format="GIF", interlace=False, save_all=True, append_images=list(frames), loop=0, ) return result_bytes.getvalue() def _send_2023_data_fw2(self, data, bulkInfo): """ This method is intended for Kraken 2023 firmware version 2.X.Y sends image or gif to device data is an array of bytes to write bulk info contains info about the transfer """ self._write_then_read([0x36, 0x01, 0x00, 0x01, 0x06]) # start data transfer header = [ 0x12, 0xFA, 0x01, 0xE8, 0xAB, 0xCD, 0xEF, 0x98, 0x76, 0x54, 0x32, 0x10, ] + bulkInfo self._bulk_write(header) for i in range(0, len(data), self.bulk_buffer_size): # start sending data in chunks self._bulk_write(list(data[i : i + self.bulk_buffer_size])) self._write_then_read([0x36, 0x02]) # end data transfer def _send_data(self, data, bulkInfo): """ sends image or gif to device data is an array of bytes to write bulk info contains info about the transfer """ assert self.bulk_device, "Cannot find bulk out device" self._write_then_read([0x36, 0x03]) # unknown buckets = self._query_buckets() # query all buckets and store their response bucketIndex = self._find_next_unoccupied_bucket( buckets ) # find the first unoccupied bucket in the list bucketIndex = self._prepare_bucket( bucketIndex if bucketIndex != -1 else 0, bucketIndex == -1 ) # prepare bucket or find a more suitable one # first bulk write message contains a standard part and information about the transfer header = [ 0x12, 0xFA, 0x01, 0xE8, 0xAB, 0xCD, 0xEF, 0x98, 0x76, 0x54, 0x32, 0x10, ] + bulkInfo dataSize = math.ceil((len(header) + len(data)) / 1024) dataSizeBytes = list( # calculates the number of needed packets dataSize.to_bytes(2, "little") ) bucketMemoryStart = self._get_bucket_memory_offset( buckets, bucketIndex, dataSize ) # extracts the bucket starting address if bucketMemoryStart == -1: # cant find a good memory start self._delete_all_buckets() bucketIndex = 0 # start from byte 0 bucketMemoryStart = [0x0, 0x0] # setup bucket for transfer if not self._setup_bucket(bucketIndex, bucketIndex + 1, bucketMemoryStart, dataSizeBytes): _LOGGER.error("Failed to setup bucket for data transfer") self._write_then_read([0x36, 0x01, bucketIndex]) # start data transfer self._bulk_write(header) for i in range(0, len(data), self.bulk_buffer_size): # start sending data in chunks self._bulk_write(list(data[i : i + self.bulk_buffer_size])) self._write([0x36, 0x02]) # end data transfer # switch to newly written bucket if not self._switch_bucket(bucketIndex): _LOGGER.error("Failed to switch active bucket") def _query_buckets(self): """ Queries all 16 buckets and stores their response Response in structures as follow: - standard part (14 bytes) - unknown ---- following is all 0x0 if bucket is unoccupied - bucket index (1 byte) - asset index (1 byte) - same as bucket index + 1 - 0x2 (1 byte) - unknown - starting memory address (2 bytes) - address sometimes changes so must be read from here - memory size (2 bytes) - size sometimes changes so must be read from here - 0x1 (1 byte) - unknown - 0x0|0x1 (1 byte) - most likely used/unused but could also be something else """ buckets = {} for bI in range(16): response = self._write_then_read([0x30, 0x04, bI]) # query bucket buckets[bI] = response return buckets def _find_next_unoccupied_bucket(self, buckets): """ finds the first available unoccupied bucket buckets are unoccupied when bytes 14 onward are 0x0 returns -1 if unoccupied buckets are found """ for bucketIndex, bucketInfo in buckets.items(): if not any(bucketInfo[15:]): return bucketIndex return -1 def _get_bucket_memory_offset(self, buckets, bucketIndex, dataSize): """ returns the memory start address for the selected bucket memory offset is calculated by first checking if the bucket can already accommodate the new data, this avoids any additional calculations if uploading the same or smaller image otherwise, we check if we can expand the current bucket without overlapping the memory space of any other bucket otherwise, we set the offset after max utilized memory if there is space left on the device otherwise, we check if there is space at the beginning of the memory space finally, if all else fails then we clear the device and start over """ currentBucket = buckets[bucketIndex] currentBucketOffset = int.from_bytes([currentBucket[17], currentBucket[18]], "little") currentBucketSize = int.from_bytes([currentBucket[19], currentBucket[20]], "little") # check if we can fit content in existing bucket space if dataSize <= currentBucketSize: return [currentBucket[17], currentBucket[18]] # find max byte number minOccupiedByte = currentBucketOffset maxOccupiedByte = 0 existingBucketWithinRange = False for bI in buckets: bucket = buckets[bI] startByte = int.from_bytes([bucket[17], bucket[18]], "little") endByte = startByte + int.from_bytes([bucket[19], bucket[20]], "little") if endByte > maxOccupiedByte: maxOccupiedByte = endByte if startByte < minOccupiedByte: minOccupiedByte = startByte if ( (startByte > currentBucketOffset and startByte < currentBucketOffset + dataSize) or (startByte < currentBucketOffset and endByte > startByte) or (startByte == currentBucketOffset and bI != bucketIndex) ): existingBucketWithinRange = True # check if we can use current offset without overlapping other buckets if not existingBucketWithinRange: return [currentBucket[17], currentBucket[18]] # check if we would exceed available memory if we put data at the end if maxOccupiedByte + dataSize < _LCD_TOTAL_MEMORY: return list(maxOccupiedByte.to_bytes(2, "little")) # if the lowest used byte is more than zero and we can fit the data then start from zero if dataSize < minOccupiedByte: return [0x0, 0x0] # if all else fails return -1 to reset and start over return -1 def _prepare_bucket(self, bucketIndex, bucketFilled): """ if a bucket delete returns 0x9 then try next bucket if bucket already had data then delete it twice """ assert bucketIndex < 16, "reached max bucket" delete_response = self._delete_bucket(bucketIndex) if not delete_response: return self._prepare_bucket(bucketIndex + 1, True) else: if bucketFilled: return self._prepare_bucket(bucketIndex, False) return bucketIndex def _delete_bucket(self, bucketIndex): """ deletes bucket, returns true if successful, false otherwise """ self._write([0x32, 0x2, bucketIndex]) def parse_delete_result(msg): return msg[14] == 0x1 return self._read_until_first_match({b"\x33\x02": parse_delete_result}) def _delete_all_buckets(self): """ Switches to liquid mode then deletes all buckets """ self._switch_bucket(0, 2) # switch to liquid mode for bI in range(16): self._delete_bucket(bI) # delete bucket def _switch_bucket(self, bucketIndex, mode=0x4): """ switches active bucket, returns true if successful, false otherwise """ response = self._write_then_read([0x38, 0x1, mode, bucketIndex]) return response[14] == 0x1 def _setup_bucket(self, startBucketIndex, endBucketIndex, startingMemoryAddress, memorySize): """ sets bucket for transmission, returns true if successful, false otherwise """ response = self._write_then_read( [ 0x32, 0x1, startBucketIndex, endBucketIndex, startingMemoryAddress[0], startingMemoryAddress[1], memorySize[0], memorySize[1], 0x1, ] ) return response[14] == 0x1 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/liquidctl/driver/msi.py0000644000175000017500000013644514617346724017320 0ustar00jonasjonas"""liquidctl drivers for MSI liquid coolers. Supported devices: - MPG Coreliquid K360 Copyright (C) 2021 Andrew Udvare, Aapo Kössi and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style from collections import namedtuple from collections.abc import Sequence from copy import copy from enum import Enum, unique from time import sleep import logging import io from PIL import Image from liquidctl.driver.usb import UsbHidDriver from liquidctl.keyval import RuntimeStorage from liquidctl.util import RelaxedNamesEnum, check_unsafe, clamp, u16le_from _LOGGER = logging.getLogger(__name__) EXTRA_USAGE_PAGE = 0x0001 _MAX_DATA_LENGTH = 185 _PER_LED_LENGTH = 720 _REPORT_LENGTH = 64 _MAX_DUTIES = 7 _RAD_FAN_COUNT = 3 _CYCLE_NUMBER_STRIPE_TYPE_MAPPING = {0: 41, 1: 52, 2: 63, 3: 20, 4: 30} # fmt: off _DEFAULT_FEATURE_DATA = [ 82, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 20, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 20, 1, 255, 0, 0, 40, 0, 255, 0, 130, 76, 10, 1, 255, 0, 0, 40, 0, 255, 0, 128, 0, 26, 255, 0, 0, 168, 0, 255, 0, 191, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 32, 255, 0, 0, 40, 0, 255, 0, 128, 0, 0 ] # fmt: on _DeviceSettings = namedtuple( "DeviceSettings", [ "stripe_or_fan", "fan_type", "corsair_device_quantity", "ll120_outer_individual", "led_num_jrainbow1", "led_num_jrainbow2", "led_num_jcorsair", ], ) _BoardSyncSettings = namedtuple( "BoardSyncSettings", [ "onboard_sync", "combine_jrgb", "combine_jpipe1", "combine_jpipe2", "combine_jrainbow1", "combine_jrainbow2", "combine_jcorsair", ], ) _StyleSettings = namedtuple( "StyleSettings", ["lighting_mode", "speed", "brightness", "color_selection"] ) _ColorSettings = namedtuple("ColorSettings", ["color1", "color2"]) _FanConfig = namedtuple( "FanConfig", ["mode", "duty0", "duty1", "duty2", "duty3", "duty4", "duty5", "duty6"] ) _FanTempConfig = namedtuple( "FanTempConfig", ["mode", "temp0", "temp1", "temp2", "temp3", "temp4", "temp5", "temp6"] ) @unique class _OLEDHardwareMonitorOffset(Enum): CPU_FREQ = 0 CPU_TEMP = 1 GPU_MEMORY_FREQ = 2 GPU_USAGE = 3 FAN_PUMP = 4 FAN_RADIATOR = 5 FAN_CPUMOS = 6 MAXIMUM = 7 @unique class _FanMode(RelaxedNamesEnum): SILENT = 0 BALANCE = 1 GAME = 2 CUSTOMIZE = 3 DEFAULT = 4 SMART = 5 @classmethod def _missing_(cls, value): _LOGGER.debug("falling back to BALANCE for _FanMode(%s)", value) return _FanMode.BALANCE @unique class _StripeOrFan(Enum): STRIPE = 0 FAN = 1 @unique class _FanType(Enum): SP = 0 HD = 1 LL = 2 class _LEDArea(Enum): JCORSAIR = 53 JCORSAIR_OUTER_LL120 = 0x40 JPIPE1 = 11 JPIPE2 = 21 JRAINBOW1 = 0x1F JRAINBOW2 = 42 JRGB1 = 1 JRGB2 = 174 ONBOARD_LED_0 = 74 ONBOARD_LED_1 = 84 ONBOARD_LED_10 = 174 ONBOARD_LED_2 = 94 ONBOARD_LED_3 = 104 ONBOARD_LED_4 = 114 ONBOARD_LED_5 = 124 ONBOARD_LED_6 = 134 ONBOARD_LED_7 = 144 ONBOARD_LED_8 = 154 ONBOARD_LED_9 = 164 _CYCLE_NUMBER_LED_AREA_MAPPING = { _LEDArea.JPIPE1.value: 20, _LEDArea.JPIPE2.value: 30, _LEDArea.JRAINBOW1.value: 41, _LEDArea.JRAINBOW2.value: 52, _LEDArea.JCORSAIR.value: 63, } @unique class _LightingMode(RelaxedNamesEnum): BLINK = 19 BREATHING = 2 CLOCK = 20 COLOR_PULSE = 21 COLOR_RING = 15 COLOR_RING_DOUBLE_FLASHING = 35 COLOR_RING_FLASHING = 34 COLOR_SHIFT = 22 COLOR_WAVE = 23 CORSAIR_IQUE = 37 DISABLE = 0 DISABLE2 = 33 DOUBLE_FLASHING = 4 DOUBLE_METEOR = 17 ENERGY = 18 FAN_CONTROL = 32 FIRE = 38 FLASHING = 3 JAZZ = 12 JRAINBOW = 28 LAVA = 39 LIGHTNING = 5 MARQUEE = 24 METEOR = 7 MOVIE = 14 MSI_MARQUEE = 6 MSI_RAINBOW = 9 NO_ANIMATION = 1 PLANETARY = 16 PLAY = 13 POP = 10 RAINBOW = 25 RAINBOW_DOUBLE_FLASHING = 30 RAINBOW_FLASHING = 29 RAINBOW_WAVE = 26 RANDOM = 31 RAP = 11 STACK = 36 VISOR = 27 WATER_DROP = 8 END = 40 @unique class _Speed(RelaxedNamesEnum): LOW = 0 MEDIUM = 1 HIGH = 2 @unique class _ColorSelection(Enum): RAINBOW_COLOR = 0 USER_DEFINED = 1 @unique class _JType(Enum): JRAINBOW = 3 JCORSAIR = 4 JONBOARD = 5 @unique class _UploadType(Enum): BANNER = 0 GIF = 1 @unique class _ScreenMode(Enum): HARDWARE = 0 IMAGE = 1 BANNER = 3 CLOCK = 4 SETTINGS = 5 DISABLED = 6 class MpgCooler(UsbHidDriver): _COLOR_MODES = { "blink": _LightingMode.BLINK, "breathing": _LightingMode.BREATHING, "clock": _LightingMode.CLOCK, "color pulse": _LightingMode.COLOR_PULSE, "color ring": _LightingMode.COLOR_RING, "color ring double flashing": _LightingMode.COLOR_RING_DOUBLE_FLASHING, "color ring flashing": _LightingMode.COLOR_RING_FLASHING, "color shift": _LightingMode.COLOR_SHIFT, "color wave": _LightingMode.COLOR_WAVE, "corsair ique": _LightingMode.CORSAIR_IQUE, "disable": _LightingMode.DISABLE, "disable2": _LightingMode.DISABLE2, "double flashing": _LightingMode.DOUBLE_FLASHING, "double meteor": _LightingMode.DOUBLE_METEOR, "energy": _LightingMode.ENERGY, "fan control": _LightingMode.FAN_CONTROL, "fire": _LightingMode.FIRE, "flashing": _LightingMode.FLASHING, "jazz": _LightingMode.JAZZ, "jrainbow": _LightingMode.JRAINBOW, "lava": _LightingMode.LAVA, "lightning": _LightingMode.LIGHTNING, "marquee": _LightingMode.MARQUEE, "meteor": _LightingMode.METEOR, "movie": _LightingMode.MOVIE, "msi marquee": _LightingMode.MSI_MARQUEE, "msi rainbow": _LightingMode.MSI_RAINBOW, "steady": _LightingMode.NO_ANIMATION, "planetary": _LightingMode.PLANETARY, "play": _LightingMode.PLAY, "pop": _LightingMode.POP, "rainbow": _LightingMode.RAINBOW, "rainbow double flashing": _LightingMode.RAINBOW_DOUBLE_FLASHING, "rainbow flashing": _LightingMode.RAINBOW_FLASHING, "rainbow wave": _LightingMode.RAINBOW_WAVE, "random": _LightingMode.RANDOM, "rap": _LightingMode.RAP, "stack": _LightingMode.STACK, "visor": _LightingMode.VISOR, "water drop": _LightingMode.WATER_DROP, "end": _LightingMode.END, } BUILTIN_MODES = { "silent": _FanMode.SILENT.value, "balanced": _FanMode.BALANCE.value, "game": _FanMode.GAME.value, "default": _FanMode.DEFAULT.value, "smart": _FanMode.SMART.value, } SCREEN_MODES = { "hardware": _ScreenMode.HARDWARE, "image": _ScreenMode.IMAGE, "banner": _ScreenMode.BANNER, "clock": _ScreenMode.CLOCK, "settings": _ScreenMode.SETTINGS, "disable": _ScreenMode.DISABLED, } HWMONITORDISPLAY = { "cpu_freq": _OLEDHardwareMonitorOffset.CPU_FREQ, "cpu_temp": _OLEDHardwareMonitorOffset.CPU_TEMP, "gpu_freq": _OLEDHardwareMonitorOffset.GPU_MEMORY_FREQ, "gpu_usage": _OLEDHardwareMonitorOffset.GPU_USAGE, "fan_pump": _OLEDHardwareMonitorOffset.FAN_PUMP, "fan_radiator": _OLEDHardwareMonitorOffset.FAN_RADIATOR, "fan_cpumos": _OLEDHardwareMonitorOffset.FAN_CPUMOS, } _MATCHES = [ ( 0x0DB0, 0xB130, "MSI MPG Coreliquid K360", {"fan_count": 5}, ), ( 0x0DB0, 0xCA00, "Suspected MSI MPG Coreliquid", {"_unsafe": ["experimental_coreliquid_cooler"]}, ), ( 0x0DB0, 0xCA02, "Suspected MSI MPG Coreliquid", {"_unsafe": ["experimental_coreliquid_cooler"]}, ), ] HAS_AUTOCONTROL = True def __init__(self, device, description, _unsafe=[], **kwargs): super().__init__(device, description, **kwargs) self._UNSAFE = _unsafe self._feature_data_per_led = bytearray(_PER_LED_LENGTH + 5) self._bytearray_oled_hardware_monitor_data = bytearray(_REPORT_LENGTH) self._per_led_rgb_jonboard = bytearray(_PER_LED_LENGTH) self._per_led_rgb_jrainbow1 = bytearray(_PER_LED_LENGTH) self._per_led_rgb_jrainbow2 = bytearray(_PER_LED_LENGTH) self._per_led_rgb_jcorsair = bytearray(_PER_LED_LENGTH) self._fan_count = kwargs.pop("fan_count", 5) # the following fields are only initialized in connect() self._data = None self._feature_data = None @classmethod def probe(cls, handle, **kwargs): """Probe `handle` and yield corresponding driver instances. These devices have multiple top-level HID usages, and HidapiDevice handles matching other usages have to be ignored. """ # if usage_page/usage are not available due to hidapi limitations # (version, platform or backend), they are unfortunately left # uninitialized; because of this, we explicitly exclude the undesired # usage_page, and assume in all other cases that we either # have the desired usage page, or that on that system a # single handle is returned for that device interface (see: #259) if handle.hidinfo["usage_page"] == EXTRA_USAGE_PAGE: return yield from super().probe(handle, **kwargs) def connect(self, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) ret = super().connect(**kwargs) self._data = kwargs.pop( "runtime_storage", RuntimeStorage( key_prefixes=[ f"vid{self.vendor_id:04x}_pid{self.product_id:04x}", f"serial{self.serial_number}", ] ), ) self._feature_data = self.device.get_feature_report(0x52, _MAX_DATA_LENGTH) self._fan_cfg = self.get_fan_config() self._fan_temp_cfg = self.get_fan_temp_config() aprom_hi, aprom_lo = self.get_firmware_version_aprom() self._aprom_firmware_version = aprom_hi << 4 + aprom_lo ldrom_hi, ldrom_lo = self.get_firmware_version_ldrom() self._ldrom_firmware_version = ldrom_hi << 4 + ldrom_lo self._oled_firmware_version = self.get_oled_firmware_version() return ret def initialize(self, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) pump_mode = kwargs.pop("pump_mode", "balanced") direction = kwargs.pop("direction", "default") if pump_mode == "balanced": pump_mode = "balance" pump_mode_int = _FanMode[pump_mode].value self._data.store("pump_mode", pump_mode_int) dir_int = 0 if direction not in ("default", "top", "bottom", "left", "right", "0", "1", "2", "3"): _LOGGER.warning( "unknown direction value: correct values are 0-3 or top, " "bottom, left, right, default." ) if direction in ("1", "left"): dir_int = 1 elif direction in ("2", "bottom"): dir_int = 2 elif direction in ("3", "right"): dir_int = 3 self._data.store("direction", dir_int) self.set_oled_brightness_and_direction(100, dir_int) if pump_mode_int == _FanMode.GAME.value: self.switch_to_game_mode() elif pump_mode_int == _FanMode.BALANCE.value: self.switch_to_balance_mode() elif pump_mode_int == _FanMode.DEFAULT.value: self.switch_to_default_mode() elif pump_mode_int == _FanMode.SILENT.value: self.switch_to_silent_mode() elif pump_mode_int == _FanMode.SMART.value: self.switch_to_smart_mode() return [ ("Display firmware version", self._oled_firmware_version, ""), ("APROM firmware version", self._aprom_firmware_version, ""), ("LDROM firmware version", self._ldrom_firmware_version, ""), ("Serial number", self.serial_number, ""), ("Pump mode", pump_mode, ""), ] def get_status(self, **kwargs): if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning( f"{self.description}: disabled, requires unsafe features " f"'{','.join(self._UNSAFE)}'" ) return [] self._write((0x31,)) array = self._read() assert array[1] == 0x31, "Unexpected value in response buffer" return [ ("Fan 1 speed", u16le_from(array, offset=2), "rpm"), ("Fan 1 duty", u16le_from(array, offset=0x16), "%"), ("Fan 2 speed", u16le_from(array, offset=4), "rpm"), ("Fan 2 duty", u16le_from(array, offset=0x18), "%"), ("Fan 3 speed", u16le_from(array, offset=6), "rpm"), ("Fan 3 duty", u16le_from(array, offset=0x1A), "%"), ("Water block speed", u16le_from(array, offset=8), "rpm"), ("Water block duty", u16le_from(array, offset=0x1C), "%"), ("Pump speed", u16le_from(array, offset=0xA), "rpm"), ("Pump duty", u16le_from(array, offset=0x1E), "%"), # Temperature values are not used by the K360 model, it only reports # some default values that are not meaningful for the user. # https://github.com/liquidctl/liquidctl/pull/564#discussion_r1450753883 # ('Temperature inlet', u16le_from(array, offset=12), '°C'), # ('Temperature outlet', u16le_from(array, offset=14), '°C'), # ('Temperature sensor 1', u16le_from(array, offset=16), '°C'), # ('Temperature sensor 2', u16le_from(array, offset=18), '°C'), ] def set_time(self, time, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) return self.set_oled_clock(time) def set_hardware_status(self, T, cpu_f=0, gpu_f=0, gpu_U=0, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) self.set_oled_show_cpu_status(cpu_f, T) self.set_oled_gpu_status(gpu_f, gpu_U) def get_fan_config(self): self._write((0x32,)) buf = self._read() assert buf[1] == 0x32, "Unexpected value in returned list" ret = [] for mode_index in (2, 10, 18, 26, 34): ret.append(_FanConfig(*buf[mode_index : mode_index + 8])) return ret def set_fan_config(self, configs): buf = bytearray(_REPORT_LENGTH - 1) buf[0] = 0x40 for config, offset in zip(configs, (1, 9, 17, 25, 33)): buf[offset] = config.mode buf[offset + 1] = config.duty0 buf[offset + 2] = config.duty1 buf[offset + 3] = config.duty2 buf[offset + 4] = config.duty3 buf[offset + 5] = config.duty4 buf[offset + 6] = config.duty5 buf[offset + 7] = config.duty6 return self._write(buf) def get_fan_temp_config(self): self._write((0x33,)) buf = self._read() assert buf[1] == 0x32, "Unexpected value in returned list" ret = [] for mode_index in (2, 10, 18, 26, 34): ret.append(_FanTempConfig(*buf[mode_index : mode_index + 8])) return ret def set_fan_temp_config(self, configs): buf = bytearray(_REPORT_LENGTH) buf[0] = 0x41 for config, offset in zip(configs, (1, 9, 17, 25, 33)): buf[offset] = config.mode buf[offset + 1] = config.temp0 buf[offset + 2] = config.temp1 buf[offset + 3] = config.temp2 buf[offset + 4] = config.temp3 buf[offset + 5] = config.temp4 buf[offset + 6] = config.temp5 buf[offset + 7] = config.temp6 return self._write(buf) def _send_safe_temp(self): _LOGGER.info( "duty profiles on this device require continuous communication, " "setting initial control temperature to 100C for safety." ) self.set_oled_show_cpu_status(0, 100) def set_profiles(self, channels, profiles, **kwargs): """ Set custom or device preset fan curve for multiple channels. NOTE: The device will not keep updating its duty cycles automatically after this function is called. The device manages duties according to the previous temperature sent to it via device.set_oled_show_cpu_status() """ check_unsafe(*self._UNSAFE, error=True, **kwargs) fan_cfg = self.get_fan_config() fan_temp_cfg = self.get_fan_temp_config() channel_idx = [self.parse_channel(ch) for ch in channels] for idx, prof in zip(channel_idx, profiles): if type(prof) == str: fanmode = _FanConfig(self.BUILTIN_MODES[prof], 0, 0, 0, 0, 0, 0, 0) tempmode = _FanTempConfig(self.BUILTIN_MODES[prof], 0, 0, 0, 0, 0, 0, 0) else: duties, temps = map(self.clamp_and_pad, zip(*prof)) fanmode = _FanConfig(_FanMode.CUSTOMIZE.value, *duties) tempmode = _FanTempConfig(_FanMode.CUSTOMIZE.value, *temps) for i in idx: fan_cfg[i] = fanmode fan_temp_cfg[i] = tempmode self._fan_cfg = fan_cfg self._fan_temp_cfg = fan_temp_cfg self.set_fan_config(fan_cfg) self.set_fan_temp_config(fan_temp_cfg) self._send_safe_temp() def parse_channel(self, channel): if channel == "pump": return [4] elif channel == "fans": return range(_RAD_FAN_COUNT) elif channel == "waterblock-fan": return [3] elif channel[:3] == "fan" and (int(channel[3:]) in range(_RAD_FAN_COUNT)): return [int(channel[3:])] else: raise ValueError( 'unknown channel, should be "fans", "fan1", "fan2", "fan3", "waterblock-fan" or "pump".' ) @staticmethod def clamp_and_pad(values): return ([clamp(v, 0, 100) for v in values] + [0] * _MAX_DUTIES)[:_MAX_DUTIES] def set_speed_profile(self, channel, profile, **kwargs): """ Set custom fan curve for a given channel. NOTE: The device will not keep updating its duty cycles automatically after this function is called. The device manages duties according to the previous temperature sent to it via device.set_oled_show_cpu_status() """ check_unsafe(*self._UNSAFE, error=True, **kwargs) duties_temps = list(zip(*profile)) duties, temps = tuple(self.clamp_and_pad(v) for v in duties_temps) for i in self.parse_channel(channel): self._fan_cfg[i] = _FanConfig(_FanMode.CUSTOMIZE.value, *duties) self._fan_temp_cfg[i] = _FanTempConfig(_FanMode.CUSTOMIZE.value, *temps) self.set_fan_config(self._fan_cfg) self.set_fan_temp_config(self._fan_temp_cfg) self._send_safe_temp() def set_fixed_speed(self, channel, duty, **kwargs): channel_nums = self.parse_channel(channel) check_unsafe(*self._UNSAFE, error=True, **kwargs) for i in channel_nums: self._fan_cfg[i] = _FanConfig(_FanMode.CUSTOMIZE.value, *([duty] * _MAX_DUTIES)) self._fan_temp_cfg[i] = _FanTempConfig(_FanMode.CUSTOMIZE.value, 0, 0, 0, 0, 0, 0, 0) self.set_fan_config(self._fan_cfg) self.set_fan_temp_config(self._fan_temp_cfg) def set_color(self, channel, mode, colors, speed=1, brightness=10, color_selection=1, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) assert channel == "sync", f'Unexpected lighting channel {channel}. Supported: "sync"' colors = list(colors) if not colors: color_selection = 0 else: if len(colors) == 1: colors.append((0, 0, 0)) self.set_color_setting(_LEDArea.JRAINBOW1.value, *colors[0], *colors[1]) mode = self._COLOR_MODES[mode].value self.set_style_setting( _LEDArea.JRAINBOW1.value, mode, int(speed), brightness, color_selection ) self.set_send_led_setting(1) def get_current_model_index(self): self._write((0xB1,), 0xCC, prefix=1) return self._read()[2] def set_screen(self, channel, mode, value, **kwargs): check_unsafe(*self._UNSAFE, error=True, **kwargs) assert channel.lower() == "lcd" try: mode = self.SCREEN_MODES[mode] except KeyError as e: raise Exception( f"Unknown screen mode! Should be one of: {self.SCREEN_MODES.keys()}" ) from e if value is not None: opts = value.split(";") if mode == _ScreenMode.HARDWARE: # hardware monitor options are a list of the values to display # (case-insensitive keys to MPGCooler.HWMONITORDISPLAY) self.set_oled_show_hardware_monitor(opts) if mode == _ScreenMode.IMAGE: # image options are: image-type, image-index[, image-file] if len(opts) == 3: self.set_oled_upload_gif(opts) elif len(opts) == 2: self.set_oled_show_profile(opts) else: raise ValueError( f"Unexpected options for LCD image. Expected either:" '"image-type;image-slot" or ' '"image-type;image-slot;image-file", ' "instead got: {opts}" ) elif mode == _ScreenMode.BANNER: # banner options are: banner-type, banner-index, message[, image-file] if (len(opts) == 3) or (len(opts) == 4): if len(opts) == 4: save_slot = int(opts[1]) assert save_slot >= 4, ( "Cannot overwrite preset banner images, " "please use save slots starting from 4 for your uploaded files" ) img = self._prepare_bmp(opts[3]) self.set_oled_upload_banner(img, banner_no=save_slot) self.set_oled_user_message(opts[2]) self.set_oled_show_banner(banner_type=int(opts[0]), bmp_no=int(opts[1])) else: raise ValueError( f"Unexpected options for LCD banner. Expected either:" '"banner-type;save-slot;message" or ' '"banner-type;save-slot;message;image-file", ' "instead got: {opts}" ) elif mode == _ScreenMode.CLOCK: # clock option is the display style self.set_oled_show_clock(int(opts[0])) elif mode == _ScreenMode.SETTINGS: # setting options are: brightness, direction brightness, direction = [int(x) for x in opts] self.set_oled_brightness_and_direction(brightness=brightness, direction=direction) elif mode == _ScreenMode.DISABLED: # switches off the display self.set_oled_show_disable() def _prepare_bmp(self, path): end_w, end_h = 240, 320 img = Image.open(path) w, h = img.size wrat = end_w / w hrat = end_h / h ratio = wrat if wrat > hrat else hrat img = img.resize((int(ratio * w), int(ratio * h))) w, h = img.size x_start = int((w - end_w) / 2) y_start = int((h - end_h) / 2) img = img.crop((x_start, y_start, x_start + end_w, y_start + end_h)) img = img.convert("RGB") img_bytes = io.BytesIO() img.save(img_bytes, format="BMP") return img_bytes def get_firmware_version_aprom(self): self._write((0xB0,), 0xCC, prefix=1) ret = self._read() return ( ret[2] >> 4, # high ret[2] & 0xF, # low ) def get_firmware_version_ldrom(self): self._write((0xB6,), 0xCC, prefix=1) ret = self._read() return ( ret[2] >> 4, # high ret[2] & 0xF, # low ) def get_firmware_checksum_aprom(self): self._write((0xB0,), 0xCC, prefix=1) ret = self._read() return ( ret[8], # high ret[9], # low ) def get_firmware_checksum_ldrom(self): self._write((0xB4,), 0xCC, prefix=1) ret = self._read() return ( ret[8], # high ret[9], # low ) def get_all_hardware_monitor(self): self.device.clear_enqueued_reports() return self.device.get_feature_report(0xD0, _REPORT_LENGTH) def set_buzzer(self, type, frequency=650): return self._write( (0xC2, 0, 0, 0, 0, 0, type, frequency & 0xFF, (frequency >> 8) & 0xFF), prefix=1 ) def set_led_global(self, on_off): return self._write((0xBB, 0, 0, 0, 0, int(on_off)), prefix=1) def get_led_global(self): self._write((0xBA,), 0xCC, prefix=1) ret = self._read() ok = len(ret) == _REPORT_LENGTH for j, _ in enumerate(ret): if ( (j == 0 and ret[j] != 1) or (j == 1 and ret[j] != 90) or (j == 6 and ret[j] not in (0, 1)) ): ok = False elif ret[j] != 0xCC: ok = False return ok and ret[6] == 1 def get_led_pe0(self): self._write((0xA0, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC, 1, 0xF2), 0xCC, prefix=0xFA) ret = self._read() ok = True if ( ret[0] != 250 or ret[1] != 160 or ret[2] != 205 or ret[9] != 1 or ret[10] != 242 or ret[11] not in (0, 1) ): ok = 0 for j, n in enumerate(ret, start=23): if j > 29: break if n != 0: ok = False for j, n in enumerate(ret, start=30): if n != 0xCC: ok = False return ok and ret[11] == 1 def get_device_settings(self): return _DeviceSettings( self._feature_data[61] & 1, (self._feature_data[61] & 0xE) >> 1, (self._feature_data[62] & 0xFC) >> 2, self._feature_data[72] & 1, self._feature_data[41], self._feature_data[52], self._feature_data[63], ) def get_board_sync_settings(self): return _BoardSyncSettings( (self._feature_data[82] & 1) == 1, (self._feature_data[78] & 0x80) >> 7 == 1, ((self._feature_data[82] >> 4) & 1) == 1, ((self._feature_data[82] >> 5) & 1) == 1, ((self._feature_data[82] >> 1) & 1) == 1, ((self._feature_data[82] >> 2) & 1) == 1, ((self._feature_data[82] >> 3) & 1) == 1, ) def get_style_settings(self, led_area): return _StyleSettings( self._feature_data[led_area], (self._feature_data[led_area + 4] & 3), (self._feature_data[led_area + 4] >> 2) & 0x1F, ((self._feature_data[led_area + 8] & 0x80) >> 7), ) def get_color_settings(self, led_area): return _ColorSettings( self._feature_data[led_area + 1 : led_area + 4], self._feature_data[led_area + 5 : led_area + 8], ) def get_current_led_setting(self): self._feature_data = self.device.get_feature_report(self._feature_data[0], _MAX_DATA_LENGTH) self._feature_data[62] = clamp(self._feature_data[62] - 4, 0, 255) return self._feature_data def get_all_board(self): return self.device.get_feature_report(0x52, _MAX_DATA_LENGTH) def get_oled_firmware_version(self): self._write((0xF1,)) return self._read()[2] def get_oled_gif_checksum(self): self._write((0xC2,)) ret = self._read() return ( ret[3], # high ret[2], # low ) def get_oled_banner_checksum(self): self._write((0xD2,)) buf = self._read() return ( buf[3], # high buf[2], # low ) def get_oled_m481checksum(self): self._write((0xF1,)) ret = self._read() return ( ret[3], # high ret[2], # low ) def set_volume(self, main, left, right): return self._write( (0xC0, clamp(main, 0, 100), clamp(left, 0, 100), clamp(right, 0, 100)), 0xCC, prefix=1 ) def set_oled_cpu_message(self, message): return self._write([0x90] + list(message[:60].encode("ascii", "ignore"))) def set_oled_show_hardware_monitor(self, opts, radiator_fan_smart_mode_on_off=True): """ Command device to display hardware information on the built in display. Parameters ---------- opts: Array[str] Indicates which hardware features will be presented on the screen, from CPU_FREQ, CPU_TEMP, GPU_MEMORY_FREQ, GPU_USAGE, FAN_PUMP, FAN_RADIATOR, FAN_CPUMOS radiator_fan_smart_mode_on_off: Bool Indicates whether smart control is enabled, this slightly alters the visualization """ show_idx = [self.HWMONITORDISPLAY[x.lower()].value for x in opts] show_area = [i in show_idx for i in range(_OLEDHardwareMonitorOffset.MAXIMUM.value + 1)] if len(show_area) < 7: return False buf = bytearray(_REPORT_LENGTH) buf[0] = 0xD0 buf[1] = 0x71 for item in iter(_OLEDHardwareMonitorOffset): if item.value != _OLEDHardwareMonitorOffset.MAXIMUM and show_area[item.value]: buf[item.value + 2] = 1 if show_area[5]: buf[9] = 3 if radiator_fan_smart_mode_on_off else 1 return self._write(buf[1:]) def set_return_to_default(self): """--reset-all""" self._feature_data = copy(_DEFAULT_FEATURE_DATA) self._feature_data[184] = 1 return self._set_all_board(self._feature_data) def _set_all_board(self, data): if data[41] > 200: data[41] = 100 if data[52] > 240: data[52] = 100 b = (data[62] & 0xFC) >> 2 if ((b + 1) * data[63]) > 240: b = 5 data[62] = b << 2 data[63] = 12 if len(data) < 185 or data[0] != 82: return False return bool(self.device.send_feature_report(data)) def set_cycle_number(self, stripe_type, cycle_num): """Cycle number should NOT be clamped.""" if stripe_type in _CYCLE_NUMBER_STRIPE_TYPE_MAPPING: self._feature_data[_CYCLE_NUMBER_STRIPE_TYPE_MAPPING[stripe_type]] = cycle_num def set_cycle_number_by_led_area(self, led_area, cycle_num): if led_area in _CYCLE_NUMBER_LED_AREA_MAPPING: self._feature_data[_CYCLE_NUMBER_LED_AREA_MAPPING[led_area]] = cycle_num def set_device_setting( self, stripe_or_fan, fan_type, corsair_device_qty, ll120_outer_individual ): corsair_device_qty = clamp(corsair_device_qty, 0, 63) ll120_outer_individual = clamp(int(ll120_outer_individual), 0, 1) self._feature_data[61] = (self._feature_data[61] & 0x80) | (fan_type << 1) | stripe_or_fan self._feature_data[62] = corsair_device_qty << 2 self._feature_data[72] = self._feature_data[72] | ll120_outer_individual def set_board_sync_setting( self, onboard_sync, combine_jrgb, combine_jpipe1, combine_jpipe2, combine_jrainbow1, combine_jrainbow2, combine_jcorsair, ): self._feature_data[82] |= clamp(int(onboard_sync), 0, 1) self._feature_data[78] |= 0b10000000 if combine_jrgb else 0 self._feature_data[82] |= 0b00010000 if combine_jpipe1 else 0 self._feature_data[82] |= 0b00100000 if combine_jpipe2 else 0 self._feature_data[82] |= 0b00000010 if combine_jrainbow1 else 0 self._feature_data[82] |= 0b00000100 if combine_jrainbow2 else 0 self._feature_data[82] |= 0b00001000 if combine_jcorsair else 0 def set_style_setting(self, led_area, lighting_mode, speed, brightness, color_selection): """ --led-area --lighting-mode --speed --brightness --color-selection """ lighting_mode = clamp(lighting_mode, 0, 40) speed = clamp(speed, 0, 2) brightness = clamp(brightness, 0, 10) color_selection = clamp(color_selection, 0, 1) self._feature_data[led_area] = lighting_mode self._feature_data[led_area + 4] = ( (self._feature_data[led_area + 4] & 0x80) | (brightness << 2) | speed ) self._feature_data[led_area + 8] = 0b10000000 if color_selection else 0 def set_color_setting( self, led_area, color1_r, color1_g, color1_b, color2_r, color2_g, color2_b ): """ --led-area --color1 --color2 """ self._feature_data[led_area + 1] = clamp(color1_r, 0, 255) self._feature_data[led_area + 2] = clamp(color1_g, 0, 255) self._feature_data[led_area + 3] = clamp(color1_b, 0, 255) self._feature_data[led_area + 4] = clamp(led_area, 0, 174) self._feature_data[led_area + 5] = clamp(color2_r, 0, 255) self._feature_data[led_area + 6] = clamp(color2_g, 0, 255) self._feature_data[led_area + 7] = clamp(color2_b, 0, 255) def set_send_led_setting(self, save): """applies changes, persists to device with save option""" self._feature_data[184] = int(bool(save)) return self._set_all_board(self._feature_data) def set_direction_setting_b931_only( self, board_sync, jrainbow1, jrainbow2, jrainbow3, jrainbow4, jrainbow5 ): """--b931-direction ?""" self._feature_data[83] |= int(bool(board_sync)) self._feature_data[39] |= int(bool(jrainbow1)) self._feature_data[61] |= int(bool(jrainbow2)) self._feature_data[50] |= int(bool(jrainbow3)) self._feature_data[19] |= int(bool(jrainbow4)) self._feature_data[29] |= int(bool(jrainbow5)) def set_oled_user_message(self, message): """--message""" return self._write([0x93] + list((message[:61] + " ").encode("ascii", "ignore"))) def set_oled_show_gameboot(self, selection, message): """ --selection --message """ return self._write([0x73, selection] + list(message[:60].encode("ascii", "ignore"))) def set_oled_show_profile(self, opts): """ --profile-type --gif-number """ profile_type, gif_no = map(int, opts[:2]) data = [0x70, 0, gif_no] self._write(data) data[1] = clamp(profile_type, 0, 1) return self._write(data) def set_oled_show_banner(self, banner_type=0, bmp_no=0): """ --banner-type --bmp-number """ data = [0x79, banner_type, bmp_no] return self._write(data) def set_oled_show_disable(self): """--disable-oled""" return self._write((0x7F,)) def _set_oled_upload(self, type, bytes, type_num=0): is_gif = type == _UploadType.GIF start_cmd = 0xC0 if is_gif else 0xD0 content = bytes.getbuffer() l = len(content) if l > (2**20): raise ValueError("file size of image is too large, something went wrong!") _LOGGER.debug(f"size of uploaded image is {l} bytes.") self._write( (start_cmd, l & 0xFF, (l >> 8) & 0xFF, (l >> 16) & 0xFF, (l >> 24) & 0xFF, type_num) ) sleep(2) checksum = sum(content) & 0xFFFF n = 0 while n < l: array = [start_cmd + 1] o = clamp(l - n, 0, 60) for k in range(0, o): array.append(content[n + k]) n += o self._write(array) if is_gif: high, low = self.get_oled_gif_checksum() check = (high << 8) + low else: high, low = self.get_oled_banner_checksum() check = (high << 8) + low if check != checksum: _LOGGER.error( f"invalid upload, image checksums: high {high} vs {checksum & 0xFF00}, " f"low {low} vs {checksum & 0xFF}." ) def set_oled_upload_gif(self, opts): """ --upload-gif-file --upload-gif-number """ imgtype, gif_no = map(int, opts[:2]) assert imgtype == 1, "Cannot override default images (image type 0)" file = opts[2] image_bytes = self._prepare_bmp(file) self._set_oled_upload(_UploadType.GIF, image_bytes, gif_no) def set_oled_upload_banner(self, bytes, banner_no=4): """Default is 4 to not overwrite the default banners. --upload-banner-file --upload-banner-number """ self._set_oled_upload(_UploadType.BANNER, bytes, banner_no) def set_oled_clock(self, time): """ Sends the specified time to the device. """ return self._write( ( 0x83, time.year % 100, time.month, time.day, time.weekday(), time.hour, time.minute, time.second, ) ) def set_oled_show_clock(self, style): """--set-oled-mode clock""" return self._write((0x7A, clamp(style, 0, 2))) def set_oled_show_cpu_status(self, freq, temp): """--update-cpu-status""" freq = clamp(int(freq), 0, 65536) temp = clamp(int(temp), 0, 65536) return self._write( ( 0x85, freq & 0xFF, (freq >> 8) & 0xFF, temp & 0xFF, (temp >> 8) & 0xFF, ) ) @staticmethod def _make_buffer(array, fill=0, total_size=_REPORT_LENGTH, prefix=0xD0): return bytearray([prefix] + list(array) + ((total_size - (len(array) + 1)) * [fill])) def _write(self, array, fill=0, total_size=_REPORT_LENGTH, prefix=0xD0): self.device.clear_enqueued_reports() return self.device.write(self._make_buffer(array, fill, total_size, prefix)) def _read(self, size=_REPORT_LENGTH): return bytearray(self.device.read(size)) def set_oled_gpu_status(self, mem_freq, usage): """ --set-gpu-memory-frequency --set-gpu-usage """ mem_freq = clamp(int(mem_freq), 0, 65536) usage = clamp(int(usage), 0, 65536) return self._write( ( 0x86, mem_freq & 0xFF, (mem_freq >> 8) & 0xFF, usage & 0xFF, (usage >> 8) & 0xFF, ) ) def set_oled_brightness_and_direction(self, brightness=100, direction=0): """ --set-oled-brightness --direction """ return self._write((0x7E, clamp(brightness, 0, 100), clamp(direction, 0, 3))) def set_per_led_720byte(self, jtype, area, rgb_data): b = jtype if self.product_id != 0x7B10 and self.product_id != 0x7C34: b += 1 if len(rgb_data) < 3: rgb_data = bytearray(3) rgb_data = rgb_data[:_PER_LED_LENGTH] self._feature_data_per_led = self._make_buffer( [0x37, b, area] + list(rgb_data), total_size=_PER_LED_LENGTH + 5, prefix=0x53 ) return self.device.send_feature_report(self._feature_data_per_led) def set_per_led_index(self, jtype, area, index_and_rgb, show=True, led_count=0): """ --set-per-led-index --jtype INT --area INT --index-and-rgb hex string? --show (or not passed) --maximum-leds INT """ array = bytearray(1) def rank_check(array): return ( isinstance(array, Sequence) and all(isinstance(x, Sequence) for x in array) and all(not isinstance(xa, Sequence) for x in array for xa in x) ) if len(index_and_rgb[1]) < 4 or len(index_and_rgb[0]) < 1 or not rank_check(index_and_rgb): raise ValueError("index_and_rgb should be a 2-dimensional list") if jtype == _JType.JONBOARD.value: array = self._per_led_rgb_jonboard elif jtype == _JType.JRAINBOW.value: area = clamp(area, 0, 1) array = self._per_led_rgb_jrainbow2 if area == 1 else self._per_led_rgb_jrainbow1 elif jtype == _JType.JCORSAIR.value: array = self._per_led_rgb_jcorsair end_index = led_count if led_count > 0 else len(index_and_rgb) for i in range(end_index): if index_and_rgb[i][0] * 3 + 2 < len(array): array[index_and_rgb[i][0] * 3] = index_and_rgb[i][1] array[index_and_rgb[i][0] * 3 + 1] = index_and_rgb[i][2] array[index_and_rgb[i][0] * 3 + 2] = index_and_rgb[i][3] if show: self.set_per_led_720byte(jtype, area, array) def set_switch_to_per_led_mode(self, jtype, area, show=False): """ --switch-to-per-led-mode --jtype INT --area INT --show (or not passed) """ if jtype == _JType.JONBOARD.value: self.set_style_setting( _LEDArea.ONBOARD_LED_0.value, _LightingMode.CORSAIR_IQUE.value, _Speed.MEDIUM.value, 10, _ColorSelection.USER_DEFINED.value, ) self._per_led_rgb_jonboard = bytearray(_PER_LED_LENGTH) elif jtype == _JType.JRAINBOW.value: if area == 0: self._per_led_rgb_jrainbow1 = bytearray(_PER_LED_LENGTH) self.set_cycle_number(0, 200) self.set_style_setting( _LEDArea.JRAINBOW1.value, _LightingMode.CORSAIR_IQUE.value, _Speed.MEDIUM.value, 10, _ColorSelection.USER_DEFINED.value, ) elif area == 1: self._per_led_rgb_jrainbow2 = bytearray(_PER_LED_LENGTH) self.set_cycle_number(1, 240) self.set_style_setting( _LEDArea.JRAINBOW2.value, _LightingMode.CORSAIR_IQUE.value, _Speed.MEDIUM.value, 10, _ColorSelection.USER_DEFINED.value, ) elif jtype == _JType.JCORSAIR.value: self._per_led_rgb_jcorsair = bytearray(_PER_LED_LENGTH) self.set_style_setting( _LEDArea.JCORSAIR.value, _LightingMode.CORSAIR_IQUE.value, _Speed.MEDIUM.value, 10, _ColorSelection.USER_DEFINED.value, ) if ((self._feature_data[61] & 0xE) >> 1) == 0 and (self._feature_data[61] & 1) == 1: self.set_device_setting(_StripeOrFan.FAN.value, _FanType.SP.value, 5, 0) else: self.set_cycle_number(2, 240) self.set_device_setting(_StripeOrFan.STRIPE.value, _FanType.HD.value, 0, 0) self.set_board_sync_setting(True, True, True, True, False, False, False) if show: self.set_send_led_setting(False) self.set_clear_per_led(jtype, area) def set_clear_per_led(self, jtype, area): """ --clear-per-led --jtype --area """ return self.set_per_led_720byte(jtype, area, bytearray(3)) def _set_all(self, which, r=0, g=0, b=0): self.set_board_sync_setting(True, True, True, True, True, True, True) self.set_style_setting(_LEDArea.ONBOARD_LED_0.value, which, 1, 10, 1) self.set_color_setting(_LEDArea.ONBOARD_LED_0.value, r, g, b, r, g, b) self.set_send_led_setting(False) def set_all_disable(self): """--led-disable""" self._set_all(_LightingMode.DISABLE.value) def set_all_static(self, r, g, b): """--led-all-static""" self._set_all(_LightingMode.NO_ANIMATION.value, r, g, b) def set_all_flashing(self, r, g, b): """--led-all-flashing""" self._set_all(_LightingMode.FLASHING.value, r, g, b) def set_all_breathing(self, r, g, b): """--led-all-breathing""" self._set_all(_LightingMode.BREATHING.value, r, g, b) def set_all_rainbow_wave(self): """--led-all-rainbow-wave""" self._set_all(_LightingMode.RAINBOW_WAVE.value) def switch_to_balance_mode(self): self.set_fan_config( [_FanConfig(_FanMode.BALANCE.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self.set_fan_temp_config( [_FanTempConfig(_FanMode.BALANCE.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self._send_safe_temp() def switch_to_game_mode(self): self.set_fan_config( [_FanConfig(_FanMode.GAME.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self.set_fan_temp_config( [_FanTempConfig(_FanMode.GAME.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self._send_safe_temp() def switch_to_silent_mode(self): self.set_fan_config( [_FanConfig(_FanMode.SILENT.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self.set_fan_temp_config( [_FanTempConfig(_FanMode.SILENT.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self._send_safe_temp() def switch_to_smart_mode(self): self.set_fan_config( [_FanConfig(_FanMode.SMART.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self.set_fan_temp_config( [_FanTempConfig(_FanMode.SMART.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self._send_safe_temp() def switch_to_default_mode(self): self.set_fan_config( [_FanConfig(_FanMode.DEFAULT.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self.set_fan_temp_config( [_FanTempConfig(_FanMode.DEFAULT.value, 0, 0, 0, 0, 0, 0, 0)] * self._fan_count ) self._send_safe_temp() # def set_oled_cpu_message(self, message): # return self._write([0x90] + # list(message[:60].encode('ascii', 'ignore'))) # def set_oled_memory_message(self, message): # return self._write([0x91] + # list(message[:60].encode('ascii', 'ignore'))) # def set_oled_vga_message(self, message): # return self._write([0x92] + # list(message[:60].encode('ascii', 'ignore'))) # def set_oled_show_system_message(self): # return self._write((0x72, )) # def set_oled_show_user_message(self): # return self._write((0x74, )) # def set_oled_start_isp_process(self): # return self._write((0xfa, )) # def set_oled_show_demo_mode(self): # return self._write((0x77, 0xff)) # def set_reset_mcu(self): # return self._write((0xd0, ), 0xcc, prefix=1) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/nvidia.py0000644000175000017500000005167314562144457017777 0ustar00jonasjonas"""liquidctl drivers for NVIDIA graphics cards. Copyright Jonas Malaco, Marshall Asch and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging from enum import unique from liquidctl.driver.smbus import SmbusDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import RelaxedNamesEnum, check_unsafe _LOGGER = logging.getLogger(__name__) # sources for pci device and subsystem device ids: # - https://www.nv-drivers.eu/nvidia-all-devices.html # - https://pci-ids.ucw.cz/pci.ids # - https://gitlab.com/CalcProgrammer1/OpenRGB/-/blob/master/pci_ids/pci_ids.h # vendor, devices # FWUPD_GUID = [vendor]:[device] - use hwinfo to inspect NVIDIA = 0x10de NVIDIA_GTX_1050 = 0x1c81 NVIDIA_GTX_1050_TI = 0x1c82 NVIDIA_GTX_1060 = 0x1c03 NVIDIA_GTX_1070 = 0x1b81 NVIDIA_GTX_1070_TI = 0x1b82 NVIDIA_GTX_1080 = 0x1b80 NVIDIA_GTX_1080_TI = 0x1b06 NVIDIA_GTX_1650 = 0x1f82 NVIDIA_GTX_1650S = 0x2187 NVIDIA_GTX_1660 = 0x2184 NVIDIA_GTX_1660S = 0x21c4 NVIDIA_GTX_1660_TI = 0x2182 NVIDIA_RTX_2060S = 0x1f47 NVIDIA_RTX_2060S_OC = 0x1f06 NVIDIA_RTX_2060_TU104 = 0x1e89 NVIDIA_RTX_2060_TU106 = 0x1f08 NVIDIA_RTX_2070 = 0x1f02 NVIDIA_RTX_2070S = 0x1ec7 NVIDIA_RTX_2070S_OC = 0x1e84 NVIDIA_RTX_2070_OC = 0x1f07 NVIDIA_RTX_2080 = 0x1e82 NVIDIA_RTX_2080S = 0x1e81 NVIDIA_RTX_2080_REV_A = 0x1e87 NVIDIA_RTX_2080_TI = 0x1e04 NVIDIA_RTX_2080_TI_REV_A = 0x1e07 NVIDIA_RTX_3050 = 0x2507 NVIDIA_RTX_3060 = 0x2503 NVIDIA_RTX_3060_GA104 = 0x2487 NVIDIA_RTX_3060_LHR = 0x2504 NVIDIA_RTX_3060_TI = 0x2486 NVIDIA_RTX_3060_TI_LHR = 0x2489 NVIDIA_RTX_3070 = 0x2484 NVIDIA_RTX_3070_LHR = 0x2488 NVIDIA_RTX_3070_TI = 0x2482 NVIDIA_RTX_3080 = 0x2206 NVIDIA_RTX_3080_12G_LHR = 0x220a NVIDIA_RTX_3080_LHR = 0x2216 NVIDIA_RTX_3080_TI = 0x2208 NVIDIA_RTX_3090 = 0x2204 # subsystem vendor ASUS, subsystem devices # PCI_SUBSYS_ID = [subsystem vendor]:[subsystem device] - use hwinfo to inspect ASUS = 0x1043 ASUS_STRIX_GTX_1050_O2G = 0x85d8 ASUS_STRIX_GTX_1050_TI_O4G = 0x85cd ASUS_STRIX_GTX_1050_TI_O4G_2 = 0x85d1 ASUS_STRIX_GTX_1060_6G = 0x85a4 ASUS_STRIX_GTX_1060_O6G = 0x85ac ASUS_STRIX_GTX_1070 = 0x8598 ASUS_STRIX_GTX_1070_OC = 0x8599 ASUS_STRIX_GTX_1070_TI_8G = 0x861d ASUS_STRIX_GTX_1070_TI_A8G = 0x861e ASUS_STRIX_GTX_1080 = 0x8592 ASUS_STRIX_GTX_1080_A8G = 0x85aa ASUS_STRIX_GTX_1080_O8G = 0x85f9 ASUS_STRIX_GTX_1080_TI = 0x85eb ASUS_STRIX_GTX_1080_TI_11G = 0x85f1 ASUS_STRIX_GTX_1080_TI_O11G = 0x85ea ASUS_STRIX_GTX_1080_TI_O11G_A02 = 0x85e4 ASUS_STRIX_GTX_1650S_OC = 0x874f ASUS_STRIX_GTX_1660S_O6G = 0x8752 ASUS_STRIX_GTX_1660_TI_OC = 0x86a5 ASUS_STRIX_RTX_2060S_8G = 0x8730 ASUS_STRIX_RTX_2060S_A8G = 0x86fc ASUS_STRIX_RTX_2060S_A8G_EVO = 0x8703 ASUS_STRIX_RTX_2060S_O8G = 0x86fb ASUS_STRIX_RTX_2060_EVO = 0x86d3 ASUS_STRIX_RTX_2060_O6G = 0x868e ASUS_STRIX_RTX_2060_O6G_EVO = 0x8775 ASUS_STRIX_RTX_2070S_8G_8707 = 0x8707 ASUS_STRIX_RTX_2070S_A8G = 0x8728 ASUS_STRIX_RTX_2070S_A8G_86FF = 0x86ff ASUS_STRIX_RTX_2070S_A8G_8706 = 0x8706 ASUS_STRIX_RTX_2070S_O8G = 0x8727 ASUS_STRIX_RTX_2070S_O8G_8729 = 0x8729 ASUS_STRIX_RTX_2070_A8G = 0x8671 ASUS_STRIX_RTX_2070_O8G = 0x8670 ASUS_STRIX_RTX_2080S_A8G = 0x8712 ASUS_STRIX_RTX_2080S_O8G = 0x8711 ASUS_STRIX_RTX_2080_O8G = 0x865f ASUS_STRIX_RTX_2080_TI_11G = 0x8687 ASUS_STRIX_RTX_2080_TI_OC = 0x866a ASUS_TUF_RTX_3060_TI_O8G_OC = 0x87c6 # subsystem vendor EVGA, subsystem devices # PCI_SUBSYS_ID = [subsystem vendor]:[subsystem device] - use hwinfo to inspect EVGA = 0x3842 EVGA_GTX_1070_FTW = 0x6276 EVGA_GTX_1070_FTW_DT_GAMING = 0x6274 EVGA_GTX_1070_FTW_HYBRID = 0x6278 EVGA_GTX_1070_TI_FTW2 = 0x6775 EVGA_GTX_1080_FTW = 0x6286 @unique class _ModeEnum(bytes, RelaxedNamesEnum): def __new__(cls, value, required_colors): obj = bytes.__new__(cls, [value]) obj._value_ = value obj.required_colors = required_colors return obj def __str__(self): return self.name.capitalize() class _NvidiaI2CDriver(): """Generic NVIDIA I²C driver.""" _VENDOR = None _ADDRESSES = [] _MATCHES = [] @classmethod def pre_probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): if (vendor and vendor != cls._VENDOR) \ or (address and int(address, base=16) not in cls._ADDRESSES) \ or release or serial: # filters can never match, always None return if smbus.parent_subsystem_vendor != cls._VENDOR \ or smbus.parent_vendor != NVIDIA \ or smbus.parent_driver != 'nvidia': return for dev_id, sub_dev_id, desc in cls._MATCHES: if (product and product != sub_dev_id) \ or (match and match.lower() not in desc.lower()): continue if smbus.parent_subsystem_device != sub_dev_id \ or smbus.parent_device != dev_id \ or not smbus.description.startswith('NVIDIA i2c adapter 1 '): continue yield (dev_id, sub_dev_id, desc) class EvgaPascal(SmbusDriver, _NvidiaI2CDriver): """NVIDIA series 10 (Pascal) graphics card from EVGA.""" _REG_MODE = 0x0c _REG_RED = 0x09 _REG_GREEN = 0x0a _REG_BLUE = 0x0b _REG_PERSIST = 0x23 _PERSIST = 0xe5 _VENDOR = EVGA _ADDRESSES = [0x49] _MATCHES = [ (NVIDIA_GTX_1070, EVGA_GTX_1070_FTW, 'EVGA GTX 1070 FTW'), (NVIDIA_GTX_1070, EVGA_GTX_1070_FTW_DT_GAMING, 'EVGA GTX 1070 FTW DT Gaming'), (NVIDIA_GTX_1070, EVGA_GTX_1070_FTW_HYBRID, 'EVGA GTX 1070 FTW Hybrid'), (NVIDIA_GTX_1070_TI, EVGA_GTX_1070_TI_FTW2, 'EVGA GTX 1070 Ti FTW2'), (NVIDIA_GTX_1080, EVGA_GTX_1080_FTW, 'EVGA GTX 1080 FTW'), ] @unique class Mode(_ModeEnum): OFF = (0x00, 0) FIXED = (0x01, 1) RAINBOW = (0x02, 0) BREATHING = (0x05, 1) @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): assert len(cls._ADDRESSES) == 1, 'unexpected extra address candidates' pre_probed = super().pre_probe(smbus, vendor, product, address, match, release, serial, **kwargs) for dev_id, sub_dev_id, desc in pre_probed: dev = cls(smbus, desc, vendor_id=EVGA, product_id=EVGA_GTX_1080_FTW, address=cls._ADDRESSES[0]) _LOGGER.debug('%s identified: %s', cls.__name__, desc) yield dev def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'experimental' in self.description: self._UNSAFE = ['smbus', 'experimental_evga_gpu'] else: self._UNSAFE = ['smbus'] def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning("%s: disabled, requires unsafe features '%s'", self.description, ','.join(self._UNSAFE)) return [] mode = self.Mode(self._smbus.read_byte_data(self._address, self._REG_MODE)) status = [('Mode', mode, '')] if mode.required_colors > 0: r = self._smbus.read_byte_data(self._address, self._REG_RED) g = self._smbus.read_byte_data(self._address, self._REG_GREEN) b = self._smbus.read_byte_data(self._address, self._REG_BLUE) status.append(('Color', f'{r:02x}{g:02x}{b:02x}', '')) return status def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the RGB lighting mode and, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe(*self._UNSAFE, error=True, **kwargs) colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError(f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: # the following write always fails, but nonetheless induces persistence try: self._smbus.write_byte_data(self._address, self._REG_PERSIST, self._PERSIST) except OSError as err: _LOGGER.debug('expected OSError when writing to _REG_PERSIST: %s', err) def initialize(self, **kwargs): """Initialize the device.""" pass def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class RogTuring(SmbusDriver, _NvidiaI2CDriver): """NVIDIA series 10 (Pascal) or 20 (Turing) graphics card from ASUS.""" _REG_RED = 0x04 _REG_GREEN = 0x05 _REG_BLUE = 0x06 _REG_MODE = 0x07 _REG_APPLY = 0x0e _SYNC_REG = 0x0c # unused _VENDOR = ASUS _ADDRESSES = [0x29, 0x2a, 0x60] _MATCHES = [ # description normalization rules: # - remove redundant ROG and meaningless GAMING; # - remove redundancies within a particular GPU core; # - keep OC when appropriate; # - uppercase ASUS and TUF, titlecase Evo, Ti and Stix; # - enforce ASUS Strix|TUF GTX|RTX [Super] [Ti] [Evo] [OC] [...] order; # - use consistent memory sizes: 2GB, 4GB, 6GB, 8GB (but omit when redundant). (NVIDIA_GTX_1050, ASUS_STRIX_GTX_1050_O2G, 'ASUS Strix GTX 1050 OC'), (NVIDIA_GTX_1050_TI, ASUS_STRIX_GTX_1050_TI_O4G, 'ASUS Strix GTX 1050 Ti OC'), (NVIDIA_GTX_1050_TI, ASUS_STRIX_GTX_1050_TI_O4G_2, 'ASUS Strix GTX 1050 Ti OC'), (NVIDIA_GTX_1060, ASUS_STRIX_GTX_1060_6G, 'ASUS Strix GTX 1060 6GB'), (NVIDIA_GTX_1060, ASUS_STRIX_GTX_1060_O6G, 'ASUS Strix GTX 1060 OC 6GB'), (NVIDIA_GTX_1070, ASUS_STRIX_GTX_1070, 'ASUS Strix GTX 1070'), (NVIDIA_GTX_1070, ASUS_STRIX_GTX_1070_OC, 'ASUS Strix GTX 1070 OC'), (NVIDIA_GTX_1070_TI, ASUS_STRIX_GTX_1070_TI_8G, 'ASUS Strix GTX 1070 Ti'), (NVIDIA_GTX_1070_TI, ASUS_STRIX_GTX_1070_TI_A8G, 'ASUS Strix GTX 1070 Ti Advanced'), (NVIDIA_GTX_1080, ASUS_STRIX_GTX_1080, 'ASUS Strix GTX 1080'), (NVIDIA_GTX_1080, ASUS_STRIX_GTX_1080_A8G, 'ASUS Strix GTX 1080 Advanced'), (NVIDIA_GTX_1080, ASUS_STRIX_GTX_1080_O8G, 'ASUS Strix GTX 1080 OC'), (NVIDIA_GTX_1080_TI, ASUS_STRIX_GTX_1080_TI, 'ASUS Strix GTX 1080 Ti'), (NVIDIA_GTX_1080_TI, ASUS_STRIX_GTX_1080_TI_11G, 'ASUS Strix GTX 1080 Ti'), (NVIDIA_GTX_1080_TI, ASUS_STRIX_GTX_1080_TI_O11G, 'ASUS Strix GTX 1080 Ti OC'), (NVIDIA_GTX_1080_TI, ASUS_STRIX_GTX_1080_TI_O11G_A02, 'ASUS Strix GTX 1080 Ti OC'), (NVIDIA_GTX_1650S, ASUS_STRIX_GTX_1650S_OC, 'ASUS Strix GTX 1650 Super OC'), (NVIDIA_GTX_1660S, ASUS_STRIX_GTX_1660S_O6G, 'ASUS Strix GTX 1660 Super OC'), (NVIDIA_GTX_1660_TI, ASUS_STRIX_GTX_1660_TI_OC, 'ASUS Strix GTX 1660 Ti OC'), (NVIDIA_RTX_2060S, ASUS_STRIX_RTX_2060S_A8G_EVO, 'ASUS Strix RTX 2060 Super Evo Advanced'), (NVIDIA_RTX_2060S_OC, ASUS_STRIX_RTX_2060S_8G, 'ASUS Strix RTX 2060 Super'), (NVIDIA_RTX_2060S_OC, ASUS_STRIX_RTX_2060S_A8G, 'ASUS Strix RTX 2060 Super Advanced'), (NVIDIA_RTX_2060S_OC, ASUS_STRIX_RTX_2060S_O8G, 'ASUS Strix RTX 2060 Super OC'), (NVIDIA_RTX_2060_TU104, ASUS_STRIX_RTX_2060_O6G_EVO, 'ASUS Strix RTX 2060 Evo OC'), (NVIDIA_RTX_2060_TU106, ASUS_STRIX_RTX_2060_EVO, 'ASUS Strix RTX 2060 Evo'), (NVIDIA_RTX_2060_TU106, ASUS_STRIX_RTX_2060_O6G, 'ASUS Strix RTX 2060 OC'), (NVIDIA_RTX_2070S, ASUS_STRIX_RTX_2070S_A8G_86FF, 'ASUS Strix RTX 2070 Super Advanced'), (NVIDIA_RTX_2070S_OC, ASUS_STRIX_RTX_2070S_8G_8707, 'ASUS Strix RTX 2070'), (NVIDIA_RTX_2070S_OC, ASUS_STRIX_RTX_2070S_A8G, 'ASUS Strix RTX 2070 Super Advanced'), (NVIDIA_RTX_2070S_OC, ASUS_STRIX_RTX_2070S_A8G_8706, 'ASUS Strix RTX 2070 Super Advanced'), (NVIDIA_RTX_2070S_OC, ASUS_STRIX_RTX_2070S_O8G, 'ASUS Strix RTX 2070 Super OC'), (NVIDIA_RTX_2070S_OC, ASUS_STRIX_RTX_2070S_O8G_8729, 'ASUS Strix RTX 2070 Super OC'), (NVIDIA_RTX_2070_OC, ASUS_STRIX_RTX_2070_A8G, 'ASUS Strix RTX 2070 Advanced'), (NVIDIA_RTX_2070_OC, ASUS_STRIX_RTX_2070_O8G, 'ASUS Strix RTX 2070 OC'), (NVIDIA_RTX_2080S, ASUS_STRIX_RTX_2080S_A8G, 'ASUS Strix RTX 2080 Super Advanced'), (NVIDIA_RTX_2080S, ASUS_STRIX_RTX_2080S_O8G, 'ASUS Strix RTX 2080 Super OC'), (NVIDIA_RTX_2080_REV_A, ASUS_STRIX_RTX_2080_O8G, 'ASUS Strix RTX 2080 OC'), (NVIDIA_RTX_2080_TI, ASUS_STRIX_RTX_2080_TI_11G, 'ASUS Strix RTX 2080 Ti'), (NVIDIA_RTX_2080_TI_REV_A, ASUS_STRIX_RTX_2080_TI_OC, 'ASUS Strix RTX 2080 Ti OC'), (NVIDIA_RTX_3060_TI_LHR, ASUS_TUF_RTX_3060_TI_O8G_OC, 'ASUS TUF RTX 3060 Ti OC'), ] _SENTINEL_ADDRESS = 0xffff # intentionally invalid _ASUS_GPU_APPLY_VAL = 0x01 @unique class Mode(_ModeEnum): OFF = (0x00, 0) # not a real mode; fixed is sent with RGB = 0 FIXED = (0x01, 1) BREATHING = (0x02, 1) FLASH = (0x03, 1) RAINBOW = (0x04, 0) @classmethod def probe(cls, smbus, vendor=None, product=None, address=None, match=None, release=None, serial=None, **kwargs): ASUS_GPU_MAGIC_VALUE = 0x1589 pre_probed = super().pre_probe(smbus, vendor, product, address, match, release, serial, **kwargs) for dev_id, sub_dev_id, desc in pre_probed: selected_address = None if check_unsafe('smbus', **kwargs): for address in cls._ADDRESSES: val1 = 0 val2 = 0 smbus.open() try: val1 = smbus.read_byte_data(address, 0x20) val2 = smbus.read_byte_data(address, 0x21) except: pass smbus.close() if val1 << 8 | val2 == ASUS_GPU_MAGIC_VALUE: selected_address = address break else: selected_address = cls._SENTINEL_ADDRESS _LOGGER.debug('unsafe features not enabled, using sentinel address') if selected_address is not None: dev = cls(smbus, desc, vendor_id=ASUS, product_id=dev_id, address=selected_address) _LOGGER.debug('%s identified: %s at address %02x', cls.__name__, desc, selected_address) yield dev def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if 'experimental' in self.description: self._UNSAFE = ['smbus', 'experimental_asus_gpu'] else: self._UNSAFE = ['smbus'] def get_status(self, verbose=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ # only RGB lighting information can be fetched for now; as that isn't # super interesting, only enable it in verbose mode if not verbose: return [] if not check_unsafe(*self._UNSAFE, **kwargs): _LOGGER.warning("%s: disabled, requires unsafe features '%s'", self.description, ','.join(self._UNSAFE)) return [] assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' mode = self._smbus.read_byte_data(self._address, self._REG_MODE) red = self._smbus.read_byte_data(self._address, self._REG_RED) green = self._smbus.read_byte_data(self._address, self._REG_GREEN) blue = self._smbus.read_byte_data(self._address, self._REG_BLUE) # emulate `OFF` both ways if red == green == blue == 0: mode = 0 mode = self.Mode(mode) status = [('Mode', mode, '')] if mode.required_colors > 0: status.append(('Color', f'{red:02x}{green:02x}{blue:02x}', '')) return status def set_color(self, channel, mode, colors, non_volatile=False, **kwargs): """Set the lighting mode, when applicable, color. The table bellow summarizes the available channels, modes and their associated number of required colors. | Channel | Mode | Required colors | | -------- | --------- | --------------- | | led | off | 0 | | led | fixed | 1 | | led | flash | 1 | | led | breathing | 1 | | led | rainbow | 0 | The settings configured on the device are normally volatile, and are cleared whenever the graphics card is powered down (OS and UEFI power saving settings can affect when this happens). It is possible to store them in non-volatile controller memory by passing `non_volatile=True`. But as this memory has some unknown yet limited maximum number of write cycles, volatile settings are preferable, if the use case allows for them. """ check_unsafe(*self._UNSAFE, error=True, **kwargs) assert self._address != self._SENTINEL_ADDRESS, \ 'invalid address (probing may not have had access to SMbus)' colors = list(colors) try: mode = self.Mode[mode] except KeyError: raise ValueError(f'invalid mode: {mode!r}') from None if len(colors) < mode.required_colors: raise ValueError(f'{mode} mode requires {mode.required_colors} colors') if len(colors) > mode.required_colors: _LOGGER.debug('too many colors, dropping to %d', mode.required_colors) colors = colors[:mode.required_colors] if mode == self.Mode.OFF: self._smbus.write_byte_data(self._address, self._REG_MODE, self.Mode.FIXED.value) self._smbus.write_byte_data(self._address, self._REG_RED, 0x00) self._smbus.write_byte_data(self._address, self._REG_GREEN, 0x00) self._smbus.write_byte_data(self._address, self._REG_BLUE, 0x00) else: self._smbus.write_byte_data(self._address, self._REG_MODE, mode.value) for r, g, b in colors: self._smbus.write_byte_data(self._address, self._REG_RED, r) self._smbus.write_byte_data(self._address, self._REG_GREEN, g) self._smbus.write_byte_data(self._address, self._REG_BLUE, b) if non_volatile: self._smbus.write_byte_data(self._address, self._REG_APPLY, self._ASUS_GPU_APPLY_VAL) def initialize(self, **kwargs): """Initialize the device.""" pass def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/driver/nzxt_epsu.py0000644000175000017500000001225114562144457020551 0ustar00jonasjonas"""liquidctl driver for NZXT E-series PSUs. Supported devices: NZXT E500, E650 and E850. Features: - electrical output monitoring: complete - general device monitoring: partial - fan control: missing - 12V multiple rail configuration: missing Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import time from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.pmbus import CommandCode as CMD from liquidctl.pmbus import linear_to_float _REPORT_LENGTH = 64 _MIN_DELAY = 0.0025 _ATTEMPTS = 3 _SEASONIC_READ_FIRMWARE_VERSION = CMD.MFR_SPECIFIC_FC _RAILS = ['+12V peripherals', '+12V EPS/ATX12V', '+12V motherboard/PCI-e', '+5V combined', '+3.3V combined'] class NzxtEPsu(UsbHidDriver): """NZXT E-series power supply unit.""" _MATCHES = [ (0x7793, 0x5911, 'NZXT E500', {}), (0x7793, 0x5912, 'NZXT E650', {}), (0x7793, 0x2500, 'NZXT E850', {}), ] def initialize(self, **kwargs): """Initialize the device. Apparently not required. """ pass def get_status(self, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ self.device.clear_enqueued_reports() fw_human, fw_cam = self._get_fw_versions() status = [ ('Temperature', self._get_float(CMD.READ_TEMPERATURE_2), '°C'), ('Fan speed', self._get_float(CMD.READ_FAN_SPEED_1), 'rpm'), ('Firmware version', f'{fw_human}/{fw_cam}', ''), ] for i, name in enumerate(_RAILS): status.append((f'{name} output voltage', self._get_vout(i), 'V')) status.append((f'{name} output current', self._get_float(CMD.READ_IOUT, page=i), 'A')) status.append((f'{name} output power', self._get_float(CMD.READ_POUT, page=i), 'W')) return status def set_color(self, channel, mode, colors, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def _write(self, data): assert len(data) <= _REPORT_LENGTH packet = bytearray(1 + _REPORT_LENGTH) packet[1: 1 + len(data)] = data # device doesn't use numbered reports self.device.write(packet) def _read(self): return self.device.read(_REPORT_LENGTH) def _wait(self): """Give the device some time and avoid error responses. Not well understood but probably related to the PIC16F1455 microcontroller. It is possible that it isn't just used for a "dumb" PMBus/HID bridge, requiring time to be left for other tasks. """ time.sleep(_MIN_DELAY) def _exec_read(self, cmd, data_len): data = None msg = [0xad, 0, data_len + 1, 1, 0x60, cmd] for _ in range(_ATTEMPTS): self._wait() self._write(msg) res = self._read() # see comment in _exec_page_plus_read, but res[1] == 0xff has not # been seen in the wild yet # TODO replace with PEC byte check if res[0] == 0xaa and res[1] == data_len + 1: data = res break assert data, f'invalid response (attempts={_ATTEMPTS})' return data[2:(2 + data_len)] def _exec_page_plus_read(self, page, cmd, data_len): data = None msg = [0xad, 0, data_len + 2, 4, 0x60, CMD.PAGE_PLUS_READ, 2, page, cmd] for _ in range(_ATTEMPTS): self._wait() self._write(msg) res = self._read() # in captured traffic res[2] == 0xff appears to signal invalid data # (possibly due to the device being busy, see PMBus spec) # TODO replace with PEC byte check if res[0] == 0xaa and res[1] == data_len + 2 and res[2] == data_len: data = res break assert data, f'invalid response (attempts={_ATTEMPTS})' return data[3:(3 + data_len)] def _get_float(self, cmd, page=None): if page is None: return linear_to_float(self._exec_read(cmd, 2)) else: return linear_to_float(self._exec_page_plus_read(page, cmd, 2)) def _get_vout(self, rail): mode = self._exec_page_plus_read(rail, CMD.VOUT_MODE, 1)[0] assert mode >> 5 == 0 # assume vout_mode is always ulinear16 vout = self._exec_page_plus_read(rail, CMD.READ_VOUT, 2) return linear_to_float(vout, mode & 0x1f) def _get_fw_versions(self): minor, major = self._exec_read(_SEASONIC_READ_FIRMWARE_VERSION, 2) human_ver = f'{bytes([major]).decode()}{minor:03}' ascam_ver = int.from_bytes(bytes.fromhex(human_ver), byteorder='big') return (human_ver, ascam_ver) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() # deprecated aliases SeasonicEDriver = NzxtEPsu ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1741176960.0 liquidctl-1.15.0/liquidctl/driver/rgb_fusion2.py0000644000175000017500000002417114762040200020714 0ustar00jonasjonas"""liquidctl driver for Gigabyte RGB Fusion 2.0 USB controllers. Supported controllers: - ITE 5702: found in Gigabyte Z490 Vision D - ITE 8297: found in Gigabyte X570 Aorus Elite Copyright CaseySJ, Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys from collections import namedtuple from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp _LOGGER = logging.getLogger(__name__) _USAGE_PAGE = 0xff89 _RGB_CONTROL_USAGE = 0xcc _OTHER_USAGE = 0x10 _REPORT_ID = 0xcc _REPORT_BYTE_LENGTH = 63 _INIT_CMD = 0x60 _COLOR_CHANNELS = { 'led1': (0x20, 0x01), 'led2': (0x21, 0x02), 'led3': (0x22, 0x04), 'led4': (0x23, 0x08), 'led5': (0x24, 0x10), 'led6': (0x25, 0x20), 'led7': (0x26, 0x40), 'led8': (0x27, 0x80), } _PULSE_SPEEDS = { 'slowest': (0x40, 0x06, 0x40, 0x06, 0x20, 0x03), 'slower': (0x78, 0x05, 0x78, 0x05, 0xbc, 0x02), 'normal': (0xb0, 0x04, 0xb0, 0x04, 0xf4, 0x01), 'faster': (0xe8, 0x03, 0xe8, 0x03, 0xf4, 0x01), 'fastest': (0x84, 0x03, 0x84, 0x03, 0xc2, 0x01), 'ludicrous': (0x20, 0x03, 0x20, 0x03, 0x90, 0x01), } _FLASH_SPEEDS = { 'slowest': (0x64, 0x00, 0x64, 0x00, 0x60, 0x09), 'slower': (0x64, 0x00, 0x64, 0x00, 0x90, 0x08), 'normal': (0x64, 0x00, 0x64, 0x00, 0xd0, 0x07), 'faster': (0x64, 0x00, 0x64, 0x00, 0x08, 0x07), 'fastest': (0x64, 0x00, 0x64, 0x00, 0x40, 0x06), 'ludicrous': (0x64, 0x00, 0x64, 0x00, 0x78, 0x05), } _DOUBLE_FLASH_SPEEDS = { 'slowest': (0x64, 0x00, 0x64, 0x00, 0x28, 0x0a), 'slower ': (0x64, 0x00, 0x64, 0x00, 0x60, 0x09), 'normal': (0x64, 0x00, 0x64, 0x00, 0x90, 0x08), 'faster': (0x64, 0x00, 0x64, 0x00, 0xd0, 0x07), 'fastest': (0x64, 0x00, 0x64, 0x00, 0x08, 0x07), 'ludicrous': (0x64, 0x00, 0x64, 0x00, 0x40, 0x06), } _COLOR_CYCLE_SPEEDS = { 'slowest': (0x78, 0x05, 0xb0, 0x04, 0x00, 0x00), 'slower': (0x7e, 0x04, 0x1a, 0x04, 0x00, 0x00), 'normal': (0x52, 0x03, 0xee, 0x02, 0x00, 0x00), 'faster': (0xf8, 0x02, 0x94, 0x02, 0x00, 0x00), 'fastest': (0x26, 0x02, 0xc2, 0x01, 0x00, 0x00), 'ludicrous': (0xcc, 0x01, 0x68, 0x01, 0x00, 0x00), } _ColorMode = namedtuple('_ColorMode', ['name', 'value', 'pulses', 'flash_count', 'cycle_count', 'max_brightness', 'takes_color', 'speed_values']) _COLOR_MODES = { mode.name: mode for mode in [ _ColorMode('off', 0x01, pulses=False, flash_count=0, cycle_count=0, max_brightness=0, takes_color=False, speed_values=None), _ColorMode('fixed', 0x01, pulses=False, flash_count=0, cycle_count=0, max_brightness=90, takes_color=True, speed_values=None), _ColorMode('pulse', 0x02, pulses=True, flash_count=0, cycle_count=0, max_brightness=90, takes_color=True, speed_values=_PULSE_SPEEDS), _ColorMode('flash', 0x03, pulses=True, flash_count=1, cycle_count=0, max_brightness=100, takes_color=True, speed_values=_FLASH_SPEEDS), _ColorMode('double-flash', 0x03, pulses=True, flash_count=2, cycle_count=0, max_brightness=100, takes_color=True, speed_values=_DOUBLE_FLASH_SPEEDS), _ColorMode('color-cycle', 0x04, pulses=False, flash_count=0, cycle_count=7, max_brightness=100, takes_color=False, speed_values=_COLOR_CYCLE_SPEEDS), ] } class RgbFusion2(UsbHidDriver): """liquidctl driver for Gigabyte RGB Fusion 2.0 USB controllers.""" _MATCHES = [ (0x048d, 0x5702, 'Gigabyte RGB Fusion 2.0 5702 Controller', {}), (0x048d, 0x8297, 'Gigabyte RGB Fusion 2.0 8297 Controller', {}), ] @classmethod def probe(cls, handle, **kwargs): """Probe `handle` and yield corresponding driver instances. These devices have multiple top-level HID usages, and HidapiDevice handles matching other usages have to be ignored. """ # if usage_page/usage are not available due to hidapi limitations # (version, platform or backend), they are unfortunately left # uninitialized; because of this, we explicitly exclude the undesired # usage_page/usage pair, and assume in all other cases that we either # have the desired usage page/usage pair, or that on that system a # single handle is returned for that device interface (see: 259) if (handle.hidinfo['usage_page'] == _USAGE_PAGE and handle.hidinfo['usage'] == _OTHER_USAGE): return yield from super().probe(handle, **kwargs) def initialize(self, **kwargs): """Initialize the device. Returns a list of `(property, value, unit)` tuples, containing the firmware version and other useful information provided by the hardware. """ self._send_feature_report([_REPORT_ID, _INIT_CMD]) data = self._get_feature_report(_REPORT_ID) # be tolerant: 8297 controllers support report IDs yet return 0 in the # first byte, which is out of spec assert data[0] in (_REPORT_ID, 0) and data[1] == 0x01 null = data.index(0, 12) dev_name = str(bytes(data[12:null]), 'ascii', errors='replace') fw_version = tuple(data[4:8]) return [ ('Hardware name', dev_name, ''), ('Firmware version', '{}.{}.{}.{}'.format(*fw_version), ''), ] def get_status(self, **kwargs): """Get a status report. Currently returns an empty list, but this behavior is not guaranteed as in the future the device may start to report useful information. A non-empty list would contain `(property, value, unit)` tuples. """ _LOGGER.info('status reports not available from %s', self.description) return [] def set_color(self, channel, mode, colors, speed='normal', **kwargs): """Set the color mode for a specific channel. Up to eight individual channels are available, named 'led1' through 'led8'. In addition to these, the 'sync' channel can be used to apply the same settings to all channels. The table bellow summarizes the available channels. | Mode | Colors required | Speed is customizable | | ------------ | --------------- | --------------------- | | off | zero | no | | fixed | one | no | | pulse | one | yes | | flash | one | yes | | double-flash | one | yes | | color-cycle | zero | yes | `colors` should be an iterable of zero or one `[red, blue, green]` triples, where each red/blue/green component is a value in the range 0–255. `speed`, when supported by the `mode`, can be one of: `slowest`, `slower`, `normal` (default), `faster`, `fastest` or `ludicrous`. """ mode = _COLOR_MODES[mode] colors = iter(colors) if mode.takes_color: try: r, g, b = next(colors) single_color = (b, g, r) except StopIteration: raise ValueError(f'one color required for mode={mode.name}') from None else: single_color = (0, 0, 0) remaining = sum(1 for _ in colors) if remaining: _LOGGER.warning('too many colors for mode=%s, dropping %d', mode.name, remaining) brightness = clamp(100, 0, mode.max_brightness) # hardcode this for now data = [_REPORT_ID, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, mode.value, brightness, 0x00] data += single_color data += [0x00, 0x00, 0x00, 0x00, 0x00] if mode.speed_values: data += mode.speed_values[speed] else: data += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00] data += [0x00, 0x00, mode.cycle_count, int(mode.pulses), mode.flash_count] if channel == 'sync': selected_channels = _COLOR_CHANNELS.values() else: selected_channels = (_COLOR_CHANNELS[channel],) for addr1, addr2 in selected_channels: data[1:3] = addr1, addr2 self._send_feature_report(data) self._execute_report() def set_speed_profile(self, channel, profile, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def set_fixed_speed(self, channel, duty, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() def reset_all_channels(self): """Reset all LED channels.""" for addr1, _ in _COLOR_CHANNELS.values(): self._send_feature_report([_REPORT_ID, addr1, 0]) self._execute_report() def _get_feature_report(self, report_id): return self.device.get_feature_report(report_id, _REPORT_BYTE_LENGTH + 1) def _send_feature_report(self, data): padding = [0x0]*(_REPORT_BYTE_LENGTH + 1 - len(data)) self.device.send_feature_report(data + padding) def _execute_report(self): """Request for the previously sent lighting settings to be applied.""" self._send_feature_report([_REPORT_ID, 0x28, 0xff]) def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() # Acknowledgments: # # Thanks to SgtSixPack for capturing USB traffic on 0x8297 and testing the driver on Windows. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1726842438.0 liquidctl-1.15.0/liquidctl/driver/smart_device.py0000644000175000017500000007254014673303106021155 0ustar00jonasjonas"""liquidctl drivers for NZXT Smart Device V1/V2, Grid+ V3, HUE 2 and HUE 2 Ambient. Smart Device (V1) ----------------- The NZXT Smart Device is a fan and LED controller that ships with the H200i, H400i, H500i and H700i cases. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode for each channel. Additionally, up to four chained HUE+ LED strips, or five Aer RGB fans, can be driven from only RGB channel available. The firmware installed on the device exposes several color presets, most of them common to other NZXT products. The device recognizes the type of accessory connected by measuring the resistance between the FD and GND lines.[1][2] In normal usage accessories should not be mixed. A microphone is also present onboard, for noise level optimization through CAM and AI. NZXT calls this feature Adaptive Noise Reduction (ANR). [1] https://forum.level1techs.com/t/nzxt-hue-a-look-inside/104836 [2] In parallel: 10 kOhm per HUE+ strip, 16 kOhm per Aer RGB fan. Grid+ V3 -------- The NZXT Grid+ V3 is a fan controller very similar to the Smart Device (V1). Comparing the two, the Grid+ has more fan channels (six in total), and no support for LEDs. Smart Device V2 --------------- The NZXT Smart Device V2 is a newer model of the original fan and LED controller. It ships with NZXT's cases released in mid-2019 including the H510 Elite, H510i, H710i, and H210i. It provides three independent fan channels with standard 4-pin connectors. Both PWM and DC control is supported, and the device automatically chooses the appropriate mode for each channel. Additionally, it features two independent lighting (Addressable RGB) channels, unlike the single channel in the original. NZXT Aer RGB 2 fans and HUE 2 lighting accessories (HUE 2 LED strip, HUE 2 Unerglow, HUE 2 Cable Comb) can be connected. The firmware installed on the device exposes several color presets, most of them common to other NZXT products. HUE 2 and HUE+ devices (including Aer RGB and Aer RGB 2 fans) are supported, but HUE 2 components cannot be mixed with HUE+ components in the same channel. Each lighting channel supports up to 6 accessories and a total of 40 LEDs. A microphone is still present onboard for noise level optimization through CAM and AI. NZXT H1 V2 ---------- The second revision of the NZXT H1 case, labeled H1 V2, ships with a variant of the NZXT Smart Device V2 that handles both the internal fans and the AIO pump. Two fan and one pump channels are available, where the formers can be controlled via PWM or DC. The pump speed is not user controllable. The device reports the state, speed and duty of each fan channel, as well as the pump speed. There are no lighting channels available nor an onboard microphone. RGB & Fan Controller -------------------- The NZXT RGB & Fan Controller is a retail version of the NZXT Smart Device V2. HUE 2 ----- The NZXT HUE 2 is an LED controller from the same generation of the Smart Device V2. The presets and limitations of the four LED channels are the same as in the Smart Device V2. HUE 2 Ambient ------------- HUE 2 Ambient is a variant of HUE 2 featuring 2 LED control channels. Driver ------ This driver implements all features available at the hardware level: - initialization - detection of connected fans and LED strips - control of fan speeds per channel - monitoring of fan presence, control mode, speed, voltage and current - control of lighting modes and colors - reporting of LED accessory count and type - monitoring of noise level (from the onboard microphone) - reporting of firmware version Software based features offered by CAM, like ANR, have not been implemented. After powering on from Mechanical Off, or if there have been hardware changes, the devices must be manually initialized by calling `initialize()`. This will cause all connected fans and LED accessories to be detected, and enable status updates. It is recommended to initialize the devices at every boot. Copyright Jonas Malaco, CaseySJ and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import itertools import logging import time from liquidctl.driver.usb import UsbHidDriver from liquidctl.error import NotSupportedByDevice from liquidctl.util import clamp, map_direction, Hue2Accessory, \ HUE2_MAX_ACCESSORIES_IN_CHANNEL _LOGGER = logging.getLogger(__name__) _ANIMATION_SPEEDS = { 'slowest': 0x0, 'slower': 0x1, 'normal': 0x2, 'faster': 0x3, 'fastest': 0x4, } _MIN_DUTY = 0 _MAX_DUTY = 100 class _BaseSmartDevice(UsbHidDriver): """Common functions of Smart Device and Grid drivers.""" def __init__(self, device, description, speed_channels, color_channels, **kwargs): super().__init__(device, description) self._speed_channels = speed_channels self._color_channels = color_channels def set_color(self, channel, mode, colors, speed='normal', direction='forward', **kwargs): """Set the color mode for a specific channel. Only supported by Smart Device V1/V2 and HUE 2 controllers. """ if not self._color_channels: raise NotSupportedByDevice() if 'backwards' in mode: _LOGGER.warning('deprecated mode, move to direction=backward option') mode = mode.replace('backwards-', '') direction = 'backward' cid = self._color_channels[channel] _, _, _, mincolors, maxcolors = self._COLOR_MODES[mode] colors = [[g, r, b] for [r, g, b] in colors] if len(colors) < mincolors: raise ValueError(f'not enough colors for mode={mode}, at least {mincolors} required') elif maxcolors == 0: if colors: _LOGGER.warning('too many colors for mode=%s, none needed', mode) colors = [[0, 0, 0]] # discard the input but ensure at least one step elif len(colors) > maxcolors: _LOGGER.warning('too many colors for mode=%s, dropping to %d', mode, maxcolors) colors = colors[:maxcolors] sval = _ANIMATION_SPEEDS[speed] self._write_colors(cid, mode, colors, sval, direction) def set_fixed_speed(self, channel, duty, **kwargs): """Set channel to a fixed speed duty.""" if channel == 'sync': selected_channels = self._speed_channels else: selected_channels = {channel: self._speed_channels[channel]} for cname, (cid, dmin, dmax) in selected_channels.items(): duty = clamp(duty, dmin, dmax) _LOGGER.info('setting %s duty to %d%%', cname, duty) self._write_fixed_duty(cid, duty) def set_speed_profile(self, channel, profile, **kwargs): raise NotSupportedByDevice() def _write(self, data): padding = [0x0]*(self._WRITE_LENGTH - len(data)) self.device.write(data + padding) def _write_colors(self, cid, mode, colors, sval, direction): raise NotImplementedError() def _write_fixed_duty(self, cid, duty): raise NotImplementedError() def set_screen(self, channel, mode, value, **kwargs): """Not supported by this device.""" raise NotSupportedByDevice() class SmartDevice(_BaseSmartDevice): """NZXT Smart Device (V1) or Grid+ V3.""" # support for hwmon: nzxt-grid3, liquidtux # https://github.com/liquidctl/liquidtux/blob/3b80dafead6f/nzxt-grid3.c _MATCHES = [ (0x1e71, 0x1714, 'NZXT Smart Device (V1)', { 'speed_channel_count': 3, 'color_channel_count': 1 }), (0x1e71, 0x1711, 'NZXT Grid+ V3', { 'speed_channel_count': 6, 'color_channel_count': 0 }), ] _READ_LENGTH = 21 _WRITE_LENGTH = 65 _COLOR_MODES = { # (byte2/mode, byte3/variant, byte4/size, min colors, max colors) 'off': (0x00, 0x00, 0x00, 0, 0), 'fixed': (0x00, 0x00, 0x00, 1, 1), 'super-fixed': (0x00, 0x00, 0x00, 1, 40), # independent leds 'fading': (0x01, 0x00, 0x00, 1, 8), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'marquee-3': (0x03, 0x00, 0x00, 1, 1), 'marquee-4': (0x03, 0x00, 0x08, 1, 1), 'marquee-5': (0x03, 0x00, 0x10, 1, 1), 'marquee-6': (0x03, 0x00, 0x18, 1, 1), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8), 'alternating': (0x05, 0x00, 0x00, 2, 2), 'moving-alternating': (0x05, 0x08, 0x00, 2, 2), 'pulse': (0x06, 0x00, 0x00, 1, 8), 'breathing': (0x07, 0x00, 0x00, 1, 8), # colors for each step 'super-breathing': (0x07, 0x00, 0x00, 1, 40), # one step, independent leds 'candle': (0x09, 0x00, 0x00, 1, 1), 'wings': (0x0c, 0x00, 0x00, 1, 1), 'super-wave': (0x0d, 0x00, 0x00, 1, 40), # independent ring leds # deprecated in favor of direction=backward 'backwards-spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'backwards-marquee-3': (0x03, 0x00, 0x00, 1, 1), 'backwards-marquee-4': (0x03, 0x00, 0x08, 1, 1), 'backwards-marquee-5': (0x03, 0x00, 0x10, 1, 1), 'backwards-marquee-6': (0x03, 0x00, 0x18, 1, 1), 'covering-backwards-marquee': (0x04, 0x00, 0x00, 1, 8), 'backwards-moving-alternating': (0x05, 0x08, 0x00, 2, 2), 'backwards-super-wave': (0x0d, 0x00, 0x00, 1, 40), } def __init__(self, device, description, speed_channel_count, color_channel_count, **kwargs): """Instantiate a driver with a device handle.""" speed_channels = {f'fan{i + 1}': (i, _MIN_DUTY, _MAX_DUTY) for i in range(speed_channel_count)} color_channels = {'led': (i) for i in range(color_channel_count)} super().__init__(device, description, speed_channels, color_channels, **kwargs) def initialize(self, direct_access=False, **kwargs): """Initialize the device and the driver. Connected fans and LED accessories are detected. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, assuming it is already initialized', self._hwmon.driver) else: if self._hwmon: _LOGGER.warning('forcing re-initialization despite %s kernel driver', self._hwmon.driver) self._write([0x1, 0x5c]) # initialize/detect connected devices and their type self._write([0x1, 0x5d]) # start reporting self.device.clear_enqueued_reports() msg = self.device.read(self._READ_LENGTH) fw = tuple(msg[0xb:0xf]) _LOGGER.debug('raw firmware version: %r', fw) # NZXT/CAM has simplified how they report the firmware version from its # raw 4-component form fw_human = f'{fw[0]}.{fw[3]}' ret = [('Firmware version', fw_human, '')] if self._color_channels: lcount = msg[0x11] ret.append(('LED accessories', lcount, '')) if lcount > 0: ltype, lsize = [('HUE+ Strip', 10), ('Aer RGB', 8)][msg[0x10] >> 3] ret.append(('LED accessory type', ltype, '')) ret.append(('LED count (total)', lcount*lsize, '')) return ret def _get_status_directly(self): fans = [None] * len(self._speed_channels) noise = [] self.device.clear_enqueued_reports() for i, _ in enumerate(fans): msg = self.device.read(self._READ_LENGTH) num = (msg[15] >> 4) + 1 state = msg[15] & 0x3 fans[num - 1] = [ (f'Fan {num} speed', msg[3] << 8 | msg[4], 'rpm'), (f'Fan {num} voltage', msg[7] + msg[8]/100, 'V'), (f'Fan {num} current', msg[9] + msg[10]/100, 'A'), (f'Fan {num} control mode', [None, 'DC', 'PWM'][state], ''), ] noise.append(msg[1]) # flatten fan data while checking for holes ret = [] for i, fan in enumerate(fans): if fan: ret = ret + fan else: _LOGGER.warning('missing data fan for %d', i + 1) ret.append(('Noise level', round(sum(noise)/len(noise)), 'dB')) return ret def _get_status_from_hwmon(self): ret = [] mode = ['DC', 'PWM'] # slightly simplified, but the device treats undetected == PWM for i in range(len(self._speed_channels)): n = i + 1 ret.append((f'Fan {n} speed', self._hwmon.read_int(f'fan{n}_input'), 'rpm')), ret.append((f'Fan {n} voltage', self._hwmon.read_int(f'in{i}_input') * 1e-3, 'V')), ret.append((f'Fan {n} current', self._hwmon.read_int(f'curr{n}_input') * 1e-3, 'A')), ret.append((f'Fan {n} control mode', mode[self._hwmon.read_int(f'pwm{n}_mode')], '')), # noise level is not available through hwmon, but also not very accurate or useful return ret def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) return self._get_status_directly() def _write_colors(self, cid, mode, colors, sval, direction='forward'): mval, mod3, mod4, _, _ = self._COLOR_MODES[mode] # generate steps from mode and colors: usually each color set by the user generates # one step, where it is specified to all leds and the device handles the animation; # but in super mode there is a single step and each color directly controls a led mod3 += map_direction(direction, 0, 0x10) if 'super' in mode: steps = [list(itertools.chain(*colors))] else: steps = [color*40 for color in colors] for i, leds in enumerate(steps): seq = i << 5 byte4 = sval | seq | mod4 self._write([0x2, 0x4b, mval, mod3, byte4] + leds[0:57]) self._write([0x3] + leds[57:]) def _write_fixed_duty(self, cid, duty): self._write([0x2, 0x4d, cid, 0, duty]) class SmartDevice2(_BaseSmartDevice): """NZXT HUE 2 lighting and, optionally, fan controller.""" # support for hwmon: nzxt-smart2, Linux 5.17 _MATCHES = [ (0x1e71, 0x2006, 'NZXT Smart Device V2', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x200d, 'NZXT Smart Device V2', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x200f, 'NZXT Smart Device V2', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x2001, 'NZXT HUE 2', { 'speed_channel_count': 0, 'color_channel_count': 4 }), (0x1e71, 0x2002, 'NZXT HUE 2 Ambient', { 'speed_channel_count': 0, 'color_channel_count': 2 }), (0x1e71, 0x2009, 'NZXT RGB & Fan Controller', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x200e, 'NZXT RGB & Fan Controller', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x2010, 'NZXT RGB & Fan Controller', { 'speed_channel_count': 3, 'color_channel_count': 2 }), (0x1e71, 0x2011, 'NZXT RGB & Fan Controller (3+6 channels)', { 'speed_channel_count': 3, 'color_channel_count': 0 # protocol changed, see #541 }), (0x1e71, 0x2019, 'NZXT RGB & Fan Controller (3+6 channels)', { 'speed_channel_count': 3, 'color_channel_count': 0 # protocol changed, see #541 }), (0x1e71, 0x201f, 'NZXT RGB & Fan Controller (3+6 channels)', { 'speed_channel_count': 3, 'color_channel_count': 0 # protocol changed, see #541 }), (0x1e71, 0x2020, 'NZXT RGB & Fan Controller (3+6 channels)', { 'speed_channel_count': 3, 'color_channel_count': 6 }), ] _MAX_READ_ATTEMPTS = 12 _READ_LENGTH = 64 _WRITE_LENGTH = 64 _COLOR_MODES = { # (mode, size/variant, moving, min colors, max colors) 'off': (0x00, 0x00, 0x00, 0, 0), 'fixed': (0x00, 0x00, 0x00, 1, 1), 'super-fixed': (0x01, 0x00, 0x00, 1, 40), # independent leds 'fading': (0x01, 0x00, 0x00, 1, 8), 'spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'marquee-3': (0x03, 0x00, 0x00, 1, 1), 'marquee-4': (0x03, 0x01, 0x00, 1, 1), 'marquee-5': (0x03, 0x02, 0x00, 1, 1), 'marquee-6': (0x03, 0x03, 0x00, 1, 1), 'covering-marquee': (0x04, 0x00, 0x00, 1, 8), 'alternating-3': (0x05, 0x00, 0x00, 2, 2), 'alternating-4': (0x05, 0x01, 0x00, 2, 2), 'alternating-5': (0x05, 0x02, 0x00, 2, 2), 'alternating-6': (0x05, 0x03, 0x00, 2, 2), 'moving-alternating-3': (0x05, 0x00, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-4': (0x05, 0x01, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-5': (0x05, 0x02, 0x10, 2, 2), # byte4: 0x10 = moving 'moving-alternating-6': (0x05, 0x03, 0x10, 2, 2), # byte4: 0x10 = moving 'pulse': (0x06, 0x00, 0x00, 1, 8), 'breathing': (0x07, 0x00, 0x00, 1, 8), # colors for each step 'super-breathing': (0x03, 0x19, 0x00, 1, 40), # independent leds 'candle': (0x08, 0x00, 0x00, 1, 1), 'starry-night': (0x09, 0x00, 0x00, 1, 1), 'rainbow-flow': (0x0b, 0x00, 0x00, 0, 0), 'super-rainbow': (0x0c, 0x00, 0x00, 0, 0), 'rainbow-pulse': (0x0d, 0x00, 0x00, 0, 0), 'wings': (None, 0x00, 0x00, 1, 1), # wings requires special handling # deprecated in favor of direction=backward 'backwards-spectrum-wave': (0x02, 0x00, 0x00, 0, 0), 'backwards-marquee-3': (0x03, 0x00, 0x00, 1, 1), 'backwards-marquee-4': (0x03, 0x01, 0x00, 1, 1), 'backwards-marquee-5': (0x03, 0x02, 0x00, 1, 1), 'backwards-marquee-6': (0x03, 0x03, 0x00, 1, 1), 'covering-backwards-marquee': (0x04, 0x00, 0x00, 1, 8), 'backwards-moving-alternating-3': (0x05, 0x00, 0x01, 2, 2), 'backwards-moving-alternating-4': (0x05, 0x01, 0x01, 2, 2), 'backwards-moving-alternating-5': (0x05, 0x02, 0x01, 2, 2), 'backwards-moving-alternating-6': (0x05, 0x03, 0x01, 2, 2), 'backwards-rainbow-flow': (0x0b, 0x00, 0x00, 0, 0), 'backwards-super-rainbow': (0x0c, 0x00, 0x00, 0, 0), 'backwards-rainbow-pulse': (0x0d, 0x00, 0x00, 0, 0), } def __init__(self, device, description, speed_channel_count, color_channel_count, **kwargs): """Instantiate a driver with a device handle.""" speed_channels = {f'fan{i + 1}': (i, _MIN_DUTY, _MAX_DUTY) for i in range(speed_channel_count)} color_channels = {f'led{i + 1}': (1 << i) for i in range(color_channel_count)} if color_channels: color_channels['sync'] = (1 << color_channel_count) - 1 super().__init__(device, description, speed_channels, color_channels, **kwargs) def initialize(self, direct_access=False, **kwargs): """Initialize the device and the driver. Connected fans and LED accessories are detected. This method should be called every time the systems boots, resumes from a suspended state, or if the device has just been (re)connected. In those scenarios, no other method, except `connect()` or `disconnect()`, should be called until the device and driver has been (re-)initialized. Returns None or a list of `(property, value, unit)` tuples, similarly to `get_status()`. """ self.device.clear_enqueued_reports() # if fan controller, initialize fan reporting (#331) if self._speed_channels: if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, assuming it is already initialized', self._hwmon.driver) else: if self._hwmon: _LOGGER.warning('forcing re-initialization despite %s kernel driver', self._hwmon.driver) update_interval = (lambda secs: 1 + round((secs - .5) / .25))(.5) # see issue #128 self._write([0x60, 0x02, 0x01, 0xe8, update_interval, 0x01, 0xe8, update_interval]) self._write([0x60, 0x03]) # request static infos self._write([0x10, 0x01]) # firmware info self._write([0x20, 0x03]) # lighting info ret = [] def parse_firm_info(msg): fw = f'{msg[0x11]}.{msg[0x12]}.{msg[0x13]}' ret.append(('Firmware version', fw, '')) def parse_led_info(msg): channel_count = msg[14] offset = 15 # offset of first channel/first accessory for c in range(channel_count): for a in range(HUE2_MAX_ACCESSORIES_IN_CHANNEL): accessory_id = msg[offset + c * HUE2_MAX_ACCESSORIES_IN_CHANNEL + a] if accessory_id == 0: break ret.append((f'LED {c + 1} accessory {a + 1}', Hue2Accessory(accessory_id), '')) parsers = {b'\x11\x01': parse_firm_info} if self._color_channels: parsers[b'\x21\x03'] = parse_led_info self._read_until(parsers) return sorted(ret) def _get_status_directly(self): ret = [] def parse_fan_info(msg): mode_offset = 16 rpm_offset = 24 duty_offset = 40 noise_offset = 56 raw_modes = [None, 'DC', 'PWM'] for i, _ in enumerate(self._speed_channels): mode = raw_modes[msg[mode_offset + i]] ret.append((f'Fan {i + 1} speed', msg[rpm_offset + 1] << 8 | msg[rpm_offset], 'rpm')) ret.append((f'Fan {i + 1} duty', msg[duty_offset + i], '%')) ret.append((f'Fan {i + 1} control mode', mode, '')) rpm_offset += 2 ret.append(('Noise level', msg[noise_offset], 'dB')) self.device.clear_enqueued_reports() self._read_until({b'\x67\x02': parse_fan_info}) return sorted(ret) def _get_status_from_hwmon(self): ret = [] modes = ['DC', 'PWM'] for n in range(1, len(self._speed_channels) + 1): ret.append((f'Fan {n} speed', self._hwmon.read_int(f'fan{n}_input'), 'rpm')), ret.append((f'Fan {n} duty', self._hwmon.read_int(f'pwm{n}') * 100. / 255, '%')), ret.append((f'Fan {n} control mode', modes[self._hwmon.read_int(f'pwm{n}_mode')], '')), # noise level is not available through hwmon, but also not very accurate or useful return sorted(ret) def get_status(self, direct_access=False, **kwargs): """Get a status report. Returns a list of `(property, value, unit)` tuples. """ if not self._speed_channels: return [] if self._hwmon and not direct_access: _LOGGER.info('bound to %s kernel driver, reading status from hwmon', self._hwmon.driver) return self._get_status_from_hwmon() if self._hwmon: _LOGGER.warning('directly reading the status despite %s kernel driver', self._hwmon.driver) return self._get_status_directly() def _read_until(self, parsers): for _ in range(self._MAX_READ_ATTEMPTS): msg = self.device.read(self._READ_LENGTH) prefix = bytes(msg[0:2]) func = parsers.pop(prefix, None) if func: func(msg) if not parsers: return assert False, f'missing messages (attempts={self._MAX_READ_ATTEMPTS}, missing={len(parsers)})' def _write_colors(self, cid, mode, colors, sval, direction='forward',): mval, mod3, mod4, mincolors, maxcolors = self._COLOR_MODES[mode] assert self._color_channels, "color channels should be available and enabled" color_count = len(colors) if maxcolors == 40: led_padding = [0x00, 0x00, 0x00]*(maxcolors - color_count) # turn off remaining LEDs leds = list(itertools.chain(*colors)) + led_padding self._write([0x22, 0x10, cid, 0x00] + leds[0:60]) # send first 20 colors to device (3 bytes per color) self._write([0x22, 0x11, cid, 0x00] + leds[60:]) # send remaining colors to device self._write([0x22, 0xa0, cid, 0x00, mval, mod3, 0x00, 0x00, 0x00, 0x00, 0x64, 0x00, 0x32, 0x00, 0x00, 0x01]) elif mode == 'wings': # wings requires special handling for [g, r, b] in colors: self._write([0x22, 0x10, cid]) # clear out all independent LEDs self._write([0x22, 0x11, cid]) # clear out all independent LEDs color_lists = [] * 3 color_lists[0] = [g, r, b] * 8 color_lists[1] = [int(x // 2.5) for x in color_lists[0]] color_lists[2] = [int(x // 4) for x in color_lists[1]] for i in range(8): # send color scheme first, before enabling wings mode mod = 0x05 if i in [3, 7] else 0x01 msg = ([0x22, 0x20, cid, i, 0x04, 0x39, 0x00, mod, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x05, 0x85, 0x05, 0x85, 0x05, 0x85, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) self._write(msg + color_lists[i % 4]) self._write([0x22, 0x03, cid, 0x08]) # this actually enables wings mode else: byte7 = (mod4 & 0x10) >> 4 # sets 'moving' flag for moving alternating modes byte8 = map_direction(direction, 0, 1) # sets 'backward' flag byte9 = mod3 if mval == 0x03 else color_count # specifies 'marquee' LED size byte10 = mod3 if mval == 0x05 else 0x00 # specifies LED size for 'alternating' modes header = [0x28, 0x03, cid, 0x00, mval, sval, byte7, byte8, byte9, byte10] self._write(header + list(itertools.chain(*colors))) def _write_fixed_duty(self, cid, duty): msg = [0x62, 0x01, 0x01 << cid, 0x00, 0x00, 0x00] # fan channel passed as bitflag in last 3 bits of 3rd byte msg[cid + 3] = duty # duty percent in 4th, 5th, and 6th bytes for, respectively, fan1, fan2 and fan3 self._write(msg) class H1V2(SmartDevice2): _MATCHES = [ (0x1e71, 0x2015, 'NZXT H1 V2', { 'speed_channel_count': 2, 'color_channel_count': 0 }), ] def get_status(self, direct_access=False, **kwargs): ret = [] def parse_fan_info(msg): mode_offset = 21 rpm_offset = 24 duty_offset = 25 pump_offset = 18 raw_modes = [None, 'DC', 'PWM'] for i, _ in enumerate(self._speed_channels): mode = raw_modes[msg[mode_offset + i]] ret.append((f'Fan {i + 1} speed', msg[rpm_offset] << 8 | msg[rpm_offset - 1], 'rpm')) ret.append((f'Fan {i + 1} duty', msg[duty_offset], '%')) ret.append((f'Fan {i + 1} control mode', mode, '')) rpm_offset += 5 duty_offset += 5 ret.append(('Pump speed', msg[pump_offset] << 8 | msg[pump_offset - 1], 'rpm')) # parse fans and pump status self.device.clear_enqueued_reports() self._read_until({b'\x75\x02': parse_fan_info}) return sorted(ret) # backward compatibility NzxtSmartDeviceDriver = SmartDevice SmartDeviceDriver = SmartDevice SmartDeviceV2Driver = SmartDevice2 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728751175.0 liquidctl-1.15.0/liquidctl/driver/smbus.py0000644000175000017500000003030314702523107017626 0ustar00jonasjonas"""Base SMBus bus and driver APIs. For now, these are unstable APIs, and only Linux is supported. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import os import sys from collections import namedtuple from pathlib import Path from liquidctl.driver.base import BaseDriver, BaseBus, find_all_subclasses from liquidctl.util import check_unsafe, LazyHexRepr _LOGGER = logging.getLogger(__name__) if sys.platform == 'linux': # WARNING: the tests rely on being able to override which SMBus # implementation is used here; this is done through the SMBus attribute # created below, do not move/replace/change it, nor access it during module # initialization from smbus import SMBus LinuxEeprom = namedtuple('LinuxEeprom', 'name data') class LinuxI2c(BaseBus): """The Linux I²C (`/sys/bus/i2c`) bus.""" def __init__(self, i2c_root='/sys/bus/i2c'): self._i2c_root = Path(i2c_root) def find_devices(self, bus=None, usb_port=None, **kwargs): """Find compatible SMBus devices.""" if usb_port: # a usb_port filter implies an USB bus return devices = self._i2c_root.joinpath('devices') if not devices.exists(): _LOGGER.debug('skipping %s, %s not available', self.__class__.__name__, devices) return drivers = sorted(find_all_subclasses(SmbusDriver), key=lambda x: x.__name__) _LOGGER.debug('searching %s', self.__class__.__name__) _LOGGER.debug( '%s drivers: %s', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers)) ) for i2c_dev in devices.iterdir(): try: i2c_bus = LinuxI2cBus(i2c_dev) except ValueError as err: _LOGGER.debug('I²C adapter: %s skipped: %s', i2c_dev.name, err) continue if bus and bus != i2c_bus.name: continue _LOGGER.debug('I²C adapter: %s (%s)', i2c_bus.name, i2c_bus.description or "N/A") yield from i2c_bus.find_devices(drivers, **kwargs) class LinuxI2cBus: """A Linux I²C device, which is itself an I²C bus. Should not be instantiated directly; use `LinuxI2c.find_devices` instead. This type mimics the `smbus.SMBus` read/write/close APIs. However, `open` does not take any parameters, and not all APIs are available. """ # note: this is not a liquidctl BaseBus, as that would cause # find_liquidctl_devices to try to directly instantiate it def __init__(self, i2c_dev): if not i2c_dev.name.startswith('i2c-'): raise ValueError('not a bus or unsupported adapter') self._i2c_dev = i2c_dev self._smbus = None self._number = int(i2c_dev.name[4:]) def find_devices(self, drivers, **kwargs): """Probe drivers and find compatible devices in this bus.""" for drv in drivers: yield from drv.probe(self, **kwargs) def open(self): """Open the I²C bus.""" if not self._smbus: try: self._smbus = SMBus(self._number) except FileNotFoundError: if Path('/sys/class/i2c-dev').exists(): raise raise OSError('kernel module i2c-dev not loaded') from None def read_byte(self, address): """Read a single byte from a device.""" value = self._smbus.read_byte(address) _LOGGER.debug('read byte @ 0x%02x:0x%02x', address, value) return value def read_byte_data(self, address, register): """Read a single byte from a designated register.""" value = self._smbus.read_byte_data(address, register) _LOGGER.debug('read byte data @ 0x%02x:0x%02x 0x%02x', address, register, value) return value def read_word_data(self, address, register): """Read a single 2-byte word from a given register.""" value = self._smbus.read_word_data(address, register) _LOGGER.debug('read word data @ 0x%02x:0x%02x 0x%02x', address, register, value) return value def read_block_data(self, address, register): """Read a block of up to 32 bytes from a given register.""" data = self._smbus.read_block_data(address, register) _LOGGER.debug('read block data @ 0x%02x:0x%02x: %r', address, register, LazyHexRepr(data)) return data def write_byte(self, address, value): """Write a single byte to a device.""" _LOGGER.debug('writing byte @ 0x%02x: 0x%02x', address, value) return self._smbus.write_byte(address, value) def write_byte_data(self, address, register, value): """Write a single byte to a designated register.""" _LOGGER.debug('writing byte data @ 0x%02x:0x%02x 0x%02x', address, register, value) return self._smbus.write_byte_data(address, register, value) def write_word_data(self, address, register, value): """Write a single 2-byte word to a designated register.""" _LOGGER.debug('writing word data @ 0x%02x:0x%02x 0x%02x', address, register, value) return self._smbus.write_word_data(address, register, value) def write_block_data(self, address, register, data): """Write a block of byte data to a given register.""" _LOGGER.debug('writing block data @ 0x%02x:0x%02x %r', address, register, LazyHexRepr(data)) return self._smbus.write_block_data(address, register, data) def close(self): """Close the I²C connection.""" if self._smbus: self._smbus.close() self._smbus = None def load_eeprom(self, address): """Return EEPROM name and data in `address`, or None if N/A.""" # uses kernel facilities to avoid directly reading from the EEPROM # or managing its pages, also avoiding the need for unsafe=smbus dev = f'{self._number}-{address:04x}' try: name = self._i2c_dev.joinpath(dev, 'name').read_text().strip() eeprom = self._i2c_dev.joinpath(dev, 'eeprom').read_bytes() return LinuxEeprom(name, eeprom) except FileNotFoundError: return None except OSError as err: _LOGGER.debug('%s exists but not readable: %r', self._i2c_dev.joinpath(dev, 'eeprom'), err) return None @property def name(self): return self._i2c_dev.name @property def description(self): return self._try_sysfs_read('name') @property def parent_vendor(self): return self._try_sysfs_read_hex('device/vendor') @property def parent_device(self): return self._try_sysfs_read_hex('device/device') @property def parent_subsystem_vendor(self): return self._try_sysfs_read_hex('device/subsystem_vendor') @property def parent_subsystem_device(self): return self._try_sysfs_read_hex('device/subsystem_device') @property def parent_driver(self): try: return Path(os.readlink(self._i2c_dev.joinpath('device/driver'))).name except FileNotFoundError: return None def __str__(self): if self.description: return f'{self.name}: {self.description}' return self.name def __repr__(self): def hexid(maybe): if maybe is not None: return f'{maybe:#06x}' return 'None' return f'{self.__class__.__name__}: name: {self.name!r}, description:' \ f' {self.description!r}, parent_vendor: {hexid(self.parent_vendor)},' \ f' parent_device: {hexid(self.parent_device)}, parent_subsystem_vendor:' \ f' {hexid(self.parent_subsystem_vendor)},' \ f' parent_subsystem_device: {hexid(self.parent_subsystem_device)},' \ f' parent_driver: {self.parent_driver!r}' def _try_sysfs_read(self, *sub, default=None): try: return self._i2c_dev.joinpath(*sub).read_text().rstrip() except FileNotFoundError: return default def _try_sysfs_read_hex(self, *sub, default=None): try: return int(self._i2c_dev.joinpath(*sub).read_text(), base=16) except FileNotFoundError: return default class SmbusDriver(BaseDriver): """Base driver class for SMBus devices.""" @classmethod def probe(cls, smbus, **kwargs): raise NotImplementedError() @classmethod def find_supported_devices(cls, root_bus=None, **kwargs): """Find devices specifically compatible with this driver.""" if sys.platform != 'linux': return [] if not root_bus: root_bus = LinuxI2c() devs = filter(lambda x: isinstance(x, cls), root_bus.find_devices(**kwargs)) return list(devs) def __init__(self, smbus, description, vendor_id=None, product_id=None, address=None, **kwargs): # note: vendor_id and product_id are liquidctl properties intended to # allow the user to differentiate and ultimately filter devices; in the # context of SMBus, drivers may choose to use the parent's PCI # **subsystem** vendor/device IDs for this task, as those are more # specific and closer to the product the user purchased than the less # specific PCI vendor/device IDs. assert address is not None self._smbus = smbus self._description = description self._vendor_id = vendor_id self._product_id = product_id self._address = address def connect(self, **kwargs): """Connect to the device.""" if check_unsafe('smbus', **kwargs): self._smbus.open() else: # do not raise: some driver APIs may not access the bus after all, # and allowing the device to pseudo-connect is convenient given the # current API structure; APIs that do access the bus should check # for the 'smbus' feature themselves and, if necessary, raise # UnsafeFeaturesNotEnabled(requirements) # (see also: check_unsafe(..., error=True)) _LOGGER.debug("SMBus disabled, missing unsafe feature 'smbus'") return self def disconnect(self, **kwargs): """Disconnect from the device.""" self._smbus.close() @property def description(self): """Human readable description of the corresponding device.""" return self._description @property def vendor_id(self): """Numeric vendor identifier, or None if N/A.""" return self._vendor_id @property def product_id(self): """Numeric product identifier, or None if N/A.""" return self._product_id @property def release_number(self): """Device versioning number, or None if N/A. In USB devices this is bcdDevice. """ return None @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" return None @property def bus(self): """Bus the device is connected to, or None if N/A.""" return self._smbus.name @property def address(self): """Address of the device on the corresponding bus, or None if N/A.""" return f'{self._address:#04x}' @property def port(self): """Physical location of the device, or None if N/A.""" return None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1734380417.0 liquidctl-1.15.0/liquidctl/driver/usb.py0000644000175000017500000005470714730105601017300 0ustar00jonasjonas"""Base USB bus, driver and device APIs. This modules provides abstractions over several platform and implementation differences. As such, there is a lot of boilerplate here, but callers should be able to disregard almost everything and simply work on the UsbDriver/ UsbHidDriver level. BaseUsbDriver └── device: PyUsbDevice ├── uses PyUSB └── backed by (in order of priority) ├── libusb-1.0 ├── libusb-0.1 └── OpenUSB UsbHidDriver ├── extends: BaseUsbDriver └── device: HidapiDevice ├── uses hidapi └── backed by ├── hid.dll on Windows ├── hidraw on Linux if it was enabled during the build of hidapi ├── IOHidManager on MacOS └── libusb-1.0 on all other cases UsbDriver ├── extends: BaseUsbDriver └── allows to differentiate between UsbHidDriver and (non HID) UsbDriver UsbDriver and UsbHidDriver are meant to be used as base classes to the actual device drivers. The users of those drivers generally do not care about read, write or other low level operations; thus, these low level operations are placed in .device. However, there still are legitimate reasons as to why someone would want to directly access the lower layers (device wrapper level, device implementation level, or lower). We do not hide or mark those references as private, but good judgement should be exercised when calling anything within .device. The USB drivers are organized into two buses. The recommended way to initialize and bind drivers is through their respective buses, though .find_supported_devices can also be useful in certain scenarios. HidapiBus └── drivers: all (recursive) subclasses of UsbHidDriver PyUsbBus └── drivers: all (recursive) subclasses of UsbDriver The subclass constructor can generally be kept unaware of the implementation details of the device parameter, and find_supported_devices already accepts keyword arguments and forwards them to the driver constructor. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import sys import usb from usb.core import USBTimeoutError try: # The hidapi package, depending on how it's compiled, exposes one or two # top level modules: hid and, optionally, hidraw. When both are available, # hid will be a libusb-based fallback implementation, and we prefer hidraw. import hidraw as hid except ModuleNotFoundError: import hid try: import libusb_package except ModuleNotFoundError: libusb_package = None from liquidctl.driver.base import BaseDriver, BaseBus, find_all_subclasses from liquidctl.driver.hwmon import HwmonDevice from liquidctl.error import Timeout from liquidctl.util import LazyHexRepr # During initialization, SmartDevice2 devices take ~2.5 seconds to reply with # the lighting info (#526). This is the slowest response time we're aware off, # so set the timeout to double that value. _DEFAULT_TIMEOUT_MS = 5000 _LOGGER = logging.getLogger(__name__) class BaseUsbDriver(BaseDriver): """Base driver class for generic USB devices. Each driver should provide its own list of _MATCHES, as well as implementations for all methods applicable to the devices is supports. _MATCHES should consist of a list of (vendor id, product id, description, and extra kwargs) tuples. find_supported_devices will pass these extra kwargs, as well as any it receives, to the constructor. """ _MATCHES = [] @classmethod def probe(cls, handle, vendor=None, product=None, release=None, serial=None, match=None, **kwargs): """Probe `handle` and yield corresponding driver instances.""" for vid, pid, desc, devargs in cls._MATCHES: if (vendor and vendor != vid) or handle.vendor_id != vid: continue if (product and product != pid) or handle.product_id != pid: continue if release and handle.release_number != release: continue if serial and handle.serial_number != serial: continue if match and match.lower() not in desc.lower(): continue consargs = devargs.copy() consargs.update(kwargs) dev = cls(handle, desc, **consargs) _LOGGER.debug('%s identified: %s', cls.__name__, desc) yield dev def __init__(self, device, description, **kwargs): self.device = device self._description = description def connect(self, **kwargs): """Connect to the device.""" self.device.open() return self def disconnect(self, **kwargs): """Disconnect from the device.""" self.device.close() @property def description(self): """Human readable description of the corresponding device.""" return self._description @property def vendor_id(self): """16-bit numeric vendor identifier.""" return self.device.vendor_id @property def product_id(self): """16-bit umeric product identifier.""" return self.device.product_id @property def release_number(self): """16-bit BCD device versioning number.""" return self.device.release_number @property def serial_number(self): """Serial number reported by the device, or None if N/A.""" return self.device.serial_number @property def bus(self): """Bus the device is connected to, or None if N/A.""" return self.device.bus @property def address(self): """Address of the device on the corresponding bus, or None if N/A. Dependendent on bus enumeration order. """ return self.device.address @property def port(self): """Physical location of the device, or None if N/A. Tuple of USB port numbers, from the root hub to this device. Only available for PyUSB-backed devices, and only when using LibUSB 1.0. But has the advantage of not depending on the bus enumeration order. """ return self.device.port class UsbHidDriver(BaseUsbDriver): """Base driver class for USB Human Interface Devices (HIDs).""" @classmethod def find_supported_devices(cls, **kwargs): """Find devices specifically compatible with this driver.""" devs = [] for vid, pid, _, _ in cls._MATCHES: for dev in HidapiBus().find_devices(vendor=vid, product=pid, **kwargs): if type(dev) == cls: devs.append(dev) return devs def __init__(self, device, description, **kwargs): # compatibility with v1.1.0 drivers, which could be directly # instantiated with a usb.core.Device if isinstance(device, usb.core.Device): clname = self.__class__.__name__ _LOGGER.warning('constructing a %s instance from a usb.core.Device has been deprecated, ' 'use %s.find_supported_devices() or pass a HidapiDevice handle', clname, clname) usbdev = device hidinfo = next(info for info in hid.enumerate(usbdev.idVendor, usbdev.idProduct) if info['serial_number'] == usbdev.serial_number) assert hidinfo, 'Could not find device in HID bus' device = HidapiDevice(hid, hidinfo) super().__init__(device, description, **kwargs) self._hwmon = HwmonDevice.from_hidraw(device.path) if self._hwmon: _LOGGER.debug('has kernel driver: %s (%s)', self._hwmon.driver, self._hwmon.path) class UsbDriver(BaseUsbDriver): """Base driver class for regular USB devices. Specifically, regular USB devices are *not* Human Interface Devices (HIDs). """ @classmethod def find_supported_devices(cls, **kwargs): """Find devices specifically compatible with this driver.""" devs = [] for vid, pid, _, _ in cls._MATCHES: for dev in PyUsbBus().find_devices(vendor=vid, product=pid, **kwargs): if type(dev) == cls: devs.append(dev) return devs class PyUsbDevice: """"A PyUSB backed device. PyUSB will automatically pick the first available backend (at runtime). The supported backends are: - libusb-1.0 - libusb-0.1 - OpenUSB """ def __init__(self, usbdev, bInterfaceNumber=None): self.api = usb self.usbdev = usbdev self.bInterfaceNumber = bInterfaceNumber self._attached = False def _select_interface(self, cfg): return self.bInterfaceNumber or 0 def open(self, bInterfaceNumber=0): """Connect to the device. Ensure the device is configured and replace the kernel kernel on the selected interface, if necessary. """ # we assume the device is already configured, there is only one # configuration, or the first one is desired try: cfg = self.usbdev.get_active_configuration() except usb.core.USBError as err: if err.args[0] == 'Configuration not set': _LOGGER.debug('setting the (first) configuration') self.usbdev.set_configuration() # FIXME device or handle might not be ready for use yet cfg = self.usbdev.get_active_configuration() else: raise self.bInterfaceNumber = self._select_interface(cfg) _LOGGER.debug('selected interface: %d', self.bInterfaceNumber) if (sys.platform.startswith('linux') and self.usbdev.is_kernel_driver_active(self.bInterfaceNumber)): _LOGGER.debug('replacing stock kernel driver with libusb') self.usbdev.detach_kernel_driver(self.bInterfaceNumber) self._attached = True def claim(self): """Explicitly claim the device from other programs.""" _LOGGER.debug('explicitly claim interface') usb.util.claim_interface(self.usbdev, self.bInterfaceNumber) def release(self): """Release the device to other programs.""" if sys.platform == 'win32': # on Windows we need to release the entire device for other # programs to be able to access it _LOGGER.debug('explicitly release device') usb.util.dispose_resources(self.usbdev) else: # on Linux, and possibly on Mac and BSDs, releasing the specific # interface is enough _LOGGER.debug('explicitly release interface') usb.util.release_interface(self.usbdev, self.bInterfaceNumber) def close(self): """Disconnect from the device. Clean up and (Linux only) reattach the kernel driver. """ self.release() if self._attached: _LOGGER.debug('restoring stock kernel driver') self.usbdev.attach_kernel_driver(self.bInterfaceNumber) self._attached = False def read(self, endpoint, length, *, timeout=_DEFAULT_TIMEOUT_MS): """Read from endpoint.""" try: data = self.usbdev.read(endpoint, length, timeout=timeout) except USBTimeoutError: _LOGGER.debug('failed to read, timed out after %d ms', timeout) raise Timeout() _LOGGER.debug('read %d bytes: %r', len(data), LazyHexRepr(data)) return data def write(self, endpoint, data, *, timeout=_DEFAULT_TIMEOUT_MS): """Write to endpoint.""" _LOGGER.debug('writing %d bytes: %r', len(data), LazyHexRepr(data)) try: return self.usbdev.write(endpoint, data, timeout=timeout) except USBTimeoutError: _LOGGER.debug('write failed, timed out after %d ms', timeout) raise Timeout() def ctrl_transfer(self, *args, timeout=_DEFAULT_TIMEOUT_MS, **kwargs): """Submit a contrl transfer.""" _LOGGER.debug('sending control transfer with %r, %r', args, kwargs) try: return self.usbdev.ctrl_transfer(*args, **kwargs) except USBTimeoutError: _LOGGER.debug('control transfers failed, timed out after %d ms', timeout) raise Timeout() @classmethod def enumerate(cls, vid=None, pid=None): args = {} if vid: args['idVendor'] = vid if pid: args['idProduct'] = pid if libusb_package and (sys.platform == 'win32' or sys.platform == 'cygwin'): _LOGGER.debug('using libusb_package.find') find = libusb_package.find else: find = usb.core.find for handle in find(find_all=True, **args): yield cls(handle) @property def vendor_id(self): return self.usbdev.idVendor @property def product_id(self): return self.usbdev.idProduct @property def release_number(self): return self.usbdev.bcdDevice @property def serial_number(self): return self.usbdev.serial_number @property def bus(self): return f'usb{self.usbdev.bus}' # follow Linux model @property def address(self): return self.usbdev.address @property def port(self): return self.usbdev.port_numbers def __eq__(self, other): return type(self) == type(other) and self.bus == other.bus and self.address == other.address class HidapiDevice: """A hidapi backed device. Depending on the platform, the selected `hidapi` and how it was built, this might use any of the following backends: - hid.dll on Windows - hidraw on Linux, if it was enabled during the build of hidapi - IOHidManager on MacOS - libusb-1.0 on all other cases The default hidapi API is the module 'hid'. On standard Linux builds of the hidapi package, this might default to a libusb-1.0 backed implementation; at the same time an alternate 'hidraw' module may also be provided. The latter is prefered, when available. Note: if a libusb-backed 'hid' is used on Linux (assuming default build options) it will detach the kernel driver, making hidraw and hwmon unavailable for that device. To fix, rebind the device to usbhid with: echo '-:1.0' | sudo tee /sys/bus/usb/drivers/usbhid/bind """ def __init__(self, hidapi, hidapi_dev_info): self.api = hidapi self.hidinfo = hidapi_dev_info self.hiddev = self.api.device() def open(self): """Connect to the device.""" self.hiddev.open_path(self.hidinfo['path']) def close(self): """NOOP.""" self.hiddev.close() def clear_enqueued_reports(self): """Clear already enqueued incoming reports. The OS generally enqueues incomming reports for open HIDs, and hidapi emulates this when running on top of libusb. On Linux, up to 64 reports can be enqueued. This method quickly reads and discards any already enqueued reports, and is useful when later reads are not expected to return stale data. """ if self.hiddev.set_nonblocking(True) == 0: timeout_ms = 0 # use hid_read; wont block because call succeeded else: timeout_ms = 1 # smallest timeout forwarded to hid_read_timeout discarded = 0 while self.hiddev.read(max_length=1, timeout_ms=timeout_ms): discarded += 1 _LOGGER.debug('discarded %d previously enqueued reports', discarded) def read(self, length, *, timeout=_DEFAULT_TIMEOUT_MS): """Read raw report from HID. The returned data follows the semantics of the Linux HIDRAW API. > On a device which uses numbered reports, the first byte of the > returned data will be the report number; the report data follows, > beginning in the second byte. For devices which do not use numbered > reports, the report data will begin at the first byte. Unlike the underlying cython-hidapi API this method wraps, pass `timeout=None` to disable the default timeout. """ self.hiddev.set_nonblocking(False) if timeout is None: timeout = 0 # cython-hidapi uses 0 for no timeout elif timeout == 0: timeout = 1 # smallest timeout forwarded to hid_read_timeout data = self.hiddev.read(max_length=length, timeout_ms=timeout) if timeout and not data: _LOGGER.debug('failed to read, timed out after %d ms', timeout) raise Timeout() _LOGGER.debug('read %d bytes: %r', len(data), LazyHexRepr(data)) return data def write(self, data): """Write raw report to HID. The buffer should follow the semantics of the Linux HIDRAW API. > The first byte of the buffer passed to write() should be set to the > report number. If the device does not use numbered reports, the > first byte should be set to 0. The report data itself should begin > at the second byte. """ _LOGGER.debug('writing report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) res = self.hiddev.write(data) if res < 0: raise OSError('Could not write to device') if res != len(data): _LOGGER.debug('wrote %d total bytes, expected %d', res, len(data)) return res def get_feature_report(self, report_id, length): """Get feature report that matches `report_id` from HID. If the device does not use numbered reports, set `report_id` to 0. Unlike `read`, the returned data follows semantics similar to `write` and `send_feature_report`: the first byte will always contain the report ID (or 0), and the report data itself will being at the second byte. """ data = self.hiddev.get_feature_report(report_id, length) _LOGGER.debug('got feature report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) return data def send_feature_report(self, data): """Send feature report to HID. The buffer should follow the semantics of `write`. > The first byte of the buffer passed to write() should be set to the > report number. If the device does not use numbered reports, the > first byte should be set to 0. The report data itself should begin > at the second byte. """ _LOGGER.debug('sending feature report 0x%02x with %d bytes: %r', data[0], len(data) - 1, LazyHexRepr(data, start=1)) res = self.hiddev.send_feature_report(data) if res < 0: raise OSError('Could not send feature report to device') if res != len(data): _LOGGER.debug('sent %d total bytes, expected %d', res, len(data)) return res @classmethod def enumerate(cls, api, vid=None, pid=None): infos = api.enumerate(vid or 0, pid or 0) if sys.platform == 'darwin': infos = sorted(infos, key=lambda info: info['path']) for info in infos: yield cls(api, info) @property def path(self): return self.hidinfo['path'] @property def vendor_id(self): return self.hidinfo['vendor_id'] @property def product_id(self): return self.hidinfo['product_id'] @property def release_number(self): return self.hidinfo['release_number'] @property def serial_number(self): return self.hidinfo['serial_number'] @property def bus(self): return 'hid' # follow Linux model @property def address(self): return self.hidinfo['path'].decode(errors='replace') @property def port(self): return None def __eq__(self, other): return type(self) == type(other) and self.bus == other.bus and self.address == other.address class HidapiBus(BaseBus): def find_devices(self, vendor=None, product=None, bus=None, address=None, usb_port=None, **kwargs): """Find compatible HID devices.""" handles = HidapiDevice.enumerate(hid, vendor, product) drivers = sorted(find_all_subclasses(UsbHidDriver), key=lambda x: x.__name__) _LOGGER.debug('searching %s', self.__class__.__name__) _LOGGER.debug( '%s drivers: %s', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers)) ) for handle in handles: if bus and handle.bus != bus: continue if address and handle.address != address: continue if usb_port and handle.port != usb_port: continue # each handle is a HIDAPI hid_device, and that can either mean one # entire HID interface, or one interface ⨯ usage page ⨯ usage id # product, depending on the platform and backend; but, for brevity, # refer them simply as "HID devices" if 'usage' in handle.hidinfo and 'usage_page' in handle.hidinfo: _LOGGER.debug( 'HID device: %04x:%04x (usage_page=%#06x usage=%#06x)', handle.vendor_id, handle.product_id, handle.hidinfo['usage_page'], handle.hidinfo['usage'], ) else: _LOGGER.debug( 'HID device: %04x:%04x (usage n/a)', handle.vendor_id, handle.product_id, ) for drv in drivers: yield from drv.probe(handle, vendor=vendor, product=product, **kwargs) class PyUsbBus(BaseBus): def find_devices(self, vendor=None, product=None, bus=None, address=None, usb_port=None, **kwargs): """ Find compatible regular USB devices.""" drivers = sorted(find_all_subclasses(UsbDriver), key=lambda x: x.__name__) _LOGGER.debug('searching %s', self.__class__.__name__) _LOGGER.debug( '%s drivers: %s', self.__class__.__name__, ', '.join(map(lambda x: x.__name__, drivers)) ) for handle in PyUsbDevice.enumerate(vendor, product): if bus and handle.bus != bus: continue if address and str(handle.address) != address: continue if usb_port and handle.port != usb_port: continue _LOGGER.debug('USB device: %04x:%04x', handle.vendor_id, handle.product_id) for drv in drivers: yield from drv.probe(handle, vendor=vendor, product=product, **kwargs) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/liquidctl/error.py0000644000175000017500000000264614617346724016361 0ustar00jonasjonas"""liquidctl errors types. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style from typing import * class LiquidctlError(Exception): """Unspecified liquidctl error. Unstable. """ def __str__(self) -> str: return "unspecified liquidctl error" class ExpectationNotMet(LiquidctlError): """Unstable.""" class NotSupportedByDevice(LiquidctlError): """Operation not supported by the device.""" def __str__(self) -> str: return "operation not supported by the device" class NotSupportedByDriver(LiquidctlError): """Operation not supported by the driver.""" def __init__(self, explanation=None): self._explanation = explanation def __str__(self) -> str: return f"operation not supported by the driver{f': {self._explanation}' if self._explanation is not None else ''}" class UnsafeFeaturesNotEnabled(LiquidctlError): """Required unsafe features have not been enabled.""" def __init__(self, missing_features: Iterable[str]) -> None: self._missing_features = missing_features def __str__(self) -> str: features = ",".join(self._missing_features) return f"required unsafe features have not been enabled: {features}" class Timeout(LiquidctlError): """Operation timed out. Unstable. """ def __str__(self) -> str: return "operation timed out" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736041515.0 liquidctl-1.15.0/liquidctl/keyval.py0000644000175000017500000002123714736362053016513 0ustar00jonasjonas"""Simple key-value based storage for liquidctl drivers. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import logging import os import stat import sys import tempfile from ast import literal_eval from contextlib import contextmanager if sys.platform == 'win32': import msvcrt else: import fcntl _LOGGER = logging.getLogger(__name__) XDG_RUNTIME_DIR = os.getenv('XDG_RUNTIME_DIR') def get_runtime_dirs(appname='liquidctl'): """Return base directories for application runtime data. Directories are returned in order of preference. """ if sys.platform == 'win32': dirs = [os.path.join(os.getenv('TEMP'), appname)] elif sys.platform == 'darwin': dirs = [os.path.expanduser(os.path.join('~/Library/Caches', appname))] elif sys.platform == 'linux': # threat all other platforms as *nix and conform to XDG basedir spec dirs = [] if XDG_RUNTIME_DIR: dirs.append(os.path.join(XDG_RUNTIME_DIR, appname)) # regardless whether XDG_RUNTIME_DIR is set, fallback to /var/run if it # is available; this allows a user with XDG_RUNTIME_DIR set to still # find data stored by another user as long as it is in the fallback # path (see #37 for a real world use case) if os.path.isdir('/var/run'): dirs.append(os.path.join('/var/run', appname)) assert dirs, 'Could not get a suitable place to store runtime data' else: dirs = [os.path.join('/tmp', appname)] return dirs @contextmanager def _open_with_lock(path, flags, *, shared=False): # os.O_ACCMODE does not exist on Windows access = flags & (os.O_RDONLY | os.O_WRONLY | os.O_RDWR) if access == os.O_RDWR: write_mode = 'r+' elif access == os.O_WRONLY: write_mode = 'w' elif access == os.O_RDONLY: write_mode = 'r' else: raise ValueError(f'Invalid os.open() flags: {flags}') with os.fdopen(os.open(path, flags, 0o666), mode=write_mode) as f: if sys.platform == 'win32': msvcrt.locking(f.fileno(), msvcrt.LK_LOCK, 1) elif shared: fcntl.flock(f, fcntl.LOCK_SH) else: fcntl.flock(f, fcntl.LOCK_EX) yield f if access != os.O_RDONLY: f.flush() # ensure flushing before automatic unlocking if sys.platform != 'win32' and {os.stat, os.chmod} <= os.supports_fd: @contextmanager def _os_open(path, flags, mode=0o777, *, dir_fd=None): """Helper function that wraps os.open() and os.close() in a context manager""" fd = os.open(path, flags, mode, dir_fd=dir_fd) try: yield fd finally: os.close(fd) @contextmanager def _umask_bits(mode): """Context manager that temporarily sets the specified bits in the umask of the current process""" old_umask = os.umask(0o777) os.umask(old_umask | mode) try: yield finally: os.umask(old_umask) def _chmod_bits(path, mode, *, flags=0): """Helper function that sets the specified bits in the Unix permissions of a file""" with _os_open(path, os.O_RDONLY | flags) as fd: st = os.stat(fd) st_mode = stat.S_IMODE(st.st_mode) # do not chmod() if there's nothing to change -- we might not be the owner if st_mode | mode != st_mode: os.chmod(fd, st_mode | mode) else: @contextmanager def _umask_bits(umask): yield def _chmod_bits(path, mode): pass class _FilesystemBackend: def _sanitize(self, key): if not isinstance(key, str): raise TypeError('key must str') if not key.isidentifier(): raise ValueError('key must be valid Python identifier') return key def __init__(self, key_prefixes, runtime_dirs=get_runtime_dirs()): key_prefixes = [self._sanitize(p) for p in key_prefixes] # compute read and write dirs from base runtime dirs: the first base # dir is selected for writes and prefered for reads self._read_dirs = [os.path.join(x, *key_prefixes) for x in runtime_dirs] self._write_dir = self._read_dirs[0] # create leading directories with default permissions os.makedirs(os.path.dirname(self._write_dir), exist_ok=True) with _umask_bits(0o077): # create leaf directory as 0o0700, but do not break ACLs os.makedirs(self._write_dir, exist_ok=True) # set the sticky bit to prevent removal during cleanup _chmod_bits(self._write_dir, 0o1000) # flags=os.O_DIRECTORY, but !win32 _LOGGER.debug('data in %s', self._write_dir) def load(self, key): for base in self._read_dirs: path = os.path.join(base, key) if not os.path.isfile(path): continue try: with _open_with_lock(path, os.O_RDONLY, shared=True) as f: data = f.read().strip() if not data: continue value = literal_eval(data) _LOGGER.debug('loaded %s=%r (from %s)', key, value, path) except OSError as err: _LOGGER.warning('%s exists but could not be read: %s', path, err) except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError) as err: _LOGGER.warning('%s exists but was corrupted: %s', key, err) else: return value _LOGGER.debug('no data (file) found for %s', key) return None def store(self, key, value): data = repr(value) assert literal_eval(data) == value, 'encode/decode roundtrip fails' path = os.path.join(self._write_dir, key) with _open_with_lock(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) as f: f.write(data) _LOGGER.debug('stored %s=%r (in %s)', key, value, path) def load_store(self, key, func): value = None new_value = None path = os.path.join(self._write_dir, key) # lock the destination as soon as possible with _open_with_lock(path, os.O_RDWR | os.O_CREAT) as f: # still traverse all possible locations to find the current value for base in self._read_dirs: read_path = os.path.join(base, key) if not os.path.isfile(read_path): continue try: if os.path.samefile(read_path, path): # we already have an exclusive lock to this file data = f.read().strip() f.seek(0) else: with _open_with_lock(read_path, os.O_RDONLY, shared=True) as aux: data = aux.read().strip() if not data: continue value = literal_eval(data) _LOGGER.debug('loaded %s=%r (from %s)', key, value, read_path) break except OSError as err: _LOGGER.warning('%s exists but could not be read: %s', read_path, err) except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError) as err: _LOGGER.warning('%s exists but was corrupted: %s', key, err) else: _LOGGER.debug('no data (file) found for %s', key) new_value = func(value) data = repr(new_value) assert literal_eval(data) == new_value, 'encode/decode roundtrip fails' f.write(data) f.truncate() _LOGGER.debug('replaced with %s=%r (stored in %s)', key, new_value, path) return (value, new_value) class RuntimeStorage: """Unstable API.""" def __init__(self, key_prefixes, backend=None): if not backend: backend = _FilesystemBackend(key_prefixes) self._backend = backend def load(self, key, of_type=None, default=None): """Unstable API.""" value = self._backend.load(key) if value is None: return default elif of_type and not isinstance(value, of_type): return default else: return value def load_store(self, key, func, of_type=None, default=None): """Unstable API.""" def l(value): if value is None: value = default elif of_type and not isinstance(value, of_type): value = default return func(value) return self._backend.load_store(key, l) def store(self, key, value): """Unstable API.""" self._backend.store(key, value) return value ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/pmbus.py0000644000175000017500000001214114562144457016343 0ustar00jonasjonas"""Constants and methods for interfacing with PMBus compliant devices. Specifications: - Power Systems Management Protocol Specification. Revision 1.3.1, 2015. Available uppon request, check the PMBus website. - Power Systems Management Protocol Specification. Revision 1.2, 2010. Available on the PMBus website. http://pmbus.org/Assets/PDFS/Public/PMBus_Specification_Part_I_Rev_1-2_20100906.pdf http://pmbus.org/Assets/PDFS/Public/PMBus_Specification_Part_II_Rev_1-2_20100906.pdf - System Management Bus (SMBus) Specification. Version 3.1, 2018. Available on the SMBus website. http://smbus.org/specs/SMBus_3_1_20180319.pdf Additional references: - Milios, John. CRC-8 firmware implementations for SMBus. 1999. http://sbs-forum.org/marcom/dc2/20_crc-8_firmware_implementations.pdf - Pircher, Thomas. pycrc -- parameterisable CRC calculation utility and C source code generator: CRC algorithms implemented in Python. https://github.com/tpircher/pycrc/blob/master/pycrc/algorithms.py - White, Robert V. Using the PMBus Protocol. 2005. http://pmbus.org/Assets/Present/Using_The_PMBus_20051012.pdf Copyright Jonas Malaco and contributors Includes a CRC-8 implementation adapted from pycrc by Thomas Pircher. Copyright (c) 2006-2017 Thomas Pircher SPDX-License-Identifier: GPL-3.0-or-later """ import math from enum import IntEnum, IntFlag, unique from liquidctl.util import mkCrcFun @unique class WriteBit(IntFlag): WRITE = 0x00 READ = 0x01 @unique class CommandCode(IntEnum): """Incomplete enumeration of the PMBus command codes.""" PAGE = 0x00 CLEAR_FAULTS = 0x03 PAGE_PLUS_WRITE = 0x05 PAGE_PLUS_READ = 0x06 VOUT_MODE = 0x20 FAN_CONFIG_1_2 = 0x3a FAN_COMMAND_1 = 0x3b FAN_COMMAND_2 = 0x3c FAN_CONFIG_3_4 = 0x3d FAN_COMMAND_3 = 0x3e FAN_COMMAND_4 = 0x3f READ_EIN = 0x86 READ_EOUT = 0x87 READ_VIN = 0x88 READ_IIN = 0x89 READ_VCAP = 0x8a READ_VOUT = 0x8b READ_IOUT = 0x8c READ_TEMPERATURE_1 = 0x8d READ_TEMPERATURE_2 = 0x8e READ_TEMPERATURE_3 = 0x8f READ_FAN_SPEED_1 = 0x90 READ_FAN_SPEED_2 = 0x91 READ_FAN_SPEED_3 = 0x92 READ_FAN_SPEED_4 = 0x93 READ_DUTY_CYCLE = 0x94 READ_FREQUENCY = 0x95 READ_POUT = 0x96 READ_PIN = 0x97 READ_PMBUS_REVISON = 0x98 MFR_ID = 0x99 MFR_MODEL = 0x9a MFR_REVISION = 0x9b MFR_LOCATION = 0x9c MFR_DATE = 0x9d MFR_SERIAL = 0x9e MFR_SPECIFIC_D1 = 0xd1 MFR_SPECIFIC_D2 = 0xd2 MFR_SPECIFIC_D8 = 0xd8 MFR_SPECIFIC_DC = 0xdc MFR_SPECIFIC_EE = 0xee MFR_SPECIFIC_F0 = 0xf0 MFR_SPECIFIC_FC = 0xfc def linear_to_float(bytes, vout_exp=None): """Read PMBus LINEAR11 and ULINEAR16 numeric values. If `vout_exp` is None the value is interpreted as a 2 byte LINEAR11 value. The mantissa is stored in the lower 11 bits, in two's-complement, and the exponent is is stored in the upper 5 bits, also in two's-complement. Otherwise the value is assumed to be encoded in ULINEAR16, where the exponent is read from the lower 5 bits of `vout_exp` (which is assumed to be the output from VOUT_MOE) and the mantissa is the unsigned 2 byte integer in `bytes`. Per the SMBus specification, the lowest order byte is sent first (endianess is little). >>> linear_to_float(bytes.fromhex('67e3')) 54.4375 >>> linear_to_float(bytes.fromhex('6703'), vout_exp=0x1c) 54.4375 """ tmp = int.from_bytes(bytes[:2], byteorder='little') if vout_exp is None: exp = tmp >> 11 fra = tmp & 0x7ff if fra > 1023: fra = fra - 2048 else: exp = vout_exp & 0x1f fra = tmp if exp > 15: exp = exp - 32 return fra * 2**exp def float_to_linear11(float): """Encode float in PMBus LINEAR11 format. A LINEAR11 number is a 2 byte value with an 11 bit two's complement mantissa and a 5 bit two's complement exponent. Per the SMBus specification, the lowest order byte is sent first (endianess is little). >>> float_to_linear11(3.3).hex() '4dc3' >>> float_to_linear11(0.0).hex() '0000' >>> linear_to_float(float_to_linear11(2812)) 2812 >>> linear_to_float(float_to_linear11(-2812)) -2812 """ if float == 0: return b'\x00\x00' max_y = 1023 n = math.ceil(math.log(math.fabs(float)/max_y, 2)) y = round(float * 2**(-n)) if n < 0: n = n + 32 if y < 0: y = y + 2048 return int.to_bytes((n << 11) | y, length=2, byteorder='little') def compute_pec(bytes): """ Compute a 8-bit Packet Error Code (PEC) for `bytes`. According to the SMBus specification, the PEC is computed using a 8-bit cyclic rendundancy check (CRC-8) with the polynominal x⁸ + x² + x¹ + x⁰. The computation uses a 256-byte lookup table. Based on https://github.com/tpircher/pycrc/blob/master/pycrc/algorithms.py. >>> hex(compute_pec(bytes('123456789', 'ascii'))) '0xf4' >>> hex(compute_pec(bytes.fromhex('5c'))) '0x93' >>> hex(compute_pec(bytes.fromhex('5c93'))) '0x0' """ return mkCrcFun('crc-8')(bytes) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/liquidctl/util.py0000644000175000017500000003700714617346724016204 0ustar00jonasjonas"""Assorted utilities used by drivers and the CLI. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ import colorsys import logging from ast import literal_eval from enum import Enum, EnumMeta, unique from typing import Optional from functools import lru_cache import crcmod.predefined from liquidctl.error import UnsafeFeaturesNotEnabled _LOGGER = logging.getLogger(__name__) HUE2_MAX_ACCESSORIES_IN_CHANNEL = 6 @unique class Hue2Accessory(Enum): """Mapping of HUE 2 accessory IDs and names. >>> Hue2Accessory(4) >>> str(Hue2Accessory(4)) 'HUE 2 LED Strip 300 mm' Unknown IDs are automatically translated to equivalent pseudo-names. >>> Hue2Accessory(59) >>> Hue2Accessory(59).value == Hue2Accessory(59).value True >>> Hue2Accessory(59) != Hue2Accessory(58) True """ HUE_PLUS_LED_STRIP = (0x01, 'HUE+ LED Strip') AER_RGB1_FAN = (0x02, 'AER RGB 1') HUE2_LED_STRIP_300 = (0x04, 'HUE 2 LED Strip 300 mm') HUE2_LED_STRIP_250 = (0x05, 'HUE 2 LED Strip 250 mm') HUE2_LED_STRIP_200 = (0x06, 'HUE 2 LED Strip 200 mm') HUE2_CABLE_COMB = (0x07, 'HUE 2 Cable Comb') HUE2_UNDERGLOW_300 = (0x09, 'HUE 2 Underglow 300 mm') HUE2_UNDERGLOW_200 = (0x0a, 'HUE 2 Underglow 200 mm') AER_RGB2_120 = (0x0b, 'AER RGB 2 120 mm') AER_RGB2_140 = (0x0c, 'AER RGB 2 140 mm') KRAKENX_GEN4_RING = (0x10, 'Kraken X (X53, X63 or X73) Pump Ring') KRAKENX_GEN4_LOGO = (0x11, 'Kraken X (X53, X63 or X73) Pump Logo') F120_RGB = (0x13, 'F120 RGB') F140_RGB = (0x14, 'F140 RGB') def __new__(cls, value, pretty_name): member = object.__new__(cls) member.pretty_name = pretty_name member._value_ = value return member @classmethod def _missing_(cls, value): dummy = object.__new__(cls) dummy.pretty_name = 'Unknown' dummy._name_ = f'UNKNOWN_{value}' dummy._value_ = value return dummy def __str__(self): return self.pretty_name def __eq__(self, other): return self.value == other.value class LazyHexRepr: """Wrap an indexed collection of bytes with a lazy hex __repr__. This is useful for logging, which uses `%` string formatting to lazily generate the messages, only when needed. >>> '%r' % LazyHexRepr(b'abc') '61:62:63' Start and end indices may also be specified. >>> '%r' % LazyHexRepr(b'abc', start=1) '62:63' >>> '%r' % LazyHexRepr(b'abc', end=-1) '61:62' """ def __init__(self, data, start=None, end=None, sep=':'): self.data = data self.start = start self.end = end self.sep = sep def __repr__(self): hexvals = map(lambda x: f'{x:02x}', self.data[self.start: self.end]) return self.sep.join(hexvals) class _RelaxedNamesEnum(EnumMeta): def __getitem__(cls, name): return super().__getitem__(name.upper()) class RelaxedNamesEnum(Enum, metaclass=_RelaxedNamesEnum): """Enum class where name lookup is case insensitive.""" pass def rpadlist(list, width, fillitem=0): """Pad `list` with `fillitem` to `width`. >>> rpadlist([1, 2, 3], 5) [1, 2, 3, 0, 0] >>> rpadlist([1, 2, 3], 5, fillitem=None) [1, 2, 3, None, None] """ pad_width = width - len(list) list.extend([fillitem] * pad_width) return list def clamp(value, clampmin, clampmax): """Clamp numeric `value` to interval [`clampmin`, `clampmax`].""" clamped = max(clampmin, min(clampmax, value)) if clamped != value: _LOGGER.debug('clamped %s to interval [%s, %s]', value, clampmin, clampmax) return clamped def fraction_of_byte(ratio=None, percentage=None): """Return `ratio` xor `percentage` expressed as a fraction of 255. >>> fraction_of_byte(ratio=.8) 204 >>> fraction_of_byte(percentage=20) 51 """ if percentage is not None: ratio = percentage / 100 if ratio is not None: if ratio < 0 or ratio > 1: raise ValueError('cannot express ratios outside of [0, 1]') return round(ratio * 255) raise ValueError('either ratio or percentage must not be None') def u16le_from(buffer, offset=0): """Read an unsigned 16-bit little-endian integer from `buffer`. >>> u16le_from(b'\x45\x05\x03') 1349 >>> u16le_from(b'\x45\x05\x03', offset=1) 773 """ return int.from_bytes(buffer[offset: offset + 2], byteorder='little') def u16be_from(buffer, offset=0): """Read an unsigned 16-bit big-endian integer from `buffer`. >>> u16be_from(b'\x45\x05\x03') 17669 >>> u16be_from(b'\x45\x05\x03', offset=1) 1283 """ return int.from_bytes(buffer[offset: offset + 2], byteorder='big') def delta(profile): """Compute a profile's Δx and Δy.""" return [(cur[0]-prev[0], cur[1]-prev[1]) for cur, prev in zip(profile[1:], profile[:-1])] def normalize_profile(profile, critx, max_value=100): """Normalize a [(x:int, y:int), ...] profile. The normalized profile will ensure that: - the profile is a monotonically increasing function (i.e. for every i, i > 1, x[i] - x[i-1] > 0 and y[i] - y[i-1] >= 0) - the profile is sorted - a (critx, 100) failsafe is enforced - only the first point that sets y := 100 is kept >>> normalize_profile([(30, 40), (25, 25), (35, 30), (40, 35), (40, 80)], 60) [(25, 25), (30, 40), (35, 40), (40, 80), (60, 100)] >>> normalize_profile([(30, 40), (25, 25), (35, 30), (40, 100)], 60) [(25, 25), (30, 40), (35, 40), (40, 100)] >>> normalize_profile([(30, 40), (25, 25), (35, 100), (40, 100)], 60) [(25, 25), (30, 40), (35, 100)] >>> normalize_profile([], 60) [(60, 100)] >>> normalize_profile([], 60, 300) [(60, 300)] """ profile = sorted(list(profile) + [(critx, max_value)], key=lambda p: (p[0], -p[1])) mono = profile[0:1] for (x, y), (xb, yb) in zip(profile[1:], profile[:-1]): if x == xb: continue if y < yb: y = yb mono.append((x, y)) if y == max_value: break return mono def interpolate_profile(profile, x): """Interpolate y given x and a [(x: int, y: int), ...] profile. Requires the profile to be sorted by x, with no duplicate x values (see normalize_profile). Expects profiles with integer x and y values, and returns duty rounded to the nearest integer. >>> interpolate_profile([(20, 50), (50, 70), (60, 100)], 33) 59 >>> interpolate_profile([(20, 50), (50, 70)], 19) 50 >>> interpolate_profile([(20, 50), (50, 70)], 51) 70 >>> interpolate_profile([(20, 50)], 20) 50 """ lower, upper = profile[0], profile[-1] for step in profile: if step[0] <= x: lower = step if step[0] >= x: upper = step break if lower[0] == upper[0]: return lower[1] return round(lower[1] + (x - lower[0])/(upper[0] - lower[0])*(upper[1] - lower[1])) def color_from_str(x): """Parse a color, and, if necessary, translate it into the RGB model. The input string can be encoded in several formats: - ffffff: hexadecimal RGB implicit tuple (with or without the prefix '0x') - rgb(255, 255, 255): explicit RGB, R,G,B ∊ [0, 255] - hsv(360, 100, 100): explicit HSV, H ∊ [0, 360], SV ∊ [0, 100] - hsl(360, 100, 100): explicit HSL, H ∊ [0, 360], SV ∊ [0, 100] >>> color_from_str('fF7f3f') [255, 127, 63] >>> color_from_str('0xfF7f3f') [255, 127, 63] >>> color_from_str('0XfF7f3f') [255, 127, 63] >>> color_from_str('#fF7f3f') [255, 127, 63] >>> color_from_str('Rgb(255, 127, 63)') [255, 127, 63] >>> color_from_str('Hsv(20, 75, 100)') [255, 128, 64] >>> color_from_str('Hsl(20, 100, 62)') [255, 126, 61] >>> color_from_str('fF7f3f1f') Traceback (most recent call last): ... ValueError: cannot parse color: fF7f3f1f >>> color_from_str('0bff00ff') Traceback (most recent call last): ... ValueError: cannot parse color: 0bff00ff >>> color_from_str('rgb()') Traceback (most recent call last): ... ValueError: expected 3-element triple: rgb() >>> color_from_str('rgb(255)') Traceback (most recent call last): ... ValueError: expected 3-element triple: rgb(255) >>> color_from_str('rgb(300, 255, 255)') Traceback (most recent call last): ... ValueError: expected value in range [0, 255]: 300 in rgb(300, 255, 255) >>> color_from_str('hsv(360, 150, 100)') Traceback (most recent call last): ... ValueError: expected value in range [0, 100]: 150 in hsv(360, 150, 100) >>> color_from_str('hsl(360, 100, 150)') Traceback (most recent call last): ... ValueError: expected value in range [0, 100]: 150 in hsl(360, 100, 150) """ def parse_triple(sub, maxvalues): literal = literal_eval(sub) if not isinstance(literal, tuple) or len(literal) != 3: raise ValueError(f'expected 3-element triple: {x}') for value, maxvalue in zip(literal, maxvalues): if not isinstance(value, int) and not isinstance(value, float): raise ValueError(f'expected float or int: {value} in {x}') if value < 0 or value > maxvalue: raise ValueError(f'expected value in range [0, {maxvalue}]: {value} in {x}') return literal xl = x.lower() if xl.startswith('rgb('): r, g, b = parse_triple(x[3:], (255, 255, 255)) return [r, g, b] elif xl.startswith('hsv('): h, s, v = parse_triple(x[3:], (360, 100, 100)) return list(map(lambda b: round(b*255), colorsys.hsv_to_rgb(h/360, s/100, v/100))) elif xl.startswith('hsl('): h, s, l = parse_triple(x[3:], (360, 100, 100)) return list(map(lambda b: round(b*255), colorsys.hls_to_rgb(h/360, l/100, s/100))) elif len(x) == 6: return list(bytes.fromhex(x)) elif len(x) == 7 and x.startswith('#'): return list(bytes.fromhex(x[1:])) elif len(x) == 8 and xl.startswith('0x'): return list(bytes.fromhex(x[2:])) else: raise ValueError(f'cannot parse color: {x}') def check_unsafe(*reqs, unsafe=None, error=False, **kwargs): """Check if unsafe feature requirements are met. Unstable. Checks if the requirements in the positional arguments (`*reqs`) are all met by the `unsafe` string list of enabled features. >>> check_unsafe('foo', unsafe='foo,bar') True >>> check_unsafe('foo', 'bar', unsafe='foo,bar') True >>> check_unsafe('foo', unsafe=None) False >>> check_unsafe('foo', 'baz', unsafe='foo,bar') False If `error=True` and some requirements have not been met, raises `liquidctl.error.UnsafeFeaturesNotEnabled`. In the default `error=False` mode, a boolean is return indicating whether all requirements were met. >>> check_unsafe('foo', 'baz', unsafe='foo,bar', error=True) Traceback (most recent call last): ... liquidctl.error.UnsafeFeaturesNotEnabled: required unsafe features have not been enabled: baz In driver code, `unsafe` is normally passed in `**kwargs`. >>> kwargs = {'unsafe': 'foo,bar'} >>> check_unsafe('foo', 'bar', **kwargs) True >>> check_unsafe('foo', 'baz', error=True, **kwargs) Traceback (most recent call last): ... liquidctl.error.UnsafeFeaturesNotEnabled: required unsafe features have not been enabled: baz """ if unsafe: reqs = tuple(filter(lambda x: x not in unsafe, reqs)) if not reqs: return True if error: raise UnsafeFeaturesNotEnabled(reqs) return False def map_direction(direction, forward=None, backward=None): """Check a `direction` option and run the appropriate closure. Unstable. Accepts both US and UK spellings. Raises a `ValueError` if the option value is neither forward[s] nor backward[s]. >>> map_direction('forward', 3, 42) 3 >>> map_direction('forwards', 3, 42) 3 >>> map_direction('backward', 3, 42) 42 >>> map_direction('backwards', 3, 42) 42 >>> map_direction('fooowwd', 3, 42) Traceback (most recent call last): ... ValueError: invalid direction: 'fooowwd' """ if 'forwards'.startswith(direction): return forward elif 'backwards'.startswith(direction): return backward else: raise ValueError(f'invalid direction: {direction!r}') def fan_mode_parser(value: Optional[str], max_fans: Optional[int] = None) -> dict: """Convert the --fan-mode=:[,...] options into a {key: value, ....} dictionary. The default is an empty dictionary. Unstable. >>> fan_mode_parser(None, 5) {} >>> fan_mode_parser('', 5) {} >>> fan_mode_parser('1:dc', 5) {'1': 'dc'} >>> fan_mode_parser('2:auto', 5) {'2': 'auto'} >>> fan_mode_parser('3:off', 5) {'3': 'off'} >>> fan_mode_parser('1:DC', 5) {'1': 'dc'} >>> fan_mode_parser('2:AUTO', 5) {'2': 'auto'} >>> fan_mode_parser('3:OFF', 5) {'3': 'off'} >>> fan_mode_parser('5:OFF', 5) {'5': 'off'} >>> fan_mode_parser('5:OFF') {'5': 'off'} >>> fan_mode_parser('1:dc,2:auto,3:off,4:pwm', 5) {'1': 'dc', '2': 'auto', '3': 'off', '4': 'pwm'} >>> fan_mode_parser('1:dc, 2:auto, 3:off', 5) {'1': 'dc', '2': 'auto', '3': 'off'} >>> fan_mode_parser('4:dc, 3:auto, 2:off', 5) {'2': 'off', '3': 'auto', '4': 'dc'} >>> fan_mode_parser('1 :dc, 2: auto, 3 : off, 4 : auto ', 5) {'1': 'dc', '2': 'auto', '3': 'off', '4': 'auto'} >>> fan_mode_parser('1:dc:dc', 5) Traceback (most recent call last): ... ValueError: invalid format, should be ':' >>> fan_mode_parser('-1:dc', 5) Traceback (most recent call last): ... ValueError: invalid fan number: '-1' >>> fan_mode_parser('0:pwm', 5) Traceback (most recent call last): ... ValueError: invalid fan number: '0' >>> fan_mode_parser('5:dc', 4) Traceback (most recent call last): ... ValueError: invalid fan number: '5' >>> fan_mode_parser('a:dc', 5) Traceback (most recent call last): ... ValueError: invalid fan number: 'a' >>> fan_mode_parser('1:PMW', 5) Traceback (most recent call last): ... ValueError: invalid fan mode: 'PMW' """ if not value: return {} parts = value.split(',') opts = {} for p in parts: p2 = p.split(':') if len(p2) != 2: raise ValueError("invalid format, should be ':'") [key, val] = [i.strip() for i in p2] try: key_val = int(key, 10) except ValueError: raise ValueError(f"invalid fan number: '{key}'") if key_val <= 0 or (max_fans is not None and key_val > max_fans): raise ValueError(f"invalid fan number: '{key}'") if val.lower() not in ['off', 'auto', 'dc', 'pwm']: raise ValueError(f"invalid fan mode: '{val}'") opts.update({key: val.lower()}) return dict(sorted(opts.items(), key=lambda item: item[0])) @lru_cache(maxsize=None) def mkCrcFun(crc_name): """Efficiently construct a predefined CRC function. Unstable. For the available algorithms, see . This function implements memoization, only constructing each requested CRC algorithm implementation once. """ return crcmod.predefined.mkCrcFun(crc_name) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/liquidctl/version.py0000644000175000017500000000317014562144457016704 0ustar00jonasjonas"""Interfaces to read the liquidctl version. Copyright Jonas Malaco and contributors SPDX-License-Identifier: GPL-3.0-or-later """ # uses the psf/black style # keep setuptools_scm parameters in sync with pyproject.toml _SETUPTOOLS_SCM_PARAMS = {"version_scheme": "release-branch-semver"} def _build_version(): try: from liquidctl._version import version, version_tuple return (version, version_tuple) except ModuleNotFoundError: return None def _runtime_version(): try: from setuptools_scm import get_version except ModuleNotFoundError: return None # first, assume that we're a git checkout try: version = get_version(**_SETUPTOOLS_SCM_PARAMS) if version: return (version, None) except LookupError: pass # if that also failed, assume that we're a tarball try: guess = get_version(parentdir_prefix_version="liquidctl-", **_SETUPTOOLS_SCM_PARAMS) if guess: if "+" in guess: guess += "-guessed" else: guess += "+guessed" return (guess, None) except LookupError: pass return None # - try to get the version written by setuptools_scm during the build; # - otherwise try to compute one right now; # - failing that too, use an obviously invalid value # # (_version_tuple is kept private as it's only available in some cases and # don't want to commit to it yet) (version, _version_tuple) = _build_version() or _runtime_version() or ("0.0.0-unknown", None) # old field name (liquidctl.__version__ is preferred now) __version__ = version ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525812.0 liquidctl-1.15.0/liquidctl.80000644000175000017500000004631414776654764014763 0ustar00jonasjonas'\" t .nr is_macos 0 .TH LIQUIDCTL 8 2025\-04\-01 "liquidctl" "System Manager's Manual" . .SH NAME liquidctl \- monitor and control liquid coolers and other devices . .SH SYNOPSIS .SY liquidctl .RI [ options ] .B list .SY liquidctl .RI [ options ] .B initialize .RB [ all ] .SY liquidctl .RI [ options ] .B status .SY liquidctl .RI [ options ] .B set .I channel .B speed .RI ( temperature .IR percentage ) \&.\|.\|.\& .SY liquidctl .RI [ options ] .B set .I channel .B speed .I percentage .SY liquidctl .RI [ options ] .B set .I channel .B color .I mode .RI [ color \&.\|.\|.\&] .SY liquidctl .B \-\-version .SY liquidctl .B \-\-help .YS . .SH DESCRIPTION \fBliquidctl\fR is a utility for overseeing and controlling some hardware monitoring devices not yet supported at the kernel level. .if !\n[is_macos]\{ Because \fBliquidctl\fR directly accesses the hardware devices, root privileges are generally required, though this can be avoided with appropriate udev rules. .\} .PP \fBliquidctl list\fR outputs all compatible devices found on the system. In case more than one device is found, the desired one can be selected for later invocations with \fB--match=\fIsubstring\fR, where \fIsubstring\fR matches part of the desired device's description using a case insensitive comparison. .PP \fBliquidctl list \fI\-\-verbose\fR enables the display of additional identifiers and addresses that can also be used to select specific devices. These can be better suited for certain use cases, such as non-interactive scripts. .PP \fBliquidctl initialize\fR prepares a device for later commands, and most devices must be initialized after every boot or when resuming from a suspended state. Unless finer control is required, all devices can be initialized at once with \fBliquidctl initialize all\fR. Some devices may output some information at this stage. .PP \fBliquidctl status\fR displays the status of all devices that match the provided filtering options. .PP \fBliquidctl set \fIchannel\fB speed\fR allows the user to set fan and pump speeds. These, depending on the device, can be set to fixed duty values, variable temperature\–duty curves, or both. .PP \fBliquidctl set \fIchannel\fB color\fR allows the user to configure and set lighting modes. Supported lighting modes and additional options vary by device and are listed in later sections of this manual. Each color can be specified as: .IP \(bu hexadecimal RGB with or without prefix '0x': \fIff7f3f\fR; .IP \(bu decimal RGB triple, R,G,B ∊ [0, 255]: \fIrgb(255,127,63)\fR; .IP \(bu hue\-saturation\-value HSV triple, H ∊ [0, 360], S,V ∊ [0, 100]: \fIhsv(20,75,100)\fR; .IP \(bu hue\-saturation\-lightness HSL triple, H ∊ [0, 360], S,L ∊ [0, 100]: \fIhsl(20,100,62)\fR. .PP \fBliquidctl set \fIchannel\fB screen\fR allows the user to configure the LCD screen integrated into some AIO models. . .if !\n[is_macos]\{ .PP \fBliquidctl\fR automatically detects when a kernel driver is bound to the device and, whenever possible, uses it instead of directly accessing the device. Alternatively, direct access to the device can be forced with \fI\-\-direct\-access\fP. . .\} .SH OPTIONS . .SS Device selection options Devices can be selected using one or more values taken from \fBlist \fI\-\-verbose\fP. .TP .BI \-m\ substring\fR,\ \fP \-\-match= substring Filter devices by case insensitive substring of device description. .TP .BI \-n\ number\fR,\ \fP \-\-pick= number Pick among many results for a given filter. .TP .BI \-\-vendor= id Filter devices by hexadecimal vendor id. .TP .BI \-\-product= id Filter devices by hexadecimal product id. .TP .BI \-\-release= number Filter devices by hexadecimal release number. .TP .BI \-\-serial= number Filter devices by serial number. .TP .BI \-\-bus= bus Filter devices by bus. .TP .BI \-\-address= address Filter devices by address in bus. .TP .BI \-\-usb\-port= port Filter devices by USB port in bus. .TP .BI \-d\ index\fR,\ \fP \-\-device= index (Deprecated), select device by listing index. . .SS Animation options Some devices and animation modes support additional options. .TP .BI \-\-speed= value Abstract animation speed (device/mode specific). .TP .BI \-\-time\-per\-color= value Time to wait on each color (seconds). .TP .BI \-\-time\-off= value Time to wait with the LED turned off (seconds). .TP .BI \-\-alert\-threshold= number Threshold temperature for a visual alert (degrees Celsius). .TP .BI \-\-alert\-color= color Color used by the visual high temperature alert. .TP .BI \-\-direction= string If the pattern should move forward or backward. .TP .BI \-\-start\-led= number The first led to start the effect at. .TP .BI \-\-maximum\-leds= number The number of LED's the effect should apply to. . .SS Other options .TP .BI \-\-fan\-mode= channel:mode[,...] Set the fan modes. .TP .B \-\-single\-12v\-ocp Enable single rail +12V OCP. .TP .BI \-\-pump\-mode= mode Set the pump mode. .TP .BI \-\-temperature\-sensor= number The temperature sensor number for the Commander Pro. .TP .B \-\-legacy\-690lc Use Asetek 690LC in legacy mode (old Krakens). .TP .B \-\-non\-volatile Store on non\-volatile controller memory. .TP .B \-\-direct\-access Directly access the device despite kernel drivers. .TP .BI \-\-unsafe= features Comman-separated bleeding-edge features to enable. .TP .B \-v\fR, \fP\-\-verbose Output additional information. .TP .B \-g\fR, \fP\-\-debug Show debug information on \fIstderr\fR. .TP .B \-\-json Output machine-readable JSON. Only supported with .BR list ,\ initialize \ and\ status . .TP .B \-\-version Display the version number. .TP .B \-\-help Show the embedded help. . .SH EXIT STATUS 1 if there was an error, 0 otherwise. . .SH ENVIRONMENT If \fBLANG\fR is set to \fIC\fR, non-ASCII characters are escaped from the output of \fB\-\-json\fR. . .SH FILES .TP .ie \n[is_macos] .I ~/Library/Caches/liquidctl/* .el .IR $XDG_RUNTIME_DIR/liquidctl/* ,\ /var/run/liquidctl/* ,\ /tmp/liquidctl/* Internal data used by some drivers. .\" e.g. RuntimeStorage for Legacy690Lc and HydroPlatinum . .SH EXAMPLE .SY liquidctl .B list \-\-verbose .SY liquidctl .B initialize all .SY liquidctl .BI \-\-match\ kraken\ set\ pump\ speed\ 90 .SY liquidctl .BI \-\-product\ 170e\ set\ led\ color\ fading .I 350017 ff2608 .SY liquidctl .B status .YS . .SH DEVICE SPECIFICS . .SS Aquacomputer D5 Next .SS Aquacomputer Farbwerk 360 .SS Aquacomputer Octo .SS Aquacomputer Quadro Cooling channels: (D5 Next): \fIfan\fR, \fIpump\fR; (Octo): \fIfan[1\-8]\fR; (Quadro:) \fIfan[1\-4]\fR; (Farbwerk 360:) not applicable. .PP Lighting channels: not yet supported. .SS ASUS Ryujin II 360 Cooling channels: \fIpump\fR, \fIfans\fR, \fIpump\-fan\fR, \fIexternal\-fans\fR. .PP Screen: not yet supported. .SS Corsair iCUE Elite Capellix H100i, H115i, H150i .SS Corsair Commander Core .SS Corsair Commander Core XT .SS Corsair Commander ST Cooling channels: \fIfans\fR, \fIfan[1\-6]\fR; (only non-XT/Elite Capellix:) \fIpump\fR. .PP Lighting channels: not yet supported. .SS Corsair Commander Pro .SS Corsair Lighting Node Pro .SS Corsair Lighting Node Core .SS Corsair Obsidian 1000D Cooling channels (only Commander Pro and Obsidian 1000D): \fIsync\fR, \fIfan[1\-6]\fR. .PP Where the fan connection types can be set with .BI \-\-fan\-mode= channel:mode[,...] , where the allowed modes are: \fIoff\fR, \fIdc\fR, \fIpwm\fR. .PP Lighting channels: (only Lighting Node Core:) \fIled\fR; (only Commander Pro and Lighting Node Pro:) \fIsync\fR, \fIled[1\-2]\fR. .TS l c --- l c . Mode #colors \fIclear\fR 0 \fIoff\fR 0 \fIfixed\fR 1 \fIcolor_shift\fR 2 \fIcolor_pulse\fR 2 \fIcolor_wave\fR 2 \fIvisor\fR 2 \fIblink\fR 2 \fImarquee\fR 1 \fIsequential\fR 1 \fIrainbow\fR 0 \fIrainbow2\fR 0 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIfast\fR, \fImedium\fR, \fIslow\fR. .PP The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. .BI \-\-start\-led= number , the first LED that the lighting effect should be for. .BI \-\-start\-led= number , the first LED that the lighting effect should be for. .BI \-\-maximum\-led= number , the number of LEDs that the lighting effect should applied to. .BI \-\-temperature\-sensor= number , The temperature sensor that should be used to control the fan curves, probe 1 by default. .SS Corsair Hydro H110i GT Cooling channels: \fIfan1\fR, \fIfan2\fR. .PP Pump mode (\fBinitialize \-\-pump\-mode \fImode\fR): \fIquiet\fR (default), \fIextreme\fR. .PP Lighting channels: not yet supported. .SS Corsair Hydro H80i GT, H100i GTX, H110i GTX .SS Corsair Hydro H80i v2, H100i v2, H115i .SS EVGA CLC 120 (CL12), 240, 280, 360 Cooling channels: \fIpump\fR, \fIfan\fR. .PP Lighting channels: \fIlogo\fR. .TS l c c --- l c c . Mode #colors notes \fIrainbow\fR 0 only available on EVGA coolers \fIfading\fR 2 \fIblinking\fR 1 \fIfixed\fR 1 \fIblackout\fR 0 no high-temperature alerts .TE .PP The \fIrainbow\fR mode speed can be configured with .BI \-\-speed= [1\(en6] . The speed of the other modes is instead customized with .B \-\-time\-per\-color .RI ( fading\ and\ blinking ) and .B \-\-time\-off .RI ( blinking\ only). .PP All modes except .I blackout support a visual high-temperature alert configured with .B \-\-alert\-threshold and .BR \-\-alert\-color . .PP All current settings can be saved on non\-volatile on\-board memory by passing .B \-\-non\-volatile to any write command. However, since write\-cycles are limited, this option should be used sparingly. . .SS Corsair H100i Pro, H115i Pro, H150i Pro Cooling channels: \fIfan\fR, \fIfan[1\(en2]\fR; (only H150i Pro:) \fIfan3\fR. .PP Pump mode (\fBinitialize \-\-pump\-mode \fImode\fR): \fIquiet\fR, \fIbalanced\fR (default), \fIperformance\fR. .PP Lighting channel: \fIlogo\fR. .TS l c -- l c . Mode #colors \fIalert\fR 3 \fIshift\fR 2\(en4 \fIpulse\fR 1\(en4 \fIblinking\fR 1\(en4 \fIfixed\fR 1 .TE . .SS Corsair Hydro H100i Platinum, H100i Platinum SE, H115i Platinum .SS Corsair Hydro H60i Pro XT, H100i Pro XT, H115i Pro XT, H150i Pro XT .SS Corsair Hydro H100i, H115i, H150i Elite RGB Cooling channels: \fIfan\fR, \fIfan[1\(en2]\fR; (only H150i Pro XT/Elite RGB:) \fIfan3\fR. .PP Pump mode (\fBinitialize \-\-pump\-mode \fImode\fR): \fIquiet\fR, \fIbalanced\fR (default), \fIextreme\fR. .PP Lighting channels: \fIsync\fR, \fIled\fR. .TS l l c c c ----- l l c c c . Channel Mode #colors (Platinum) #colors (Pro XT/Elite RGB) #colors (Platinum SE) \fIled\fR \fIoff\fR 0 0 0 \fIled\fR \fIfixed\fR 1 1 1 \fIled\fR \fIsuper\-fixed\fR 24 16 48 .TE . .SS MSI MPG Coreliquid K360 Cooling channels: \fIfan1\fR, \fIfan2\fR, \fIfan3\fR, \fIfans\fR, \fIwaterblock-fan\fR, \fIpump\fR. .PP Lighting channels: \fIsync\fR. .PP LCD screens: \fIlcd\fR. .TS l c ---- l c . Mode #colors \fIdisable\fR 0 \fIsteady\fR 1 \fIblink\fR 0 \fIbreathing\fR 1 \fIclock\fR 2 \fIcolor pulse\fR 0 \fIcolor ring\fR 0 \fIcolor ring double flashing\fR 0 \fIcolor ring flashing\fR 0 \fIcolor shift\fR 0 \fIcolor wave\fR 2 \fIcorsair ique\fR 0 \fIdisable2\fR 0 \fIdouble flashing\fR 1 \fIdouble meteor\fR 0 \fIenergy\fR 0 \fIfan control\fR 0 \fIfire\fR 2 \fIflashing\fR 1 \fIjazz\fR 0 \fIjrainbow\fR 0 \fIlava\fR 0 \fIlightning\fR 1 \fImarquee\fR 1 \fImeteor\fR 1\(en2 \fImovie\fR 0 \fImsi marquee\fR 1 \fImsi rainbow\fR 0 \fIplanetary\fR 0 \fIplay\fR 0 \fIpop\fR 0 \fIrainbow\fR 0 \fIrainbow double flashing\fR 0 \fIrainbow flashing\fR 0 \fIrainbow wave\fR 0 \fIrandom\fR 0 \fIrap\fR 0 \fIstack\fR 1 \fIvisor\fR 2 \fIwater drop\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fI0\fR, \fI1\fR and \fI2\fR. The LCD screen settings can be controlled with the \fBsettings\fR flag, that requires the arguments "\fIbrightness\fR;\fIdirection\fR" where an allowed brightness is an integer in the range \fI0-100\fR, and orientations \fI0-3\fR correspond to the orientations up, right, down and left. . .SS NZXT Kraken X40, X60 .SS NZXT Kraken X31, X41, X61 Supports the same modes and options as a Corsair Hydro H80i GT (or similar), but requires \fB\-\-legacy\-690lc\fR to be passed on all invocations. . .SS NZXT Kraken M22 .SS NZXT Kraken X42, X52, X62, X72 Cooling channels (only X42, X52, X62, X72): \fIpump\fR, \fIfan\fR. .PP Lighting channels: \fIlogo\fR, \fIring\fR, \fIsync\fR. .TS l c c c ---- l c c c . Mode logo ring #colors \fIoff\fR yes yes 0 \fIfixed\fR yes yes 1 \fIsuper\-fixed\fR yes yes 1\(en9 \fIfading\fR yes yes 2\(en8 \fIalternating\fR no yes 2 \fIbreathing\fR yes yes 1\(en8 \fIsuper\-breathing\fR yes yes 1\(en9 \fIpulse\fR yes yes 1\(en8 \fItai\-chi\fR no yes 2 \fIwater\-cooler\fR no yes 0 \fIloading\fR no yes 1 \fIwings\fR no yes 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS NZXT Kraken X53, X63, X73 Cooling channels: \fIpump\fR. .PP Lighting channels: \fIexternal\fR, \fIring\fR, \fIlogo\fR, \fIsync\fR. . .SS NZXT Kraken Z53, Z63, Z73 Cooling channels: \fIpump\fR, \fIfan\fR. .PP Lighting channels: \fIexternal\fR. .PP LCD screens: \fIlcd\fR. . .SS NZXT Kraken 2023 Standard, Elite .SS NZXT Kraken 2024 Elite RGB Cooling channels: \fIpump\fR, \fIfan\fR. .PP Lighting channels: none. .PP LCD screens: \fIlcd\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIfading\fR 2\(en8 \fIsuper\-fixed\fR 1\(en40 \fIalternating\-[3\-6]\fR 1\(en2 \fIpulse\fR 1\(en8 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIcandle\fR 1 \fIstarry\-night\fR 1 \fIloading\fR 1 \fItai\-chi\fR 1\(en2 \fIwater\-cooler\fR 2 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS Corsair HX750i, HX850i, HX1000i, HX1200i, HX1500i .SS Corsair RM650i, RM750i, RM850i, RM1000i Cooling channels: \fIfan\fR. .PP Lighting channels: none. .PP Setting a fixed fan speed changes the fan mode to software control. To revert back to hardware control, run \fBinitialize\fR again. .PP (Experimental feature) The +12V rails normally function in multiple-rail mode. Single-rail mode can be selected by passing \fB\-\-single\-12v\-ocp\fR to \fBinitialize\fR. To revert back to multiple-rail mode, run \fBinitialize\fR again without that flag. . .SS NZXT E500, E650, E850 Cooling channels: none (feature not supported yet). .PP Lighting channels: none. . .SS NZXT Grid+ V3 Cooling channels: \fIfan[1\(en6]\fR, \fIsync\fR. .PP Lighting channels: none. . .SS NZXT Smart Device (V1) Cooling channels: \fIfan[1\(en3]\fR, \fIsync\fR. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIsuper\-fixed\fR 1\(en40 \fIfading\fR 2\(en8 \fIalternating\fR 2 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIpulse\fR 1\(en8 \fIcandle\fR 1 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS NZXT Smart Device V2 .SS NZXT RGB & Fan Controller .SS NZXT RGB & Fan Controller (3+6 channels) .SS NZXT HUE 2 .SS NZXT HUE 2 Ambient .SS NZXT H1 V2 Cooling channels (only Smart Device V2, RGB & Fan Controller and H1 V2): \fIfan[1\(en3]\fR. .PP Lighting channels (all but H1 V2): \fIled[1\(en2]\fR, \fIsync\fR. .PP Additional lighting channels (HUE 2): \fIled[3\(en4]\fR. .PP Additional lighting channels (RGB & Fan Controller (3+6 channels)): \fIled[3\(en6]\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIsuper\-fixed\fR 1\(en40 \fIfading\fR 2\(en8 \fIalternating\-[3\-6]\fR 2 \fIpulse\fR 1\(en8 \fIbreathing\fR 1\(en8 \fIsuper\-breathing\fR 1\(en40 \fIcandle\fR 1 \fIstarry\-night\fR 1 \fIwings\fR 1 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. The animation direction can be set with .BI \-\-direction= value , where the allowed values are: \fIforward\fR or \fIbackward\fR. . .SS ASUS Strix GTX 1050 OC .SS ASUS Strix GTX 1050 Ti OC .SS ASUS Strix GTX 1060 6GB .SS ASUS Strix GTX 1060 OC 6GB .SS ASUS Strix GTX 1070 .SS ASUS Strix GTX 1070 OC .SS ASUS Strix GTX 1070 Ti .SS ASUS Strix GTX 1070 Ti Advanced .SS ASUS Strix GTX 1080 .SS ASUS Strix GTX 1080 Advanced .SS ASUS Strix GTX 1080 OC .SS ASUS Strix GTX 1080 Ti .SS ASUS Strix GTX 1080 Ti OC .SS ASUS Strix GTX 1650 Super OC .SS ASUS Strix GTX 1660 Super OC .SS ASUS Strix GTX 1660 Ti OC .SS ASUS Strix RTX 2060 Evo .SS ASUS Strix RTX 2060 Evo OC .SS ASUS Strix RTX 2060 OC .SS ASUS Strix RTX 2060 Super .SS ASUS Strix RTX 2060 Super Advanced .SS ASUS Strix RTX 2060 Super Evo Advanced .SS ASUS Strix RTX 2060 Super OC .SS ASUS Strix RTX 2070 .SS ASUS Strix RTX 2070 Advanced .SS ASUS Strix RTX 2070 OC .SS ASUS Strix RTX 2070 Super Advanced .SS ASUS Strix RTX 2070 Super OC .SS ASUS Strix RTX 2080 OC .SS ASUS Strix RTX 2080 Super Advanced .SS ASUS Strix RTX 2080 Super OC .SS ASUS Strix RTX 2080 Ti .SS ASUS Strix RTX 2080 Ti OC .SS ASUS TUF RTX 3060 Ti OC Cooling channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIflash\fR 1 \fIbreathing\fR 1 \fIrainbow\fR 0 .TE . .SS Corsair Vengeance RGB Cooling channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIbreathing\fR 1\(en7 \fIfading\fR 2\(en7 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR. . .SS ASUS Aura LED Controller Cooling channels: none. .PP Lighting channels: \fIled[1\(en4]\fR, \fIsync\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIstatic\fR 1 \fIbreathing\fR 1 \fIflashing\fR 1 \fIspectrum_cycle\fR 0 \fIrainbow\fR 0 \fIspectrum_cycle_breathing\fR 0 \fIchase_fade\fR 1 \fIspectrum_cycle_chase_fade\fR 0 \fIchase\fR 1 \fIspectrum_cycle_chase\fR 0 \fIspectrum_cycle_wave\fR 0 \fIchase_rainbow_pulse\fR 0 \fIrainbow_flicker\fR 0 \fIgentle_transition\fR 0 \fIwave_propagation\fR 0 \fIwave_propagation_pause\fR 0 \fIred_pulse\fR 0 .TE . .SS Gigabyte RGB Fusion 2.0 5702 Controller .SS Gigabyte RGB Fusion 2.0 8297 Controller Cooling channels: none. .PP Lighting channels: \fIled[1\(en8]\fR, \fIsync\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIpulse\fR 1 \fI(double\-)?flash\fR 1 \fIcolor\-cycle\fR 0 .TE .PP When applicable the animation speed can be set with .BI \-\-speed= value , where the allowed values are: \fIslowest\fR, \fIslower\fR, \fInormal\fR, \fIfaster\fR, \fIfastest\fR, \fIludicrous\fR. . .SS EVGA GTX 1070 FTW .SS EVGA GTX 1070 FTW DT Gaming .SS EVGA GTX 1070 FTW Hybrid .SS EVGA GTX 1070 Ti FTW2 .SS EVGA GTX 1080 FTW Cooling channels: none. .PP Lighting channels: \fIled\fR. .TS l c ---- l c . Mode #colors \fIoff\fR 0 \fIfixed\fR 1 \fIbreathing\fR 1 \fIrainbow\fR 0 .TE . .SH SEE ALSO The complete documentation is available in the project's sources and .UR https://github.com/liquidctl/liquidctl homepage .UE . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7383947 liquidctl-1.15.0/liquidctl.egg-info/0000755000175000017500000000000014776655074016347 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/PKG-INFO0000644000175000017500000000641014776655073017444 0ustar00jonasjonasMetadata-Version: 2.4 Name: liquidctl Version: 1.15.0 Summary: Cross-platform tool and drivers for liquid coolers and other devices Home-page: https://github.com/liquidctl/liquidctl Author: Jonas Malaco Author-email: jonas@protocubo.io Project-URL: Documentation, https://github.com/liquidctl/liquidctl/blob/main/README.md Project-URL: Changelog, https://github.com/liquidctl/liquidctl/blob/main/CHANGELOG.md Project-URL: Source, https://github.com/liquidctl/liquidctl Keywords: aio,aquacomputer,asus,clc,cli,corsair,cross-platform,dram,driver,evga,fan-controller,gigabyte,hue2,kraken,led-controller,liquid-cooler,nzxt,power-supply,smart-device Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: Intended Audience :: End Users/Desktop Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.9 Classifier: Topic :: System :: Hardware :: Hardware Drivers Requires-Python: >=3.9 Description-Content-Type: text/markdown License-File: LICENSE.txt Requires-Dist: colorlog Requires-Dist: crcmod==1.7 Requires-Dist: docopt Requires-Dist: hidapi Requires-Dist: pyusb Requires-Dist: pillow Requires-Dist: libusb-package; sys_platform == "win32" or sys_platform == "cygwin" Requires-Dist: winusbcdc>=1.5; sys_platform == "win32" Requires-Dist: smbus; sys_platform == "linux" Dynamic: license-file # liquidctl – liquid cooler control Cross-platform tool and drivers for liquid coolers and other devices. Go to the [project homepage] for more information. ``` $ liquidctl list Device #0: Corsair Vengeance RGB DIMM2 Device #1: Corsair Vengeance RGB DIMM4 Device #2: NZXT Smart Device (V1) Device #3: NZXT Kraken X (X42, X52, X62 or X72) # liquidctl initialize all NZXT Smart Device (V1) ├── Firmware version 1.7 ├── LED accessories 2 ├── LED accessory type HUE+ Strip └── LED count (total) 20 NZXT Kraken X (X42, X52, X62 or X72) └── Firmware version 6.2 # liquidctl status NZXT Smart Device (V1) ├── Fan 1 speed 1499 rpm ├── Fan 1 voltage 11.91 V ├── Fan 1 current 0.05 A ├── Fan 1 control mode PWM ├── Fan 2 [...] ├── Fan 3 [...] └── Noise level 61 dB NZXT Kraken X (X42, X52, X62 or X72) ├── Liquid temperature 34.7 °C ├── Fan speed 798 rpm └── Pump speed 2268 rpm # liquidctl status --match vengeance --unsafe=smbus,vengeance_rgb Corsair Vengeance RGB DIMM2 └── Temperature 37.5 °C Corsair Vengeance RGB DIMM4 └── Temperature 37.8 °C # liquidctl --match kraken set fan speed 20 30 30 50 34 80 40 90 50 100 # liquidctl --match kraken set pump speed 70 # liquidctl --match kraken set sync color fixed 0080ff # liquidctl --match "smart device" set led color moving-alternating "hsv(30,98,100)" "hsv(30,98,10)" --speed slower ``` [project homepage]: https://github.com/liquidctl/liquidctl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/SOURCES.txt0000644000175000017500000001145314776655073020236 0ustar00jonasjonas.gitignore CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE.txt MANIFEST.in README.md SECURITY.md conftest.py liquidctl.8 pyproject.toml setup.cfg tox.ini docs/README.md docs/aquacomputer-d5next-guide.md docs/aquacomputer-farbwerk360-guide.md docs/aquacomputer-octo-guide.md docs/aquacomputer-quadro-guide.md docs/asetek-690lc-guide.md docs/asetek-pro-guide.md docs/asus-aura-led-guide.md docs/asus-ryujin-guide.md docs/coolit-guide.md docs/corsair-commander-core-guide.md docs/corsair-commander-guide.md docs/corsair-hxi-rmi-psu-guide.md docs/corsair-platinum-pro-xt-guide.md docs/ddr4-guide.md docs/gigabyte-rgb-fusion2-guide.md docs/kraken-x2-m2-guide.md docs/kraken-x3-z3-guide.md docs/msi-mpg-coreliquid-guide.md docs/nvidia-guide.md docs/nzxt-e-series-psu-guide.md docs/nzxt-hue2-guide.md docs/nzxt-smart-device-v1-guide.md docs/developer/capturing-usb-traffic.md docs/developer/creating-vm-for-capture.md docs/developer/hwmon.md docs/developer/porting-drivers-from-opencorsairlink.md docs/developer/process.md docs/developer/release-checklist.md docs/developer/style-guide.md docs/developer/techniques-for-analyzing-usb-protocols.md docs/developer/images/create_vm.png docs/developer/images/create_vm_1.png docs/developer/images/create_vm_10.png docs/developer/images/create_vm_2.png docs/developer/images/create_vm_3.png docs/developer/images/create_vm_4.png docs/developer/images/create_vm_5.png docs/developer/images/create_vm_6.png docs/developer/images/create_vm_7.png docs/developer/images/create_vm_8.png docs/developer/images/create_vm_9.png docs/developer/images/icue.png docs/developer/images/vendor_product_id.png docs/developer/images/wireshark_1.png docs/developer/images/wireshark_2.png docs/developer/images/wireshark_3.png docs/developer/images/wireshark_4.png docs/developer/protocol/aquacomputer.md docs/developer/protocol/asus_ryujin.md docs/developer/protocol/commander_core.md docs/developer/protocol/coolit.md docs/developer/protocol/coreliquid.md docs/developer/protocol/lighting_node_rgb.md docs/developer/protocol/nzxt_x3-z3-2023.md docs/developer/protocol/vengeance_rgb.md docs/linux/making-systemd-units-wait-for-devices.md docs/windows/running-your-first-command-line-program.md extra/krakenduty-poc.py extra/liquiddump.py extra/prometheus-liquidctl-exporter.py extra/yoda.py extra/completions/liquidctl.bash extra/contrib/fusion_rgb_cycle.py extra/contrib/liquidctlfan/README.md extra/contrib/liquidctlfan/liquidctlfan extra/contrib/liquidctlfan/systemd/liquidctlfan.service extra/dist/pypi-readme.md extra/linux/71-liquidctl.rules extra/linux/generate-uaccess-udev-rules.py extra/old-tests/asetek_legacy extra/old-tests/asetek_modern extra/old-tests/asetek_modern_rainbow extra/old-tests/kraken_two extra/windows/LQiNFO.py extra/windows/liquidctl_logo_v1_circle_256.ico liquidctl/__init__.py liquidctl/__main__.py liquidctl/_version.py liquidctl/cli.py liquidctl/error.py liquidctl/keyval.py liquidctl/pmbus.py liquidctl/util.py liquidctl/version.py liquidctl.egg-info/PKG-INFO liquidctl.egg-info/SOURCES.txt liquidctl.egg-info/dependency_links.txt liquidctl.egg-info/entry_points.txt liquidctl.egg-info/requires.txt liquidctl.egg-info/top_level.txt liquidctl/driver/__init__.py liquidctl/driver/aquacomputer.py liquidctl/driver/asetek.py liquidctl/driver/asetek_pro.py liquidctl/driver/asus_ryujin.py liquidctl/driver/aura_led.py liquidctl/driver/base.py liquidctl/driver/commander_core.py liquidctl/driver/commander_pro.py liquidctl/driver/coolit.py liquidctl/driver/corsair_hid_psu.py liquidctl/driver/ddr4.py liquidctl/driver/hwmon.py liquidctl/driver/hydro_platinum.py liquidctl/driver/kraken2.py liquidctl/driver/kraken3.py liquidctl/driver/msi.py liquidctl/driver/nvidia.py liquidctl/driver/nzxt_epsu.py liquidctl/driver/rgb_fusion2.py liquidctl/driver/smart_device.py liquidctl/driver/smbus.py liquidctl/driver/usb.py tests/_testutils.py tests/rgb.gif tests/test_api.py tests/test_aquacomputer.py tests/test_asetek.py tests/test_asetek_pro.py tests/test_asus_ryujin.py tests/test_aura_led.py tests/test_backward_compatibility_10.py tests/test_backward_compatibility_11.py tests/test_backward_compatibility_112.py tests/test_backward_compatibility_12.py tests/test_backward_compatibility_13.py tests/test_backward_compatibility_14.py tests/test_backward_compatibility_15.py tests/test_backward_compatibility_18.py tests/test_cli.py tests/test_commander_core.py tests/test_commander_pro.py tests/test_coolit.py tests/test_corsair_hid_psu.py tests/test_ddr4.py tests/test_hidapi_device.py tests/test_hwmon.py tests/test_hydro_platinum.py tests/test_keyval.py tests/test_kraken2.py tests/test_kraken3.py tests/test_krakenz3_response.py tests/test_msi.py tests/test_nvidia.py tests/test_nzxt_epsu.py tests/test_nzxt_h1_v2.py tests/test_rgb_fusion2.py tests/test_smart_device.py tests/test_smart_device2.py tests/test_smbus.py tests/test_usb.py tests/yellow.jpg././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/dependency_links.txt0000644000175000017500000000000114776655073022414 0ustar00jonasjonas ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/entry_points.txt0000644000175000017500000000006114776655073021641 0ustar00jonasjonas[console_scripts] liquidctl = liquidctl.cli:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/requires.txt0000644000175000017500000000030414776655073020743 0ustar00jonasjonascolorlog crcmod==1.7 docopt hidapi pyusb pillow [:sys_platform == "linux"] smbus [:sys_platform == "win32"] winusbcdc>=1.5 [:sys_platform == "win32" or sys_platform == "cygwin"] libusb-package ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1744525883.0 liquidctl-1.15.0/liquidctl.egg-info/top_level.txt0000644000175000017500000000001214776655073021071 0ustar00jonasjonasliquidctl ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/pyproject.toml0000644000175000017500000000501214677373312015565 0ustar00jonasjonas[build-system] requires = [ "setuptools >= 45", "setuptools_scm[toml] >= 6.2", "wheel", ] build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "liquidctl/_version.py" # keep the following parameters in sync with liquidctl/version.py version_scheme = "release-branch-semver" [tool.pytest.ini_options] addopts = "--doctest-modules -ra" [tool.black] line-length = 100 target-version = ["py38"] extend-exclude = ''' /* To have all items start with |, this isn't a real comment; still, it shouldn't match any real path. */ # don't override what's automatically generated by setuptools_scm | (^/liquidctl/_version\.py$) # exclude some old files, pending a future conversion; by default, any new # files should *not* be excluded | (^/extra/contrib/fusion_rgb_cycle\.py$) | (^/extra/windows/LQiNFO\.py$) | (^/liquidctl/cli\.py$) | (^/liquidctl/driver/__init__\.py$) | (^/liquidctl/driver/asetek\.py$) | (^/liquidctl/driver/asetek_pro\.py$) | (^/liquidctl/driver/commander_core\.py$) | (^/liquidctl/driver/commander_pro\.py$) | (^/liquidctl/driver/corsair_hid_psu\.py$) | (^/liquidctl/driver/ddr4\.py$) | (^/liquidctl/driver/hydro_platinum\.py$) | (^/liquidctl/driver/kraken2\.py$) | (^/liquidctl/driver/nvidia\.py$) | (^/liquidctl/driver/nzxt_epsu\.py$) | (^/liquidctl/driver/rgb_fusion2\.py$) | (^/liquidctl/driver/smart_device\.py$) | (^/liquidctl/driver/smbus\.py$) | (^/liquidctl/driver/usb\.py$) | (^/liquidctl/error\.py$) | (^/liquidctl/extraversion\.py$) | (^/liquidctl/keyval\.py$) | (^/liquidctl/pmbus\.py$) | (^/liquidctl/util\.py$) | (^/tests/_testutils\.py$) | (^/tests/test_api\.py$) | (^/tests/test_asetek\.py$) | (^/tests/test_asetek_pro\.py$) | (^/tests/test_backward_compatibility_10\.py$) | (^/tests/test_backward_compatibility_11\.py$) | (^/tests/test_backward_compatibility_12\.py$) | (^/tests/test_backward_compatibility_13\.py$) | (^/tests/test_backward_compatibility_14\.py$) | (^/tests/test_backward_compatibility_15\.py$) | (^/tests/test_cli\.py$) | (^/tests/test_commander_core\.py$) | (^/tests/test_commander_pro\.py$) | (^/tests/test_corsair_hid_psu\.py$) | (^/tests/test_ddr4\.py$) | (^/tests/test_hidapi_device\.py$) | (^/tests/test_hydro_platinum\.py$) | (^/tests/test_keyval\.py$) | (^/tests/test_kraken2\.py$) | (^/tests/test_nvidia\.py$) | (^/tests/test_nzxt_epsu\.py$) | (^/tests/test_rgb_fusion2\.py$) | (^/tests/test_smart_device\.py$) | (^/tests/test_smbus\.py$) | (^/tests/test_usb\.py$) ''' ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1744525883.739173 liquidctl-1.15.0/setup.cfg0000644000175000017500000000313514776655074014506 0ustar00jonasjonas[metadata] name = liquidctl url = https://github.com/liquidctl/liquidctl description = Cross-platform tool and drivers for liquid coolers and other devices long_description = file: extra/dist/pypi-readme.md long_description_content_type = text/markdown author = Jonas Malaco author_email = jonas@protocubo.io classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers Intended Audience :: End Users/Desktop License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+) Operating System :: OS Independent Programming Language :: Python :: 3.13 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.9 Topic :: System :: Hardware :: Hardware Drivers keywords = aio aquacomputer asus clc cli corsair cross-platform dram driver evga fan-controller gigabyte hue2 kraken led-controller liquid-cooler nzxt power-supply smart-device project_urls = Documentation = https://github.com/liquidctl/liquidctl/blob/main/README.md Changelog = https://github.com/liquidctl/liquidctl/blob/main/CHANGELOG.md Source = https://github.com/liquidctl/liquidctl [options] packages = find: python_requires = >=3.9 setup_requires = setuptools_scm install_requires = colorlog crcmod==1.7 docopt hidapi pyusb pillow libusb-package; sys_platform == 'win32' or sys_platform == 'cygwin' winusbcdc>=1.5; sys_platform == 'win32' smbus; sys_platform == "linux" [options.entry_points] console_scripts = liquidctl = liquidctl.cli:main [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1744525883.7381845 liquidctl-1.15.0/tests/0000755000175000017500000000000014776655074014025 5ustar00jonasjonas././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715785037.0 liquidctl-1.15.0/tests/_testutils.py0000644000175000017500000002054714621146515016566 0ustar00jonasjonasimport os from collections import deque, namedtuple from datetime import timedelta from enum import Enum, unique from tempfile import mkdtemp from liquidctl.driver.base import * from liquidctl.keyval import RuntimeStorage, _FilesystemBackend from liquidctl.driver.usb import _DEFAULT_TIMEOUT_MS Report = namedtuple('Report', ['number', 'data']) def noop(*args, **kwargs): return None class MockRuntimeStorage(RuntimeStorage): def __init__(self, key_prefixes, backend=None): if not backend: run_dir = mkdtemp('run_dir') backend = _FilesystemBackend(key_prefixes, runtime_dirs=[run_dir]) super().__init__(key_prefixes, backend) class MockHidapiDevice: def __init__(self, vendor_id=None, product_id=None, release_number=None, serial_number=None, bus=None, address=None, path=None): self.vendor_id = vendor_id self.product_id = product_id self.release_number = release_number self.serial_number = serial_number self.bus = bus self.address = address self.path = path or b'' self.port = None self.open = noop self.close = noop self.clear_enqueued_reports = noop self._read = deque() self.sent = list() def preload_read(self, report): self._read.append(report) def read(self, length, *, timeout=_DEFAULT_TIMEOUT_MS): if self._read: number, data = self._read.popleft() if number: return [number] + list(data)[:length] else: return list(data)[:length] return None def write(self, data): data = bytes(data) # ensure data is convertible to bytes self.sent.append(Report(data[0], list(data[1:]))) return len(data) def get_feature_report(self, report_id, length): if self._read: try: report = next(filter(lambda x: x.number == report_id, self._read)) number, data = report self._read.remove(report) except StopIteration: return None # length dictates the size of the buffer, and if it's not large # enough "ioctl (GFEATURE): Value too large for defined data type" # may happen on Linux; see: # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-665119675 assert length >= len(data) + 1, 'buffer not large enough for received report' return [number] + list(data)[:length] return None def send_feature_report(self, data): return self.write(data) class MockPyusbDevice(): def __init__(self, vendor_id=None, product_id=None, release_number=None, serial_number=None, bus=None, address=None, port=None): assert port is None, \ 'port numbers are only available on libusb-1.0 and should not be dependended on' self.vendor_id = vendor_id self.product_id = product_id self.release_number = release_number self.serial_number = serial_number self.bus = bus self.address = address self.port = port self.open = noop self.claim = noop self.release = noop self.close = noop self._reset_sent() def read(self, endpoint, length, *, timeout=_DEFAULT_TIMEOUT_MS): if len(self._responses): return self._responses.popleft() return [0] * length def write(self, endpoint, data, *, timeout=_DEFAULT_TIMEOUT_MS): self._sent_xfers.append(('write', endpoint, data)) def ctrl_transfer(self, bmRequestType, bRequest, wValue=0, wIndex=0, data_or_wLength=None, timeout=_DEFAULT_TIMEOUT_MS): self._sent_xfers.append(('ctrl_transfer', bmRequestType, bRequest, wValue, wIndex, data_or_wLength)) def _reset_sent(self): self._sent_xfers = list() self._responses = deque() VirtualEeprom = namedtuple('VirtualEeprom', ['name', 'data']) class VirtualSmbus: def __init__(self, address_count=256, register_count=256, name='i2c-99', description='Virtual', parent_vendor=0xff01, parent_device=0xff02, parent_subsystem_vendor=0xff10, parent_subsystem_device=0xff20, parent_driver='virtual'): self._open = False self._data = [[0] * register_count for _ in range(address_count)] self.name = name self.description = description self.parent_vendor = parent_vendor self.parent_device = parent_device self.parent_subsystem_vendor = parent_subsystem_vendor self.parent_subsystem_device = parent_subsystem_device self.parent_driver = parent_driver def open(self): self._open = True def read_byte(self, address): if not self._open: raise OSError('closed') return self._data[address][0] def read_byte_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def read_word_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def read_block_data(self, address, register): if not self._open: raise OSError('closed') return self._data[address][register] def write_byte(self, address, value): if not self._open: raise OSError('closed') self._data[address][0] = value def write_byte_data(self, address, register, value): if not self._open: raise OSError('closed') self._data[address][register] = value def write_word_data(self, address, register, value): if not self._open: raise OSError('closed') self._data[address][register] = value def write_block_data(self, address, register, data): if not self._open: raise OSError('closed') self._data[address][register] = data def close(self): self._open = False def emulate_eeprom_at(self, address, name, data): self._data[address] = VirtualEeprom(name, data) # hack def load_eeprom(self, address): return self._data[address] # hack @unique class VirtualControlMode(Enum): QUIET = 0x0 BALANCED = 0x1 EXTREME = 0x2 CallArgs = namedtuple('CallArgs', ['args', 'kwargs']) class VirtualBusDevice(BaseDriver): def __init__(self, *args, **kwargs): self.call_args = dict() self.call_args['__init__'] = CallArgs(args, kwargs) self.connected = False def connect(self, *args, **kwargs): self.call_args['connect'] = CallArgs(args, kwargs) self.connected = True return self def disconnect(self, *args, **kwargs): self.call_args['disconnect'] = CallArgs(args, kwargs) self.connected = False def initialize(self, *args, **kwargs): self.call_args['initialize'] = CallArgs(args, kwargs) return [ ('Firmware version', '3.14.16', ''), ] def get_status(self, *args, **kwargs): self.call_args['status'] = CallArgs(args, kwargs) return [ ('Temperature', 30.4, '°C'), ('Fan control mode', VirtualControlMode.QUIET, ''), ('Animation', None, ''), ('Uptime', timedelta(hours=18, minutes=23, seconds=12), ''), ('Hardware mode', True, ''), ] def set_fixed_speed(self, *args, **kwargs): self.call_args['set_fixed_speed'] = CallArgs(args, kwargs) def set_speed_profile(self, *args, **kwargs): self.call_args['set_speed_profile'] = CallArgs(args, kwargs) def set_color(self, *args, **kwargs): self.call_args['set_color'] = CallArgs(args, kwargs) @property def description(self): return 'Virtual Bus Device' @property def vendor_id(self): return 0x1234 @property def product_id(self): return 0xabcd @property def release_number(self): None @property def serial_number(self): raise OSError() @property def bus(self): return 'virtual' @property def address(self): return 'virtual_address' @property def port(self): return None class VirtualBus(BaseBus): def find_devices(self, **kwargs): yield from [VirtualBusDevice()] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/rgb.gif0000644000175000017500000001417614562144457015266 0ustar00jonasjonasGIF89a! NETSCAPE2.0! XMP DataXMP ~}|{zyxwvutsrqponmlkjihgfedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543210/.-,+*)('&%$#"!  !,ڋ޼H扦ʶ L ĢL*̦ JԪjܮ N (8HXhx)9IYiy *:JZjz +;K[k{ ,N^n~/?O_o0 <0… :|1ĉ+Z1ƍ;z2ȑ$K<2ʕ,[| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 5ԩTZ5֭\z 6رd˚=6ڵlۺ} 7ܹtڽ7޽| 8 >8Ō;~ 9ɔ+[9͜;{ :ѤK>:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> iNJi^ini~ jJjjj kJkފkk lKll.l> mNKm^mnm~ nKn枋nn oKoދoo pLpp /p? qOLq_qoq r"Lr&r*r. s2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{{|O||/|?}OO}_}o}~O~柏~~Oߏ p,*p lJp/ jp?p$, Op,l _p4 op< qD,$*qLl(JqT,jq\0qd,ψ4qll8qtN^n~/?O_o0 <0… :|1ĉ+Z1ƍ;z2ȑ$K<2ʕ,[| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 5ԩTZ5֭\z 6رd˚=6ڵlۺ} 7ܹtڽ7޽| 8 >8Ō;~ 9ɔ+[9͜;{ :ѤK>:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> iNJi^ini~ jJjjj kJkފkk lKll.l> mNKm^mnm~ nKn枋nn oKoދoo pLpp /p? qOLq_qoq r"Lr&r*r. s2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{{|O||/|?}OO}_}o}~O~柏~~Oߏ p,*p lJp/ jp?p$, Op,l _p4 op< qD,$*qLl(JqT,jq\0qd,ψ4qll8qtN^n~/?O_o0 <0… :|1ĉ+Z1ƍ;z2ȑ$K<2ʕ,[| 3̙4kڼ3Ν<{ 4СD=4ҥL:} 5ԩTZ5֭\z 6رd˚=6ڵlۺ} 7ܹtڽ7޽| 8 >8Ō;~ 9ɔ+[9͜;{ :ѤK>:լ[~ ;ٴk۾;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdFdJ.dN> eRNIeV^eZne^~ fbIfffjfn grIgvމgzg~ hJhh.h> iNJi^ini~ jJjjj kJkފkk lKll.l> mNKm^mnm~ nKn枋nn oKoދoo pLpp /p? qOLq_qoq r"Lr&r*r. s2Ls6ߌs:s> tBMtFtJ/tN? uROMuV_uZou^ vbMvfvjvn wrMwvߍwzw~ xNxx/x?yONy_yoyz袏Nz馟zꪯz뮿{N{ߎ{{|O||/|?}OO}_}o}~O~柏~~Oߏ p,*p lJp/ jp?p$, Op,l _p4 op< qD,$*qLl(JqT,jq\0qd,ψ4qll8qt assert fan_report.number == 3 assert fan_report.data[0x40:0x43] == [0, 19, 136] # 0, <5000> # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" assert (tmp_path / "pwm2_enable").read_text() == "0" assert (tmp_path / "pwm2").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_d5next_set_fixed_speeds_hwmon(mockD5NextDevice, has_support, tmp_path): mockD5NextDevice._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1").write_text("0\n") (tmp_path / "pwm1_enable").write_text("0\n") (tmp_path / "pwm2").write_text("0\n") (tmp_path / "pwm2_enable").write_text("0\n") mockD5NextDevice.set_fixed_speed("pump", 84) mockD5NextDevice.set_fixed_speed("fan", 50) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "1" assert (tmp_path / "pwm1").read_text() == "214" assert (tmp_path / "pwm2_enable").read_text() == "1" assert (tmp_path / "pwm2").read_text() == "127" else: # Assert fallback to direct access pump_report, fan_report = mockD5NextDevice.device.sent assert pump_report.number == 3 assert pump_report.data[0x95:0x98] == [0, 32, 208] # 0, <8400> assert fan_report.number == 3 assert fan_report.data[0x40:0x43] == [0, 19, 136] # 0, <5000> def test_d5next_speed_profiles_not_supported(mockD5NextDevice): with pytest.raises(NotSupportedByDriver): mockD5NextDevice.set_speed_profile("fan", None) with pytest.raises(NotSupportedByDriver): mockD5NextDevice.set_speed_profile("pump", None) @pytest.fixture def mockFarbwerk360Device(): device = _MockFarbwerk360Device() dev = Aquacomputer( device, "Mock Aquacomputer Farbwerk 360", device_info=Aquacomputer._DEVICE_INFO[Aquacomputer._DEVICE_FARBWERK360], ) dev.connect() return dev class _MockFarbwerk360Device(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0x0C70, product_id=0xF010) self.preload_read(Report(1, FARBWERK360_SAMPLE_STATUS_REPORT)) def read(self, length): pre = super().read(length) if pre: return pre return Report(1, FARBWERK360_SAMPLE_STATUS_REPORT) def test_farbwerk360_connect(mockFarbwerk360Device): def mock_open(): nonlocal opened opened = True mockFarbwerk360Device.device.open = mock_open opened = False with mockFarbwerk360Device.connect() as cm: assert cm == mockFarbwerk360Device assert opened def test_farbwerk360_initialize(mockFarbwerk360Device): init_result = mockFarbwerk360Device.initialize() # Verify firmware version assert init_result[0][1] == 1022 # Verify serial number assert init_result[1][1] == "16827-56978" @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_farbwerk360_get_status_directly(mockFarbwerk360Device, has_hwmon, direct_access): if has_hwmon: mockFarbwerk360Device._hwmon = HwmonDevice(None, None) got = mockFarbwerk360Device.get_status(direct_access=direct_access) expected = [ ("Sensor 1", pytest.approx(26.2, 0.1), "°C"), ("Sensor 2", pytest.approx(26.3, 0.1), "°C"), ("Sensor 3", pytest.approx(26.7, 0.1), "°C"), ("Sensor 4", pytest.approx(25.5, 0.1), "°C"), ("Soft. Sensor 1", pytest.approx(52.00, 0.1), "°C"), ] assert sorted(got) == sorted(expected) def test_farbwerk360_get_status_from_hwmon(mockFarbwerk360Device, tmp_path): mockFarbwerk360Device._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "temp1_input").write_text("26200\n") (tmp_path / "temp2_input").write_text("26310\n") (tmp_path / "temp3_input").write_text("26710\n") (tmp_path / "temp4_input").write_text("25520\n") (tmp_path / "temp5_input").write_text("50000\n") # Soft. Sensor 1 temperature (tmp_path / "temp6_input").write_text("60000\n") # Soft. Sensor 2 temperature (tmp_path / "temp7_input").write_text("50000\n") # Soft. Sensor 3 temperature (tmp_path / "temp8_input").write_text("50000\n") # Soft. Sensor 4 temperature (tmp_path / "temp9_input").write_text("50000\n") # Soft. Sensor 5 temperature (tmp_path / "temp10_input").write_text("50000\n") # Soft. Sensor 6 temperature (tmp_path / "temp11_input").write_text("50000\n") # Soft. Sensor 7 temperature (tmp_path / "temp12_input").write_text("50000\n") # Soft. Sensor 8 temperature (tmp_path / "temp13_input").write_text("50000\n") # Soft. Sensor 9 temperature (tmp_path / "temp14_input").write_text("50000\n") # Soft. Sensor 10 temperature (tmp_path / "temp15_input").write_text("50000\n") # Soft. Sensor 11 temperature (tmp_path / "temp16_input").write_text("50000\n") # Soft. Sensor 12 temperature (tmp_path / "temp17_input").write_text("50000\n") # Soft. Sensor 13 temperature (tmp_path / "temp18_input").write_text("50000\n") # Soft. Sensor 14 temperature (tmp_path / "temp19_input").write_text("50000\n") # Soft. Sensor 15 temperature (tmp_path / "temp20_input").write_text("50000\n") # Soft. Sensor 16 temperature got = mockFarbwerk360Device.get_status() expected = [ ("Sensor 1", pytest.approx(26.2, 0.1), "°C"), ("Sensor 2", pytest.approx(26.3, 0.1), "°C"), ("Sensor 3", pytest.approx(26.7, 0.1), "°C"), ("Sensor 4", pytest.approx(25.5, 0.1), "°C"), ("Soft. Sensor 1", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 2", pytest.approx(60, 0.1), "°C"), ("Soft. Sensor 3", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 4", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 5", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 6", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 7", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 8", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 9", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 10", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 11", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 12", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 13", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 14", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 15", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 16", pytest.approx(50, 0.1), "°C"), ] assert sorted(got) == sorted(expected) def test_farbwerk360_set_fixed_speeds_not_supported(mockFarbwerk360Device): with pytest.raises(NotSupportedByDevice): mockFarbwerk360Device.set_fixed_speed("fan", 42) def test_farbwerk360_speed_profiles_not_supported(mockFarbwerk360Device): with pytest.raises(NotSupportedByDevice): mockFarbwerk360Device.set_speed_profile("fan", None) @pytest.fixture def mockOctoDevice(): device = _MockOctoDevice() dev = Aquacomputer( device, "Mock Aquacomputer Octo", device_info=Aquacomputer._DEVICE_INFO[Aquacomputer._DEVICE_OCTO], ) dev.connect() return dev class _MockOctoDevice(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0x0C70, product_id=0xF011) self.preload_read(Report(1, OCTO_SAMPLE_STATUS_REPORT)) self.preload_read(Report(3, OCTO_SAMPLE_CONTROL_REPORT)) self.preload_read(Report(3, OCTO_SAMPLE_CONTROL_REPORT)) def read(self, length): pre = super().read(length) if pre: return pre return Report(1, OCTO_SAMPLE_STATUS_REPORT) def test_octo_connect(mockOctoDevice): def mock_open(): nonlocal opened opened = True mockOctoDevice.device.open = mock_open opened = False with mockOctoDevice.connect() as cm: assert cm == mockOctoDevice assert opened def test_octo_initialize(mockOctoDevice): init_result = mockOctoDevice.initialize() # Verify firmware version assert init_result[0][1] == 1019 # Verify serial number assert init_result[1][1] == "14994-51690" @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_octo_get_status_directly(mockOctoDevice, has_hwmon, direct_access): if has_hwmon: mockOctoDevice._hwmon = HwmonDevice(None, None) got = mockOctoDevice.get_status(direct_access=direct_access) expected = [ ("Sensor 1", pytest.approx(27.5, 0.1), "°C"), ("Sensor 2", pytest.approx(27.7, 0.1), "°C"), ("Sensor 3", pytest.approx(28.4, 0.1), "°C"), ("Sensor 4", pytest.approx(34.2, 0.1), "°C"), ("Soft. Sensor 1", pytest.approx(37.84, 0.1), "°C"), ("Fan 1 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 1 power", pytest.approx(0.01, 0.1), "W"), ("Fan 1 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 1 current", pytest.approx(0.001, 0.1), "A"), ("Fan 2 speed", pytest.approx(576, 0.1), "rpm"), ("Fan 2 power", pytest.approx(1.03, 0.1), "W"), ("Fan 2 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 2 current", pytest.approx(0.35, 0.1), "A"), ("Fan 3 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 3 power", pytest.approx(0, 0.1), "W"), ("Fan 3 voltage", pytest.approx(0, 0.1), "V"), ("Fan 3 current", pytest.approx(0, 0.1), "A"), ("Fan 4 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 4 power", pytest.approx(0, 0.1), "W"), ("Fan 4 voltage", pytest.approx(0, 0.1), "V"), ("Fan 4 current", pytest.approx(0, 0.1), "A"), ("Fan 5 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 5 power", pytest.approx(0, 0.1), "W"), ("Fan 5 voltage", pytest.approx(0, 0.1), "V"), ("Fan 5 current", pytest.approx(0, 0.1), "A"), ("Fan 6 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 6 power", pytest.approx(0, 0.1), "W"), ("Fan 6 voltage", pytest.approx(0, 0.1), "V"), ("Fan 6 current", pytest.approx(0, 0.1), "A"), ("Fan 7 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 7 power", pytest.approx(0, 0.1), "W"), ("Fan 7 voltage", pytest.approx(0, 0.1), "V"), ("Fan 7 current", pytest.approx(0, 0.1), "A"), ("Fan 8 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 8 power", pytest.approx(0.02, 0.1), "W"), ("Fan 8 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 8 current", pytest.approx(0.002, 0.1), "A"), ] assert sorted(got) == sorted(expected) def test_octo_get_status_from_hwmon(mockOctoDevice, tmp_path): mockOctoDevice._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "temp1_input").write_text("27580\n") (tmp_path / "temp2_input").write_text("27670\n") (tmp_path / "temp3_input").write_text("28370\n") (tmp_path / "temp4_input").write_text("34240\n") (tmp_path / "fan1_input").write_text("0\n") (tmp_path / "power1_input").write_text("10000\n") (tmp_path / "in0_input").write_text("12090\n") (tmp_path / "curr1_input").write_text("1\n") (tmp_path / "fan2_input").write_text("576\n") (tmp_path / "power2_input").write_text("1030000\n") (tmp_path / "in1_input").write_text("12090\n") (tmp_path / "curr2_input").write_text("350\n") (tmp_path / "fan3_input").write_text("0\n") (tmp_path / "power3_input").write_text("0\n") (tmp_path / "in2_input").write_text("0\n") (tmp_path / "curr3_input").write_text("0\n") (tmp_path / "fan4_input").write_text("0\n") (tmp_path / "power4_input").write_text("0\n") (tmp_path / "in3_input").write_text("0\n") (tmp_path / "curr4_input").write_text("0\n") (tmp_path / "fan5_input").write_text("0\n") (tmp_path / "power5_input").write_text("0\n") (tmp_path / "in4_input").write_text("0\n") (tmp_path / "curr5_input").write_text("0\n") (tmp_path / "fan6_input").write_text("0\n") (tmp_path / "power6_input").write_text("0\n") (tmp_path / "in5_input").write_text("0\n") (tmp_path / "curr6_input").write_text("0\n") (tmp_path / "fan7_input").write_text("0\n") (tmp_path / "power7_input").write_text("0\n") (tmp_path / "in6_input").write_text("0\n") (tmp_path / "curr7_input").write_text("0\n") (tmp_path / "fan8_input").write_text("0\n") (tmp_path / "power8_input").write_text("20000\n") (tmp_path / "in7_input").write_text("12090\n") (tmp_path / "curr8_input").write_text("2\n") (tmp_path / "temp5_input").write_text("50000\n") # Soft. Sensor 1 temperature (tmp_path / "temp6_input").write_text("60000\n") # Soft. Sensor 2 temperature (tmp_path / "temp7_input").write_text("50000\n") # Soft. Sensor 3 temperature (tmp_path / "temp8_input").write_text("50000\n") # Soft. Sensor 4 temperature (tmp_path / "temp9_input").write_text("50000\n") # Soft. Sensor 5 temperature (tmp_path / "temp10_input").write_text("50000\n") # Soft. Sensor 6 temperature (tmp_path / "temp11_input").write_text("50000\n") # Soft. Sensor 7 temperature (tmp_path / "temp12_input").write_text("50000\n") # Soft. Sensor 8 temperature (tmp_path / "temp13_input").write_text("50000\n") # Soft. Sensor 9 temperature (tmp_path / "temp14_input").write_text("50000\n") # Soft. Sensor 10 temperature (tmp_path / "temp15_input").write_text("50000\n") # Soft. Sensor 11 temperature (tmp_path / "temp16_input").write_text("50000\n") # Soft. Sensor 12 temperature (tmp_path / "temp17_input").write_text("50000\n") # Soft. Sensor 13 temperature (tmp_path / "temp18_input").write_text("50000\n") # Soft. Sensor 14 temperature (tmp_path / "temp19_input").write_text("50000\n") # Soft. Sensor 15 temperature (tmp_path / "temp20_input").write_text("50000\n") # Soft. Sensor 16 temperature got = mockOctoDevice.get_status() expected = [ ("Sensor 1", pytest.approx(27.5, 0.1), "°C"), ("Sensor 2", pytest.approx(27.7, 0.1), "°C"), ("Sensor 3", pytest.approx(28.4, 0.1), "°C"), ("Sensor 4", pytest.approx(34.2, 0.1), "°C"), ("Fan 1 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 1 power", pytest.approx(0.01, 0.1), "W"), ("Fan 1 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 1 current", pytest.approx(0.001, 0.1), "A"), ("Fan 2 speed", pytest.approx(576, 0.1), "rpm"), ("Fan 2 power", pytest.approx(1.03, 0.1), "W"), ("Fan 2 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 2 current", pytest.approx(0.35, 0.1), "A"), ("Fan 3 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 3 power", pytest.approx(0, 0.1), "W"), ("Fan 3 voltage", pytest.approx(0, 0.1), "V"), ("Fan 3 current", pytest.approx(0, 0.1), "A"), ("Fan 4 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 4 power", pytest.approx(0, 0.1), "W"), ("Fan 4 voltage", pytest.approx(0, 0.1), "V"), ("Fan 4 current", pytest.approx(0, 0.1), "A"), ("Fan 5 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 5 power", pytest.approx(0, 0.1), "W"), ("Fan 5 voltage", pytest.approx(0, 0.1), "V"), ("Fan 5 current", pytest.approx(0, 0.1), "A"), ("Fan 6 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 6 power", pytest.approx(0, 0.1), "W"), ("Fan 6 voltage", pytest.approx(0, 0.1), "V"), ("Fan 6 current", pytest.approx(0, 0.1), "A"), ("Fan 7 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 7 power", pytest.approx(0, 0.1), "W"), ("Fan 7 voltage", pytest.approx(0, 0.1), "V"), ("Fan 7 current", pytest.approx(0, 0.1), "A"), ("Fan 8 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 8 power", pytest.approx(0.02, 0.1), "W"), ("Fan 8 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 8 current", pytest.approx(0.002, 0.1), "A"), ("Soft. Sensor 1", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 2", pytest.approx(60, 0.1), "°C"), ("Soft. Sensor 3", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 4", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 5", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 6", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 7", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 8", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 9", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 10", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 11", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 12", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 13", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 14", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 15", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 16", pytest.approx(50, 0.1), "°C"), ] assert sorted(got) == sorted(expected) @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_octo_set_fixed_speeds_directly(mockOctoDevice, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mockOctoDevice._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") (tmp_path / "pwm2").write_text("0") (tmp_path / "pwm2_enable").write_text("0") mockOctoDevice.set_fixed_speed("fan1", 84, direct_access=direct_access) mockOctoDevice.set_fixed_speed("fan2", 50, direct_access=direct_access) pump_report, fan_report = mockOctoDevice.device.sent assert pump_report.number == 3 assert pump_report.data[0x59:0x5C] == [0, 32, 208] # 0, <8400> assert fan_report.number == 3 assert fan_report.data[0xAE:0xB1] == [0, 19, 136] # 0, <5000> # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" assert (tmp_path / "pwm2_enable").read_text() == "0" assert (tmp_path / "pwm2").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_octo_set_fixed_speeds_hwmon(mockOctoDevice, has_support, tmp_path): mockOctoDevice._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1").write_text("0\n") (tmp_path / "pwm1_enable").write_text("0\n") (tmp_path / "pwm2").write_text("0\n") (tmp_path / "pwm2_enable").write_text("0\n") mockOctoDevice.set_fixed_speed("fan1", 84) mockOctoDevice.set_fixed_speed("fan2", 50) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "1" assert (tmp_path / "pwm1").read_text() == "214" assert (tmp_path / "pwm2_enable").read_text() == "1" assert (tmp_path / "pwm2").read_text() == "127" else: # Assert fallback to direct access pump_report, fan_report = mockOctoDevice.device.sent assert pump_report.number == 3 assert pump_report.data[0x59:0x5C] == [0, 32, 208] # 0, <8400> assert fan_report.number == 3 assert fan_report.data[0xAE:0xB1] == [0, 19, 136] # 0, <5000> def test_octo_speed_profiles_not_supported(mockOctoDevice): with pytest.raises(NotSupportedByDriver): mockOctoDevice.set_speed_profile("fan", None) @pytest.fixture def mockQuadroDevice(): device = _MockQuadroDevice() dev = Aquacomputer( device, "Mock Aquacomputer Quadro", device_info=Aquacomputer._DEVICE_INFO[Aquacomputer._DEVICE_QUADRO], ) dev.connect() return dev class _MockQuadroDevice(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0x0C70, product_id=0xF00D) self.preload_read(Report(1, QUADRO_SAMPLE_STATUS_REPORT)) self.preload_read(Report(3, QUADRO_SAMPLE_CONTROL_REPORT)) self.preload_read(Report(3, QUADRO_SAMPLE_CONTROL_REPORT)) def read(self, length): pre = super().read(length) if pre: return pre return Report(1, QUADRO_SAMPLE_STATUS_REPORT) def test_quadro_connect(mockQuadroDevice): def mock_open(): nonlocal opened opened = True mockQuadroDevice.device.open = mock_open opened = False with mockQuadroDevice.connect() as cm: assert cm == mockQuadroDevice assert opened def test_quadro_initialize(mockQuadroDevice): init_result = mockQuadroDevice.initialize() # Verify firmware version assert init_result[0][1] == 1032 # Verify serial number assert init_result[1][1] == "23410-65344" @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_quadro_get_status_directly(mockQuadroDevice, has_hwmon, direct_access): if has_hwmon: mockQuadroDevice._hwmon = HwmonDevice(None, None) got = mockQuadroDevice.get_status(direct_access=direct_access) expected = [ ("Sensor 3", pytest.approx(16.17, 0.1), "°C"), ("Soft. Sensor 1", pytest.approx(23.9, 0.1), "°C"), ("Soft. Sensor 13", pytest.approx(50.0, 0.1), "°C"), ("Fan 1 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 1 power", pytest.approx(0, 0.1), "W"), ("Fan 1 voltage", pytest.approx(0, 0.1), "V"), ("Fan 1 current", pytest.approx(0, 0.1), "A"), ("Fan 2 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 2 power", pytest.approx(0, 0.1), "W"), ("Fan 2 voltage", pytest.approx(12.07, 0.1), "V"), ("Fan 2 current", pytest.approx(0, 0.1), "A"), ("Fan 3 speed", pytest.approx(356, 0.1), "rpm"), ("Fan 3 power", pytest.approx(0, 0.1), "W"), ("Fan 3 voltage", pytest.approx(12.07, 0.1), "V"), ("Fan 3 current", pytest.approx(0, 0.1), "A"), ("Fan 4 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 4 power", pytest.approx(0, 0.1), "W"), ("Fan 4 voltage", pytest.approx(12.07, 0.1), "V"), ("Fan 4 current", pytest.approx(0, 0.1), "A"), ("Flow sensor", pytest.approx(0, 0.1), "dL/h"), ] assert sorted(got) == sorted(expected) def test_quadro_get_status_from_hwmon(mockQuadroDevice, tmp_path): mockQuadroDevice._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "temp1_input").write_text("27580\n") (tmp_path / "temp2_input").write_text("27670\n") (tmp_path / "temp3_input").write_text("28370\n") (tmp_path / "temp4_input").write_text("34240\n") (tmp_path / "fan1_input").write_text("0\n") (tmp_path / "power1_input").write_text("10000\n") (tmp_path / "in0_input").write_text("12090\n") (tmp_path / "curr1_input").write_text("1\n") (tmp_path / "fan2_input").write_text("576\n") (tmp_path / "power2_input").write_text("1030000\n") (tmp_path / "in1_input").write_text("12090\n") (tmp_path / "curr2_input").write_text("350\n") (tmp_path / "fan3_input").write_text("0\n") (tmp_path / "power3_input").write_text("0\n") (tmp_path / "in2_input").write_text("0\n") (tmp_path / "curr3_input").write_text("0\n") (tmp_path / "fan4_input").write_text("0\n") (tmp_path / "power4_input").write_text("0\n") (tmp_path / "in3_input").write_text("0\n") (tmp_path / "curr4_input").write_text("0\n") (tmp_path / "fan5_input").write_text("603\n") (tmp_path / "temp5_input").write_text("50000\n") # Soft. Sensor 1 temperature (tmp_path / "temp6_input").write_text("60000\n") # Soft. Sensor 2 temperature (tmp_path / "temp7_input").write_text("50000\n") # Soft. Sensor 3 temperature (tmp_path / "temp8_input").write_text("50000\n") # Soft. Sensor 4 temperature (tmp_path / "temp9_input").write_text("50000\n") # Soft. Sensor 5 temperature (tmp_path / "temp10_input").write_text("50000\n") # Soft. Sensor 6 temperature (tmp_path / "temp11_input").write_text("50000\n") # Soft. Sensor 7 temperature (tmp_path / "temp12_input").write_text("50000\n") # Soft. Sensor 8 temperature (tmp_path / "temp13_input").write_text("50000\n") # Soft. Sensor 9 temperature (tmp_path / "temp14_input").write_text("50000\n") # Soft. Sensor 10 temperature (tmp_path / "temp15_input").write_text("50000\n") # Soft. Sensor 11 temperature (tmp_path / "temp16_input").write_text("50000\n") # Soft. Sensor 12 temperature (tmp_path / "temp17_input").write_text("50000\n") # Soft. Sensor 13 temperature (tmp_path / "temp18_input").write_text("50000\n") # Soft. Sensor 14 temperature (tmp_path / "temp19_input").write_text("50000\n") # Soft. Sensor 15 temperature (tmp_path / "temp20_input").write_text("50000\n") # Soft. Sensor 16 temperature got = mockQuadroDevice.get_status() expected = [ ("Sensor 1", pytest.approx(27.5, 0.1), "°C"), ("Sensor 2", pytest.approx(27.7, 0.1), "°C"), ("Sensor 3", pytest.approx(28.4, 0.1), "°C"), ("Sensor 4", pytest.approx(34.2, 0.1), "°C"), ("Fan 1 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 1 power", pytest.approx(0.01, 0.1), "W"), ("Fan 1 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 1 current", pytest.approx(0.001, 0.1), "A"), ("Fan 2 speed", pytest.approx(576, 0.1), "rpm"), ("Fan 2 power", pytest.approx(1.03, 0.1), "W"), ("Fan 2 voltage", pytest.approx(12.09, 0.1), "V"), ("Fan 2 current", pytest.approx(0.35, 0.1), "A"), ("Fan 3 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 3 power", pytest.approx(0, 0.1), "W"), ("Fan 3 voltage", pytest.approx(0, 0.1), "V"), ("Fan 3 current", pytest.approx(0, 0.1), "A"), ("Fan 4 speed", pytest.approx(0, 0.1), "rpm"), ("Fan 4 power", pytest.approx(0, 0.1), "W"), ("Fan 4 voltage", pytest.approx(0, 0.1), "V"), ("Fan 4 current", pytest.approx(0, 0.1), "A"), ("Flow sensor", pytest.approx(603, 0.1), "dL/h"), ("Soft. Sensor 1", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 2", pytest.approx(60, 0.1), "°C"), ("Soft. Sensor 3", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 4", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 5", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 6", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 7", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 8", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 9", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 10", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 11", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 12", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 13", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 14", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 15", pytest.approx(50, 0.1), "°C"), ("Soft. Sensor 16", pytest.approx(50, 0.1), "°C"), ] assert sorted(got) == sorted(expected) @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_quadro_set_fixed_speeds_directly(mockQuadroDevice, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mockQuadroDevice._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") (tmp_path / "pwm2").write_text("0") (tmp_path / "pwm2_enable").write_text("0") mockQuadroDevice.set_fixed_speed("fan1", 84, direct_access=direct_access) mockQuadroDevice.set_fixed_speed("fan2", 50, direct_access=direct_access) pump_report, fan_report = mockQuadroDevice.device.sent assert pump_report.number == 3 assert pump_report.data[0x35:0x38] == [0, 32, 208] # 0, <8400> assert fan_report.number == 3 assert fan_report.data[0x8A:0x8D] == [0, 19, 136] # 0, <5000> # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" assert (tmp_path / "pwm2_enable").read_text() == "0" assert (tmp_path / "pwm2").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_quadro_set_fixed_speeds_hwmon(mockQuadroDevice, has_support, tmp_path): mockQuadroDevice._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1").write_text("0\n") (tmp_path / "pwm1_enable").write_text("0\n") (tmp_path / "pwm2").write_text("0\n") (tmp_path / "pwm2_enable").write_text("0\n") mockQuadroDevice.set_fixed_speed("fan1", 84) mockQuadroDevice.set_fixed_speed("fan2", 50) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "1" assert (tmp_path / "pwm1").read_text() == "214" assert (tmp_path / "pwm2_enable").read_text() == "1" assert (tmp_path / "pwm2").read_text() == "127" else: # Assert fallback to direct access pump_report, fan_report = mockQuadroDevice.device.sent assert pump_report.number == 3 assert pump_report.data[0x35:0x38] == [0, 32, 208] # 0, <8400> assert fan_report.number == 3 assert fan_report.data[0x8A:0x8D] == [0, 19, 136] # 0, <5000> def test_quadro_speed_profiles_not_supported(mockQuadroDevice): with pytest.raises(NotSupportedByDriver): mockQuadroDevice.set_speed_profile("fan", None) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715785037.0 liquidctl-1.15.0/tests/test_asetek.py0000644000175000017500000001320114621146515016667 0ustar00jonasjonasimport pytest from _testutils import MockPyusbDevice, MockRuntimeStorage from collections import deque from liquidctl.driver.asetek import Modern690Lc, Legacy690Lc, Hydro690Lc @pytest.fixture def mockModern690LcDevice(): device = MockPyusbDevice() dev = Modern690Lc(device, 'Mock Modern 690LC') dev.connect() return dev @pytest.fixture def mockLegacy690LcDevice(): device = MockPyusbDevice(vendor_id=0xffff, product_id=0xb200, bus=1, address=2) dev = Legacy690Lc(device, 'Mock Legacy 690LC') runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev def test_modern690Lc_device_not_totally_broken(mockModern690LcDevice): """A few reasonable example calls do not raise exceptions.""" dev = mockModern690LcDevice dev.initialize() dev.get_status() dev.set_color(channel='led', mode='blinking', colors=iter([[3, 2, 1]]), time_per_color=3, time_off=1, alert_threshold=42, alert_color=[90, 80, 10]) dev.set_color(channel='led', mode='rainbow', colors=[], speed=5) dev.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 50), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) def test_modern690Lc_device_connect(mockModern690LcDevice): def mock_open(): nonlocal opened opened = True mockModern690LcDevice.device.open = mock_open opened = False with mockModern690LcDevice.connect() as cm: assert cm == mockModern690LcDevice assert opened def test_modern690Lc_device_begin_transaction(mockModern690LcDevice): mockModern690LcDevice.device._reset_sent() mockModern690LcDevice.get_status() (begin, _) = mockModern690LcDevice.device._sent_xfers xfer_type, bmRequestType, bRequest, wValue, wIndex, datalen = begin assert xfer_type == 'ctrl_transfer' assert bRequest == 2 assert bmRequestType == 0x40 assert wValue == 1 assert wIndex == 0 assert datalen is None def test_modern690Lc_initialize_can_persist_settings(mockModern690LcDevice): dev = mockModern690LcDevice dev.device._reset_sent() dev.initialize(non_volatile=True) _begin, (cmd_msgtype, cmd_ep, cmd_data) = dev.device._sent_xfers[-2:] assert cmd_msgtype == 'write' assert cmd_ep == 2 assert cmd_data == [0x21] def test_modern690Lc_set_color_can_persist_settings(mockModern690LcDevice): dev = mockModern690LcDevice dev.device._reset_sent() dev.set_color(channel='led', mode='rainbow', colors=[], non_volatile=True) _begin, (cmd_msgtype, cmd_ep, cmd_data) = dev.device._sent_xfers[-2:] assert cmd_msgtype == 'write' assert cmd_ep == 2 assert cmd_data == [0x21] def test_modern690Lc_set_speed_profile_can_persist_settings(mockModern690LcDevice): dev = mockModern690LcDevice dev.device._reset_sent() dev.set_speed_profile(channel='fan', profile=[(20, 20), (30, 50), (40, 100)], non_volatile=True) _begin, (cmd_msgtype, cmd_ep, cmd_data) = dev.device._sent_xfers[-2:] assert cmd_msgtype == 'write' assert cmd_ep == 2 assert cmd_data == [0x21] def test_modern690Lc_set_fixed_speed_can_persist_settings(mockModern690LcDevice): dev = mockModern690LcDevice dev.device._reset_sent() dev.set_fixed_speed(channel='pump', duty=50, non_volatile=True) _begin, (cmd_msgtype, cmd_ep, cmd_data) = dev.device._sent_xfers[-2:] assert cmd_msgtype == 'write' assert cmd_ep == 2 assert cmd_data == [0x21] def test_legacy690Lc_device_not_totally_broken(mockLegacy690LcDevice): """A few reasonable example calls do not raise exceptions.""" dev = mockLegacy690LcDevice dev.initialize() status = dev.get_status() dev.set_color(channel='led', mode='blinking', colors=iter([[3, 2, 1]]), time_per_color=3, time_off=1, alert_threshold=42, alert_color=[90, 80, 10]) dev.set_fixed_speed(channel='fan', duty=80) dev.set_fixed_speed(channel='pump', duty=50) def test_legacy690Lc_device_matches_leviathan_updates(mockLegacy690LcDevice): dev = mockLegacy690LcDevice dev.initialize() dev.set_fixed_speed(channel='pump', duty=50) dev.device._reset_sent() dev.set_color(channel='led', mode='fading', colors=[[0, 0, 255], [0, 255, 0]], time_per_color=1, alert_threshold=60, alert_color=[0, 0, 0]) _begin, (color_msgtype, color_ep, color_data) = dev.device._sent_xfers assert color_msgtype == 'write' assert color_ep == 2 assert color_data[0:12] == [0x10, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0x3c, 1] dev.device._reset_sent() dev.set_fixed_speed(channel='fan', duty=50) _begin, pump_message, fan_message = dev.device._sent_xfers assert pump_message == ('write', 2, [0x13, 50]) assert fan_message == ('write', 2, [0x12, 50]) def test_legacy690Lc_device_initialize_warns_on_non_volatile(mockLegacy690LcDevice, caplog): mockLegacy690LcDevice.initialize(non_volatile=True) assert 'device does not support non-volatile' in caplog.text def test_legacy690Lc_device_set_color_warns_on_non_volatile(mockLegacy690LcDevice, caplog): mockLegacy690LcDevice.set_color(channel='led', mode='blinking', colors=iter([[3, 2, 1]]), non_volatile=True) assert 'device does not support non-volatile' in caplog.text def test_legacy690Lc_device_set_fixed_speed_warns_on_non_volatile(mockLegacy690LcDevice, caplog): mockLegacy690LcDevice.set_fixed_speed(channel='fan', duty=80, non_volatile=True) assert 'device does not support non-volatile' in caplog.text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_asetek_pro.py0000644000175000017500000000701414562144457017563 0ustar00jonasjonasimport pytest from _testutils import MockPyusbDevice from collections import deque from liquidctl.driver.asetek_pro import HydroPro @pytest.fixture def emulate(): usb_dev = MockPyusbDevice() cooler = HydroPro(usb_dev, 'Emulated Asetek Pro cooler', fan_count=2) return (usb_dev, cooler) def test_not_totally_broken(emulate): _, cooler = emulate with cooler.connect(): cooler.initialize() cooler.get_status() cooler.set_color(channel='logo', mode='blinking', colors=iter([[3, 2, 1]])) cooler.set_color(channel='logo', mode='pulse', colors=[[3, 2, 1]], speed='normal') cooler.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 50), (40, 100)])) cooler.set_speed_profile(channel='fan1', profile=iter([(20, 20), (30, 50), (40, 100)])) cooler.set_fixed_speed(channel='fan', duty=50) def test_initialize_with_pump_mode(emulate): usb_dev, cooler = emulate with cooler.connect(): cooler.initialize(pump_mode='balanced') _begin, set_pump, _fw_version = usb_dev._sent_xfers assert set_pump == ('write', 1, [0x32, 0x01]) def test_set_profile_of_all_fans(emulate): usb_dev, cooler = emulate # When setting the speed of all fans the driver first gets the speed of all # fans to work out how many fans are present. Set 2 valid responses to # simulate a setup with 2 fans present. The first response is for the pump # setup in the initialize function. with cooler.connect(): cooler.initialize() cooler.set_speed_profile(channel='fan', profile=[(0, 10), (25, 50), (40, 100)]) _begin, _pump, _fw_version, fan1, fan2 = usb_dev._sent_xfers assert fan1 == ('write', 1, [0x40, 0x00, 0x00, 0x19, 0x28, 0x3c, 0x3c, 0x3c, 0x3c, 0x0a, 0x32, 0x64, 0x64, 0x64, 0x64, 0x64]) assert fan2 == ('write', 1, [0x40, 0x01, 0x00, 0x19, 0x28, 0x3c, 0x3c, 0x3c, 0x3c, 0x0a, 0x32, 0x64, 0x64, 0x64, 0x64, 0x64]) def test_setting_speed_on_single_fan_2(emulate): usb_dev, cooler = emulate with cooler.connect(): cooler.initialize() cooler.set_fixed_speed('fan2', 100) _begin, _pump, _fw_version, fan2 = usb_dev._sent_xfers assert fan2 == ('write', 1, [0x42, 0x01, 0x64]) def test_set_pump_to_fixed_color(emulate): usb_dev, cooler = emulate with cooler.connect(): cooler.initialize() cooler.set_color(channel='logo', mode='fixed', colors=iter([[0xff, 0x88, 0x44]])) _begin, _pump, _fw_version, color_change, color_end = usb_dev._sent_xfers assert color_change == ('write', 1, [0x56, 0x02, 0xff, 0x88, 0x44, 0xff, 0x88, 0x44]) assert color_end == ('write', 1, [0x55, 0x01]) def test_set_pump_to_blinking_mode(emulate): usb_dev, cooler = emulate with cooler.connect(): cooler.initialize() cooler.set_color(channel='logo', mode='blinking', speed='normal', colors=iter([[0xff, 0x88, 0x44], [0xff, 0xff, 0xff], [0x00, 0x00, 0x00]])) _begin, _pump, _fw_version, color_change, color_mode, color_end = usb_dev._sent_xfers assert color_change == ('write', 1, [0x56, 0x03, 0xff, 0x88, 0x44, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00]) assert color_mode == ('write', 1, [0x53, 0x0A]) assert color_end == ('write', 1, [0x58, 0x01]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714328404.0 liquidctl-1.15.0/tests/test_asus_ryujin.py0000644000175000017500000000615414613511524017774 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice from liquidctl.driver.asus_ryujin import AsusRyujin @pytest.fixture def mockRyujin(): return AsusRyujin(_MockRyujinDevice(), "Mock ASUS Ryujin II") class _MockRyujinDevice(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0x0B05, product_id=0x1988) self.requests = [] self.response = None def write(self, data): super().write(data) self.requests.append(data) assert data[0] == 0xEC header = data[1] # Sampled responses without trailing zeros if header == 0x82: # Get firmware info self.response = "ec02004155524a312d533735302d30313034" elif header == 0x99: # Get cooler status (temperature, pump speed, embedded fan speed) self.response = "ec19001b056405100e" elif header == 0x9A: # Get pump and embedded fan duty self.response = "ec1a0000223c" elif header == 0xA0: # Get AIO fan controller fan speeds self.response = "ec200000000c03ee02" elif header == 0xA1: # Get AIO fan controller duty self.response = "ec2100005b" elif header == 0x1A: # Set pump and embedded fan duty self.response = "ec1a" elif header == 0x21: # Set AIO fan controller duty self.response = "ec21" else: self.response = None def read(self, length, **kwargs): pre = super().read(length, **kwargs) if pre: return pre buf = bytearray(65) buf[0] = 0xEC if self.response: response = bytes.fromhex(self.response) buf[: len(response)] = response return buf[:length] def test_initialize(mockRyujin): with mockRyujin.connect(): (firmware_status,) = mockRyujin.initialize() assert firmware_status[1] == "AURJ1-S750-0104" def test_status(mockRyujin): with mockRyujin.connect(): actual = mockRyujin.get_status() expected = [ ("Liquid temperature", pytest.approx(27.5), "°C"), ("Pump speed", 1380, "rpm"), ("Pump fan speed", 3600, "rpm"), ("Pump duty", 34, "%"), ("Pump fan duty", 60, "%"), ("External fan duty", 36, "%"), ("External fan 1 speed", 780, "rpm"), ("External fan 2 speed", 750, "rpm"), ("External fan 3 speed", 0, "rpm"), ("External fan 4 speed", 0, "rpm"), ] assert sorted(actual) == sorted(expected) def test_set_fixed_speeds(mockRyujin): with mockRyujin.connect(): mockRyujin.set_fixed_speed(channel="pump", duty=10) assert mockRyujin.device.requests[-1][3] == 0x0A mockRyujin.set_fixed_speed(channel="pump-fan", duty=20) assert mockRyujin.device.requests[-1][4] == 0x14 mockRyujin.set_fixed_speed(channel="external-fans", duty=30) assert mockRyujin.device.requests[-1][4] == 0x4C mockRyujin.set_fixed_speed(channel="fans", duty=40) assert mockRyujin.device.requests[-2][4] == 0x28 assert mockRyujin.device.requests[-1][4] == 0x66 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_aura_led.py0000644000175000017500000001135414562144457017205 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report from collections import deque from liquidctl.driver.aura_led import AuraLed # Sample data for Aura LED controller from ASUS ProArt Z690-Creator WiFi _INIT_19AF_FIRMWARE_DATA = bytes.fromhex( "ec0241554c41332d415233322d30323037000000000000000000000000000000" "000000000000000000000000000000000000000000000000000000000000000000" ) _INIT_19AF_FIRMWARE = Report(_INIT_19AF_FIRMWARE_DATA[0], _INIT_19AF_FIRMWARE_DATA[1:]) _INIT_19AF_CONFIG_DATA = bytes.fromhex( "ec3000001e9f03010000783c00010000783c00010000783c0000000000000001" "040201f40000000000000000000000000000000000000000000000000000000000" ) _INIT_19AF_CONFIG = Report(_INIT_19AF_CONFIG_DATA[0], _INIT_19AF_CONFIG_DATA[1:]) @pytest.fixture def mockAuraLed_19AFDevice(): device = MockHidapiDevice(vendor_id=0x0B05, product_id=0x19AF, address="addr") dev = AuraLed(device, "mock Aura LED Controller") dev.connect() return dev def test_aura_led_19AF_device_command_format(mockAuraLed_19AFDevice): mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_FIRMWARE) mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_CONFIG) mockAuraLed_19AFDevice.initialize() # should perform 3 writes mockAuraLed_19AFDevice.set_color( channel="sync", mode="off", colors=[] ) # should perform 14 writes assert len(mockAuraLed_19AFDevice.device.sent) == 2 + 14 for i, (report, data) in enumerate(mockAuraLed_19AFDevice.device.sent): assert report == 0xEC assert len(data) == 64 def test_aura_led_19AF_device_get_status(mockAuraLed_19AFDevice): mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_CONFIG) assert mockAuraLed_19AFDevice.get_status() != [] def test_aura_led_19AF_device_initialize_status(mockAuraLed_19AFDevice): mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_FIRMWARE) mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_CONFIG) status_list = mockAuraLed_19AFDevice.initialize() firmware_tuple = status_list[0] assert firmware_tuple[1] == "AULA3-AR32-0207" def test_aura_led_19AF_device_off_with_some_channel(mockAuraLed_19AFDevice): colors = [[0xFF, 0, 0x80]] # should be ignored mockAuraLed_19AFDevice.set_color(channel="led2", mode="off", colors=iter(colors)) assert len(mockAuraLed_19AFDevice.device.sent) == 5 data1 = mockAuraLed_19AFDevice.device.sent[0].data data2 = mockAuraLed_19AFDevice.device.sent[1].data assert data1[1] == 0x01 # key for led2 assert data1[4] == 0x00 # off assert data2[2] == 0x02 # channel led2 assert data2[7:10] == [0x00, 0x00, 0x00] def test_aura_led_19AF_static_with_some_channel(mockAuraLed_19AFDevice): colors = [[0xFF, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockAuraLed_19AFDevice.set_color(channel="led2", mode="static", colors=iter(colors)) assert len(mockAuraLed_19AFDevice.device.sent) == 5 data1 = mockAuraLed_19AFDevice.device.sent[0].data data2 = mockAuraLed_19AFDevice.device.sent[1].data assert data1[1] == 0x01 # key for led2 assert data1[4] == 0x01 # static mode assert data2[2] == 0x02 # channel led2 assert data2[7:10] == [0xFF, 0x00, 0x80] def test_aura_led_19AF_spectrum_cycle_with_some_channel(mockAuraLed_19AFDevice): colors = [[0xFF, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockAuraLed_19AFDevice.set_color(channel="led3", mode="spectrum_cycle", colors=iter(colors)) assert len(mockAuraLed_19AFDevice.device.sent) == 5 data1 = mockAuraLed_19AFDevice.device.sent[0].data data2 = mockAuraLed_19AFDevice.device.sent[1].data assert data1[1] == 0x01 # key for led3 assert data1[4] == 0x04 # spectrum cycle assert data2[2] == 0x04 # channel led3 assert data2[7:10] == [0x00, 0x00, 0x00] def test_aura_led_19AF_device_sync_channel(mockAuraLed_19AFDevice): colors = [[0xFF, 0, 0x80]] mockAuraLed_19AFDevice.set_color(channel="sync", mode="static", colors=iter(colors)) assert len(mockAuraLed_19AFDevice.device.sent) == 14 # 14 writes def test_aura_led_19AF_device_invalid_set_color_arguments(mockAuraLed_19AFDevice): with pytest.raises(KeyError): mockAuraLed_19AFDevice.set_color("invalid", "off", []) with pytest.raises(KeyError): mockAuraLed_19AFDevice.set_color("led2", "invalid", []) with pytest.raises(ValueError): mockAuraLed_19AFDevice.set_color("led3", "static", []) def test_aura_led_19AF_device_initialize_status(mockAuraLed_19AFDevice): mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_FIRMWARE) mockAuraLed_19AFDevice.device.preload_read(_INIT_19AF_CONFIG) status_list = mockAuraLed_19AFDevice.initialize() firmware_tuple = status_list[0] assert firmware_tuple[1] == "AULA3-AR32-0207" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_10.py0000644000175000017500000000131614562144457022435 0ustar00jonasjonas"""Test API backward compatibility with liquidctl 1.0.x. While at the time all APIs were undocumented, we choose to support the use cases from GKraken as that software is a substantial contribution to the community. """ import pytest from test_kraken2 import mockKrakenXDevice from liquidctl.version import __version__ SPECTRUM = [ (235, 77, 40), (255, 148, 117), (126, 66, 45), (165, 87, 0), (56, 193, 66), (116, 217, 170), (166, 158, 255), (208, 0, 122) ] def test_pre11_apis_deprecated_super_mode(mockKrakenXDevice): # deprecated in favor of super-fixed, super-breathing and super-wave mockKrakenXDevice.set_color('sync', 'super', [(128, 0, 255)] + SPECTRUM, 'normal') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_11.py0000644000175000017500000000311214562144457022432 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.1.x.""" import pytest import usb from liquidctl.driver.kraken2 import Kraken2 from liquidctl.driver.usb import hid, HidapiDevice class _MockPyUsbHandle(usb.core.Device): def __init__(self, serial_number): self.idVendor = 0x1e71 self.idProduct = 0x170e self._serial_number = serial_number class MockResourceManager(): def dispose(self, *args, **kwargs): pass self._ctx = MockResourceManager() def _mock_enumerate(vendor_id=0, product_id=0): return [ {'vendor_id': vendor_id, 'product_id': product_id, 'serial_number': '_21', 'path': b'/_21'}, {'vendor_id': vendor_id, 'product_id': product_id, 'serial_number': '_89', 'path': b'/_89'} ] def test_construct_with_raw_pyusb_handle(monkeypatch): monkeypatch.setattr(hid, 'enumerate', _mock_enumerate) pyusb_handle = _MockPyUsbHandle(serial_number='_89') liquidctl_device = Kraken2(pyusb_handle, 'Some device') assert liquidctl_device.device.vendor_id == pyusb_handle.idVendor, \ '.device points to incorrect physical device' assert liquidctl_device.device.product_id == pyusb_handle.idProduct, \ '.device points to incorrect physical device' assert liquidctl_device.device.serial_number == pyusb_handle.serial_number, \ '.device points to different physical unit' assert isinstance(liquidctl_device.device, HidapiDevice), \ '.device not properly converted to HidapiDevice instance' ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_112.py0000644000175000017500000000230214562144457022514 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.12.x.""" # uses the psf/black style import pytest from _testutils import MockHidapiDevice, Report, MockRuntimeStorage from test_commander_pro import commanderProDevice def test_initialize_commander_pro_fan_modes(commanderProDevice, caplog): """Fix #615 but preserve API compatibility.""" responses = [ "000009d4000000000000000000000000", # firmware "00000500000000000000000000000000", # bootloader "00010100010000000000000000000000", # temp probes "00010102000000000000000000000000", # fan set (throw away) "00010102000000000000000000000000", # fan set (throw away) "00010102000000000000000000000000", # fan set (throw away) "00010102000000000000000000000000", # fan probes ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.initialize(direct_access=True, fan_modes={"4": "dc"}) sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x28 assert sent[3].data[2] == 3 assert sent[3].data[3] == 1 assert "deprecated parameter name `fan_modes`" in caplog.text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1673152806.0 liquidctl-1.15.0/tests/test_backward_compatibility_12.py0000644000175000017500000000026514356444446022443 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.2.x.""" import pytest def test_pre13_old_driver_names(): from liquidctl.driver.nzxt_smart_device import NzxtSmartDeviceDriver ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1673152806.0 liquidctl-1.15.0/tests/test_backward_compatibility_13.py0000644000175000017500000000151514356444446022443 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.3.x.""" import pytest def test_pre14_old_module_names(): import liquidctl.driver.asetek import liquidctl.driver.corsair_hid_psu import liquidctl.driver.kraken_two import liquidctl.driver.nzxt_smart_device import liquidctl.driver.seasonic def test_pre14_old_driver_names(): from liquidctl.driver.asetek import AsetekDriver from liquidctl.driver.asetek import LegacyAsetekDriver from liquidctl.driver.asetek import CorsairAsetekDriver from liquidctl.driver.corsair_hid_psu import CorsairHidPsuDriver from liquidctl.driver.kraken_two import KrakenTwoDriver from liquidctl.driver.nzxt_smart_device import SmartDeviceDriver from liquidctl.driver.nzxt_smart_device import SmartDeviceV2Driver from liquidctl.driver.seasonic import SeasonicEDriver ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_14.py0000644000175000017500000001225314562144457022443 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.4.x.""" import pytest from _testutils import MockHidapiDevice RADICAL_RED = [0xff, 0x35, 0x5e] MOUNTAIN_MEADOW = [0x1a, 0xb3, 0x85] def test_find_from_driver_package_still_available(): from liquidctl.driver import find_liquidctl_devices def test_kraken2_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'covering-backwards-marquee', 'backwards-moving-alternating', 'backwards-super-wave'] from liquidctl.driver.kraken2 import Kraken2 for mode in modes: base_mode = mode.replace('backwards-', '') old = Kraken2(MockHidapiDevice(), 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) new = Kraken2(MockHidapiDevice(), 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('ring', mode, colors) new.set_color('ring', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_kraken3_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'backwards-moving-alternating-3', 'covering-backwards-marquee', 'backwards-moving-alternating-4', 'backwards-moving-alternating-5', 'backwards-moving-alternating-6', 'backwards-rainbow-flow', 'backwards-super-rainbow', 'backwards-rainbow-pulse'] from liquidctl.driver.kraken3 import KrakenX3 from liquidctl.driver.kraken3 import _COLOR_CHANNELS_KRAKENX from liquidctl.driver.kraken3 import _SPEED_CHANNELS_KRAKENX from liquidctl.driver.kraken3 import _HWMON_CTRL_MAPPING_KRAKENX for mode in modes: base_mode = mode.replace('backwards-', '') old = KrakenX3(MockHidapiDevice(), 'Mock X63', speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX, hwmon_ctrl_mapping=_HWMON_CTRL_MAPPING_KRAKENX) new = KrakenX3(MockHidapiDevice(), 'Mock X63', speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX, hwmon_ctrl_mapping=_HWMON_CTRL_MAPPING_KRAKENX) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('ring', mode, colors) new.set_color('ring', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_smart_device_v1_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'covering-backwards-marquee', 'backwards-moving-alternating', 'backwards-super-wave'] from liquidctl.driver.smart_device import SmartDevice for mode in modes: base_mode = mode.replace('backwards-', '') old = SmartDevice(MockHidapiDevice(), 'Mock Smart Device', speed_channel_count=3, color_channel_count=1) new = SmartDevice(MockHidapiDevice(), 'Mock Smart Device', speed_channel_count=3, color_channel_count=1) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('led', mode, colors) new.set_color('led', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text def test_hue2_backwards_modes_are_deprecated(caplog): modes = ['backwards-spectrum-wave', 'backwards-marquee-3', 'backwards-marquee-4', 'backwards-marquee-5', 'backwards-marquee-6', 'backwards-moving-alternating-3', 'covering-backwards-marquee', 'backwards-moving-alternating-4', 'backwards-moving-alternating-5', 'backwards-moving-alternating-6', 'backwards-rainbow-flow', 'backwards-super-rainbow', 'backwards-rainbow-pulse'] from liquidctl.driver.smart_device import SmartDevice2 for mode in modes: base_mode = mode.replace('backwards-', '') old = SmartDevice2(MockHidapiDevice(), 'Mock Smart Device V2', speed_channel_count=3, color_channel_count=2) new = SmartDevice2(MockHidapiDevice(), 'Mock Smart Device V2', speed_channel_count=3, color_channel_count=2) colors = [RADICAL_RED, MOUNTAIN_MEADOW] old.set_color('led1', mode, colors) new.set_color('led1', base_mode, colors, direction='backward') assert old.device.sent == new.device.sent, \ f'{mode} != {base_mode} + direction=backward' assert 'deprecated mode' in caplog.text ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_15.py0000644000175000017500000000344614562144457022450 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.5.x.""" import pytest from liquidctl.driver.hydro_platinum import HydroPlatinum from liquidctl.driver.usb import hid def test_matches_platinum_and_pro_xt_coolers_regardless_of_hydro(monkeypatch): mock_match = [ {'vendor_id': 0x1b1c, 'product_id': 0x0c18}, # H100i Platinum {'vendor_id': 0x1b1c, 'product_id': 0x0c19}, # H100i Platinum SE {'vendor_id': 0x1b1c, 'product_id': 0x0c17}, # H115i Platinum {'vendor_id': 0x1b1c, 'product_id': 0x0c29}, # H60i Pro XT {'vendor_id': 0x1b1c, 'product_id': 0x0c20}, # H100i Pro XT {'vendor_id': 0x1b1c, 'product_id': 0x0c21}, # H115i Pro XT {'vendor_id': 0x1b1c, 'product_id': 0x0c22}, # H150i Pro XT ] mock_skip = [ {'vendor_id': 0x1b1c, 'product_id': 0xffff}, # nothing {'vendor_id': 0xffff, 'product_id': 0x0c22}, # nothing ] mock_hids = mock_match + mock_skip for info in mock_hids: info.setdefault('path', b'') info.setdefault('path', b'') info.setdefault('serial_number', '') info.setdefault('release_number', 0) info.setdefault('manufacturer_string', '') info.setdefault('product_string', '') info.setdefault('usage_page', 0) info.setdefault('usage', 0) info.setdefault('interface_number', 0) def mock_enumerate(vid=0, pid=0): return mock_hids monkeypatch.setattr(hid, 'enumerate', mock_enumerate) def find(match): return HydroPlatinum.find_supported_devices(match=match) assert len(find('corsair hydro')) == 7 assert len(find('hydro')) == 7 assert len(find('corsair')) == 7 assert len(find('corsair hydro h100i')) == 3 assert len(find('hydro h100i')) == 3 assert len(find('corsair h100i')) == 3 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_backward_compatibility_18.py0000644000175000017500000000054014562144457022443 0ustar00jonasjonas"""Test backward compatibility with liquidctl 1.8.x.""" # uses the psf/black style import pytest def test_version_version_dunder_still_imports(caplog): from liquidctl.version import __version__ assert type(__version__) is str def test_pre19_driver_names_still_import(): from liquidctl.driver.asetek_pro import CorsairAsetekProDriver ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1722818575.0 liquidctl-1.15.0/tests/test_cli.py0000644000175000017500000000444014654020017016161 0ustar00jonasjonasimport pytest from _testutils import VirtualBusDevice, VirtualControlMode import json import sys import liquidctl.cli @pytest.fixture def main(monkeypatch, capsys): """Return a function f(*args) to run main with `args` as `sys.argv`.""" def call_with_args(*args): monkeypatch.setattr(sys, 'argv', list(args)) try: liquidctl.cli.main() except SystemExit as exit: code = exit.code else: code = 0 out, err = capsys.readouterr() return code, out, err return call_with_args def test_json_list(main): code, out, _ = main('test', '--bus', 'virtual', 'list', '--json') assert code == 0 got = json.loads(out) exp = [ { 'description': 'Virtual Bus Device', 'vendor_id': 0x1234, 'product_id': 0xabcd, 'release_number': None, 'serial_number': None, 'bus': 'virtual', 'address': 'virtual_address', 'port': None, 'driver': 'VirtualBusDevice', 'experimental': False, } ] assert got == exp def test_json_initialize(main): code, out, _ = main('test', '--bus', 'virtual', 'initialize', '--json') assert code == 0 got = json.loads(out) exp = [ { 'bus': 'virtual', 'address': 'virtual_address', 'description': 'Virtual Bus Device', 'status': [ { 'key': 'Firmware version', 'value': '3.14.16', 'unit': '' }, ] } ] assert got == exp def test_json_status(main): code, out, _ = main('test', '--bus', 'virtual', 'status', '--json') assert code == 0 got = json.loads(out) exp = [ { 'bus': 'virtual', 'address': 'virtual_address', 'description': 'Virtual Bus Device', 'status': [ { 'key': 'Temperature', 'value': 30.4, 'unit': '°C' }, { 'key': 'Fan control mode', 'value': 'VirtualControlMode.QUIET', 'unit': '' }, { 'key': 'Animation', 'value': None, 'unit': '' }, { 'key': 'Uptime', 'value': 66192.0, 'unit': 's' }, { 'key': 'Hardware mode', 'value': True, 'unit': '' }, ] } ] assert got == exp ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/tests/test_commander_core.py0000644000175000017500000004262614617346724020416 0ustar00jonasjonasimport pytest from collections import deque from liquidctl import ExpectationNotMet from liquidctl.driver.commander_core import CommanderCore from _testutils import noop from liquidctl.util import u16le_from def int_to_le(num, length=2, byteorder='little', signed=False): return int(num).to_bytes(length=length, byteorder=byteorder, signed=signed) class MockCommanderCoreDevice: def __init__(self): self.vendor_id = 0x1b1c self.product_id = 0x0c1c self.address = 'addr' self.path = b'path' self.release_number = None self.serial_number = None self.bus = None self.port = None self.open = noop self.close = noop self.clear_enqueued_reports = noop self._read = deque() self.sent = list() self._last_write = bytes() self._modes = {} self._awake = False self.response_prefix = () self.firmware_version = (0x00, 0x00, 0x00) self.led_counts = (None, None, None, None, None, None, None) self.speeds_mode = (0, 0, 0, 0, 0, 0, 0) self.speeds = (None, None, None, None, None, None, None) self.fixed_speeds = (0, 0, 0, 0, 0, 0, 0) self.temperatures = (None, None) self.curve_points_by_device = [[],[],[],[],[],[],[]] def read(self, length): data = bytearray([0x00, self._last_write[2], 0x00]) data.extend(self.response_prefix) if self._last_write[2] == 0x02: # Firmware version for i in range(0, 3): data.append(self.firmware_version[i]) if self._awake: if self._last_write[2] == 0x08 or self._last_write[2] == 0x09: # Get data channel = self._last_write[3] mode = self._modes.get(channel) if mode[1] == 0x00: if mode[0] == 0x17: # Get speeds data.extend([0x06, 0x00]) data.append(len(self.speeds)) for i in self.speeds: if i is None: data.extend([0x00, 0x00]) else: data.extend(int_to_le(i)) elif mode[0] == 0x1a: # Speed devices connected data.extend([0x09, 0x00]) data.append(len(self.speeds)) for i in self.speeds: data.extend([0x01 if i is None else 0x07]) elif mode[0] == 0x20: # LED detect data.extend([0x0f, 0x00]) data.append(len(self.led_counts)) for i in self.led_counts: if i is None: data.extend(int_to_le(3)+int_to_le(0)) else: data.extend(int_to_le(2)) data.extend(int_to_le(i)) elif mode[0] == 0x21: # Get temperatures data.extend([0x10, 0x00]) data.append(len(self.temperatures)) for i in self.temperatures: if i is None: data.append(1) data.extend(int_to_le(0)) else: data.append(0) data.extend(int_to_le(int(i*10))) else: raise NotImplementedError(f'Read for {mode.hex(":")}') elif mode[1] == 0x6d: if mode[0] == 0x60: data.extend([0x03, 0x00]) data.append(len(self.speeds_mode)) for i in self.speeds_mode: data.append(i) elif mode[0] == 0x61: data.extend([0x04, 0x00]) data.append(len(self.fixed_speeds)) for i in self.fixed_speeds: data.extend(int_to_le(i)) elif mode[0] == 0x62: data.extend([0x05, 0x00]) # data type num_ports = len(self.curve_points_by_device) data.append(num_ports) for i in range(num_ports): data.append(0) # temp sensor data.append(len(self.curve_points_by_device[i])) # num curve points for (temp, duty) in self.curve_points_by_device[i]: data.extend(int_to_le(temp * 10)) # temperature data.extend(int_to_le(duty)) # duty else: raise NotImplementedError(f'Read for {mode.hex(":")}') else: raise NotImplementedError(f'Read for {mode.hex(":")}') elif self._last_write[2] == 0x09: # Get more data channel = self._last_write[3] mode = self._modes.get(channel) if mode[1] == 0x6d: if mode[0] == 0x62: for i in range(3, 7): data.append(0) # temp sensor data.append(len(self.curve_points_by_device[i])) # num curve points for (temp, duty) in self.curve_points_by_device[i]: data.extend(int_to_le(temp * 10)) data.extend(int_to_le(duty)) return list(data)[:length] def write(self, data): data = bytes(data) # ensure data is convertible to bytes self._last_write = data if data[0] != 0x00 or data[1] != 0x08: raise ValueError('Start of packets going out should be 00:08') if data[2] == 0x0d: channel = data[3] if self._modes.get(channel) is None: self._modes[channel] = data[4:6] else: raise ExpectationNotMet('Previous channel was not reset') elif data[2] == 0x05 and data[3] == 0x01: self._modes[data[4]] = None elif data[2] == 0x01 and data[3] == 0x03 and data[4] == 0x00: self._awake = data[5] == 0x02 elif self._awake: channel = data[3] mode = self._modes.get(channel) if data[2] == 0x06: # Write command self.data_length = u16le_from(data[4:6]) self.written_data_type = data[8:10] self.written_data = data[10:8+self.data_length] if len(self.written_data) < self.data_length - 2: return if data[2] == 0x07: # Write More command self.written_data += data[4:] if len(self.written_data) < self.data_length - 2: return if data[2] == 0x06 or data[2] == 0x07: if mode[1] == 0x6d: if mode[0] == 0x60 and list(self.written_data_type) == [0x03, 0x00]: self.speeds_mode = tuple(self.written_data[i+1] for i in range(0, self.written_data[0])) elif mode[0] == 0x61 and list(self.written_data_type) == [0x04, 0x00]: self.fixed_speeds = tuple(u16le_from(self.written_data[i*2+1:i*2+3]) for i in range(0, self.written_data[0])) elif mode[0] == 0x62 and list(self.written_data_type) == [0x05, 0x00]: curve_index = 1 for port_index in range(0, self.written_data[0]): self.curve_points_by_device[port_index] = [] # get number of curve points num_points = self.written_data[curve_index + 1] # store temperature and duty for i in range(num_points): point_index = curve_index + 2 + i*4 cp_temp = u16le_from(self.written_data[point_index: point_index + 2]) / 10 cp_duty = u16le_from(self.written_data[point_index + 2: point_index + 4]) self.curve_points_by_device[port_index].append((cp_temp, cp_duty)) # update index to next curve curve_index += 4 * num_points + 2 else: raise NotImplementedError('Invalid Write command') else: raise NotImplementedError('Invalid Write command') return len(data) @pytest.fixture def commander_core_device(): device = MockCommanderCoreDevice() core = CommanderCore(device, 'Corsair Commander Core', True) core.connect() return core def test_initialize_commander_core(commander_core_device): commander_core_device.device.firmware_version = (0x01, 0x02, 0x21) commander_core_device.device.speeds = (None, 104, None, None, None, None, 918) commander_core_device.device.led_counts = (27, None, 1, 2, 4, 8, 16) commander_core_device.device.temperatures = (None, 45.6) res = commander_core_device.initialize() assert len(res) == 17 assert res[0][1] == '1.2.33' # Firmware # LED counts assert res[1][1] == 27 assert res[2][1] is None assert res[3][1] == 1 assert res[4][1] == 2 assert res[5][1] == 4 assert res[6][1] == 8 assert res[7][1] == 16 # Speed devices connected assert not res[8][1] assert res[9][1] assert not res[10][1] assert not res[11][1] assert not res[12][1] assert not res[13][1] assert res[14][1] # Temperature sensors connected assert not res[15][1] assert res[16][1] # Ensure device is asleep at end assert not commander_core_device.device._awake def test_initialize_error_commander_core(commander_core_device): """This tests sends invalid data to ensure the device gets set back to hardware mode on error""" commander_core_device.device.response_prefix = (0x00, 0x00) with pytest.raises(ExpectationNotMet): commander_core_device.initialize() # Ensure device is asleep at end assert not commander_core_device.device._awake def test_status_commander_core(commander_core_device): commander_core_device.device.speeds = (2357, 918, 903, 501, 1104, 1824, 104) commander_core_device.device.temperatures = (12.3, 45.6) res = commander_core_device.get_status() assert len(res) == 9 # Speeds of pump and fans assert res[0][1] == 2357 assert res[1][1] == 918 assert res[2][1] == 903 assert res[3][1] == 501 assert res[4][1] == 1104 assert res[5][1] == 1824 assert res[6][1] == 104 # Temperatures assert res[7][1] == 12.3 assert res[8][1] == 45.6 # Ensure device is asleep at end assert not commander_core_device.device._awake def test_status_error_commander_core(commander_core_device): """This tests sends invalid data to ensure the device gets set back to hardware mode on error""" commander_core_device.device.response_prefix = (0x00, 0x00) with pytest.raises(ExpectationNotMet): commander_core_device.initialize() # Ensure device is asleep at end assert not commander_core_device.device._awake def test_set_fixed_speed_fan2_commander_core(commander_core_device): """This tests setting the speed of a single channel""" commander_core_device.device.speeds_mode = (1, 2, 3, 4, 5, 6, 7) commander_core_device.device.fixed_speeds = (8, 9, 10, 11, 12, 13, 14) commander_core_device.set_fixed_speed('fan2', 95) assert commander_core_device.device.speeds_mode == (1, 2, 0, 4, 5, 6, 7) assert commander_core_device.device.fixed_speeds == (8, 9, 95, 11, 12, 13, 14) # Ensure device is asleep at end assert not commander_core_device.device._awake def test_set_fixed_speed_fans_commander_core(commander_core_device): """This tests setting the speed of all the fans""" commander_core_device.device.speeds_mode = (1, 2, 3, 4, 5, 6, 7) commander_core_device.device.fixed_speeds = (8, 9, 10, 11, 12, 13, 14) commander_core_device.set_fixed_speed('fans', 61) assert commander_core_device.device.speeds_mode == (1, 0, 0, 0, 0, 0, 0) assert commander_core_device.device.fixed_speeds == (8, 61, 61, 61, 61, 61, 61) # Ensure device is asleep at end assert not commander_core_device.device._awake def test_set_fixed_speed_error_commander_core(commander_core_device): """This tests sends invalid data to ensure the device gets set back to hardware mode on error""" commander_core_device.device.response_prefix = (0x00, 0x00) with pytest.raises(ExpectationNotMet): commander_core_device.set_fixed_speed('fan1', 95) # Ensure device is asleep at end assert not commander_core_device.device._awake def test_set_speed_profile_fans_commander_core(commander_core_device): """This tests setting the speed of all the fans""" commander_core_device.device.speeds_mode = (0, 0, 0, 0, 0, 0, 0) commander_core_device.device.fixed_speeds = (8, 9, 10, 11, 12, 13, 14) commander_core_device.set_speed_profile('fans', [ (22, 0), (32, 25), (34, 50), (36, 75), (38,85), (40,95), (42, 100) ]) assert commander_core_device.device.speeds_mode == (0, 2, 2, 2, 2, 2, 2) assert commander_core_device.device.curve_points_by_device[0] == [] assert commander_core_device.device.curve_points_by_device[1] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] assert commander_core_device.device.curve_points_by_device[2] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] assert commander_core_device.device.curve_points_by_device[3] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] assert commander_core_device.device.curve_points_by_device[4] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] assert commander_core_device.device.curve_points_by_device[5] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] assert commander_core_device.device.curve_points_by_device[6] == [(22, 0), (32, 25), (34, 50), (36, 75), (38, 85), (40, 95), (42, 100)] # Ensure device is asleep at end assert not commander_core_device.device._awake def test_set_speed_curve_profile_single_channel_commander_core(commander_core_device): """This tests setting the speed curve profile of a single fan""" commander_core_device.device.speeds_mode = (0, 0, 0, 0, 0, 0, 0) commander_core_device.device.fixed_speeds = (8, 9, 10, 11, 12, 13, 14) commander_core_device.device.curve_points_by_device[0] = [] commander_core_device.device.curve_points_by_device[1] = [] commander_core_device.device.curve_points_by_device[2] = [] commander_core_device.device.curve_points_by_device[3] = [] commander_core_device.device.curve_points_by_device[4] = [] commander_core_device.device.curve_points_by_device[5] = [] commander_core_device.device.curve_points_by_device[6] = [] commander_core_device.set_speed_profile('pump', [(22, 0), (42, 100)]) commander_core_device.set_speed_profile('fan1', [(22, 1), (42, 10)]) commander_core_device.set_speed_profile('fan2', [(22, 2), (42, 20)]) commander_core_device.set_speed_profile('fan3', [(22, 3), (42, 30)]) commander_core_device.set_speed_profile('fan4', [(22, 4), (42, 40)]) commander_core_device.set_speed_profile('fan5', [(22, 5), (42, 50)]) commander_core_device.set_speed_profile('fan6', [(22, 6), (42, 60)]) assert commander_core_device.device.speeds_mode == (2, 2, 2, 2, 2, 2, 2) assert commander_core_device.device.curve_points_by_device[0] == [(22, 0), (42, 100)] assert commander_core_device.device.curve_points_by_device[1] == [(22, 1), (42, 10)] assert commander_core_device.device.curve_points_by_device[2] == [(22, 2), (42, 20)] assert commander_core_device.device.curve_points_by_device[3] == [(22, 3), (42, 30)] assert commander_core_device.device.curve_points_by_device[4] == [(22, 4), (42, 40)] assert commander_core_device.device.curve_points_by_device[5] == [(22, 5), (42, 50)] assert commander_core_device.device.curve_points_by_device[6] == [(22, 6), (42, 60)] # Ensure device is asleep at end assert not commander_core_device.device._awake def test_parse_channels_commander_core(): """This test will go through and thoroughly test CommanderCore._parse_channels so we don't have to in other tests""" core = CommanderCore(MockCommanderCoreDevice(), 'Corsair Commander Core', True) tests = [ ('pump', [0]), ('fans', [1, 2, 3, 4, 5, 6]), ('fan1', [1]), ('fan2', [2]), ('fan3', [3]), ('fan4', [4]), ('fan5', [5]), ('fan6', [6]) ] for (val, answer) in tests: assert list(core._parse_channels(val)) == answer def test_parse_channels_commander_core_xt(): """Test Core XT-specific channel layout""" corext = CommanderCore(MockCommanderCoreDevice(), 'Corsair Commander Core', False) tests = [ ('fans', [0, 1, 2, 3, 4, 5]), ('fan1', [0]), ('fan2', [1]), ('fan3', [2]), ('fan4', [3]), ('fan5', [4]), ('fan6', [5]) ] for (val, answer) in tests: assert list(corext._parse_channels(val)) == answer def test_parse_channels_error_commander_core(): """This tests to make sure we get an error with an invalid channel""" core = CommanderCore(MockCommanderCoreDevice(), 'Corsair Commander Core', True) with pytest.raises(ValueError): core._parse_channels('fan') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_commander_pro.py0000644000175000017500000012776714562144457020276 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report, MockRuntimeStorage from liquidctl.driver.commander_pro import _quoted, _prepare_profile, _fan_mode_desc, CommanderPro from liquidctl.driver.hwmon import HwmonDevice from liquidctl.error import NotSupportedByDevice # hardcoded responce data expected for some of the calls: # commander pro: firmware request (0.9.214) # commander pro: bootloader req (2.3) # commander pro: get temp config ( 3 sensors) # commander pro: get fan configs (3 DC fans, 1 PWM fan) # note I have not tested it with pwm fans # commander pro: @pytest.fixture def commanderProDeviceUnconnected(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c10, address='addr') return CommanderPro(device, 'Corsair Commander Pro', 6, 4, 2) @pytest.fixture def lightingNodeProDeviceUnconnected(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c0b, address='addr') return CommanderPro(device, 'Corsair Lighting Node Pro', 0, 0, 2) @pytest.fixture def commanderProDevice(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c10, address='addr') pro = CommanderPro(device, 'Corsair Commander Pro', 6, 4, 2) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) pro.connect(runtime_storage=runtime_storage) return pro @pytest.fixture def lightingNodeProDevice(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c0b, address='addr') node = CommanderPro(device, 'Corsair Lighting Node Pro', 0, 0, 2) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) node.connect(runtime_storage=runtime_storage) return node @pytest.fixture def lightingNodeCoreDevice(): device = MockHidapiDevice(vendor_id=0x1b1c, product_id=0x0c1a, address='addr') node = CommanderPro(device, 'Corsair Lighting Node Core', 0, 0, 1) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) node.connect(runtime_storage=runtime_storage) return node # prepare profile def test_prepare_profile_valid_max_rpm(): assert _prepare_profile([[10, 400], [20, 5000]], 60) == [(10, 400), (20, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_add_max_rpm(): assert _prepare_profile([[10, 400]], 60) == [(10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] assert _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800]], 60) == [(10, 400), (20, 500), (30, 600), (40, 700), (50, 800), (60, 5000)] def test_prepare_profile_missing_max_rpm(): with pytest.raises(ValueError): _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800], [55, 900]], 60) def test_prepare_profile_full_set(): assert _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [45, 2000], [50, 5000]], 60) == [(10, 400), (20, 500), (30, 600), (40, 700), (45, 2000), (50, 5000)] def test_prepare_profile_too_many_points(): with pytest.raises(ValueError): _prepare_profile([[10, 400], [20, 500], [30, 600], [40, 700], [50, 800], [55, 900]], 60) def test_prepare_profile_no_points(): assert _prepare_profile([], 60) == [(60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_empty_list(): assert _prepare_profile([], 60) == [(60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_above_max_temp(): assert _prepare_profile([[10, 400], [70, 2000]], 60) == [(10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_temp_low(): assert _prepare_profile([[-10, 400], [70, 2000]], 60) == [(-10, 400), (60, 5000), (60, 5000), (60, 5000), (60, 5000), (60, 5000)] def test_prepare_profile_max_temp(): assert _prepare_profile([], 100) == [(100, 5000), (100, 5000), (100, 5000), (100, 5000), (100, 5000), (100, 5000)] # quoted def test_quoted_empty(): assert _quoted() == '' def test_quoted_single(): assert _quoted('one arg') == "'one arg'" def test_quoted_valid(): assert _quoted('one', 'two') == "'one', 'two'" def test_quoted_not_string(): assert _quoted('test', 500) == "'test', 500" # fan modes def test_get_fan_mode_description_auto(): assert _fan_mode_desc(0x00) == None def test_get_fan_mode_description_unknown(): assert _fan_mode_desc(0x03) == None assert _fan_mode_desc(0x04) == None assert _fan_mode_desc(0x10) == None assert _fan_mode_desc(0xff) == None def test_get_fan_mode_description_dc(): assert _fan_mode_desc(0x01) == 'DC' def test_get_fan_mode_description_pwm(): assert _fan_mode_desc(0x02) == 'PWM' # class methods def test_commander_constructor(commanderProDeviceUnconnected): assert commanderProDeviceUnconnected._data is None assert commanderProDeviceUnconnected._fan_names == ['fan1', 'fan2', 'fan3', 'fan4', 'fan5', 'fan6'] assert commanderProDeviceUnconnected._led_names == ['led1', 'led2'] assert commanderProDeviceUnconnected._temp_probs == 4 assert commanderProDeviceUnconnected._fan_count == 6 def test_lighting_constructor(lightingNodeProDeviceUnconnected): assert lightingNodeProDeviceUnconnected._data is None assert lightingNodeProDeviceUnconnected._fan_names == [] assert lightingNodeProDeviceUnconnected._led_names == ['led1', 'led2'] assert lightingNodeProDeviceUnconnected._temp_probs == 0 assert lightingNodeProDeviceUnconnected._fan_count == 0 def test_connect_commander(commanderProDeviceUnconnected): commanderProDeviceUnconnected.connect() assert commanderProDeviceUnconnected._data is not None def test_connect_lighting(lightingNodeProDeviceUnconnected): lightingNodeProDeviceUnconnected.connect() assert lightingNodeProDeviceUnconnected._data is not None @pytest.mark.parametrize('exp,fan_mode', [([(3, 0x01)], {'4': 'dc'}), ([(3, 0x00)], {'4': 'off'}), ([(4, 0x02)], {'5': 'pwm'}), ([(0, 0x01),(2, 0x02),(1, 0x01)], {'1': 'dc', '3':'pwm', '2': 'dc'})]) def test_initialize_commander_pro_fan_mode(commanderProDevice, exp, fan_mode, tmp_path): responses = [ '000009d4000000000000000000000000', # firmware '00000500000000000000000000000000', # bootloader '00010100010000000000000000000000', # temp probes '00010102000000000000000000000000', # fan set (throw away) '00010102000000000000000000000000', # fan set (throw away) '00010102000000000000000000000000', # fan set (throw away) '00010102000000000000000000000000' # fan probes ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice.initialize(direct_access=True, fan_mode=fan_mode) sent = commanderProDevice.device.sent assert len(sent) == 4+len(exp) for i in range(len(exp)): assert sent[3+i].data[0] == 0x28 assert sent[3+i].data[2] == exp[i][0] assert sent[3+i].data[3] == exp[i][1] @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True), (True, False)]) def test_initialize_commander_pro(commanderProDevice, has_hwmon, direct_access, tmp_path): if has_hwmon and not direct_access: commanderProDevice._hwmon = HwmonDevice('mock_module', tmp_path) (tmp_path / 'temp1_input').write_text('10000\n') (tmp_path / 'temp2_input').write_text('20000\n') (tmp_path / 'temp4_input').write_text('40000\n') (tmp_path / 'fan1_input').write_text('1100\n') (tmp_path / 'fan1_label').write_text('fan1 3pin\n') (tmp_path / 'fan2_input').write_text('1200\n') (tmp_path / 'fan2_label').write_text('fan1 3pin\n') (tmp_path / 'fan3_input').write_text('1300\n') (tmp_path / 'fan3_label').write_text('fan1 4pin\n') else: if has_hwmon: commanderProDevice._hwmon = HwmonDevice('invalid_module', None) responses = [ '000009d4000000000000000000000000', # firmware '00000500000000000000000000000000', # bootloader '00010100010000000000000000000000', # temp probes '00010102000000000000000000000000' # fan probes ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) res = commanderProDevice.initialize(direct_access=direct_access) if has_hwmon and not direct_access: assert len(res) == 10 i = 0 else: assert len(res) == 12 assert res[0] == ('Firmware version', '0.9.212', '') assert res[1] == ('Bootloader version', '0.5', '') i = 2 assert res[i + 0] == ('Temperature probe 1', True, '') assert res[i + 1] == ('Temperature probe 2', True, '') assert res[i + 2] == ('Temperature probe 3', False, '') assert res[i + 3] == ('Temperature probe 4', True, '') assert res[i + 4] == ('Fan 1 control mode', 'DC', '') assert res[i + 5] == ('Fan 2 control mode', 'DC', '') assert res[i + 6] == ('Fan 3 control mode', 'PWM', '') assert res[i + 7] == ('Fan 4 control mode', None, '') assert res[i + 8] == ('Fan 5 control mode', None, '') assert res[i + 9] == ('Fan 6 control mode', None, '') data = commanderProDevice._data.load('fan_modes', None) assert data is not None assert len(data) == 6 assert data[0] == 0x01 assert data[1] == 0x01 assert data[2] == 0x02 assert data[3] == 0x00 assert data[4] == 0x00 assert data[5] == 0x00 data = commanderProDevice._data.load('temp_sensors_connected', None) assert data is not None assert len(data) == 4 assert data[0] == 0x01 assert data[1] == 0x01 assert data[2] == 0x00 assert data[3] == 0x01 def test_initialize_lighting_node(lightingNodeProDevice): responses = [ '000009d4000000000000000000000000', # firmware '00000500000000000000000000000000' # bootloader ] for d in responses: lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(d))) res = lightingNodeProDevice.initialize() assert len(res) == 2 assert res[0][1] == '0.9.212' assert res[1][1] == '0.5' data = lightingNodeProDevice._data.load('fan_modes', None) assert data is None data = lightingNodeProDevice._data.load('temp_sensors_connected', None) assert data is None @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True), (True, False)]) def test_get_status_commander_pro(commanderProDevice, has_hwmon, direct_access, tmp_path): if has_hwmon and not direct_access: commanderProDevice._hwmon = HwmonDevice('mock_module', tmp_path) (tmp_path / 'temp1_input').write_text('26910\n') (tmp_path / 'temp2_input').write_text('29220\n') (tmp_path / 'temp4_input').write_text('25740\n') (tmp_path / 'fan1_input').write_text('940\n') (tmp_path / 'fan1_label').write_text('fan1 3pin\n') (tmp_path / 'fan2_input').write_text('939\n') (tmp_path / 'fan2_label').write_text('fan1 3pin\n') (tmp_path / 'fan3_input').write_text('987\n') (tmp_path / 'fan3_label').write_text('fan1 4pin\n') (tmp_path / 'in0_input').write_text('12066\n') (tmp_path / 'in1_input').write_text('4965\n') (tmp_path / 'in2_input').write_text('3359\n') else: if has_hwmon: commanderProDevice._hwmon = HwmonDevice('invalid_module', None) responses = [ '000a8300000000000000000000000000', # temp sensor 1 '000b6a00000000000000000000000000', # temp sensor 2 '000a0e00000000000000000000000000', # temp sensor 4 '0003ac00000000000000000000000000', # fan speed 1 '0003ab00000000000000000000000000', # fan speed 2 '0003db00000000000000000000000000', # fan speed 3 '002f2200000000000000000000000000', # get 12v '00136500000000000000000000000000', # get 5v '000d1f00000000000000000000000000', # get 3.3v ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) res = commanderProDevice.get_status(direct_access=direct_access) print(res) assert len(res) == 9 # temp probes assert res[0] == ('Temperature 1', pytest.approx(26.91, abs=1e-3), '°C') assert res[1] == ('Temperature 2', pytest.approx(29.22, abs=1e-3), '°C') assert res[2] == ('Temperature 4', pytest.approx(25.74, abs=1e-3), '°C') # fans rpm assert res[3] == ('Fan 1 speed', 940, 'rpm') assert res[4] == ('Fan 2 speed', 939, 'rpm') assert res[5] == ('Fan 3 speed', 987, 'rpm') # voltages assert res[6] == ('+12V rail', pytest.approx(12.066, abs=1e-3), 'V') assert res[7] == ('+5V rail', pytest.approx(4.965, abs=1e-3), 'V') assert res[8] == ('+3.3V rail', pytest.approx(3.359, abs=1e-3), 'V') if not has_hwmon or direct_access: # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 9 assert sent[0].data[0] == 0x11 assert sent[1].data[0] == 0x11 assert sent[2].data[0] == 0x11 assert sent[3].data[0] == 0x21 assert sent[4].data[0] == 0x21 assert sent[5].data[0] == 0x21 assert sent[6].data[0] == 0x12 assert sent[7].data[0] == 0x12 assert sent[8].data[0] == 0x12 def test_get_status_lighting_pro(lightingNodeProDevice): res = lightingNodeProDevice.get_status() assert len(res) == 0 def test_get_temp_valid_sensor_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) res = commanderProDevice._get_temp(1) assert res == 26.91 # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x11 assert sent[0].data[1] == 1 def test_get_temp_invalid_sensor_low_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) with pytest.raises(ValueError): commanderProDevice._get_temp(-1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_temp_invalid_sensor_high_commander(commanderProDevice): response = '000a8300000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x01, 0x01]) with pytest.raises(ValueError): commanderProDevice._get_temp(4) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_temp_lighting(lightingNodeProDevice): response = '000a8300000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) lightingNodeProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): lightingNodeProDevice._get_temp(2) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_valid_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) res = commanderProDevice._get_fan_rpm(1) assert res == 940 # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x21 assert sent[0].data[1] == 1 def test_get_fan_rpm_invalid_low_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice._get_fan_rpm(-1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_invalid_high_commander(commanderProDevice): response = '0003ac00000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x02, 0x00, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice._get_fan_rpm(7) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_get_fan_rpm_lighting(lightingNodeProDevice): response = '0003ac00000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) with pytest.raises(ValueError): lightingNodeProDevice._get_fan_rpm(7) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_get_hw_fan_channels_all(commanderProDevice): res = commanderProDevice._get_hw_fan_channels('sync') assert res == [0, 1, 2, 3, 4, 5] def test_get_hw_fan_channels_lowercase(commanderProDevice): res = commanderProDevice._get_hw_fan_channels('fan2') assert res == [1] @pytest.mark.parametrize('channel', [ 'fan23', 'fan7', 'fan0', 'fan', 'led', 'led1', 'bob' ]) def test_get_hw_fan_channels_invalid(commanderProDevice, channel): with pytest.raises(ValueError): commanderProDevice._get_hw_fan_channels(channel) @pytest.mark.parametrize('channel,expected', [ ('led1', [0]), ('led2', [1]), ('sync', [0, 1]) ]) def test_get_hw_led_channel_valid(commanderProDevice, channel, expected): res = commanderProDevice._get_hw_led_channels(channel) assert res == expected @pytest.mark.parametrize('channel,expected', [('led', [0]), ('any', [0])]) def test_get_hw_led_channel_valid_node_core(lightingNodeCoreDevice, channel, expected): res = lightingNodeCoreDevice._get_hw_led_channels(channel) assert res == expected @pytest.mark.parametrize('channel', [ 'led0', 'led3', 'led', 'fan', 'fan1', 'bob' ]) def test_get_hw_led_channels_invalid(commanderProDevice, channel): with pytest.raises(ValueError): commanderProDevice._get_hw_led_channels(channel) def test_set_fixed_speed_low(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan4', -10) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x03 assert sent[0].data[2] == 0x00 def test_set_fixed_speed_high(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan3', 110) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x02 assert sent[0].data[2] == 0x64 def test_set_fixed_speed_valid(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x01, 0x01, 0x01, 0x01, 0x01, 0x01]) commanderProDevice.set_fixed_speed('fan2', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x01 assert sent[0].data[2] == 0x32 def test_set_fixed_speed_valid_unconfigured(commanderProDevice): response = '00000000000000000000000000000000' commanderProDevice.device.preload_read(Report(0, bytes.fromhex(response))) commanderProDevice._data.store('fan_modes', [0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) commanderProDevice.set_fixed_speed('fan2', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_fixed_speed_valid_multi_fan(commanderProDevice): responses = [ '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000', '00000000000000000000000000000000' ] for d in responses: commanderProDevice.device.preload_read(Report(0, bytes.fromhex(d))) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_fixed_speed('sync', 50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 3 assert sent[0].data[0] == 0x23 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x32 assert sent[1].data[0] == 0x23 assert sent[1].data[1] == 0x02 assert sent[1].data[2] == 0x32 assert sent[2].data[0] == 0x23 assert sent[2].data[1] == 0x03 assert sent[2].data[2] == 0x32 def test_set_fixed_speed_lighting(lightingNodeProDevice): response = '00000000000000000000000000000000' lightingNodeProDevice.device.preload_read(Report(0, bytes.fromhex(response))) with pytest.raises(NotSupportedByDevice): lightingNodeProDevice.set_fixed_speed('sync', 50) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid_multi_fan(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(5): commanderProDevice.device.preload_read(ignore) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 3 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x00 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 assert sent[1].data[0] == 0x25 assert sent[1].data[1] == 0x02 assert sent[1].data[2] == 0x00 assert sent[2].data[0] == 0x25 assert sent[2].data[1] == 0x03 assert sent[2].data[2] == 0x00 def test_set_speed_profile_invalid_temp_sensor(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(5): commanderProDevice.device.preload_read(ignore) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('fan1', [(10, 500), (20, 1000)], temperature_sensor=10) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x00 assert sent[0].data[2] == 0x03 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 def test_set_speed_profile_no_temp_sensors(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(5): commanderProDevice.device.preload_read(ignore) commanderProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)], temperature_sensor=1) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(5): commanderProDevice.device.preload_read(ignore) commanderProDevice._data.store('temp_sensors_connected', [0x01, 0x01, 0x00, 0x01]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) commanderProDevice.set_speed_profile('fan3', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert sent[0].data[0] == 0x25 assert sent[0].data[1] == 0x02 assert sent[0].data[2] == 0x00 assert sent[0].data[3] == 0x03 assert sent[0].data[4] == 0xe8 assert sent[0].data[15] == 0x01 assert sent[0].data[16] == 0xf4 def test_set_speed_profile_node(lightingNodeProDevice): ignore = Report(0, bytes(16)) for _ in range(5): lightingNodeProDevice.device.preload_read(ignore) lightingNodeProDevice._data.store('temp_sensors_connected', [0x01, 0x00, 0x00, 0x00]) lightingNodeProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(NotSupportedByDevice): lightingNodeProDevice.set_speed_profile('sync', [(10, 500), (20, 1000)]) # check the commands sent sent = lightingNodeProDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_core(lightingNodeCoreDevice): ignore = Report(0, bytes(16)) for _ in range(5): lightingNodeCoreDevice.device.preload_read(ignore) lightingNodeCoreDevice._data.store('temp_sensors_connected', [0x01, 0x00, 0x00, 0x00]) lightingNodeCoreDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(NotSupportedByDevice): lightingNodeCoreDevice.set_speed_profile('sync', [(10, 500), (20, 1000)]) # check the commands sent sent = lightingNodeCoreDevice.device.sent assert len(sent) == 0 def test_set_speed_profile_valid_unconfigured(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(5): commanderProDevice.device.preload_read(ignore) commanderProDevice._data.store('temp_sensors_connected', [0x00, 0x00, 0x00, 0x00]) commanderProDevice._data.store('fan_modes', [0x01, 0x00, 0x01, 0x01, 0x00, 0x00]) with pytest.raises(ValueError): commanderProDevice.set_speed_profile('fan2', [(10, 500), (20, 1000)]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_set_color_hardware_clear(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [] } commanderProDevice._data.store('saved_effects', [effect]) commanderProDevice.set_color('led1', 'clear', [], ) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None def test_set_color_hardware_off(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [] } commanderProDevice._data.store('saved_effects', [effect]) commanderProDevice.set_color('led1', 'off', []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[4] == 0x04 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None @pytest.mark.parametrize('directionStr,expected', [('forward', 0x01), ('backward', 0x00)]) def test_set_color_hardware_direction(commanderProDevice, directionStr, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, direction=directionStr) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[6] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_direction_default(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[6] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_speed_default(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[5] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('speedStr,expected', [('slow', 0x02), ('fast', 0x00), ('medium', 0x01)]) def test_set_color_hardware_speed(commanderProDevice, speedStr, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, speed=speedStr) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[5] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_default_start_end(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0 # start led assert sent[3].data[3] == 204 # num leds effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('startLED,expected', [ (1, 0x00), (30, 0x1d), (92, 0x5b) ]) def test_set_color_hardware_start_set(commanderProDevice, startLED, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=startLED) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == expected # start led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('numLED,expected', [ (1, 1), (30, 30), (96, 96), (203, 203), (204, 204), (205, 204) ]) def test_set_color_hardware_num_leds(commanderProDevice, numLED, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=1, maximum_leds=numLED) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[3] == expected # num leds effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_many_leds(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=200, maximum_leds=50) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0xc7 # start led assert sent[3].data[3] == 5 # num led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_few_leds(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors, start_led=1, maximum_leds=0) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[2] == 0x00 # start led assert sent[3].data[3] == 0x01 # num led effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('channel,expected', [('led1', 0x00), ('led2', 0x01)]) def test_set_color_hardware_channel(commanderProDevice, channel, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color(channel, 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[0].data[1] == expected assert sent[1].data[1] == expected assert sent[2].data[1] == expected assert sent[3].data[0] == 0x35 assert sent[3].data[1] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_sync_channel(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(9): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('sync', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 2*(3 + 1) + 1 assert sent[0].data[1] == 0 assert sent[1].data[1] == 0 assert sent[2].data[1] == 0 assert sent[3].data[1] == 1 assert sent[4].data[1] == 1 assert sent[5].data[1] == 1 assert sent[6].data[0] == 0x35 assert sent[6].data[1] == 0 assert sent[7].data[0] == 0x35 assert sent[7].data[1] == 1 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 2 def test_set_color_hardware_random_color(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x01 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_not_random_color(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x00 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_many_colors(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc], [0x00, 0x11, 0x22], [0x33, 0x44, 0x55]] commanderProDevice.set_color('led1', 'fixed', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x00 assert sent[3].data[9] == 0xaa assert sent[3].data[10] == 0xbb assert sent[3].data[11] == 0xcc effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_too_few_colors(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) commanderProDevice.set_color('led1', 'fixed', []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[7] == 0x01 assert sent[3].data[9] == 0x00 assert sent[3].data[10] == 0x00 assert sent[3].data[11] == 0x00 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 @pytest.mark.parametrize('modeStr,expected', [ ('rainbow', 0x00), ('color_shift', 0x01), ('color_pulse', 0x02), ('color_wave', 0x03), ('fixed', 0x04), ('visor', 0x06), ('marquee', 0x07), ('blink', 0x08), ('sequential', 0x09), ('rainbow2', 0x0a), ]) def test_set_color_hardware_valid_mode(commanderProDevice, modeStr, expected): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) commanderProDevice.set_color('led1', modeStr, []) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 5 assert sent[3].data[0] == 0x35 assert sent[3].data[4] == expected effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 1 def test_set_color_hardware_invalid_mode(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) colors = [[0xaa, 0xbb, 0xcc]] with pytest.raises(ValueError): commanderProDevice.set_color('led1', 'invalid', colors) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is None def test_set_color_hardware_multipe_commands(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [0xaa, 0xbb, 0xcc] } commanderProDevice._data.store('saved_effects', [effect]) commanderProDevice.set_color('led1', 'fixed', [[0x00, 0x11, 0x22]], start_led=16, maximum_leds=5) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 6 assert sent[3].data[0] == 0x35 assert sent[4].data[0] == 0x35 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 2 def test_set_color_hardware_last_commands(commanderProDevice): for i in range(15): commanderProDevice.device.preload_read(Report(0, bytes.fromhex('00000000000000000000000000000000'))) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [0xaa, 0xbb, 0xcc] } commanderProDevice._data.store('saved_effects', [effect, effect, effect, effect, effect, effect, effect]) commanderProDevice.set_color('led1', 'fixed', [[0x00, 0x11, 0x22]], start_led=16, maximum_leds=5) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 12 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 8 def test_set_color_hardware_max_commands(commanderProDevice): for i in range(15): commanderProDevice.device.preload_read(Report(0, bytes.fromhex('00000000000000000000000000000000'))) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [0xaa, 0xbb, 0xcc] } commanderProDevice._data.store('saved_effects', [effect, effect, effect, effect, effect, effect, effect, effect]) commanderProDevice.set_color('led1', 'fixed', [[0x00, 0x11, 0x22]], start_led=16, maximum_leds=5) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 effects = commanderProDevice._data.load('saved_effects', default=None) assert effects is not None assert len(effects) == 8 def test_set_color_hardware_too_many_commands(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(6): commanderProDevice.device.preload_read(ignore) effect = { 'channel': 0x01, 'start_led': 0x00, 'num_leds': 0x0f, 'mode': 0x0a, 'speed': 0x00, 'direction': 0x00, 'random_colors': 0x00, 'colors': [0xaa, 0xbb, 0xcc] } commanderProDevice._data.store('saved_effects', [effect, effect, effect, effect, effect, effect, effect, effect, effect]) commanderProDevice.set_color('led1', 'fixed', [[0x00, 0x11, 0x22]], start_led=16, maximum_leds=5) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 0 def test_send_command_valid_data(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(2): commanderProDevice.device.preload_read(ignore) commanderProDevice._send_command(6, [255, 0, 20, 10, 15]) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 6 assert sent[0].data[1] == 255 def test_send_command_no_data(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(2): commanderProDevice.device.preload_read(ignore) commanderProDevice._send_command(6) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 6 assert sent[0].data[1] == 0 def test_send_command_data_too_long(commanderProDevice): ignore = Report(0, bytes(16)) for _ in range(2): commanderProDevice.device.preload_read(ignore) data = bytearray(100) commanderProDevice._send_command(3, data) # check the commands sent sent = commanderProDevice.device.sent assert len(sent) == 1 assert len(sent[0].data) == 64 assert sent[0].data[0] == 3 assert sent[0].data[1] == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/tests/test_coolit.py0000644000175000017500000000147614617346724016730 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, MockRuntimeStorage from liquidctl.driver.coolit import Coolit class MockDevice(MockHidapiDevice): def read(self, length, **kwargs): return [0] * length @pytest.fixture def mock_h110i_gt(): mock_raw_dev = MockDevice(vendor_id=0xDEAD, product_id=0xBEEF, address="42") mock_storage = MockRuntimeStorage(key_prefixes=["mock_h110i_gt"]) dev = Coolit( mock_raw_dev, "Mock H100i GT", fan_count=2, rgb_fans=False, ) return dev.connect(runtime_storage=mock_storage) def test_driver_not_totally_broken(mock_h110i_gt): cooler = mock_h110i_gt cooler.initialize() _ = cooler.get_status(pump_mode="extreme") cooler.set_fixed_speed("fan1", 42) cooler.set_speed_profile("fan2", [(20, 30), (40, 90)]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1725443357.0 liquidctl-1.15.0/tests/test_corsair_hid_psu.py0000644000175000017500000001545014666026435020607 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report from pytest import approx from datetime import timedelta from liquidctl.driver.corsair_hid_psu import CorsairHidPsu, OCPMode, FanControlMode from liquidctl.driver.hwmon import HwmonDevice # https://github.com/liquidctl/liquidctl/issues/300#issuecomment-788302513 SAMPLE_PAGED_RESPONSES = [ [ '038bffd2', '038c2bf0', '03963e08', ], [ '038b41d1', '038c1be0', '039610f8', ], [ '038bd3d0', '038c09e0', '039603f8', ], ] SAMPLE_RESPONSES = [ # https://github.com/liquidctl/liquidctl/issues/300#issuecomment-788302513 '033b1b', '034013d1', '03441ad2', '034680e2', '034f46', '0388ccf9', '038d86f0', '038e6af0', '0399434f5253414952', '039a524d3130303069', '03d46d9febfe', '03d802', '03ee4608', 'fe03524d3130303069', # https://github.com/liquidctl/liquidctl/pull/54#issuecomment-543760522 '03d1224711', # https://github.com/liquidctl/liquidctl/issues/575#issue-1600123046 '03d213c60000414952204858313530306920505355', # with extra non-nil bytes # artificial '0390c803', '03f001', ] class MockPsu(MockHidapiDevice): def __init__(self, *args, **kwargs): self._page = 0; super().__init__(*args, **kwargs) def write(self, data): super().write(data) data = data[1:] # skip unused report ID reply = bytearray(64) if data[0] == 2 and data[1] == 0: self._page = data[2] reply[0:3] = data[0:3] self.preload_read(Report(0, reply)) else: cmd = f'{data[1]:02x}' samples = [x for x in SAMPLE_PAGED_RESPONSES[self._page] if x[2:4] == cmd] if not samples: samples = [x for x in SAMPLE_RESPONSES if x[2:4] == cmd] if not samples: raise KeyError(cmd) reply[0] = data[0] reply[1:len(data)] = bytes.fromhex(samples[0][2:]) self.preload_read(Report(0, reply)) @pytest.fixture def mock_psu(): pid, vid, desc, kwargs = CorsairHidPsu._MATCHES[0] device = MockPsu(vendor_id=vid, product_id=pid, address='addr') return CorsairHidPsu(device, f'Mock {desc}', **kwargs) def test_not_totally_broken(mock_psu): mock_psu.set_fixed_speed(channel='fan', duty=50) report_id, report_data = mock_psu.device.sent[0] assert report_id == 0 assert len(report_data) == 64 def test_dont_inject_report_ids(mock_psu): mock_psu.set_fixed_speed(channel='fan', duty=50) report_id, report_data = mock_psu.device.sent[0] assert report_id == 0 assert len(report_data) == 64 @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True), (True, False)]) def test_initializes(mock_psu, has_hwmon, direct_access, tmp_path): if has_hwmon: mock_psu._hwmon = HwmonDevice('mock_module', tmp_path) # TODO check the result _ = mock_psu.initialize(direct_access=direct_access) writes = len(mock_psu.device.sent) if not has_hwmon or direct_access: assert writes > 0 else: assert writes == 0 @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True)]) def test_reads_status_directly(mock_psu, has_hwmon, direct_access): if has_hwmon: mock_psu._hwmon = HwmonDevice(None, None) got = mock_psu.get_status(direct_access=direct_access) expected = [ ('Current uptime', timedelta(seconds=50707), ''), ('Total uptime', timedelta(days=13, seconds=9122), ''), ('VRM temperature', approx(33.5, rel=1e-3), '°C'), ('Case temperature', approx(26.5, rel=1e-3), '°C'), ('Fan control mode', FanControlMode.SOFTWARE, ''), ('Fan speed', approx(968, rel=1e-3), 'rpm'), ('Input voltage', approx(230, rel=1e-3), 'V'), ('Total power output', approx(140, rel=1e-3), 'W'), ('+12V OCP mode', OCPMode.MULTI_RAIL, ''), ('+12V output voltage', approx(11.98, rel=1e-3), 'V'), ('+12V output current', approx(10.75, rel=1e-3), 'A'), ('+12V output power', approx(124, rel=1e-3), 'W'), ('+5V output voltage', approx(5.016, rel=1e-3), 'V'), ('+5V output current', approx(1.688, rel=1e-3), 'A'), ('+5V output power', approx(8, rel=1e-3), 'W'), ('+3.3V output voltage', approx(3.297, rel=1e-3), 'V'), ('+3.3V output current', approx(0.562, rel=1e-3), 'A'), ('+3.3V output power', approx(1.5, rel=1e-3), 'W'), ('Estimated input power', approx(153, abs=1), 'W'), ('Estimated efficiency', approx(92, abs=1), '%'), ] assert sorted(got) == sorted(expected) def test_reads_status_from_hwmon(mock_psu, tmp_path): mock_psu.device.write = None # make sure we aren't writing to the mock device mock_psu._hwmon = HwmonDevice('mock_module', tmp_path) (tmp_path / 'temp1_input').write_text('33500\n') (tmp_path / 'temp2_input').write_text('26500\n') (tmp_path / 'fan1_input').write_text('968\n') (tmp_path / 'in0_input').write_text('230000\n') (tmp_path / 'power1_input').write_text('140000000\n') (tmp_path / 'in1_input').write_text('11980\n') (tmp_path / 'curr2_input').write_text('10750\n') (tmp_path / 'power2_input').write_text('124000000\n') (tmp_path / 'in2_input').write_text('5016\n') (tmp_path / 'curr3_input').write_text('1688\n') (tmp_path / 'power3_input').write_text('8000000\n') (tmp_path / 'in3_input').write_text('3297\n') (tmp_path / 'curr4_input').write_text('562\n') (tmp_path / 'power4_input').write_text('1500000\n') got = mock_psu.get_status() expected = [ ('VRM temperature', approx(33.5, rel=1e-3), '°C'), ('Case temperature', approx(26.5, rel=1e-3), '°C'), ('Fan speed', approx(968, rel=1e-3), 'rpm'), ('Input voltage', approx(230, rel=1e-3), 'V'), ('Total power output', approx(140, rel=1e-3), 'W'), ('+12V output voltage', approx(11.98, rel=1e-3), 'V'), ('+12V output current', approx(10.75, rel=1e-3), 'A'), ('+12V output power', approx(124, rel=1e-3), 'W'), ('+5V output voltage', approx(5.016, rel=1e-3), 'V'), ('+5V output current', approx(1.688, rel=1e-3), 'A'), ('+5V output power', approx(8, rel=1e-3), 'W'), ('+3.3V output voltage', approx(3.297, rel=1e-3), 'V'), ('+3.3V output current', approx(0.562, rel=1e-3), 'A'), ('+3.3V output power', approx(1.5, rel=1e-3), 'W'), ('Estimated input power', approx(153, abs=1), 'W'), ('Estimated efficiency', approx(92, abs=1), '%'), ] assert sorted(got) == sorted(expected) def test_enforce_minimum_user_set_fan_duty(mock_psu): mock_psu.set_fixed_speed(channel='fan', duty=20) _, report_data = mock_psu.device.sent[1] assert report_data[2] == 30 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727919818.0 liquidctl-1.15.0/tests/test_ddr4.py0000644000175000017500000003074314677373312016272 0ustar00jonasjonasimport pytest from _testutils import VirtualSmbus from liquidctl.driver.ddr4 import * from liquidctl.error import * # SPD samples def patch_spd_dump(spd_dump, slice, new): spd_dump = bytearray(spd_dump) spd_dump[slice] = new return bytes(spd_dump) _VENGEANCE_RGB_SAMPLE = bytes.fromhex( '23100c028521000800000003090300000000080cfc0300006c6c6c110874f00a' '2008000500a81e2b2b0000000000000000000000000000000000000016361636' '1636163600002b0c2b0c2b0c2b0c000000000000000000000000000000000000' '000000000000000000000000000000000000000000edb5ce0000000000c24da7' '1111010100000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '000000000000000000000000000000000000000000000000000000000000de27' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '029e00000000000000434d5233324758344d32433333333343313620200080ce' '0000000000000000000000000000000000000000000000000000000000000000' '0c4a01200000000000a3000005fc3f04004d575710ac03f00a2008000500b022' '2c00000000000000009cceb5b5b5e7e700000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' '0000000000000000000000000000000000000000000000000000000000000000' ) # clear the part number; the Vengeance RGB sample already didn't set the TS bit _NON_TS_SPD = patch_spd_dump(_VENGEANCE_RGB_SAMPLE, slice(0x149, 0x15d), b' ' * 20) # set the TS bit _TS_SPD = patch_spd_dump(_NON_TS_SPD, 0x0e, 0x80) # DDR4 SPD decoding @pytest.fixture def cmr_spd(): return Ddr4Spd(_VENGEANCE_RGB_SAMPLE) def test_spd_bytes_used(cmr_spd): assert cmr_spd.spd_bytes_used == 384 def test_spd_bytes_total(cmr_spd): assert cmr_spd.spd_bytes_total == 512 def test_spd_revision(cmr_spd): assert cmr_spd.spd_revision == (1, 0) def test_dram_device_type(cmr_spd): assert cmr_spd.dram_device_type == Ddr4Spd.DramDeviceType.DDR4_SDRAM def test_module_type(cmr_spd): assert cmr_spd.module_type == (Ddr4Spd.BaseModuleType.UDIMM, None) def test_module_thermal_sensor(cmr_spd): assert not cmr_spd.module_thermal_sensor def test_module_manufacturer(cmr_spd): assert cmr_spd.module_manufacturer == 'Corsair' def test_module_part_number(cmr_spd): assert cmr_spd.module_part_number == 'CMR32GX4M2C3333C16' def test_dram_manufacturer(cmr_spd): assert cmr_spd.dram_manufacturer == 'Samsung' @pytest.fixture def smbus(): smbus = VirtualSmbus(parent_driver='i801_smbus') # hack: clear all spd addresses for address in range(0x50, 0x58): smbus._data[address] = None return smbus # DDR4 modules using a TSE2004-compatible SPD EEPROM with temperature sensor def test_tse2004_ignores_not_allowed_buses(smbus, monkeypatch): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) checks = [ ('parent_driver', 'other'), ] for attr, val in checks: with monkeypatch.context() as m: m.setattr(smbus, attr, val) assert list(Ddr4Temperature.probe(smbus)) == [], \ f'changing {attr} did not cause a mismatch' def test_tse2004_does_not_match_non_ee1004_device(smbus): smbus.emulate_eeprom_at(0x51, 'other', _TS_SPD) assert list(map(type, Ddr4Temperature.probe(smbus))) == [] def test_tse2004_does_not_match_non_ts_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _NON_TS_SPD) assert list(map(type, Ddr4Temperature.probe(smbus))) == [] def test_tse2004_finds_ts_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x53, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x55, 'ee1004', _TS_SPD) smbus.emulate_eeprom_at(0x57, 'ee1004', _TS_SPD) devs = list(Ddr4Temperature.probe(smbus)) assert list(map(type, devs)) == [Ddr4Temperature] * 4 assert devs[1].description.startswith('Corsair DIMM4') def test_tse2004_get_status_is_unsafe(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) assert dimm.get_status() == [] def test_tse2004_get_status_reads_temperature(smbus): enable = ['smbus', 'ddr4_temperature'] smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) with dimm.connect(unsafe=enable): smbus.write_word_data(0x19, 0x05, 0x9ce1) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', 25.75, '°C'), ] assert status == expected def test_tse2004_get_status_reads_negative_temperature(smbus): enable = ['smbus', 'ddr4_temperature'] smbus.emulate_eeprom_at(0x51, 'ee1004', _TS_SPD) dimm = next(Ddr4Temperature.probe(smbus)) with dimm.connect(unsafe=enable): smbus.write_word_data(0x19, 0x05, 0x741e) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', -24.75, '°C'), ] assert status == expected # Corsair Vengeance RGB @pytest.fixture def vengeance_rgb(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _VENGEANCE_RGB_SAMPLE) dimm = next(VengeanceRgb.probe(smbus)) smbus.open() for reg in range(256): smbus.write_byte_data(0x59, reg, 0xba) smbus.close() return (smbus, dimm) def test_vengeance_rgb_finds_devices(smbus): smbus.emulate_eeprom_at(0x51, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x53, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x55, 'ee1004', _VENGEANCE_RGB_SAMPLE) smbus.emulate_eeprom_at(0x57, 'ee1004', _VENGEANCE_RGB_SAMPLE) devs = list(VengeanceRgb.probe(smbus)) assert list(map(type, devs)) == [VengeanceRgb] * 4 assert devs[1].description.startswith('Corsair Vengeance RGB DIMM4') def test_vengeance_get_status_reads_temperature(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb def forbid(*args, **kwargs): assert False, 'should not reach here' smbus.read_block_data = forbid with dimm.connect(unsafe=enable): smbus.write_word_data(0x19, 0x05, 0x9ce1) status = dimm.get_status(unsafe=enable) expected = [ ('Temperature', 25.75, '°C'), ] assert status == expected def test_vengeance_rgb_set_color_is_unsafe(vengeance_rgb): _, dimm = vengeance_rgb with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', []) with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', [], unsafe='vengeance_rgb') with pytest.raises(UnsafeFeaturesNotEnabled): assert dimm.set_color('led', 'off', [], unsafe='smbus') def test_vengeance_rgb_asserts_rgb_address_validity(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): smbus.write_byte_data(0x59, 0xa6, 0x00) with pytest.raises(ExpectationNotMet): dimm.set_color('led', 'off', [], unsafe=enable) def test_vengeance_rgb_sets_color_to_off(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): dimm.set_color('led', 'off', [], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x00 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 assert smbus.read_byte_data(0x59, 0xa7) == 0x01 for color_component in range(0xb0, 0xb3): assert smbus.read_byte_data(0x59, color_component) == 0x00 def test_vengeance_rgb_sets_color_to_fixed(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] dimm.set_color('led', 'fixed', [radical_red], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x00 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 assert smbus.read_byte_data(0x59, 0xa7) == 0x01 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e def test_vengeance_rgb_sets_color_to_breathing(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'breathing', [radical_red, mountain_meadow], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x02 assert smbus.read_byte_data(0x59, 0xa7) == 0x02 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e assert smbus.read_byte_data(0x59, 0xb3) == 0x1a assert smbus.read_byte_data(0x59, 0xb4) == 0xb3 assert smbus.read_byte_data(0x59, 0xb5) == 0x85 def test_vengeance_rgb_sets_single_color_to_breathing(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] dimm.set_color('led', 'breathing', [radical_red], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x00 # special case assert smbus.read_byte_data(0x59, 0xa7) == 0x01 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e def test_vengeance_rgb_sets_color_to_fading(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 assert smbus.read_byte_data(0x59, 0xa6) == 0x01 assert smbus.read_byte_data(0x59, 0xa7) == 0x02 assert smbus.read_byte_data(0x59, 0xb0) == 0xff assert smbus.read_byte_data(0x59, 0xb1) == 0x35 assert smbus.read_byte_data(0x59, 0xb2) == 0x5e assert smbus.read_byte_data(0x59, 0xb3) == 0x1a assert smbus.read_byte_data(0x59, 0xb4) == 0xb3 assert smbus.read_byte_data(0x59, 0xb5) == 0x85 def test_vengeance_rgb_animation_speed_presets_set_correct_timings(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] presets = ['slowest', 'slower', 'normal', 'faster', 'fastest'] timings = [0x3f, 0x30, 0x20, 0x10, 0x01] for preset, timing in zip(presets, timings): dimm.set_color('led', 'fading', [radical_red, mountain_meadow], speed=preset, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == timing, f'wrong tp1 for {preset!r}' assert smbus.read_byte_data(0x59, 0xa5) == timing, f'wrong tp2 for {preset!r}' def test_vengeance_rgb_animation_transition_ticks_overrides_tp1(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], transition_ticks=0x01, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x01 assert smbus.read_byte_data(0x59, 0xa5) == 0x20 def test_vengeance_rgb_animation_transition_ticks_overrides_tp2(vengeance_rgb): enable = ['smbus', 'vengeance_rgb'] smbus, dimm = vengeance_rgb with dimm.connect(unsafe=enable): radical_red = [0xff, 0x35, 0x5e] mountain_meadow = [0x1a, 0xb3, 0x85] dimm.set_color('led', 'fading', [radical_red, mountain_meadow], stable_ticks=0x01, unsafe=enable) assert smbus.read_byte_data(0x59, 0xa4) == 0x20 assert smbus.read_byte_data(0x59, 0xa5) == 0x01 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_hidapi_device.py0000644000175000017500000001071214562144457020203 0ustar00jonasjonasfrom pytest import fixture from liquidctl.driver.usb import HidapiDevice class _mockhidapi: @staticmethod def device(): return _mockdevice() class _mockdevice: pass _SAMPLE_HID_INFO = { 'path': b'path', 'vendor_id': 0xf001, 'product_id': 0xf002, 'serial_number': 'serial number', 'release_number': 0xf003, 'manufacturer_string': 'manufacturer', 'product_string': 'product', 'usage_page': 0xf004, 'usage': 0xf005, 'interface_number': 0x01, } @fixture def dev(): return HidapiDevice(_mockhidapi, _SAMPLE_HID_INFO) def test_opens(dev, monkeypatch): opened = False def _open_path(path): assert isinstance(path, bytes) nonlocal opened opened = True monkeypatch.setattr(dev.hiddev, 'open_path', _open_path, raising=False) dev.open() assert opened def test_closes(dev, monkeypatch): opened = True def _close(): nonlocal opened opened = False monkeypatch.setattr(dev.hiddev, 'close', _close, raising=False) dev.close() assert not opened def test_can_clear_enqueued_reports(dev, monkeypatch): queue = [[1], [2], [3]] def _set_nonblocking(v): assert isinstance(v, int) return 0 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms == 0, 'use hid_read' nonlocal queue if queue: return queue.pop() return [] monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) dev.clear_enqueued_reports() assert not queue def test_can_clear_enqueued_reports_without_nonblocking(dev, monkeypatch): queue = [[1], [2], [3]] def _set_nonblocking(v): assert isinstance(v, int) return -1 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms > 0, 'use hid_read_timeout' nonlocal queue if queue: return queue.pop() return [] monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) dev.clear_enqueued_reports() assert not queue def test_reads(dev, monkeypatch): CANARY_TIMEOUT_MS = 1234 def _set_nonblocking(v): assert isinstance(v, int) return 0 def _read(max_length, timeout_ms=0): assert isinstance(max_length, int) assert isinstance(timeout_ms, int) assert timeout_ms == CANARY_TIMEOUT_MS, 'use hid_read' return [0xff] + [0]*(max_length - 1) # report ID is part of max_length *if present* monkeypatch.setattr(dev.hiddev, 'set_nonblocking', _set_nonblocking, raising=False) monkeypatch.setattr(dev.hiddev, 'read', _read, raising=False) assert dev.read(5, timeout=CANARY_TIMEOUT_MS) == [0xff, 0, 0, 0, 0] def test_can_write(dev, monkeypatch): def _write(buff): buff = bytes(buff) return len(buff) # report ID is (always) part of returned length monkeypatch.setattr(dev.hiddev, 'write', _write, raising=False) assert dev.write([0xff, 42]) == 2 assert dev.write([0, 42]) == 2 assert dev.write(b'foo') == 3 def test_gets_feature_report(dev, monkeypatch): def _get(report_num, max_length): assert isinstance(report_num, int) assert isinstance(max_length, int) return [report_num] + [0]*(max_length - 1) # report ID is (always) part of max_length monkeypatch.setattr(dev.hiddev, 'get_feature_report', _get, raising=False) assert dev.get_feature_report(0xff, 3) == [0xff, 0, 0] assert dev.get_feature_report(0, 3) == [0, 0, 0] def test_can_send_feature_report(dev, monkeypatch): def _send(buff): buff = bytes(buff) return len(buff) # report ID is (always) part of returned length monkeypatch.setattr(dev.hiddev, 'send_feature_report', _send, raising=False) assert dev.send_feature_report([0xff, 42]) == 2 assert dev.send_feature_report([0, 42]) == 2 assert dev.send_feature_report(b'foo') == 3 def test_exposes_unified_properties(dev): assert dev.vendor_id == 0xf001 assert dev.product_id == 0xf002 assert dev.release_number == 0xf003 assert dev.serial_number == 'serial number' assert dev.bus == 'hid' assert dev.address == 'path' assert dev.port is None ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_hwmon.py0000644000175000017500000000166714562144457016567 0ustar00jonasjonas# uses the psf/black style import pytest from liquidctl.driver.hwmon import HwmonDevice @pytest.fixture def mock_hwmon(tmp_path): hwmon = tmp_path / "hwmon7" hwmon.mkdir() (hwmon / "fan1_input").write_text("1499\n") (hwmon / "fan1_label").write_text("Pump Speed\n") return HwmonDevice("mock_module", hwmon) def test_has_module(mock_hwmon): assert mock_hwmon.driver == "mock_module" def test_has_path(mock_hwmon): assert mock_hwmon.path.is_dir() def test_has_name(mock_hwmon): assert mock_hwmon.name == "hwmon7" def test_checks_existing_attribute(mock_hwmon): assert mock_hwmon.has_attribute("fan1_input") def test_checks_non_existing_attribute(mock_hwmon): assert not mock_hwmon.has_attribute("bubble1_input") def test_gets_string(mock_hwmon): assert mock_hwmon.get_string("fan1_label") == "Pump Speed" def test_gets_int(mock_hwmon): assert mock_hwmon.read_int("fan1_input") == 1499 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_hydro_platinum.py0000644000175000017500000003370214562144457020470 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report, MockRuntimeStorage from liquidctl.driver.hydro_platinum import HydroPlatinum, _sequence from liquidctl.util import mkCrcFun _SAMPLE_PATH = (r'IOService:/AppleACPIPlatformExpert/PCI0@0/AppleACPIPCI/XHC@14/XH' r'C@14000000/HS11@14a00000/USB2.0 Hub@14a00000/AppleUSB20InternalH' r'ub@14a00000/AppleUSB20HubPort@14a10000/USB2.0 Hub@14a10000/Apple' r'USB20Hub@14a10000/AppleUSB20HubPort@14a12000/H100i Platinum@14a1' r'2000/IOUSBHostInterface@0/AppleUserUSBHostHIDDevice+Win\\#!&3142') _WIN_MAX_PATH = 260 # Windows API should be the bottleneck _crc8 = mkCrcFun('crc-8') @pytest.fixture def h115iPlatinumDevice(): description = 'Mock H115i Platinum' kwargs = {'fan_count': 2, 'fan_leds': 4} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev @pytest.fixture def h100iPlatinumSeDevice(): description = 'Mock H100i Platinum SE' kwargs = {'fan_count': 2, 'fan_leds': 16} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev @pytest.fixture def h150iProXTDevice(): description = 'Mock H150i Pro XT' kwargs = {'fan_count': 3, 'fan_leds': 0} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) runtime_storage.store('leds_enabled', 0) dev.connect(runtime_storage=runtime_storage) return dev class _MockHydroPlatinumDevice(MockHidapiDevice): def __init__(self): super().__init__(vendor_id=0xffff, product_id=0x0c17, address=_SAMPLE_PATH) self.fw_version = (1, 1, 15) self.temperature = 30.9 self.fan1_speed = 1499 self.fan2_speed = 1512 self.fan3_speed = 1777 self.pump_speed = 2702 def read(self, length): pre = super().read(length) if pre: return pre buf = bytearray(64) buf[2] = self.fw_version[0] << 4 | self.fw_version[1] buf[3] = self.fw_version[2] buf[7] = int((self.temperature - int(self.temperature)) * 255) buf[8] = int(self.temperature) buf[14] = round(.10 * 255) buf[15:17] = self.fan1_speed.to_bytes(length=2, byteorder='little') buf[21] = round(.20 * 255) buf[22:24] = self.fan2_speed.to_bytes(length=2, byteorder='little') buf[28] = round(.70 * 255) buf[29:31] = self.pump_speed.to_bytes(length=2, byteorder='little') buf[42] = round(.30 * 255) buf[43:44] = self.fan3_speed.to_bytes(length=2, byteorder='little') buf[-1] = _crc8(buf[1:-1]) return buf[:length] def test_sequence_numbers_are_correctly_generated(): runtime_storage = MockRuntimeStorage(key_prefixes=['testing']) sequence = _sequence(runtime_storage) for i in range(1, 32): assert next(sequence) == i for i in range(1, 32): assert next(sequence) == i def test_h115i_platinum_device_connect(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.disconnect() # the fixture had by default connected to the device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_h115i_platinum_device_command_format(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize() dev.get_status() dev.set_fixed_speed(channel='fan', duty=100) dev.set_speed_profile(channel='fan', profile=[]) dev.set_color(channel='led', mode='off', colors=[]) assert len(dev.device.sent) == 9 for i, (report, data) in enumerate(dev.device.sent): assert report == 0 assert len(data) == 64 assert data[0] == 0x3f assert data[1] >> 3 == i + 1 assert data[-1] == _crc8(bytes(data[1:-1])) def test_h115i_platinum_device_command_format_enabled(h115iPlatinumDevice): dev = h115iPlatinumDevice # test that the led enable messages are not sent if they are sent again dev.initialize() dev._data.store('leds_enabled', 1) dev.get_status() dev.set_fixed_speed(channel='fan', duty=100) dev.set_speed_profile(channel='fan', profile=[]) dev.set_color(channel='led', mode='off', colors=[]) assert len(dev.device.sent) == 6 for i, (report, data) in enumerate(dev.device.sent): assert report == 0 assert len(data) == 64 assert data[0] == 0x3f assert data[1] >> 3 == i + 1 assert data[-1] == _crc8(bytes(data[1:-1])) def test_h115i_platinum_device_get_status(h115iPlatinumDevice): dev = h115iPlatinumDevice temp, fan1, fan1d, fan2, fan2d, pump, pumpd = dev.get_status() assert temp[1] == pytest.approx(dev.device.temperature, abs=1 / 255) assert fan1[1] == dev.device.fan1_speed assert fan1d[1] == 10 assert fan2[1] == dev.device.fan2_speed assert fan2d[1] == 20 assert pump[1] == dev.device.pump_speed assert pumpd[1] == 70 assert dev.device.sent[0].data[1] & 0b111 == 0 assert dev.device.sent[0].data[2] == 0xff def test_h150i_pro_xt_device_get_status(h150iProXTDevice): dev = h150iProXTDevice temp, fan1, fan1d, fan2, fan2d, fan3, fan3d, pump, pumpd = dev.get_status() assert temp[1] == pytest.approx(dev.device.temperature, abs=1 / 255) assert fan1[1] == dev.device.fan1_speed assert fan1d[1] == 10 assert fan2[1] == dev.device.fan2_speed assert fan2d[1] == 20 assert fan3[1] == dev.device.fan3_speed assert fan3d[1] == 30 assert pump[1] == dev.device.pump_speed assert pumpd[1] == 70 assert dev.device.sent[0].data[1] & 0b111 == 0 assert dev.device.sent[0].data[2] == 0xff def test_h115i_platinum_device_handle_real_statuses(h115iPlatinumDevice): dev = h115iPlatinumDevice samples = [ ( 'ff08110f0001002c1e0000aee803aed10700aee803aece0701aa0000aa9c0900' '0000000000000000000000000000000000000000000000000000000000000010' ), ( 'ff40110f009e14011b0102ffe8037e6a0502ffe8037e6d0501aa0000aa350901' '0000000000000000000000000000000000000000000000000000000000000098' ) ] for sample in samples: dev.device.preload_read(Report(0, bytes.fromhex(sample))) status = dev.get_status() assert len(status) == 7 assert status[0][1] != dev.device.temperature def test_h115i_platinum_device_initialize_status(h115iPlatinumDevice): dev = h115iPlatinumDevice dev._data.store('leds_enabled', 1) (fw_version, ) = dev.initialize() assert fw_version[1] == '%d.%d.%d' % dev.device.fw_version assert dev._data.load('leds_enabled', of_type=int, default=1) == 0 def test_h115i_platinum_device_common_cooling_prefix(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize(pump_mode='extreme') dev.set_fixed_speed(channel='fan', duty=42) dev.set_speed_profile(channel='fan', profile=[(20, 0), (55, 100)]) assert len(dev.device.sent) == 3 for _, data in dev.device.sent: assert data[0x1] & 0b111 == 0 assert data[0x2] == 0x14 # opaque but apparently important prefix (see @makk50's comments in #82): assert data[0x3:0xb] == [0x0, 0xff, 0x5] + 5 * [0xff] def test_h115i_platinum_device_set_pump_mode(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.initialize(pump_mode='extreme') assert dev.device.sent[0].data[0x17] == 0x2 with pytest.raises(KeyError): dev.initialize(pump_mode='invalid') def test_h115i_platinum_device_fixed_fan_speeds(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_fixed_speed(channel='fan', duty=42) dev.set_fixed_speed(channel='fan1', duty=84) assert dev.device.sent[-1].data[0x0b] == 0x2 assert dev.device.sent[-1].data[0x10] / 2.55 == pytest.approx(84, abs=1 / 2.55) assert dev.device.sent[-1].data[0x11] == 0x2 assert dev.device.sent[-1].data[0x16] / 2.55 == pytest.approx(42, abs=1 / 2.55) with pytest.raises(ValueError): dev.set_fixed_speed('invalid', 0) def test_h150i_pro_xt_device_fixed_fan_speeds(h150iProXTDevice): dev = h150iProXTDevice dev.set_fixed_speed(channel='fan', duty=42) dev.set_fixed_speed(channel='fan1', duty=84) dev.set_fixed_speed(channel='fan3', duty=50) assert dev.device.sent[-1].data[0x01] & 0x7 == 0b000 assert dev.device.sent[-2].data[0x02] == 0x14 assert dev.device.sent[-1].data[0x0b] == 0x2 assert dev.device.sent[-1].data[0x10] / 2.55 == pytest.approx(84, abs=1 / 2.55) assert dev.device.sent[-1].data[0x11] == 0x2 assert dev.device.sent[-1].data[0x16] / 2.55 == pytest.approx(42, abs=1 / 2.55) assert dev.device.sent[-2].data[0x01] & 0x7 == 0b011 assert dev.device.sent[-2].data[0x02] == 0x14 assert dev.device.sent[-2].data[0x0b] == 0x2 assert dev.device.sent[-2].data[0x10] / 2.55 == pytest.approx(50, abs=1 / 2.55) assert dev.device.sent[-2].data[0x11] == 0x00 assert dev.device.sent[-2].data[0x16] == 0x00 def test_h115i_platinum_device_custom_fan_profiles(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_speed_profile(channel='fan', profile=iter([(20, 0), (55, 100)])) dev.set_speed_profile(channel='fan1', profile=iter([(30, 20), (50, 80)])) assert dev.device.sent[-1].data[0x0b] == 0x0 assert dev.device.sent[-1].data[0x1d] == 7 assert dev.device.sent[-1].data[0x1e:0x2c] == [30, 51, 50, 204] + 5 * [60, 255] assert dev.device.sent[-1].data[0x11] == 0x0 assert dev.device.sent[-1].data[0x2c:0x3a] == [20, 0, 55, 255] + 5 * [60, 255] with pytest.raises(ValueError): dev.set_speed_profile('invalid', []) with pytest.raises(ValueError): dev.set_speed_profile('fan', zip(range(10), range(10))) def test_h115i_platinum_device_address_leds(h115iPlatinumDevice): dev = h115iPlatinumDevice colors = [[i + 3, i + 2, i + 1] for i in range(0, 24 * 3, 3)] encoded = list(range(1, 24 * 3 + 1)) dev.set_color(channel='led', mode='super-fixed', colors=iter(colors)) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:14] == encoded[60:] def test_h100i_platinum_se_device_address_leds(h100iPlatinumSeDevice): dev = h100iPlatinumSeDevice colors = [[i + 3, i + 2, i + 1] for i in range(0, 48 * 3, 3)] encoded = list(range(1, 48 * 3 + 1)) dev.set_color(channel='led', mode='super-fixed', colors=iter(colors)) assert len(dev.device.sent) == 6 # 3 for enable, 3 for the leds assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:62] == encoded[60:120] assert dev.device.sent[5].data[1] & 0b111 == 0b110 assert dev.device.sent[5].data[2:26] == encoded[120:] def test_h150i_pro_xt_device_address_leds(h150iProXTDevice): dev = h150iProXTDevice colors = [[i + 3, i + 2, i + 1] for i in range(0, 16 * 3, 3)] encoded = list(range(1, 16 * 3 + 1)) dev.set_color(channel='led', mode='super-fixed', colors=iter(colors)) assert len(dev.device.sent) == 4 # 3 for enable, 1 for the leds assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:50] == encoded[:48] def test_h115i_platinum_device_synchronize(h115iPlatinumDevice): dev = h115iPlatinumDevice colors = [[3, 2, 1]] encoded = [1, 2, 3] * 24 dev.set_color(channel='led', mode='fixed', colors=iter(colors)) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off assert dev.device.sent[0].data[1] & 0b111 == 0b001 assert dev.device.sent[1].data[1] & 0b111 == 0b010 assert dev.device.sent[2].data[1] & 0b111 == 0b011 assert dev.device.sent[3].data[1] & 0b111 == 0b100 assert dev.device.sent[3].data[2:62] == encoded[:60] assert dev.device.sent[4].data[1] & 0b111 == 0b101 assert dev.device.sent[4].data[2:14] == encoded[60:] def test_h115i_platinum_device_leds_off(h115iPlatinumDevice): dev = h115iPlatinumDevice dev.set_color(channel='led', mode='off', colors=iter([])) assert len(dev.device.sent) == 5 # 3 for enable, 2 for off for _, data in dev.device.sent[3:5]: assert data[2:62] == [0] * 60 def test_h115i_platinum_device_invalid_color_modes(h115iPlatinumDevice): dev = h115iPlatinumDevice with pytest.raises(ValueError): dev.set_color('led', 'invalid', []) with pytest.raises(ValueError): dev.set_color('invalid', 'off', []) assert len(dev.device.sent) == 0 def test_h115i_platinum_device_short_enough_storage_path(): description = 'Mock H115i Platinum' kwargs = {'fan_count': 2, 'fan_leds': 4} device = _MockHydroPlatinumDevice() dev = HydroPlatinum(device, description, **kwargs) dev.connect() assert len(dev._data._backend._write_dir) < _WIN_MAX_PATH assert dev._data._backend._write_dir.endswith('3142') def test_h115i_platinum_device_bad_stored_data(h115iPlatinumDevice): h115iPlatinumDevice # TODO pass ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736040960.0 liquidctl-1.15.0/tests/test_keyval.py0000644000175000017500000002000514736361000016701 0ustar00jonasjonasimport multiprocessing import os import pytest import sys import time from pathlib import Path from liquidctl.keyval import RuntimeStorage, _FilesystemBackend mp_ctx = multiprocessing.get_context("spawn") @pytest.fixture def tmpstore(tmpdir): run_dir = tmpdir.mkdir("run_dir") prefixes = ["prefix"] backend = _FilesystemBackend(key_prefixes=prefixes, runtime_dirs=[run_dir]) return RuntimeStorage(prefixes, backend=backend) def test_loads_and_stores(tmpstore): assert tmpstore.load("key") is None assert tmpstore.load("key", default=42) == 42 tmpstore.store("key", "42") assert tmpstore.load("key") == "42" assert tmpstore.load("key", of_type=int) is None def test_updates_with_load_store(tmpstore): assert tmpstore.load_store("key", lambda x: x) == (None, None) assert tmpstore.load_store("key", lambda x: x, default=42) == (None, 42) assert tmpstore.load_store("key", lambda x: str(x)) == (42, "42") assert tmpstore.load_store("key", lambda x: x, of_type=int) == ("42", None) def test_fs_backend_stores_truncate_appropriately(tmpdir): run_dir = tmpdir.mkdir("run_dir") # use a separate reader to prevent caching from masking issues writer = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) reader = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) writer.store("key", 42) assert reader.load("key") == 42 writer.store("key", 1) assert reader.load("key") == 1 writer.load_store("key", lambda _: 42) assert reader.load("key") == 42 writer.load_store("key", lambda _: 1) assert reader.load("key") == 1 def test_fs_backend_loads_from_fallback_dir(tmpdir): run_dir = tmpdir.mkdir("run_dir") fb_dir = tmpdir.mkdir("fb_dir") fallback = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[fb_dir]) fallback.store("key", 42) store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir, fb_dir]) assert store.load("key") == 42 store.store("key", -1) assert store.load("key") == -1 assert fallback.load("key") == 42, "fallback location was changed" def test_fs_backend_handles_values_corupted_with_nulls(tmpdir, caplog): run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) store.store("key", 42) key_file = Path(run_dir).joinpath("prefix", "key") assert key_file.read_bytes() == b"42", "unit test is unsound" key_file.write_bytes(b"\x00") val = store.load("key") assert val is None assert "was corrupted" in caplog.text val, new_val = store.load_store("key", lambda x: 24) assert val is None assert new_val == 24 def test_fs_backend_load_store_returns_old_and_new_values(tmpdir): run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) assert store.load_store("key", lambda _: 42) == (None, 42) assert store.load_store("key", lambda x: x + 1) == (42, 43) def test_fs_backend_load_store_loads_from_fallback_dir(tmpdir): run_dir = tmpdir.mkdir("run_dir") fb_dir = tmpdir.mkdir("fb_dir") fallback = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[fb_dir]) fallback.store("key", 42) store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir, fb_dir]) assert store.load_store("key", lambda x: x + 1) == (42, 43) assert fallback.load("key") == 42, "content in fallback location changed" def test_fs_backend_load_store_loads_from_fallback_dir_that_is_symlink(tmpdir): # should deadlock if there is a problem with the lock type or with the # handling of fallback paths that point to the same principal/write # directory run_dir = tmpdir.mkdir("run_dir") fb_dir = os.path.join(run_dir, "symlink") try: os.symlink(run_dir, fb_dir, target_is_directory=True) except OSError as _: if sys.platform == "win32": pytest.skip("unable to create Windows symlink with current permissions") else: raise # don't store any initial value so that the fallback location is checked store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir, fb_dir]) assert store.load_store("key", lambda x: 42) == (None, 42) fallback = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[fb_dir]) assert fallback.load("key") == 42, "content in fallback symlink did not change" def test_fs_backend_load_store_is_atomic(tmpdir): run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) store.store("key", 42) ps = [ mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.2)), mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.2)), mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.2)), ] start_time = time.monotonic() for p in ps: p.start() for p in ps: p.join() elapsed = time.monotonic() - start_time assert store.load("key") == 45 assert elapsed >= 0.2 * len(ps) def test_fs_backend_loads_honor_load_store_locking(tmpdir): run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) store.store("key", 42) ps = [ mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.2)), mp_ctx.Process(target=_fs_mp_check_key, args=(run_dir, "prefix", "key", 43)), ] ps[0].start() time.sleep(0.1) ps[1].start() for p in ps: p.join() def test_fs_backend_stores_honor_load_store_locking(tmpdir): run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) store.store("key", 42) ps = [ mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.2)), mp_ctx.Process(target=_fs_mp_store_key, args=(run_dir, "prefix", "key", -1)), ] start_time = time.monotonic() ps[0].start() time.sleep(0.1) ps[1].start() # join second process first ps[1].join() elapsed = time.monotonic() - start_time assert elapsed >= 0.2 ps[0].join() assert store.load("key") == -1 def test_fs_backend_releases_locks(tmpdir): # should deadlock if any method does not properly release its lock run_dir = tmpdir.mkdir("run_dir") store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) def incr_from_other_process(): other = mp_ctx.Process(target=_fs_mp_increment_key, args=(run_dir, "prefix", "key", 0.0)) other.start() other.join() store.store("key", 42) incr_from_other_process() assert store.load("key") == 43 store.load_store("key", lambda _: -1) incr_from_other_process() assert store.load("key") == 0 incr_from_other_process() assert store.load("key") == 1 def _fs_mp_increment_key(run_dir, prefix, key, sleep): """Open a _FilesystemBackend and increment `key`. For the `multiprocessing` tests. Opens the storage on `run_dir` and with `prefix`. Sleeps for `sleep` seconds within the increment closure. """ def l(x): time.sleep(sleep) return x + 1 store = _FilesystemBackend(key_prefixes=[prefix], runtime_dirs=[run_dir]) store.load_store(key, l) def _fs_mp_check_key(run_dir, prefix, key, expected): """Open a _FilesystemBackend and check `key` value against `expected`. For the `multiprocessing` tests. Opens the storage on `run_dir` and with `prefix`. """ store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) assert store.load(key) == expected def _fs_mp_store_key(run_dir, prefix, key, new_value): """Open a _FilesystemBackend and store `new_value` for `key`. For the `multiprocessing` tests. Opens the storage on `run_dir` and with `prefix`. """ store = _FilesystemBackend(key_prefixes=["prefix"], runtime_dirs=[run_dir]) store.store(key, new_value) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_kraken2.py0000644000175000017500000001252414562144457016766 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice from liquidctl.driver.hwmon import HwmonDevice from liquidctl.driver.kraken2 import Kraken2 from liquidctl.error import NotSupportedByDevice @pytest.fixture def mockKrakenXDevice(): device = _MockKrakenDevice(fw_version=(6, 0, 2)) dev = Kraken2(device, 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) dev.connect() return dev @pytest.fixture def mockOldKrakenXDevice(): device = _MockKrakenDevice(fw_version=(2, 1, 8)) dev = Kraken2(device, 'Mock X62', device_type=Kraken2.DEVICE_KRAKENX) dev.connect() return dev @pytest.fixture def mockKrakenMDevice(): device = _MockKrakenDevice(fw_version=(6, 0, 2)) dev = Kraken2(device, 'Mock M22', device_type=Kraken2.DEVICE_KRAKENM) dev.connect() return dev class _MockKrakenDevice(MockHidapiDevice): def __init__(self, fw_version): super().__init__(vendor_id=0xffff, product_id=0x1e71) self.fw_version = fw_version self.temperature = 30.9 self.fan_speed = 1499 self.pump_speed = 2702 def read(self, length): pre = super().read(length) if pre: return pre buf = bytearray(64) buf[1:3] = divmod(int(self.temperature * 10), 10) buf[3:5] = self.fan_speed.to_bytes(length=2, byteorder='big') buf[5:7] = self.pump_speed.to_bytes(length=2, byteorder='big') major, minor, patch = self.fw_version buf[0xb] = major buf[0xc:0xe] = minor.to_bytes(length=2, byteorder='big') buf[0xe] = patch return buf[:length] def test_kraken_connect(mockKrakenXDevice): def mock_open(): nonlocal opened opened = True mockKrakenXDevice.device.open = mock_open opened = False with mockKrakenXDevice.connect() as cm: assert cm == mockKrakenXDevice assert opened def test_kraken_initialize(mockKrakenXDevice): (fw_ver,) = mockKrakenXDevice.initialize() assert fw_ver[1] == '6.2' def test_old_kraken_initialize(mockOldKrakenXDevice): (fw_ver,) = mockOldKrakenXDevice.initialize() assert fw_ver[1] == '2.1.8' @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True)]) def test_kraken_get_status_directly(mockKrakenXDevice, has_hwmon, direct_access): if has_hwmon: mockKrakenXDevice._hwmon = HwmonDevice(None, None) got = mockKrakenXDevice.get_status(direct_access=direct_access) expected = [ ('Liquid temperature', pytest.approx(30.9), '°C'), ('Fan speed', 1499, 'rpm'), ('Pump speed', 2702, 'rpm'), ] assert sorted(got) == sorted(expected) def test_kraken_get_status_from_hwmon(mockKrakenXDevice, tmp_path): mockKrakenXDevice._hwmon = HwmonDevice('mock_module', tmp_path) (tmp_path / 'temp1_input').write_text('20900\n') (tmp_path / 'fan1_input').write_text('2499\n') (tmp_path / 'fan2_input').write_text('1702\n') got = mockKrakenXDevice.get_status() expected = [ ('Liquid temperature', pytest.approx(20.9), '°C'), ('Fan speed', 2499, 'rpm'), ('Pump speed', 1702, 'rpm'), ] assert sorted(got) == sorted(expected) def test_kraken_not_totally_broken(mockKrakenXDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenXDevice dev.initialize() dev.set_color(channel='ring', mode='loading', colors=iter([[90, 80, 0]]), speed='slowest') dev.set_speed_profile(channel='fan', profile=iter([(20, 20), (30, 40), (40, 100)])) dev.set_fixed_speed(channel='pump', duty=50) dev.set_instantaneous_speed(channel='pump', duty=50) def test_kraken_set_fixed_speeds(mockOldKrakenXDevice): mockOldKrakenXDevice.set_fixed_speed(channel='fan', duty=42) mockOldKrakenXDevice.set_fixed_speed(channel='pump', duty=84) fan_report, pump_report = mockOldKrakenXDevice.device.sent assert fan_report.number == 2 assert fan_report.data[0:4] == [0x4d, 0, 0, 42] assert pump_report.number == 2 assert pump_report.data[0:4] == [0x4d, 0x40, 0, 84] def test_kraken_speed_profiles_not_supported(mockOldKrakenXDevice): with pytest.raises(NotSupportedByDevice): mockOldKrakenXDevice.set_speed_profile('fan', [(20, 42)]) with pytest.raises(NotSupportedByDevice): mockOldKrakenXDevice.set_speed_profile('pump', [(20, 84)]) def test_krakenM_initialize(mockKrakenMDevice): (fw_ver,) = mockKrakenMDevice.initialize() assert fw_ver[1] == '6.2' def test_krakenM_get_status(mockKrakenMDevice): assert mockKrakenMDevice.get_status() == [] def test_krakenM_speed_control_not_supported(mockKrakenMDevice): with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_fixed_speed('fan', 42) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_fixed_speed('pump', 84) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_speed_profile('fan', [(20, 42)]) with pytest.raises(NotSupportedByDevice): mockKrakenMDevice.set_speed_profile('pump', [(20, 84)]) def test_krakenM_not_totally_broken(mockKrakenMDevice): """Reasonable example calls to untested APIs do not raise exceptions.""" dev = mockKrakenMDevice dev.initialize() dev.set_color(channel='ring', mode='loading', colors=iter([[90, 80, 0]]), speed='slowest') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/tests/test_kraken3.py0000644000175000017500000004736414617346724017003 0ustar00jonasjonas# uses the psf/black style import pytest import os from _testutils import MockHidapiDevice, MockPyusbDevice, Report from liquidctl.driver.hwmon import HwmonDevice from liquidctl.driver.kraken3 import KrakenX3, KrakenZ3 from liquidctl.driver.kraken3 import ( _COLOR_CHANNELS_KRAKENX, _SPEED_CHANNELS_KRAKENX, _COLOR_CHANNELS_KRAKENZ, _SPEED_CHANNELS_KRAKENZ, _HWMON_CTRL_MAPPING_KRAKENX, _HWMON_CTRL_MAPPING_KRAKENZ, ) from test_krakenz3_response import krakenz3_response from liquidctl.util import HUE2_MAX_ACCESSORIES_IN_CHANNEL as MAX_ACCESSORIES from liquidctl.util import Hue2Accessory # https://github.com/liquidctl/liquidctl/issues/160#issuecomment-664044103 X3_SAMPLE_STATUS = bytes.fromhex( "7502200036000B51535834353320012101A80635350000000000000000000000" "0000000000000000000000000000000000000000000000000000000000000000" ) # https://github.com/liquidctl/liquidctl/issues/160#issue-665781804 X3_FAULTY_STATUS = bytes.fromhex( "7502200036000B5153583435332001FFFFCC0A64640000000000000000000000" "0000000000000000000000000000000000000000000000000000000000000000" ) Z3_SAMPLE_STATUS = bytes.fromhex( "75012E0018001051393434363731011803690314140102000000000000000000" "0000000000000000000000000000000000000000000000000000000000000000" ) test_curve_final_pwm = [ 30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 58, 65, 72, 80, 82, 83, 85, 87, 88, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, ] @pytest.fixture def mock_krakenx3(): raw = MockKraken(raw_led_channels=len(_COLOR_CHANNELS_KRAKENX) - 1) dev = KrakenX3( raw, "Mock Kraken X73", speed_channels=_SPEED_CHANNELS_KRAKENX, color_channels=_COLOR_CHANNELS_KRAKENX, hwmon_ctrl_mapping=_HWMON_CTRL_MAPPING_KRAKENX, ) dev.connect() return dev @pytest.fixture def mock_krakenz3(): raw = MockKraken(raw_led_channels=1) dev = MockKrakenZ3( raw, "Mock Kraken Z73", speed_channels=_SPEED_CHANNELS_KRAKENZ, color_channels=_COLOR_CHANNELS_KRAKENZ, hwmon_ctrl_mapping=_HWMON_CTRL_MAPPING_KRAKENZ, bulk_buffer_size=512, lcd_resolution=(320, 320), ) dev.connect() return dev class MockKraken(MockHidapiDevice): def __init__(self, raw_led_channels): super().__init__() self.raw_led_channels = raw_led_channels def write(self, data): reply = bytearray(64) if data[0:2] == [0x10, 0x01]: reply[0:2] = [0x11, 0x01] elif data[0:2] == [0x20, 0x03]: reply[0:2] = [0x21, 0x03] reply[14] = self.raw_led_channels if self.raw_led_channels > 1: reply[15 + 1 * MAX_ACCESSORIES] = Hue2Accessory.KRAKENX_GEN4_RING.value reply[15 + 2 * MAX_ACCESSORIES] = Hue2Accessory.KRAKENX_GEN4_LOGO.value elif data[0:2] == [0x30, 0x01]: reply[0:2] = [0x31, 0x01] reply[0x18] = 50 # lcd brightness reply[0x1A] = 0 # lcd orientation elif data[0:2] == [0x32, 0x1]: # setup bucket reply[14] = 0x1 elif data[0:2] == [0x32, 0x2]: # delete bucker reply[0:2] = [0x33, 0x02] reply[14] = 0x1 elif data[0:2] == [0x38, 0x1]: # switch bucket reply[14] = 0x1 self.preload_read(Report(0, reply)) return super().write(data) class MockKrakenZ3(KrakenZ3): def __init__( self, device, description, speed_channels, color_channels, bulk_buffer_size, lcd_resolution, **kwargs, ): KrakenX3.__init__(self, device, description, speed_channels, color_channels, **kwargs) self.bulk_device = MockPyusbDevice(0x1E71, 0x3008) self.bulk_device.close_winusb_device = self.bulk_device.release self.orientation = 0 self.brightness = 50 self.bulk_buffer_size = bulk_buffer_size self.lcd_resolution = lcd_resolution self.screen_mode = None self.fw = None def set_screen(self, channel, mode, value, **kwargs): self.screen_mode = mode self.hid_data_index = 0 self.bulk_data_index = 0 super().set_screen(channel, mode, value, **kwargs) assert self.hid_data_index == len( krakenz3_response[self.screen_mode + "_hid"] ), f"Incorrect number of hid messages sent for mode: {mode}" if mode == "static" or mode == "gif": assert ( self.bulk_data_index == 801 if mode == "static" else len(krakenz3_response[self.screen_mode + "_bulk"]) ), f"Incorrect number of bulk messages sent for mode: {mode}" def _write(self, data): if self.screen_mode: assert ( data == krakenz3_response[self.screen_mode + "_hid"][self.hid_data_index] ), f"HID write failed, wrong data for mode: {self.screen_mode}, data index: {self.hid_data_index}" self.hid_data_index += 1 return super()._write(data) def _bulk_write(self, data): fixed_data_index = self.bulk_data_index if ( self.screen_mode == "static" and self.bulk_data_index > 1 ): # the rest of the message should be identical to index 1 fixed_data_index = 1 assert ( data == krakenz3_response[self.screen_mode + "_bulk"][fixed_data_index] ), f"Bulk write failed, wrong data for mode: {self.screen_mode}, data index: {self.bulk_data_index}" self.bulk_data_index += 1 return super()._bulk_write(data) @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True), (True, False)]) def test_krakenx3_initializes(mock_krakenx3, has_hwmon, direct_access, tmp_path): if has_hwmon: mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) # TODO check the result _ = mock_krakenx3.initialize(direct_access=direct_access) writes = len(mock_krakenx3.device.sent) if not has_hwmon or direct_access: assert writes == 4 else: assert writes == 2 @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenx3_reads_status_directly(mock_krakenx3, has_hwmon, direct_access): if has_hwmon: mock_krakenx3._hwmon = HwmonDevice(None, None) mock_krakenx3.device.preload_read(Report(0, X3_SAMPLE_STATUS)) temperature, pump_speed, pump_duty = mock_krakenx3.get_status(direct_access=direct_access) assert temperature == ("Liquid temperature", 33.1, "°C") assert pump_speed == ("Pump speed", 1704, "rpm") assert pump_duty == ("Pump duty", 53, "%") def test_krakenx3_reads_status_from_hwmon(mock_krakenx3, tmp_path): mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "temp1_input").write_text("33100\n") (tmp_path / "fan1_input").write_text("1704\n") (tmp_path / "pwm1").write_text("135\n") temperature, pump_speed, pump_duty = mock_krakenx3.get_status() assert temperature == ("Liquid temperature", 33.1, "°C") assert pump_speed == ("Pump speed", 1704, "rpm") assert pump_duty == ("Pump duty", pytest.approx(53, rel=1.0 / 255), "%") @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenx3_set_fixed_speeds_directly(mock_krakenx3, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") mock_krakenx3.set_fixed_speed("pump", 84, direct_access=direct_access) pump_report = mock_krakenx3.device.sent[0] assert pump_report.number == 0x72 assert pump_report.data[3:43] == [84 for i in range(0, 39)] + [100] # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_krakenx3_set_fixed_speeds_hwmon(mock_krakenx3, has_support, tmp_path): mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1").write_text("0\n") (tmp_path / "pwm1_enable").write_text("0\n") mock_krakenx3.set_fixed_speed("pump", 84) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "1" assert (tmp_path / "pwm1").read_text() == "214" else: # Assert fallback to direct access pump_report = mock_krakenx3.device.sent[0] assert pump_report.number == 0x72 assert pump_report.data[3:43] == [84 for i in range(0, 39)] + [100] @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenx3_set_speed_profile_directly(mock_krakenx3, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") for i in range(1, 40 + 1): (tmp_path / f"temp1_auto_point{i}_pwm").write_text("0") curve_profile = zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100]) mock_krakenx3.set_speed_profile("pump", curve_profile, direct_access=direct_access) pump_report = mock_krakenx3.device.sent[0] assert pump_report.number == 0x72 assert pump_report.data[3:43] == test_curve_final_pwm # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" for i in range(1, 40): assert (tmp_path / f"temp1_auto_point{i}_pwm").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_krakenx3_set_speed_profile_hwmon(mock_krakenx3, has_support, tmp_path): mock_krakenx3._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1_enable").write_text("0\n") for i in range(1, 40 + 1): (tmp_path / f"temp1_auto_point{i}_pwm").write_text("0") curve_profile = zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100]) mock_krakenx3.set_speed_profile("pump", curve_profile) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "2" for i in range(1, 40 + 1): assert int((tmp_path / f"temp1_auto_point{i}_pwm").read_text()) == ( test_curve_final_pwm[i - 1] * 255 // 100 ) else: # Assert fallback to direct access pump_report = mock_krakenx3.device.sent[0] assert pump_report.number == 0x72 assert pump_report.data[3:43] == test_curve_final_pwm @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenx3_warns_on_faulty_temperature(mock_krakenx3, has_hwmon, direct_access, caplog): if has_hwmon: mock_krakenx3._hwmon = HwmonDevice(None, None) mock_krakenx3.device.preload_read(Report(0, X3_FAULTY_STATUS)) _ = mock_krakenx3.get_status(direct_access=direct_access) assert "unexpected temperature reading" in caplog.text def test_krakenx3_not_totally_broken(mock_krakenx3): """Reasonable example calls to untested APIs do not raise exceptions.""" mock_krakenx3.initialize() mock_krakenx3.set_color(channel="ring", mode="fixed", colors=iter([[3, 2, 1]]), speed="fastest") mock_krakenx3.set_speed_profile(channel="pump", profile=iter([(20, 20), (30, 50), (40, 100)])) mock_krakenx3.set_fixed_speed(channel="pump", duty=50) @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenz3_reads_status_directly(mock_krakenz3, has_hwmon, direct_access): if has_hwmon: mock_krakenz3._hwmon = HwmonDevice(None, None) mock_krakenz3.device.preload_read(Report(0, Z3_SAMPLE_STATUS)) temperature, pump_speed, pump_duty, fan_speed, fan_duty = mock_krakenz3.get_status( direct_access=direct_access ) assert temperature == ("Liquid temperature", 24.3, "°C") assert pump_speed == ("Pump speed", 873, "rpm") assert pump_duty == ("Pump duty", 20, "%") assert fan_speed == ("Fan speed", 0, "rpm") assert fan_duty == ("Fan duty", 0, "%") def test_krakenz3_reads_status_from_hwmon(mock_krakenz3, tmp_path): mock_krakenz3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "temp1_input").write_text("33100\n") (tmp_path / "fan1_input").write_text("1704\n") (tmp_path / "pwm1").write_text("135\n") (tmp_path / "fan2_input").write_text("1704\n") (tmp_path / "pwm2").write_text("135\n") temperature, pump_speed, pump_duty, fan_speed, fan_duty = mock_krakenz3.get_status() assert temperature == ("Liquid temperature", 33.1, "°C") assert pump_speed == ("Pump speed", 1704, "rpm") assert pump_duty == ("Pump duty", pytest.approx(53, rel=1.0 / 255), "%") assert fan_speed == ("Fan speed", 1704, "rpm") assert fan_duty == ("Fan duty", pytest.approx(53, rel=1.0 / 255), "%") @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenz3_set_fixed_speeds_directly(mock_krakenz3, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mock_krakenz3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") (tmp_path / "pwm2").write_text("0") (tmp_path / "pwm2_enable").write_text("0") mock_krakenz3.set_fixed_speed("pump", 84, direct_access=direct_access) mock_krakenz3.set_fixed_speed("fan", 50, direct_access=direct_access) pump_report, fan_report = mock_krakenz3.device.sent assert pump_report.number == 0x72 assert pump_report.data[3:43] == [84 for i in range(0, 39)] + [100] assert fan_report.number == 0x72 assert fan_report.data[3:43] == [50 for i in range(0, 39)] + [100] # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" assert (tmp_path / "pwm2_enable").read_text() == "0" assert (tmp_path / "pwm2").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_krakenz3_set_fixed_speeds_hwmon(mock_krakenz3, has_support, tmp_path): mock_krakenz3._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1").write_text("0\n") (tmp_path / "pwm1_enable").write_text("0\n") (tmp_path / "pwm2").write_text("0\n") (tmp_path / "pwm2_enable").write_text("0\n") mock_krakenz3.set_fixed_speed("pump", 84) mock_krakenz3.set_fixed_speed("fan", 50) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "1" assert (tmp_path / "pwm1").read_text() == "214" assert (tmp_path / "pwm2_enable").read_text() == "1" assert (tmp_path / "pwm2").read_text() == "127" else: # Assert fallback to direct access pump_report, fan_report = mock_krakenz3.device.sent assert pump_report.number == 0x72 assert pump_report.data[3:43] == [84 for i in range(0, 39)] + [100] assert fan_report.number == 0x72 assert fan_report.data[3:43] == [50 for i in range(0, 39)] + [100] @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_krakenz3_set_speed_profile_directly(mock_krakenz3, has_hwmon, direct_access, tmp_path): """For both test cases only direct access should be used""" if has_hwmon: mock_krakenz3._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1").write_text("0") (tmp_path / "pwm1_enable").write_text("0") (tmp_path / "pwm2").write_text("0") (tmp_path / "pwm2_enable").write_text("0") for i in range(1, 40 + 1): (tmp_path / f"temp1_auto_point{i}_pwm").write_text("0") (tmp_path / f"temp2_auto_point{i}_pwm").write_text("0") mock_krakenz3.set_speed_profile( "pump", zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100]), direct_access=direct_access ) mock_krakenz3.set_speed_profile( "fan", zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100]), direct_access=direct_access ) pump_report, fan_report = mock_krakenz3.device.sent assert pump_report.number == 0x72 assert pump_report.data[3:43] == test_curve_final_pwm assert fan_report.number == 0x72 assert fan_report.data[3:43] == test_curve_final_pwm # Assert that hwmon wasn't touched if has_hwmon: assert (tmp_path / "pwm1_enable").read_text() == "0" assert (tmp_path / "pwm1").read_text() == "0" assert (tmp_path / "pwm2_enable").read_text() == "0" assert (tmp_path / "pwm2").read_text() == "0" for i in range(1, 40): assert (tmp_path / f"temp1_auto_point{i}_pwm").read_text() == "0" assert (tmp_path / f"temp2_auto_point{i}_pwm").read_text() == "0" @pytest.mark.parametrize("has_support", [False, True]) def test_krakenz3_set_speed_profile_hwmon(mock_krakenz3, has_support, tmp_path): mock_krakenz3._hwmon = HwmonDevice("mock_module", tmp_path) if has_support: (tmp_path / "pwm1_enable").write_text("0\n") (tmp_path / "pwm2_enable").write_text("0\n") for i in range(1, 40 + 1): (tmp_path / f"temp1_auto_point{i}_pwm").write_text("0") (tmp_path / f"temp2_auto_point{i}_pwm").write_text("0") mock_krakenz3.set_speed_profile("pump", zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100])) mock_krakenz3.set_speed_profile("fan", zip([20, 30, 34, 40, 50], [30, 50, 80, 90, 100])) if has_support: assert (tmp_path / "pwm1_enable").read_text() == "2" for i in range(1, 40 + 1): assert int((tmp_path / f"temp1_auto_point{i}_pwm").read_text()) == ( test_curve_final_pwm[i - 1] * 255 // 100 ) assert int((tmp_path / f"temp2_auto_point{i}_pwm").read_text()) == ( test_curve_final_pwm[i - 1] * 255 // 100 ) else: # Assert fallback to direct access pump_report, fan_report = mock_krakenz3.device.sent assert pump_report.number == 0x72 assert pump_report.data[3:43] == test_curve_final_pwm assert fan_report.number == 0x72 assert fan_report.data[3:43] == test_curve_final_pwm def test_krakenz3_not_totally_broken(mock_krakenz3): """Reasonable example calls to untested APIs do not raise exceptions.""" mock_krakenz3.initialize() mock_krakenz3.device.preload_read(Report(0, Z3_SAMPLE_STATUS)) _ = mock_krakenz3.get_status() mock_krakenz3.set_speed_profile(channel="fan", profile=iter([(20, 20), (30, 50), (40, 100)])) mock_krakenz3.set_fixed_speed(channel="pump", duty=50) def test_krakenz3_screen_not_totally_broken(mock_krakenz3): """Reasonable example calls to untested APIs do not raise exceptions.""" mock_krakenz3.initialize() mock_krakenz3.set_screen("lcd", "liquid", None) mock_krakenz3.set_screen("lcd", "brightness", "60") mock_krakenz3.set_screen("lcd", "orientation", "90") mock_krakenz3.set_screen( "lcd", "static", os.path.join(os.path.dirname(os.path.abspath(__file__)), "yellow.jpg") ) @pytest.mark.skip("Currently broken with pillow >= 10.2.0 (see #661)") def test_krakenz3_screen_not_totally_broken_part2(mock_krakenz3): """Reasonable example calls to untested APIs do not raise exceptions.""" mock_krakenz3.initialize() mock_krakenz3.set_screen( "lcd", "gif", os.path.join(os.path.dirname(os.path.abspath(__file__)), "rgb.gif") ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_krakenz3_response.py0000644000175000017500000002200414562144457021071 0ustar00jonasjonas# fmt:off krakenz3_response = { "liquid_hid": [[48, 1], [56, 1, 2, 0]], "brightness_hid": [[48, 1], [48, 2, 1, 60, 0, 0, 1, 0]], "orientation_hid": [[48, 1], [48, 2, 1, 50, 0, 0, 1, 1]], "static_hid": [ [48, 1], [54, 3], [48, 4, 0], [48, 4, 1], [48, 4, 2], [48, 4, 3], [48, 4, 4], [48, 4, 5], [48, 4, 6], [48, 4, 7], [48, 4, 8], [48, 4, 9], [48, 4, 10], [48, 4, 11], [48, 4, 12], [48, 4, 13], [48, 4, 14], [48, 4, 15], [50, 2, 0], [50, 1, 0, 1, 0, 0, 145, 1, 1], [54, 1, 0], [54, 2], [56, 1, 4, 0], ], "gif_hid": [[48,1], [54,3], [48,4,0], [48,4,1], [48,4,2], [48,4,3], [48,4,4], [48,4,5], [48,4,6], [48,4,7], [48,4,8], [48,4,9], [48,4,10], [48,4,11], [48,4,12], [48,4,13], [48,4,14], [48,4,15], [50,2,0], [50,1,0,1,0,0,2,0,1], [54,1,0], [54,2], [56,1,4,0]], "static_bulk": [ [18,250,1,232,171,205,239,152,118,84,50,16,2,0,0,0,0,64,6,0], [255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0,255,255,1,0]], "gif_bulk": [ [18,250,1,232,171,205,239,152,118,84,50,16,1,0,0,0,189,6,0,0], [71,73,70,56,57,97,64,1,64,1,129,3,0,255,0,0,0,0,0,0,0,0,0,0,0,33,255,11,78,69,84,83,67,65,80,69,50,46,48,3,1,0,0,0,44,0,0,0,0,64,1,64,1,0,8,255,0,1,8,28,72,176,160,193,131,8,19,42,92,200,176,161,195,135,16,35,74,156,72,177,162,197,139,24,51,106,220,200,177,163,199,143,32,67,138,28,73,178,164,201,147,40,83,170,92,201,178,165,203,151,48,99,202,156,73,179,166,205,155,56,115,234,220,201,179,167,207,159,64,131,10,29,74,180,168,209,163,72,147,42,93,202,180,169,211,167,80,163,74,157,74,181,170,213,171,88,179,106,221,202,181,171,215,175,96,195,138,29,75,182,172,217,179,104,211,170,93,203,182,173,219,183,112,227,202,157,75,183,174,221,187,120,243,234,221,203,183,175,223,191,128,3,11,30,76,184,176,225,195,136,19,43,94,204,184,177,227,199,144,35,75,158,76,185,178,229,203,152,51,107,222,204,185,179,231,207,160,67,139,30,77,186,180,233,211,168,83,171,94,205,186,181,235,215,176,99,203,158,77,187,182,237,219,184,115,235,222,205,187,183,239,223,192,131,11,31,78,188,184,241,227,200,147,43,95,206,188,185,243,231,208,163,75,159,78,189,186,245,235,216,179,107,223,206,189,187,247,239,224,195,139,255,31,79,190,188,249,243,232,211,171,95,207,190,189,251,247,240,227,203,159,79,191,190,253,251,248,243,235,223,207,191,191,255,255,0,6,40,224,128,4,22,104,224,129,8,38,168,224,130,12,54,232,224,131,16,70,40,225,132,20,86,104,225,133,24,102,168,225,134,28,118,232,225,135,32,134,40,226,136,36,150,104,226,137,40,166,168,226,138,44,182,232,226,139,48,198,40,227,140,52,214,104,227,141,56,230,168,227,142,60,246,232,227,143,64,6,41,228,144,68,22,105,228,145,72,38,169,228,146,76,54,233,228,147,80,70,41,229,148,84,86,105,229,149,88,102,169,229,150,92,118,233,229,151,96,134,41,230,152,100,150,105,230,153,104,166,169,230,154,108,182,233,230,155,112,198,41,231,156,116,214,105,231,157,120,230,169,231,158,124,246,233,231,159,128,6,42,232,160,132,22], [106,232,161,136,38,170,232,162,140,54,234,232,163,144,70,42,233,164,148,86,106,233,165,152,102,170,233,166,156,118,234,233,167,160,134,42,234,168,164,150,106,234,169,168,166,170,234,170,172,182,234,234,171,176,198,27,42,235,172,180,214,106,235,173,184,230,170,235,174,188,246,234,235,175,192,6,43,236,176,196,90,20,16,0,44,0,0,0,0,64,1,64,1,129,0,255,0,0,0,0,0,0,0,0,0,0,8,255,0,1,8,28,72,176,160,193,131,8,19,42,92,200,176,161,195,135,16,35,74,156,72,177,162,197,139,24,51,106,220,200,177,163,199,143,32,67,138,28,73,178,164,201,147,40,83,170,92,201,178,165,203,151,48,99,202,156,73,179,166,205,155,56,115,234,220,201,179,167,207,159,64,131,10,29,74,180,168,209,163,72,147,42,93,202,180,169,211,167,80,163,74,157,74,181,170,213,171,88,179,106,221,202,181,171,215,175,96,195,138,29,75,182,172,217,179,104,211,170,93,203,182,173,219,183,112,227,202,157,75,183,174,221,187,120,243,234,221,203,183,175,223,191,128,3,11,30,76,184,176,225,195,136,19,43,94,204,184,177,227,199,144,35,75,158,76,185,178,229,203,152,51,107,222,204,185,179,231,207,160,67,139,30,77,186,180,233,211,168,83,171,94,205,186,181,235,215,176,99,203,158,77,187,182,237,219,184,115,235,222,205,187,183,239,223,192,131,11,31,78,188,184,241,227,200,147,43,95,206,188,185,243,231,208,163,75,159,78,189,186,245,235,216,179,107,223,206,189,187,247,239,224,195,139,255,31,79,190,188,249,243,232,211,171,95,207,190,189,251,247,240,227,203,159,79,191,190,253,251,248,243,235,223,207,191,191,255,255,0,6,40,224,128,4,22,104,224,129,8,38,168,224,130,12,54,232,224,131,16,70,40,225,132,20,86,104,225,133,24,102,168,225,134,28,118,232,225,135,32,134,40,226,136,36,150,104,226,137,40,166,168,226,138,44,182,232,226,139,48,198,40,227,140,52,214,104,227,141,56,230,168,227,142,60,246,232,227,143,64,6,41,228,144,68,22,105,228,145,72,38,169,228,146,76,54,233,228,147,80,70,41,229,148,84,86,105,229,149,88,102,169,229,150], [92,118,233,229,151,96,134,41,230,152,100,150,105,230,153,104,166,169,230,154,108,182,233,230,155,112,198,41,231,156,116,214,105,231,157,120,230,169,231,158,124,246,233,231,159,128,6,42,232,160,132,22,106,232,161,136,38,170,232,162,140,54,234,232,163,144,70,42,233,164,148,86,106,233,165,152,102,170,233,166,156,118,234,233,167,160,134,42,234,168,164,150,106,234,169,168,166,170,234,170,172,182,234,234,171,176,198,27,42,235,172,180,214,106,235,173,184,230,170,235,174,188,246,234,235,175,192,6,43,236,176,196,90,20,16,0,44,0,0,0,0,64,1,64,1,129,0,0,255,0,0,0,0,0,0,0,0,0,8,255,0,1,8,28,72,176,160,193,131,8,19,42,92,200,176,161,195,135,16,35,74,156,72,177,162,197,139,24,51,106,220,200,177,163,199,143,32,67,138,28,73,178,164,201,147,40,83,170,92,201,178,165,203,151,48,99,202,156,73,179,166,205,155,56,115,234,220,201,179,167,207,159,64,131,10,29,74,180,168,209,163,72,147,42,93,202,180,169,211,167,80,163,74,157,74,181,170,213,171,88,179,106,221,202,181,171,215,175,96,195,138,29,75,182,172,217,179,104,211,170,93,203,182,173,219,183,112,227,202,157,75,183,174,221,187,120,243,234,221,203,183,175,223,191,128,3,11,30,76,184,176,225,195,136,19,43,94,204,184,177,227,199,144,35,75,158,76,185,178,229,203,152,51,107,222,204,185,179,231,207,160,67,139,30,77,186,180,233,211,168,83,171,94,205,186,181,235,215,176,99,203,158,77,187,182,237,219,184,115,235,222,205,187,183,239,223,192,131,11,31,78,188,184,241,227,200,147,43,95,206,188,185,243,231,208,163,75,159,78,189,186,245,235,216,179,107,223,206,189,187,247,239,224,195,139,255,31,79,190,188,249,243,232,211,171,95,207,190,189,251,247,240,227,203,159,79,191,190,253,251,248,243,235,223,207,191,191,255,255,0,6,40,224,128,4,22,104,224,129,8,38,168,224,130,12,54,232,224,131,16,70,40,225,132,20,86,104,225,133,24,102,168,225,134,28,118,232,225,135,32,134,40,226,136,36,150,104,226,137,40,166,168,226,138,44,182,232,226,139,48,198,40], [227,140,52,214,104,227,141,56,230,168,227,142,60,246,232,227,143,64,6,41,228,144,68,22,105,228,145,72,38,169,228,146,76,54,233,228,147,80,70,41,229,148,84,86,105,229,149,88,102,169,229,150,92,118,233,229,151,96,134,41,230,152,100,150,105,230,153,104,166,169,230,154,108,182,233,230,155,112,198,41,231,156,116,214,105,231,157,120,230,169,231,158,124,246,233,231,159,128,6,42,232,160,132,22,106,232,161,136,38,170,232,162,140,54,234,232,163,144,70,42,233,164,148,86,106,233,165,152,102,170,233,166,156,118,234,233,167,160,134,42,234,168,164,150,106,234,169,168,166,170,234,170,172,182,234,234,171,176,198,27,42,235,172,180,214,106,235,173,184,230,170,235,174,188,246,234,235,175,192,6,43,236,176,196,90,20,16,0,59]] } ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1715326420.0 liquidctl-1.15.0/tests/test_msi.py0000644000175000017500000002523414617346724016225 0ustar00jonasjonas# uses the psf/black style from struct import pack from datetime import datetime import pytest from _testutils import MockHidapiDevice, Report from liquidctl.driver.msi import MpgCooler, _REPORT_LENGTH, _DEFAULT_FEATURE_DATA, _LightingMode from liquidctl.error import UnsafeFeaturesNotEnabled @pytest.fixture def mpgCoreLiquidK360Device(): description = "Mock MPG CoreLiquid K360" device = _MockCoreLiquid(vendor_id=0xFFFF, product_id=0xB130) dev = MpgCooler(device, description) dev.connect() return dev @pytest.fixture def mpgCoreLiquidDeviceExperimental(): _, pid, desc, kwargs = MpgCooler._MATCHES[-1] description = "Mock " + desc unsafe = kwargs["_unsafe"] device = _MockCoreLiquid(vendor_id=0xFFFF, product_id=pid) dev = MpgCooler(device, description, **kwargs, unsafe=unsafe) dev.connect(unsafe=unsafe) return dev @pytest.fixture def mpgCoreLiquidK360DeviceInvalid(): description = "Mock MPG CoreLiquid K360" device = _MockCoreLiquidInvalid(vendor_id=0xFFFF, product_id=0xB130) dev = MpgCooler(device, description) dev.connect() return dev class _MockCoreLiquid(MockHidapiDevice): def __init__(self, **kwargs): super().__init__(**kwargs) self._fan_configs = (4, 20, 40, 50, 60, 70, 80, 90) self._fan_temp_configs = (4, 30, 40, 50, 60, 70, 80, 90) self._model_idx = 255 # TODO: check the correct model index from device self._feature_data = Report(_DEFAULT_FEATURE_DATA[0], _DEFAULT_FEATURE_DATA[1:]) self.preload_read(self._feature_data) def write(self, data): reply = bytearray(_REPORT_LENGTH) reply[0:2] = data[0:2] if list(data[:2]) == [0x01, 0xB1]: # get current model idx request reply[2] = self._model_idx elif list(data[:2]) == [0xD0, 0x31]: # get status request reply[2:23] = ( pack(" 1: reply[15 + 1 * 6] = 0x10 reply[15 + 2 * 6] = 0x11 self.preload_read(Report(reply[0], reply[1:])) return super().write(data) @pytest.fixture def mock_smart2(): raw = MockH1V2(raw_speed_channels=2, raw_led_channels=0) dev = H1V2(raw, "Mock H1 V2", speed_channel_count=2, color_channel_count=0) dev.connect() return dev @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True), (True, False)]) def test_initializes(mock_smart2, has_hwmon, direct_access, tmp_path): if has_hwmon: mock_smart2._hwmon = HwmonDevice("mock_module", tmp_path) _ = mock_smart2.initialize(direct_access=direct_access) writes = len(mock_smart2.device.sent) if not has_hwmon or direct_access: assert writes == 4 else: assert writes == 2 @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (False, True), (True, True)]) def test_reads_status(mock_smart2, has_hwmon, direct_access): if has_hwmon: mock_smart2._hwmon = HwmonDevice(None, None) mock_smart2.device.preload_read(Report(0, SAMPLE_STATUS)) expected = [ ("Fan 1 control mode", "PWM", ""), ("Fan 1 duty", 30, "%"), ("Fan 1 speed", 644, "rpm"), ("Fan 2 control mode", "PWM", ""), ("Fan 2 duty", 100, "%"), ("Fan 2 speed", 1785, "rpm"), ("Pump speed", 4000, "rpm"), ] got = mock_smart2.get_status(direct_access=direct_access) assert sorted(got) == sorted(expected) def test_constructor_sets_up_all_channels(mock_smart2): assert mock_smart2._speed_channels == { "fan1": (0, 0, 100), "fan2": (1, 0, 100), } def test_not_totally_broken(mock_smart2): _ = mock_smart2.initialize() mock_smart2.device.preload_read(Report(0, [0x75, 0x02] + [0] * 62)) _ = mock_smart2.get_status() mock_smart2.set_fixed_speed(channel="fan2", duty=50) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1673152806.0 liquidctl-1.15.0/tests/test_rgb_fusion2.py0000644000175000017500000001755514356444446017663 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report from collections import deque from liquidctl.driver.rgb_fusion2 import RgbFusion2 # Sample data for 5702 controller from a Gigabyte Z490 Vision D # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-663213956 _INIT_5702_DATA = bytes.fromhex( 'cc01000701000a00000000004954353730322d47494741425954452056312e30' '2e31302e30000000000102000200010002000100000102000001025700000000' ) _INIT_5702_SAMPLE = Report(_INIT_5702_DATA[0], _INIT_5702_DATA[1:]) # Sample data for 8297 controller from a Gigabyte X570 Aorus Elite rev 1.0 # https://github.com/liquidctl/liquidctl/issues/151#issuecomment-663247422 # (note: original data had a trailing 0x61 byte, but that seems to be an artifact) _INIT_8297_DATA = bytes.fromhex( '00010001010006000000000049543832393742582d4742583537300000000000' '0000000000000000000000000200010002000100000102000001978200000000' ) _INIT_8297_SAMPLE = Report(_INIT_8297_DATA[0], _INIT_8297_DATA[1:]) @pytest.fixture def mockRgbFusion2_5702Device(): device = MockHidapiDevice(vendor_id=0x048d, product_id=0x5702, address='addr') dev = RgbFusion2(device, 'mock 5702 Controller') dev.connect() return dev class Mock8297HidInterface(MockHidapiDevice): def get_feature_report(self, report_id, length): """Get a feature report emulating out of spec behavior of the device.""" return super().get_feature_report(0, length) @pytest.fixture def mockRgbFusion2_8297Device(): device = Mock8297HidInterface(vendor_id=0x048d, product_id=0x8297, address='addr') dev = RgbFusion2(device, 'mock 8297 Controller') dev.connect() return dev def test_fusion2_5702_device_command_format(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.device.preload_read(_INIT_5702_SAMPLE) mockRgbFusion2_5702Device.initialize() mockRgbFusion2_5702Device.set_color(channel='sync', mode='off', colors=[]) assert len(mockRgbFusion2_5702Device.device.sent) == 1 + 8 + 1 for i, (report, data) in enumerate(mockRgbFusion2_5702Device.device.sent): assert report == 0xcc assert len(data) == 63 # TODO double check, more likely to be 64 def test_fusion2_5702_device_get_status(mockRgbFusion2_5702Device): assert mockRgbFusion2_5702Device.get_status() == [] def test_fusion2_5702_device_initialize_status(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.device.preload_read(_INIT_5702_SAMPLE) name, fw_version = mockRgbFusion2_5702Device.initialize() assert name[1] == "IT5702-GIGABYTE V1.0.10.0" assert fw_version[1] == '1.0.10.0' def test_fusion2_5702_device_off_with_some_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] # should be ignored mockRgbFusion2_5702Device.set_color(channel='led8', mode='off', colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x27, 0x80] assert set_color.data[10] == 0x01 assert max(set_color.data[13:16]) == 0 assert max(set_color.data[21:27]) == 0 def test_fusion2_5702_device_fixed_with_some_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led7', mode='fixed', colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x26, 0x40] assert set_color.data[10] == 0x01 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert max(set_color.data[21:27]) == 0 def test_fusion2_5702_device_pulse_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led3', mode='pulse', colors=iter(colors), speed='faster') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x22, 0x04] assert set_color.data[10] == 0x02 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0xe8, 0x03, 0xe8, 0x03, 0xf4, 0x01] def test_fusion2_5702_device_flash_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led6', mode='flash', colors=iter(colors), speed='slowest') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x25, 0x20] assert set_color.data[10] == 0x03 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0x64, 0x00, 0x64, 0x00, 0x60, 0x09] def test_fusion2_5702_device_double_flash_with_some_channel_and_speed_and_uppercase(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80], [0x30, 0x30, 0x30]] # second color should be ignored mockRgbFusion2_5702Device.set_color(channel='led5', mode='double-flash', colors=iter(colors), speed='ludicrous') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x24, 0x10] assert set_color.data[10] == 0x03 assert set_color.data[13:16] == [0x80, 0x00, 0xff] assert set_color.data[21:27] == [0x64, 0x00, 0x64, 0x00, 0x40, 0x06] def test_fusion2_5702_device_color_cycle_with_some_channel_and_speed(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] # should be ignored mockRgbFusion2_5702Device.set_color(channel='led4', mode='color-cycle', colors=iter(colors), speed='fastest') set_color, execute = mockRgbFusion2_5702Device.device.sent assert set_color.data[0:2] == [0x23, 0x08] assert set_color.data[10] == 0x04 assert max(set_color.data[13:16]) == 0 assert set_color.data[21:27] == [0x26, 0x02, 0xc2, 0x01, 0x00, 0x00] # TODO brightness def test_fusion2_5702_device_common_behavior_in_all_set_color_writes(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] for mode in ['off', 'fixed', 'pulse', 'flash', 'double-flash', 'color-cycle']: mockRgbFusion2_5702Device.device.sent = deque() mockRgbFusion2_5702Device.set_color(channel='led1', mode=mode, colors=iter(colors)) set_color, execute = mockRgbFusion2_5702Device.device.sent assert execute.data[0:2] == [0x28, 0xff] assert max(execute.data[2:]) == 0 def test_fusion2_5702_device_sync_channel(mockRgbFusion2_5702Device): colors = [[0xff, 0, 0x80]] mockRgbFusion2_5702Device.set_color(channel='sync', mode='fixed', colors=iter(colors)) assert len(mockRgbFusion2_5702Device.device.sent) == 8 + 1 # 8 × set + execute def test_fusion2_5702_device_reset_all_channels(mockRgbFusion2_5702Device): mockRgbFusion2_5702Device.reset_all_channels() for addr, report in enumerate(mockRgbFusion2_5702Device.device.sent[:-1], 0x20): assert report.data[0:2] == [addr, 0] assert max(report.data[2:]) == 0 execute = mockRgbFusion2_5702Device.device.sent[-1] assert execute.data[0:2] == [0x28, 0xff] assert max(execute.data[2:]) == 0 def test_fusion2_5702_device_invalid_set_color_arguments(mockRgbFusion2_5702Device): with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('invalid', 'off', []) with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('led1', 'invalid', []) with pytest.raises(ValueError): mockRgbFusion2_5702Device.set_color('led1', 'fixed', []) with pytest.raises(KeyError): mockRgbFusion2_5702Device.set_color('led1', 'pulse', [[0xff, 0, 0x80]], speed='invalid') def test_fusion2_8297_device_initialize_status(mockRgbFusion2_8297Device): mockRgbFusion2_8297Device.device.preload_read(_INIT_8297_SAMPLE) name, fw_version = mockRgbFusion2_8297Device.initialize() assert name[1] == "IT8297BX-GBX570" assert fw_version[1] == '1.0.6.0' # other tests skipped, see Controller5702TestCase ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_smart_device.py0000644000175000017500000001105114562144457020070 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice, Report from liquidctl.driver.hwmon import HwmonDevice from liquidctl.driver.smart_device import SmartDevice SAMPLE_RESPONSES = [ '043e00056e00000b5b000301000007200002001e00', '04400005b500000b5b000201000007020002001e00', '044000053800000b5b000201000007120102001e00', ] @pytest.fixture def mockSmartDevice(): device = MockHidapiDevice(vendor_id=0x1e71, product_id=0x1714, address='addr') return SmartDevice(device, 'mock NZXT Smart Device V1', speed_channel_count=3, color_channel_count=1) # class methods def test_smart_device_constructor(mockSmartDevice): assert mockSmartDevice._speed_channels == { 'fan1': (0, 0, 100), 'fan2': (1, 0, 100), 'fan3': (2, 0, 100), } assert mockSmartDevice._color_channels == {'led': (0), } def test_smart_device_not_totally_broken(mockSmartDevice): dev = mockSmartDevice for i in range(4): dev.device.preload_read(Report(0, bytes(63))) dev.initialize() dev.get_status() dev.set_color(channel='led', mode='breathing', colors=iter([[142, 24, 68]]), speed='fastest') dev.set_fixed_speed(channel='fan3', duty=50) @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True), (True, False)]) def test_smart_device_initializes(mockSmartDevice, has_hwmon, direct_access, tmp_path): dev = mockSmartDevice if has_hwmon: dev._hwmon = HwmonDevice('mock_module', tmp_path) for _, capdata in enumerate(SAMPLE_RESPONSES): capdata = bytes.fromhex(capdata) dev.device.preload_read(Report(capdata[0], capdata[1:])) expected = [ ('Firmware version', '1.7', ''), ('LED accessories', 2, ''), ('LED accessory type', 'HUE+ Strip', ''), ('LED count (total)', 20, ''), ] got = dev.initialize(direct_access=direct_access) assert expected == got writes = len(dev.device.sent) if not has_hwmon or direct_access: assert writes == 2 else: assert writes == 0 @pytest.mark.parametrize('has_hwmon,direct_access', [(False, False), (True, True)]) def test_smart_device_reads_status_directly(mockSmartDevice, has_hwmon, direct_access): dev = mockSmartDevice if has_hwmon: dev._hwmon = HwmonDevice(None, None) for _, capdata in enumerate(SAMPLE_RESPONSES): capdata = bytes.fromhex(capdata) dev.device.preload_read(Report(capdata[0], capdata[1:])) # skip initialize for now, we're not emulating the behavior precisely # enough to require it here expected = [ ('Fan 1 speed', 1461, 'rpm'), ('Fan 1 voltage', 11.91, 'V'), ('Fan 1 current', 0.02, 'A'), ('Fan 1 control mode', 'PWM', ''), ('Fan 2 speed', 1336, 'rpm'), ('Fan 2 voltage', 11.91, 'V'), ('Fan 2 current', 0.02, 'A'), ('Fan 2 control mode', 'PWM', ''), ('Fan 3 speed', 1390, 'rpm'), ('Fan 3 voltage', 11.91, 'V'), ('Fan 3 current', 0.03, 'A'), ('Fan 3 control mode', None, ''), ('Noise level', 63, 'dB') ] got = dev.get_status(direct_access=direct_access) assert expected == got def test_smart_device_reads_status_from_hwmon(mockSmartDevice, tmp_path): dev = mockSmartDevice dev._hwmon = HwmonDevice('mock_module', tmp_path) (tmp_path / 'fan1_input').write_text('1461\n') (tmp_path / 'in0_input').write_text('11910\n') (tmp_path / 'curr1_input').write_text('20\n') (tmp_path / 'pwm1_mode').write_text('1\n') (tmp_path / 'fan2_input').write_text('1336\n') (tmp_path / 'in1_input').write_text('11910\n') (tmp_path / 'curr2_input').write_text('20\n') (tmp_path / 'pwm2_mode').write_text('0\n') (tmp_path / 'fan3_input').write_text('1390\n') (tmp_path / 'in2_input').write_text('11910\n') (tmp_path / 'curr3_input').write_text('30\n') (tmp_path / 'pwm3_mode').write_text('1\n') # skip initialize for now, we're not emulating the behavior precisely # enough to require it here expected = [ ('Fan 1 speed', 1461, 'rpm'), ('Fan 1 voltage', 11.91, 'V'), ('Fan 1 current', 0.02, 'A'), ('Fan 1 control mode', 'PWM', ''), ('Fan 2 speed', 1336, 'rpm'), ('Fan 2 voltage', 11.91, 'V'), ('Fan 2 current', 0.02, 'A'), ('Fan 2 control mode', 'DC', ''), ('Fan 3 speed', 1390, 'rpm'), ('Fan 3 voltage', 11.91, 'V'), ('Fan 3 current', 0.03, 'A'), ('Fan 3 control mode', 'PWM', ''), ] got = dev.get_status() assert expected == got ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/test_smart_device2.py0000644000175000017500000001100114562144457020145 0ustar00jonasjonas# uses the psf/black style import pytest from _testutils import MockHidapiDevice, Report from liquidctl.driver.hwmon import HwmonDevice from liquidctl.driver.smart_device import SmartDevice2 # https://github.com/liquidctl/liquidctl/issues/292#issuecomment-786876335 # (adapted: set control mode for connected fan to PWM) SAMPLE_STATUS = bytes.fromhex( "67023a003f00185732533230312003000200000000000000fc03000000000000" "0000000000000000322828000000000032282800000000003000000000000000" ) class MockSmart2(MockHidapiDevice): def __init__(self, raw_speed_channels, raw_led_channels): super().__init__() self.raw_speed_channels = raw_speed_channels self.raw_led_channels = raw_led_channels def write(self, data): reply = bytearray(64) if data[0:2] == [0x10, 0x01]: reply[0:2] = [0x11, 0x01] elif data[0:2] == [0x20, 0x03]: reply[0:2] = [0x21, 0x03] reply[14] = self.raw_led_channels if self.raw_led_channels > 1: reply[15 + 1 * 6] = 0x10 reply[15 + 2 * 6] = 0x11 self.preload_read(Report(reply[0], reply[1:])) return super().write(data) @pytest.fixture def mock_smart2(): raw = MockSmart2(raw_speed_channels=3, raw_led_channels=2) dev = SmartDevice2(raw, "Mock Smart Device V2", speed_channel_count=3, color_channel_count=2) dev.connect() return dev @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True), (True, False)]) def test_initializes(mock_smart2, has_hwmon, direct_access, tmp_path): if has_hwmon: mock_smart2._hwmon = HwmonDevice("mock_module", tmp_path) # TODO check the result _ = mock_smart2.initialize(direct_access=direct_access) writes = len(mock_smart2.device.sent) if not has_hwmon or direct_access: assert writes == 4 else: assert writes == 2 @pytest.mark.parametrize("has_hwmon,direct_access", [(False, False), (True, True)]) def test_reads_status_directly(mock_smart2, has_hwmon, direct_access): if has_hwmon: mock_smart2._hwmon = HwmonDevice(None, None) mock_smart2.device.preload_read(Report(0, SAMPLE_STATUS)) expected = [ ("Fan 1 speed", 1020, "rpm"), ("Fan 1 duty", 50, "%"), ("Fan 1 control mode", "PWM", ""), ("Fan 2 speed", 0, "rpm"), ("Fan 2 duty", 40, "%"), ("Fan 2 control mode", None, ""), ("Fan 3 speed", 0, "rpm"), ("Fan 3 duty", 40, "%"), ("Fan 3 control mode", None, ""), ("Noise level", 48, "dB"), ] got = mock_smart2.get_status(direct_access=direct_access) assert sorted(got) == sorted(expected) def test_reads_status_from_hwmon(mock_smart2, tmp_path): mock_smart2._hwmon = HwmonDevice("mock_module", tmp_path) (tmp_path / "pwm1_enable").write_text("1\n") (tmp_path / "pwm2_enable").write_text("0\n") (tmp_path / "pwm3_enable").write_text("0\n") (tmp_path / "pwm1_mode").write_text("1\n") (tmp_path / "pwm2_mode").write_text("0\n") (tmp_path / "pwm3_mode").write_text("0\n") (tmp_path / "pwm1").write_text("127\n") (tmp_path / "pwm2").write_text("102\n") (tmp_path / "pwm3").write_text("102\n") (tmp_path / "fan1_input").write_text("1020\n") (tmp_path / "fan2_input").write_text("0\n") (tmp_path / "fan3_input").write_text("0\n") expected = [ ("Fan 1 speed", 1020, "rpm"), ("Fan 1 duty", pytest.approx(50, rel=1.0 / 255), "%"), ("Fan 1 control mode", "PWM", ""), ("Fan 2 speed", 0, "rpm"), ("Fan 2 duty", pytest.approx(40, rel=1.0 / 255), "%"), ("Fan 2 control mode", "DC", ""), ("Fan 3 speed", 0, "rpm"), ("Fan 3 duty", pytest.approx(40, rel=1.0 / 255), "%"), ("Fan 3 control mode", "DC", ""), ] got = mock_smart2.get_status() assert sorted(got) == sorted(expected) def test_constructor_sets_up_all_channels(mock_smart2): assert mock_smart2._speed_channels == { "fan1": (0, 0, 100), "fan2": (1, 0, 100), "fan3": (2, 0, 100), } assert mock_smart2._color_channels == { "led1": (0b001), "led2": (0b010), "sync": (0b011), } def test_not_totally_broken(mock_smart2): _ = mock_smart2.initialize() mock_smart2.device.preload_read(Report(0, [0x67, 0x02] + [0] * 62)) _ = mock_smart2.get_status() mock_smart2.set_color( channel="led1", mode="breathing", colors=iter([[142, 24, 68]]), speed="fastest" ) mock_smart2.set_fixed_speed(channel="fan3", duty=50) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736038516.0 liquidctl-1.15.0/tests/test_smbus.py0000644000175000017500000001046114736354164016561 0ustar00jonasjonasimport pytest from pathlib import Path from liquidctl.driver.smbus import LinuxI2c, LinuxI2cBus, SmbusDriver class Canary(SmbusDriver): """Canary driver to reveal if SMBus probing is taking place.""" @classmethod def probe(cls, smbus, **kwargs): yield Canary(smbus, 'Canary', vendor_id=-1, product_id=-1, address=-1) def connect(self, **kwargs): # there is no justification for calling connect on this test driver raise RuntimeError('forbidden') def __repr__(self): return repr(self._smbus) def replace_smbus(replacement, monkeypatch): import liquidctl.driver.smbus # fragile hack: since messing with sys.meta_path (PEP 302) is tricky in a # pytest context, get into liquidctl.driver.smbus module and replace the # imported SMBus class after the fact monkeypatch.setattr(liquidctl.driver.smbus, 'SMBus', replacement) return replacement @pytest.fixture def emulated_smbus(monkeypatch): """Replace the SMBus implementation in liquidctl.driver.smbus.""" class SMBus: def __init__(self, number): pass return replace_smbus(SMBus, monkeypatch) def test__helper_fixture_replaces_real_smbus_implementation(emulated_smbus, tmpdir): i2c_dev = Path(tmpdir.mkdir('i2c-9999')) # unlikely to be valid bus = LinuxI2cBus(i2c_dev=i2c_dev) bus.open() assert type(bus._smbus) == emulated_smbus def test_filter_by_usb_port_yields_no_devices(emulated_smbus): discovered = Canary.find_supported_devices(usb_port='usb1') assert discovered == [] def test_aborts_if_sysfs_is_missing_devices(emulated_smbus, tmpdir): empty = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') virtual_bus = LinuxI2c(i2c_root=empty) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert discovered == [] def test_finds_a_device(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') i2c_root.mkdir('devices').mkdir('i2c-42') virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-42' def test_ignores_non_bus_sysfs_entries(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') devices = i2c_root.mkdir('devices') devices.mkdir('i2c-0') devices.mkdir('0-0050') # SPD info chip on i2c-0 devices.mkdir('i2c-DELL0829:00') # i2c HID chip from Dell laptop virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-0' def test_honors_a_bus_filter(emulated_smbus, tmpdir): i2c_root = tmpdir.mkdir('sys').mkdir('bus').mkdir('i2c') devices = i2c_root.mkdir('devices') devices.mkdir('i2c-0') devices.mkdir('i2c-1') virtual_bus = LinuxI2c(i2c_root=i2c_root) discovered = Canary.find_supported_devices(bus='i2c-1', root_bus=virtual_bus) assert len(discovered) == 1 assert discovered[0]._smbus.name == 'i2c-1' @pytest.fixture def emulated_device(tmpdir, emulated_smbus): i2c_dev = Path(tmpdir.mkdir('i2c-0')) bus = LinuxI2cBus(i2c_dev=i2c_dev) dev = SmbusDriver(smbus=bus, description='Test', vendor_id=-1, product_id=-1, address=-1) return (bus, dev) def test_connect_is_unsafe(emulated_device): bus, dev = emulated_device def mock_open(): nonlocal opened opened = True bus.open = mock_open opened = False dev.connect() assert not opened def test_connects(emulated_device): bus, dev = emulated_device def mock_open(): nonlocal opened opened = True bus.open = mock_open opened = False with dev.connect(unsafe='smbus') as cm: assert cm == dev assert opened def test_loading_unnavailable_eeprom_returns_none(emulated_device): bus, dev = emulated_device assert bus.load_eeprom(0x51) is None def test_loads_eeprom(emulated_device): bus, dev = emulated_device spd = bus._i2c_dev.joinpath('0-0051') spd.mkdir() spd.joinpath('eeprom').write_bytes(b'012345') spd.joinpath('name').write_text('name\n') assert bus.load_eeprom(0x51) == ('name', b'012345') ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1673152806.0 liquidctl-1.15.0/tests/test_usb.py0000644000175000017500000000262514356444446016225 0ustar00jonasjonasimport pytest from _testutils import MockHidapiDevice from liquidctl.driver.usb import UsbDriver, UsbHidDriver @pytest.fixture def emulated_hid_device(): hiddev = MockHidapiDevice() return UsbHidDriver(hiddev, 'Test') @pytest.fixture def emulated_usb_device(): usbdev = MockHidapiDevice() # hack, should mock PyUsbDevice dev = UsbDriver(usbdev, 'Test') return dev def test_hid_connects(emulated_hid_device): dev = emulated_hid_device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_hid_disconnect(emulated_hid_device): dev = emulated_hid_device def mock_close(): nonlocal opened opened = False dev.device.close = mock_close opened = True dev.disconnect() assert not opened def test_usb_connects(emulated_usb_device): dev = emulated_usb_device def mock_open(): nonlocal opened opened = True dev.device.open = mock_open opened = False with dev.connect() as cm: assert cm == dev assert opened def test_usb_disconnect(emulated_usb_device): dev = emulated_usb_device def mock_close(): nonlocal opened opened = False dev.device.close = mock_close opened = True dev.disconnect() assert not opened ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1707657519.0 liquidctl-1.15.0/tests/yellow.jpg0000644000175000017500000002227014562144457016034 0ustar00jonasjonas XICC_PROFILE HLinomntrRGB XYZ  1acspMSFTIEC sRGB-HP cprtP3desclwtptbkptrXYZgXYZ,bXYZ@dmndTpdmddvuedLview$lumimeas $tech0 rTRC< gTRC< bTRC< textCopyright (c) 1998 Hewlett-Packard CompanydescsRGB IEC61966-2.1sRGB IEC61966-2.1XYZ QXYZ XYZ o8XYZ bXYZ $descIEC http://www.iec.chIEC http://www.iec.chdesc.IEC 61966-2.1 Default RGB colour space - sRGB.IEC 61966-2.1 Default RGB colour space - sRGBdesc,Reference Viewing Condition in IEC61966-2.1,Reference Viewing Condition in IEC61966-2.1view_. \XYZ L VPWmeassig CRT curv #(-27;@EJOTY^chmrw| %+28>ELRY`gnu| &/8AKT]gqz !-8COZfr~ -;HUcq~ +:IXgw'7HYj{+=Oat 2FZn  % : O d y  ' = T j " 9 Q i  * C \ u & @ Z t .Id %A^z &Ca~1Om&Ed#Cc'Ij4Vx&IlAe@e Ek*Qw;c*R{Gp@j>i  A l !!H!u!!!"'"U"""# #8#f###$$M$|$$% %8%h%%%&'&W&&&''I'z''( (?(q(())8)k))**5*h**++6+i++,,9,n,,- -A-v--..L.../$/Z///050l0011J1112*2c223 3F3334+4e4455M555676r667$7`7788P8899B999:6:t::;-;k;;<' >`>>?!?a??@#@d@@A)AjAAB0BrBBC:C}CDDGDDEEUEEF"FgFFG5G{GHHKHHIIcIIJ7J}JK KSKKL*LrLMMJMMN%NnNOOIOOP'PqPQQPQQR1R|RSS_SSTBTTU(UuUVV\VVWDWWX/X}XYYiYZZVZZ[E[[\5\\]']x]^^l^__a_``W``aOaabIbbcCccd@dde=eef=ffg=ggh?hhiCiijHjjkOkklWlmm`mnnknooxop+ppq:qqrKrss]sttptu(uuv>vvwVwxxnxy*yyzFz{{c{|!||}A}~~b~#G k͂0WGrׇ;iΉ3dʋ0cʍ1fΏ6n֑?zM _ɖ4 uL$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km!Adobed@      _   Wnt't't'?t'?t'?t'././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1736036317.0 liquidctl-1.15.0/tox.ini0000644000175000017500000000033114736347735014171 0ustar00jonasjonas[tox] envlist = py39, py310, py311, py312, py313 isolated_build = True skip_missing_interpreters = true [testenv] setenv = XDG_RUNTIME_DIR = {toxinidir}/.test_rundir deps = pytest commands = python -m pytest