Compare commits

..

No commits in common. "master" and "1.1.8rc3" have entirely different histories.

218 changed files with 38920 additions and 67527 deletions

View File

@ -1,22 +0,0 @@
[run]
branch = True
source =
hid_parser
hidapi
keysyms
logitech_receiver
solaar
omit =
*/tests/*
*/setup.py
*/__main__.py
[report]
exclude_lines =
pragma: no cover
if __name__ == '__main__':
if typing.TYPE_CHECKING
fail_under = 40

View File

@ -9,8 +9,8 @@ assignees: ''
**Information**
<!-- Make sure that your issue is not one of the known issues in the Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
<!-- Do not bother opening an issue for a version older than 1.1.8. Upgrade to the latest version and see if your issue persists. -->
<!-- If you are not running the current version of Solaar, strongly consider upgrading to the newest version. -->
<!-- Do not bother opening an issue for a version older than 1.1.0. Upgrade to the latest version and see if your issue persists. -->
<!-- If you not running the current version of Solaar, strongly consider upgrading to the newest version. -->
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository):
- Distribution:
- Kernel version (ex. `uname -srmo`): `KERNEL VERSION HERE`

View File

@ -8,7 +8,7 @@ assignees: ''
---
**Information**
<!-- The version of Solaar in this repository has more features than released vesions. Update to this version before asking for a new feature. -->
<!-- Please update to Solaar from this repository before asking for a new feature. -->
- Solaar version (`solaar --version` and `git describe --tags`):
- Distribution:
- Kernel version (ex. `uname -srmo`):

View File

@ -7,10 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v5
- name: Setup Python
uses: actions/setup-python@v4.3.0
- name: Run pre-commit
uses: pre-commit/action@v3.0.0

View File

@ -1,52 +0,0 @@
name: Deploy to GitHub Pages
on:
push:
branches:
- master
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: 'pages'
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install mkdocs mkdocs-rtd-dropdown mkdocs-mermaid2-plugin mkdocstrings[python]
- name: Build and deploy
run: |
mkdocs build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'site'
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@ -1,90 +0,0 @@
name: tests
on: [push, pull_request]
jobs:
ubuntu-tests:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.13]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install Ubuntu dependencies for python 3.8
if: matrix.python-version == '3.8'
run: |
make install_apt
- name: Install Ubuntu dependencies for python 3.13
if: matrix.python-version == '3.13'
run: |
make install_apt_python3.13
- name: Install Python dependencies
run: |
make install_pip PIP_ARGS='.["test"]'
- name: Run tests on Ubuntu
run: |
make test
- name: Upload coverage to Codecov
if: github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v4.5.0
with:
directory: ./coverage/reports/
env_vars: OS, PYTHON
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
token: ${{ secrets.CODECOV_TOKEN }}
macos-tests:
runs-on: macos-latest
strategy:
matrix:
python-version: [3.8, 3.13]
fail-fast: false
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Set up macOS dependencies
run: |
make install_brew
- name: Add Homebrew's library directory to dyld search path
run: |
echo "DYLD_FALLBACK_LIBRARY_PATH=$(brew --prefix)/lib:$DYLD_FALLBACK_LIBRARY_PATH" >> $GITHUB_ENV
- name: Install Python dependencies
run: |
make install_pip PIP_ARGS='.["test"]'
- name: Run tests on macOS
run: |
pytest --cov --cov-report=xml
- name: Upload coverage to Codecov
if: github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v4.5.0
with:
directory: ./coverage/reports/
env_vars: OS, PYTHON
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
token: ${{ secrets.CODECOV_TOKEN }}

11
.gitignore vendored
View File

@ -13,19 +13,8 @@ __pycache__/
/deb_dist/
/MANIFEST
.coverage
/htmlcov/
/docs/captures/
/share/logitech_icons/
/share/locale/
/po/*.po~
/.idea/
.DS_Store
._*
Pipfile
Pipfile.lock

View File

@ -8,13 +8,19 @@ repos:
- id: check-yaml
- id: check-toml
- id: debug-statements
- id: double-quote-string-fixer
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.2
- repo: https://github.com/pre-commit/mirrors-yapf
rev: v0.32.0
hooks:
- id: ruff
name: ruff lint
args: [--fix, --exit-non-zero-on-fix]
- id: ruff-format
name: ruff format
- id: yapf
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.10.1
hooks:
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
additional_dependencies: ['flake8-bugbear']

View File

@ -1,358 +1,4 @@
# 1.1.15rc1
* Center labels and remove buggy entry resizing logic
* Add shape keys from Key POP Icon
* Device and Action rule conditions match on codename and name
* Fix listing hidpp10 devices - bytes vs string concatenation (#2856)
* Add present flag, unset when internal error occurs, set when notification appears
* Pause setting up features when error occurs; use ADC message to signal connection and disconnection
* Fix listing of hidpp10 peripherals
* Complete DEVICE_FEATURES to DeviceFeature transition for hidpp10 devices
* Fix NOTIFICATION_FLAG to NotificationFlag transition leftovers
* Fix github workflow stopping all matrix jobs when one of them fails
* Fix ubuntu github CI
* Update index.md
* Python documentation appears to be broken so don't set it up
* Improve documentation on onboard profiles
* Use correct LOD values for extended adjustable dpi
* Better support RGB Effects - not readable
* Fix crash when asking for help about config
* Fix error when updating ChoiceControlBig box
* Add uninstallation docs
* Handle unknown power switch locations again
* Correctly handle selection of [empty] in rule editor
* Handle `HIDError` in `hidapi.hidapi_impl._match()` (#2804)
* Give ghost devices a path
* Guard against typeerror when setting the value of a control box
* Recover from errors in ping
* Replace spaces by underscores when looking up features
* Rewrote string concatenation/format with f strings
* Fix logo not showing in about dialog box
* Make typing-extensions dependency mandatory
* Properly ignore unsupported locale
* hidapi: skip unsupported devices and handle exception on open
* Ignore macOS junk files and pipenv config
* Fix ui desktop notifications test
* hidpp20: Remove dependency to NamedInts
* Estimate accurate battery level for some rechargable devices (#2745)
* Upgrade desktop notifications tests to take notifications availability into account
* Update tests to run on Python 3.13
* Remove outdated logger enabled checks
* Introduce GTK signal types
* Introduce error types
* Remove alias for SupportedFeature
* Refactor process_device_notification
* Refactor process_receiver_notification
* Refactor receiver event handling
* Introduce custom logger
* Refactor notifications
* Rename variable to full name notification
* Test notifications
* Test extraction of serial and max. devices
* Refactor extraction of serial and max. devices
* macOS: Fix int.from_bytes, int.to_bytes for show.py
* macOS: Remove udev rule warning
* macOS: Add support for Bluetooth devices
* Add back and forward mouseclick actions
* Speedup lookup of known receivers
* Refactor device filtering
* Reorder private functions and variable definitions
* Turn filter_products_of_interest into a public function
* Improve tests of known receivers
* Refactor: Remove NamedInts and move enums where used
* Add docstrings and type hints
* Enforce rules on RuleComponentUI subclasses
* Simplify settings UI class
* Remove diversion alias
* Refactor: Convert Kind to IntEnum
* Split up huge settings module
* Remove Python 2 specific path handling
* Delete logging temp file on exit
* Update Swedish translation
# 1.1.14
* Handle fake feature enums in show
* Fix battery entries in config.yaml
* Add ratchet setting for smart shift enhanced devices
* Refactor Gesture into enum
* Replace ERROR NamedInts by IntEnum (#2645)
* Refactor hidpp20 to use enum
* Update Polish, Swedish, Norwegian Nynorsk (nn), and Norwegian Bokmål (nb) translations
* Use IntEnum for firmware and cidgroup constances
* Change pairing error values to intenums
* Fix initialization bug for PackedRangeControl
* Add tests for feature class, process_notification, and key_is_down
* Check all bits for extended report rate
* Add type hints
* Improve about dialog
* Reduce dependencies
* Refactor code
* Improve testing
* Allow unknown keys in Key rule conditions
* Improve documentation for cli actions
* Cycle sw_id to better guard against duplication of messages
* Handle error return on root feature
* Clean up documentation
* Improve github interactions
* Add information about Onboard Profiles overriding some settings
* Add wording to README.md that Solaar is not a device driver
* Clean up imports
* Handle unknown device kinds
* Fix broken links to Solaar logo
* Use mkdocs for public documentation
* Clean up setup.py
* Remove Dead links in the AppStream file
* Update about.py
* Remove check on driver
* Improve base module
* Remove unnecessary receiver info 'hid_driver'
* Convert HIDPPNotification to dataclass
* Be defensive when converting battery status to string
* Automatically detect packages in /lib
* Clean up locale code
* Improve rules documentation
* Refactor creation of devices
* Add headings to structure rules.md
* Unify imports in logitech package
* Don't ping device when getting name or codename
* Use dataclasses and enums where useful
* Introduce Device protocol and type hints
* Add typing_extensions dependency
* Move hidpp10 independent functions to module level
* Fix macOS compatibility and reenable CI tests
* Unify imports in hidapi package
* Move screenshots into dedicated folder and add high-level graph of components
* Update French and Chinese translations
* Drop support for end-of-life Python 3.7
# 1.1.13
* Update Polish and Russian translations.
* Fix bug in suspend and resume callback
* Add choices universe for backlight setting
* Add simplify diversion.py and add unit tests
* Get and use current host number for K375sFnSwap because of bug in firmware of MX Keys S
* Fix bug with logo in about window
* Don't ping device just to get logging information
* Optimize write for per-key lighting
* Add and initialize per-key lighting to a special no-change value
* Remove some Python 2 compatibility code
* Update French translation
* Refactor rule loading for testability
# 1.1.12
* Check for existence of keys file before opening
* Perform translation for all translatable strings.
* Add included hid_parser to packages installed
* Improve label and description for LED zone settings
* Add message about Onboard Profiles to LED Zone settings
* Initialize device registers to empty list
* Use bluez dbus signals to disconnect and connect bluetooth devices
* Handle a different signal for onboard profiles directory in ROM
* Introduce small delay in getting pairing information to let receiver settle after pairing
* Improve testing for settings_templates, settings, hidpp20, and device and fix small bugs found
* Add extended adjustable DPI setting
* Improve and extend infrastructure for testing setting_templates
* Update Greek, Polish, Russian, and Traditional Chinese translations
* Implement and test per-key lighting
* Refactor and test pair_window in GUI
* Handle situation when read of a setting fails in GUI
* Permit continuing when a read during pushing fails
* Fix bug in LEDZoneSetting when effect is not implemented
* Add tests for LEDEffect structures in hidpp20
* Handle BRIGHTNESS_CONTROL notifications
* Add settings for BRIGHTNESS_CONTROL and RGB_EFFECTS features
* Fix small bugs found from testing in settings
* Use f-strings for more exceptions and log message
* Tests for setting_templates
* Simple change in settings to improve testability
* Use feature_request from the device everywhere in hidpp20
* Fix bug in backlight 2 durations
* Replace deprecated code constructs
* Set up test data and classes to help test HID++ interactions
* Use pytest to test code for logitech_receiver modules
* Align init methods for all receiver classes
* Start refactoring of code base
* Allow sub-second delays in Later
* Fix bug in setting configuration cookie due to bad documentation
* Use ruff for code styling and linting
* Upgrade string formating to f-string
* Document battery-icons=solaar option
* Tell devices to delay device sending first messages until configuration is done
* Optimize some functions in FeaturesArray
* Fix bug in creating features array
* Fix bug in building battery line in show
* Refactor diversion_rules
* Fix bug in determining tray icon
* Fix bug in getting friendly name
* Move status information to Device and Receiver objects
* Add tests for get_kind_from_index and base product information
* Update EX100 documentation
* Use object attributes instead of dictionary in status objects
* Create subclasses of receiver for different variants
* Add requirement for CONFIG_HIDRAW to documentation
* Add some low-level tests for some hidpp20 functions, profiles, and lighting and some hidpp10 tests
* Fix app name casing in UI
* Add missing receiver type for Lightspeed receivers
* Add new device types
* Refactor device and receiver instantiation
* Simplify naming of distribution files
* Clean up some logging code
* Remove duplicated code to read register
* Introduce Hidpp20 and Hidpp10 class
* Remove unnecessary calls of del
* Fix bug when reading BACKLIGHT setting from device
* Replace invalid hidpp10 and hidpp20 usages
* Use only timer thread to save config.yaml
* Improve README
* Copy newer version of hid_parser
* Reorder code in settings
* Update installation documentation
* Add missing license blocks
* Clean up listener and notifications code
* Add locks to prevent multiple persisters for device
* Clean up configuration, device, and receiver code
* Move battery constants common to HID++ 1.0 and 2.0 to common
* Move mapping of device kind into hidpp20
* Move pairing information gathering to receiver
* update contributors
* Expand allowable profile numbers
* Clean up __init__ in logitech_receiver
* Modify pre-commit args to make ruff change files
* Fix bug in hidpp20 get host names
* Use ruff for formatting and linting
* Fix bug in rule Set action
* Add notify module to logitech_receiver
* Implement setting_changed callback and pass in to new devices and receivers
* Add callback to call when changing a setting
* Move exceptions, hidpp20 and hidpp10 constants into new modules
* Streamline status code
* Upgrade debugging in udev
* Fix deprecated GitHub actions
* Extend makefile and tests
* Improve features array
* Move ui_async to common.py
* Improve module imports
* Add tests of common module
# 1.1.11
* Rename light icons and install them in correct place
* Setup macOS tests using GitHub action (#2284)
* Better checking for setting in record_setting
* Fix invalid func name set logo name
* Simplify installation of udev rules
* Add document on implementation
* Tidy up scrolling appearance in configuration panel
* Correctly handle profile button with no action
* Don't unlock setting when changed by external means
* Refactor code to record change to setting
* Add GitHub action for tests
* Introduce tests with pytest
* Simplify logger instantiation
* Update label and tooltip for divert-gkeys setting
* Don't lock setting when an error occurs
* Catch assertion errors when reading setting values from devices
* Support LED Zone control feature
* Dump and load device profiles
* Select among profiles.
* Support backlight levels and duration
* Use "Report Rate" instead of "Polling" for movement report rate
* Support extended report rate setting
* Add stable branch to release.sh (#2236)
* Fix changelog parsing in release.sh
* Update installation.md with new udev rules location
* Downgrade assertion on missing notification flag to warning
* Write empty file if there are no rules to save
* Be defensive in device error messages
* Add descriptions of M650, PRO X 2, G915, MX Anywhere 2S, G305, and MX Keys S
* Report hidraw node in debugging messages
* Add names for new Logitech features
* Update Spanish, French, and Polish translations
* Defend against lightspeed receivers that contact devices for basic information
* Remove incorrect feature for M325 mice
* Add K845 keyboard
* Add partial support for macOS and minimal support for Windows
* Correctly enumerate devices on receiver
* Add wording in documentation about Logitech reusing model numbers
* Better handling and installation of icons
* Catch errors when pinging to try to put device online
* Be more cautious when creating log messages to avoid exceptions
* Correctly handle NoSuchDevice exception when pinging device
* Fix test in rules for device equality
* Add installation instructions for pipx and add not about other GTK system packages
* Fix bug in NamedInt
* Add support for MK550
* Install udev rule files to correct placces
* Expand expected ping responses
* Update codename when device status changes
# 1.1.10
* Add information about NixOS flake package
* Permit bluetooth devices in hidconsole
* Add descriptor for Logitech MX Revolution Mouse M-RCL 124
* Improve determination for short and long messages
* Add descriptor for G500s
* Fix bug in scan-registers
* Add single depress and release options for rule mouse click action
* Add rule condition for hostname
* Update keysym generation to current list of keysyms
* Allow device 0 in hidconsole
* Upgrade messages when no supported device found
* Documentation update for the gnome extension for better Solaar rule support
* Remove udev-acl tag from udev rules
* Add support for process condition in Wayland
* Update French, Chinese, and German translations
* Add G733 Headset
* Restore tools/clean.sh
* Add Bluetooth Keyboard C714
* Update several device descriptions
* Update scan-registers.sh
* Remove assertion on last byte of ping responses
* Add symbolic version of solaar icon
* Fix bug when finding name or codename
* Update documentation
* Put version in initial INFO logging message
# 1.1.9
* Add descriptors for G535 wireless gaming headset and wireless keyboard EX110
* Update Greek translation
* Fix minor issues in documentation
* Remove some deprecated GTK code
* Use zero exit code for kill interrupts
* Add rule Test condition for battery charging
* Get wpid for 28Mz devices from udev when enumerating
* Add Device condition to rules
* Don't show wireless link or battery information when unknown or not present
* Add desccriptor for G9x and LX7 mice
* Fix bug in determining kind of devices for 27Mz receivers
* Set initial lock status of smooth scrolling features to ignore
* Fix glitch in configuration file update when changing versions
* Add more debugging output for rules
* Clean up pinging code
* Put initial ping of direct-connected devices inside listener thread
* Read and check before write of range settings
* Improve pairing determination
* Cut off determination of receiver devices when all have been found
* Remove derived configuration fields when Solaar version changes
* Allow device descriptors without name and codename
* Filter and escape technical detail fields
* Add setting for ADC power managemen
* Correctly determine whether to ping with a long HID++ message
* Add description for K470 keyboard from the MK470 combo (#1945)
* Add setting value for mouse gestures
* Update German and French translations
* Remove old clean.sh and monitor.py tools
* Retry opening device if permissions error encountered
* Better handlling of IO errors at device creation
* Add KeyIsDown rule condition to check whether a diverted key is down
* Clean up device and receiver creation
# 1.1.8
# 1.1.8rc3
* Add parameter to thumb wheel rule conditions
* Rename Serbian translation file

View File

@ -1,3 +1,3 @@
include COPYRIGHT LICENSE.txt README.md CHANGELOG.md lib/solaar/version lib/solaar/commit
include COPYRIGHT COPYING README.md ChangeLog.md lib/solaar/version lib/solaar/commit
recursive-include rules.d *
recursive-include share/locale *

View File

@ -1,74 +0,0 @@
UDEV_RULE_FILE = 42-logitech-unify-permissions.rules
UDEV_RULES_SOURCE := rules.d/$(UDEV_RULE_FILE)
UDEV_RULES_SOURCE_UINPUT := rules.d-uinput/$(UDEV_RULE_FILE)
UDEV_RULES_DEST := /etc/udev/rules.d/
PIP_ARGS ?= .
.PHONY: install_ubuntu install_macos
.PHONY: install_apt install_brew install_pip
.PHONY: install_udev install_udev_uinput reload_udev uninstall_udev
.PHONY: format lint test
install_ubuntu: install_apt install_udev_uinput install_pip
install_macos: install_brew install_pip
install_apt:
@echo "Installing Solaar dependencies via apt"
sudo apt update
sudo apt install libdbus-1-dev libglib2.0-dev libgtk-3-dev libgirepository1.0-dev
install_apt_python3.13:
@echo "Installing Solaar dependencies via apt"
sudo apt update
sudo apt install libdbus-1-dev libglib2.0-dev libgtk-3-dev libgirepository-2.0-dev gobject-introspection
install_dnf:
@echo "Installing Solaar dependencies via dn"
sudo dnf install gtk3 python3-gobject python3-dbus python3-pyudev python3-psutil python3-xlib python3-yaml
install_brew:
@echo "Installing Solaar dependencies via brew"
brew update
brew install hidapi gtk+3 pygobject3 gobject-introspection
install_pip:
@echo "Installing Solaar via pip"
python -m pip install --upgrade pip
pip install $(PIP_ARGS)
install_pipx:
@echo "Installing Solaar via pipx"
pipx install --system-site-packages $(PIP_ARGS)
install_udev:
@echo "Copying Solaar udev rule to $(UDEV_RULES_DEST)"
sudo cp $(UDEV_RULES_SOURCE) $(UDEV_RULES_DEST)
make reload_udev
install_udev_uinput:
@echo "Copying Solaar udev rule (uinput) to $(UDEV_RULES_DEST)"
sudo cp $(UDEV_RULES_SOURCE_UINPUT) $(UDEV_RULES_DEST)
make reload_udev
reload_udev:
@echo "Reloading udev rules"
sudo udevadm control --reload-rules
uninstall_udev:
@echo "Removing Solaar udev rules from $(UDEV_RULES_DEST)"
sudo rm -f $(UDEV_RULES_DEST)/$(UDEV_RULE_FILE)
make reload_udev
format:
@echo "Formatting Solaar code"
ruff format .
lint:
@echo "Linting Solaar code"
ruff check . --fix
test:
@echo "Running Solaar tests"
pytest --cov --cov-report=xml

View File

@ -1,67 +0,0 @@
# <img src="https://pwr-solaar.github.io/Solaar/img/solaar.svg" width="60px"/> Solaar
Solaar is a Linux manager for many Logitech keyboards, mice, and other devices
that connect wirelessly to a Unifying, Bolt, Lightspeed or Nano receiver
as well as many Logitech devices that connect via a USB cable or Bluetooth.
Solaar is not a device driver and responds only to special messages from devices
that are otherwise ignored by the Linux input system.
<a href="https://pwr-solaar.github.io/Solaar/index">More Information</a> -
<a href="https://pwr-solaar.github.io/Solaar/usage">Usage</a> -
<a href="https://pwr-solaar.github.io/Solaar/capabilities">Capabilities</a> -
<a href="https://pwr-solaar.github.io/Solaar/rules">Rules</a> -
<a href="https://pwr-solaar.github.io/Solaar/installation">Manual Installation</a>
[![codecov](https://codecov.io/gh/pwr-Solaar/Solaar/graph/badge.svg?token=D7YWFEWID6)](https://codecov.io/gh/pwr-Solaar/Solaar)
[![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2+-blue.svg)](../LICENSE.txt)
<p align="center">
<img src="https://pwr-solaar.github.io/Solaar/screenshots/Solaar-main-window-multiple.png" width="54%"/>
&#160;
<img src="https://pwr-solaar.github.io/Solaar/screenshots/Solaar-main-window-receiver.png" width="43%"/>
</p>
<p align="center">
<img src="https://pwr-solaar.github.io/Solaar/screenshots/Solaar-main-window-back-divert.png" width="49%"/>
&#160;
<img src="https://pwr-solaar.github.io/Solaar/screenshots/Solaar-rule-editor.png" width="48%"/>
</p>
Solaar supports:
- pairing/unpairing of devices with receivers
- configuring device settings
- custom button configuration
- running rules in response to special messages from devices
For more information see
<a href="https://pwr-solaar.github.io/Solaar/index">the main Solaar documentation page.</a> -
## Installation Packages
Up-to-date prebuilt packages are available for some Linux distros
(e.g., Fedora) in their standard repositories.
If a recent version of Solaar is not
available from the standard repositories for your distribution, you can try
one of these packages:
- Arch solaar package in the [extra repository][arch]
- Ubuntu/Kubuntu package in [Solaar stable ppa][ppa stable]
- NixOS Flake package in [Svenum/Solaar-Flake][nix flake]
Solaar is available from some other repositories
but may be several versions behind the current version:
- a [Debian package][debian], courtesy of Stephen Kitt
- a Ubuntu package is available from [universe repository][ubuntu universe repository]
- a [Gentoo package][gentoo], courtesy of Carlos Silva and Tim Harder
- a [Mageia package][mageia], courtesy of David Geiger
[ppa stable]: https://launchpad.net/~solaar-unifying/+archive/ubuntu/stable
[arch]: https://www.archlinux.org/packages/extra/any/solaar/
[gentoo]: https://packages.gentoo.org/packages/app-misc/solaar
[mageia]: http://mageia.madb.org/package/show/release/cauldron/application/0/name/solaar
[ubuntu universe repository]: http://packages.ubuntu.com/search?keywords=solaar&searchon=names&suite=all&section=all
[nix flake]: https://github.com/Svenum/Solaar-Flake
[debian]: https://packages.debian.org/search?keywords=solaar&searchon=names&suite=all&section=all

1
README.md Symbolic link
View File

@ -0,0 +1 @@
docs/index.md

View File

@ -8,7 +8,7 @@ candidates (ex. `1.0.0rc1`). Release candidates must have a `rcX` suffix.
Release routine:
- Update version in `lib/solaar/version`
- Add release changes to `CHANGELOG.md`
- Add release changes to `ChangeLog.md`
- Add release information to `share/solaar/io.github.pwr_solaar.solaar.metainfo.xml`
- Create a commit that starts with `release VERSION`
- Push commit to Solaar repository

View File

@ -1,60 +1,5 @@
# Notes on Major Changes in Releases
## Version 1.1.15
* Device and Action rule conditions match on device codename and name
* Solaar supports configuration of Bluetooth devices on macOS.
## Version 1.1.13
* Solaar will drop support for Python 3.7 immediately after version 1.1.13.
* Version 1.1.12 does not push settings to many devices after a resume resulting in the device reverting to its default behaviour. This is fixed in 1.1.13.
## Version 1.1.12
* Solaar now processes DBus disconnection and connection messages from Bluez and re-initializes devices when they reconnect, to handle to a change introduced in Bluez 5.73. The HID++ driver does not re-initialize devices, which causes problems with smooth scrolling. Until the issue is resolved having Scroll Wheel Resolution set to true (and not ignored) may be helpful.
* The credits for translations have not been kept up to date. Translators who are not listed can update docs/i18n.ml and lib/solaar/ui/about.py.
* Solaar now has settings for features BRIGHTNESS_CONTROL, RGB_EFFECTS, and PER_KEY_LIGHTING features. The names of keys in the Per-key Lighting setting are for the standard US keyboard. Users who wish to modify these names should look at the section Keyboard Key Names and Locations in `https://pwr-solaar.github.io/Solaar/capabilities`
* A unit test test suite is being constructed using pytest.
* The Solaar code for communicating with receivers and devices has been significantly modified to improve testability and organization. Errors may have been introduced for uncommon hardware.
* The Later rule action uses precision timing for delays of less than one second.
* Solaar now indentifies supported devices by whether they support the HID protocols that Solaar needs. If a device does not show up at all when running Solaar, it almost certainly cannot be supported by Solaar.
## Version 1.1.11
* Solaar can dump device profiles in YAMLfor devices that support profiles and load profiles back from an edited file. See [the capabilities page](https://pwr-solaar.github.io/Solaar/capabilities) for more information.
* Solaar has settings for each LED Zone that a device supports under feature Color LED Effects.
* Solaar has settings for extended report rate, backlight levels, durations, and profile selection.
* Solaar now partly works in MacOS. Please open new issues for problems. Solaar may work in Windows. Please open new issues for problems. See https://github.com/pwr-Solaar/Solaar/pull/1971 for more information. Fixing problems in MacOS or Windows may take considerable time.
* Solaar works better when the Python package hid-parser is available. Distriubtions should try have this package installed.
## Version 1.1.10
* The mouse click rule action can now just simulate depressing or releasing the button.
* There is a new rule condition to check the hostname.
## Version 1.1.9
* Solaar now exits with at 0 exit code when killed.
* Two Solaar settings can interfere with the implementation of smooth scrolling in modern Linux HID++ drivers. These settings are initially set to ignore so that this interference does not happen.
* The Device rule condition checks for the device that produced a notification.
* The KeyIsDown rule condition checks whether a *diverted* rule key is down.
## Version 1.1.8
* The thumb wheel rule conditions take an optional parameter that checks for total signed thumb wheel movement.

View File

@ -21,22 +21,35 @@
def init_paths():
"""Make the app work in the source tree."""
import os.path
import os.path as _path
import sys
root = os.path.join(os.path.realpath(sys.path[0]), "..")
prefix = os.path.normpath(root)
src_lib = os.path.join(prefix, "lib")
share_lib = os.path.join(prefix, "share", "solaar", "lib")
# Python 2 need conversion from utf-8 filenames
# Python 3 might have problems converting back to UTF-8 in case of Unicode surrogates
try:
decoded_path = sys.path[0]
sys.path[0].encode(sys.getfilesystemencoding())
except UnicodeError:
sys.stderr.write(
'ERROR: Solaar cannot recognize encoding of filesystem path, '
'this may happen because non UTF-8 characters in the pathname.\n'
)
sys.exit(1)
prefix = _path.normpath(_path.join(_path.realpath(decoded_path), '..'))
src_lib = _path.join(prefix, 'lib')
share_lib = _path.join(prefix, 'share', 'solaar', 'lib')
for location in src_lib, share_lib:
init_py = os.path.join(location, "solaar", "__init__.py")
if os.path.exists(init_py):
init_py = _path.join(location, 'solaar', '__init__.py')
# print ("sys.path[0]: checking", init_py)
if _path.exists(init_py):
# print ("sys.path[0]: found", location, "replacing", sys.path[0])
sys.path[0] = location
break
if __name__ == "__main__":
if __name__ == '__main__':
init_paths()
import solaar.gtk
solaar.gtk.main()

View File

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

View File

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

View File

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 70 KiB

View File

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 67 KiB

View File

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 135 KiB

View File

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 69 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

View File

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 141 KiB

9
docs/_config.yml Normal file
View File

@ -0,0 +1,9 @@
title: Solaar
description: Linux Device Manager for Logitech Unifying Receivers and Devices.
tagline: Linux Device Manager for Logitech Unifying Receivers and Devices.
owner: pwr-Solaar
owner_url: https://github.com/pwr-Solaar
repository: pwr-Solaar/Solaar
show_downloads: false
encoding: utf-8
theme: jekyll-theme-slate

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,maximum-scale=2">
<link rel="stylesheet" type="text/css" media="screen" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/assets/favicon.png" />
{% seo %}
</head>
<body>
<!-- HEADER -->
<div id="header_wrap" class="outer">
<header class="inner">
{% if site.github.is_project_page %}
<a id="forkme_banner" href="{{ site.github.repository_url }}">View on GitHub</a>
{% endif %}
<h1 id="project_title">
<img src="{{ site.baseurl }}/assets/solaar.svg" style="margin-bottom: -10px; width: 48px; height: 48px; border: 0; box-shadow: none;" />
{{ site.title | default: site.github.repository_name }}</h1>
<h2 id="project_tagline">{{ site.description | default: site.github.project_tagline }}</h2>
{% if site.show_downloads %}
<section id="downloads">
<a class="zip_download_link" href="{{ site.github.zip_url }}">Download this project as a .zip file</a>
<a class="tar_download_link" href="{{ site.github.tar_url }}">Download this project as a tar.gz file</a>
</section>
{% endif %}
</header>
</div>
<!-- MAIN CONTENT -->
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
{{ content }}
</section>
</div>
<!-- FOOTER -->
<div id="footer_wrap" class="outer">
<footer class="inner">
{% if site.github.is_project_page %}
<p class="copyright">{{ site.title | default: site.github.repository_name }} maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
<p>Published with <a href="https://pages.github.com">GitHub Pages</a></p>
</footer>
</div>
</body>
</html>

47
docs/_layouts/page.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="{{ site.lang | default: "en-US" }}">
<head>
<meta charset='utf-8'>
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,maximum-scale=2">
<link rel="stylesheet" type="text/css" media="screen" href="{{ '/assets/css/style.css?v=' | append: site.github.build_revision | relative_url }}">
<link rel="icon" type="image/png" href="{{ site.baseurl }}/assets/favicon.png" />
{% seo %}
</head>
<body>
<!-- HEADER -->
<div id="header_wrap" class="outer">
<header class="inner">
{% if site.github.is_project_page %}
<a id="forkme_banner" href="{{ site.github.repository_url }}">View on GitHub</a>
{% endif %}
<h1 id="project_title">
<a href="{{ site.baseurl }}" style="color: #fff;">
<img src="{{ site.baseurl }}/assets/solaar.svg" style="margin-bottom: -10px; width: 48px; height: 48px; border: 0; box-shadow: none;" />
{{ site.title | default: site.github.repository_name }}</h1>
</a>
</header>
</div>
<!-- MAIN CONTENT -->
<div id="main_content_wrap" class="outer">
<section id="main_content" class="inner">
{{ content }}
</section>
</div>
<!-- FOOTER -->
<div id="footer_wrap" class="outer">
<footer class="inner">
{% if site.github.is_project_page %}
<p class="copyright">{{ site.title | default: site.github.repository_name }} maintained by <a href="{{ site.github.owner_url }}">{{ site.github.owner_name }}</a></p>
{% endif %}
<p>Published with <a href="https://pages.github.com">GitHub Pages</a></p>
</footer>
</div>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

View File

@ -51,7 +51,7 @@ connect via a USB cable or via bluetooth can be determined by their USB or
Bluetooth product ID.
## Pairing and Unpairing
# Pairing and Unpairing
Solaar is able to pair and unpair devices with
receivers as supported by the device and receiver.
@ -80,7 +80,7 @@ that they were bought with.
## Device Settings
Solaar can display quite a few changeable settings of receivers and devices.
For a list of HID++ features and their support see [the features page](features.md).
For a list of HID++ features and their support see [the features page](features).
Solaar does not do much beyond using the HID++ protocol to change the
behavior of receivers and devices via changing their settings.
@ -106,11 +106,11 @@ Setting information is stored in the file `~/.config/solaar/config.yaml`.
Updating of a setting can be turned off in the Solaar GUI by clicking on the icon
at the right-hand edge of the setting until a red icon appears (with tooltip
"Ignore this setting").
"Ignore this setting" ).
Solaar keeps track of settings independently on each computer.
As a result if a device is switched between different computers
Solaar may apply different settings for it on the different computers.
Solaar may apply different settings for it on the different computers
Querying a device for its current state can require quite a few HID++
interactions. These interactions can temporarily slow down the device, so
@ -140,6 +140,7 @@ change the speed of some thumb wheels. These notifications are only sent
for actions that are set in Solaar to their HID++ setting (also known as diverted).
For more information on this capability of Solaar see
[the rules page](https://pwr-solaar.github.io/Solaar/rules).
Some features of rules do not work under Wayland.
Users can edit rules using a GUI by clicking on the `Rule Editor` button in the Solaar main window.
@ -174,108 +175,6 @@ is sent to the Solaar rule system so that rules can detect these notifications.
For more information on Mouse Gestures rule conditions see
[the rules page](https://pwr-solaar.github.io/Solaar/rules).
### Keyboard Key Names and Locations
Solaar uses the standard Logitech names for keyboard keys. Some Logitech keyboards have different icons on some of their keys and have different functionality than suggested by these names.
Solaar is uses the standard US keyboard layout. This currently only matters for the `Per-key Lighting` setting. Users who want to have the key names for this setting reflect the keyboard layout that they use can create and edit `~/.config/solaar/keys.yaml` which contains a YAML dictionary of key names and locations. For example, switching the `Y` and `Z` keys can be done as:
Z: 25
Y: 26
This is an experimental feature and may be modified or even eliminated.
### Onboard Profiles
Some mice store one or more profiles onboard. An onboard profile controls certain aspects of the behavior of the mouse, including the rate at which the mouse reports movement, the resolution of the the movement reports, what the mouse buttons do, LED effects, and maybe more. Solaar has a setting that switches between profiles or disables all profiles.
When an onboard profile is active it may not be possible to change the aspects that the profile controls. This is often seen for the Report Rate setting. For some devices it is possible to make changes to the Sensitivity setting and to LED settings. These changes are likely to only be temporary and may be overridden when the device reconnects or when Solaar is restarted. This is in keeping with the intent of Onboard Profiles as controlling the device behavior. To make the changes to these settings permanent it is necessary to disable onboard profiles. Alternatively, multiple profiles can be set up as described below and these settings controlled by switching between the profiles.
Solaar can dump the entire set of profiles into a YAML file and can load the entire set of profiles from a file. Users can edit the file to effect changes to the profiles.
A profile file has some bookkeeping information, including profile version and the name of the device, and a sequence of profiles.
Each profile has the following fields:
- enabled: Whether the profile is enabled.
- sector: Where the profile is stored in device memory. Sectors greater than 0xFF are in ROM and cannot be written (use the low byte as the sector to write to Flash).
- name: A memonic name for the profile.
- report_rate: A report rate in milliseconds from 1 to 8.
- resolutions: A sequence of five sensor resolutions in DPI.
- resolution_default_index: The index of the default sensor resolution (0 to 4).
- resolution_shift_index: The index of the sensor resolution used when the DPI Shift button is pressed (0 to 4).
- buttons: The action for each button on the mouse in normal mode.
- gbuttons: The action for each button on the mouse in G-Shift mode.
- angle_snap: Enable angle snapping for devices.
- red, blue, green: Color indicator for the profile.
- lighting: Lighting information for logo and side LEDs in normal mode, then for power saving mode.
- ps_timeout: Delay in ms to go into power saving mode.
- po_timeout: Delay in ms to go from power saving mode to fully off.
- power_mode: Unknown purpose.
- write count: Unknown purpose.
Missing or unused parts of a profile are often a sequence of 0xFF bytes.
Button actions can either perform a function (behavior: 9) or send a button click or key press (behaviour: 8).
Functions are:
- 0: No Action - do nothing
- 1: Tilt Left
- 2: Tilt Right
- 3: Next DPI - change device resolution to the next DPI
- 4: Previous DPI - change device resolution to the previous DPI
- 5: Cycle DPI - change device resolution to the next DPI considered as a cycle
- 6: Default_DPI - change device resolution to the default resolution
- 7: Shift_DPI - change device resolution to the shift resolution
- 8: Next Profile - change to the next enabled profile
- 9: Previous Profile - change to the previous enabled profile
- 10: Cycle Profile - change to the next enabled profile considered as a cycle
- 11: G-Shift - change all buttons to their G-Shift state
- 12: Battery Status - show battery status on the device LEDs
- 13: Profile Select - select the n'th enabled profile
- 14: Mode Switch
- 15: Host Button - switch between hosts (unverified)
- 16: Scroll Down
- 17: Scroll Up
Some devices might not be able to perform all functions.
Buttons can send (type):
- 0: Don't send anything.
- 1: A button click (value) as a 16-bit bitmap, i.e., 1 is left click, 2 is right, 4 is middle, etc.
- 2: An 8-bit USB HID keycode (value) plus an 8-bit modifier bitmap (modifiers), i.e., 0 for no modifiers, 1 for control, 2 for shift, etc.
- 3: A 16-bit HID Consumer keycode (value).
See USB_HID_KEYCODES and HID_CONSUMERCODES in lib/logitech_receiver/special_keys.py for values to use for keycodes.
Buttons can also execute macros but Solaar does not provide any support for macros.
Lighting information is a sequence of lighting effects, with the first usually for the logo LEDs and the second usually for the side LEDs.
The fields possible in an effect are:
- ID: The kind of effect:
- color: A color parameter for the effect as a 24-bit RGB value.
- intensity: How intense to make the color (1%-100%), 0 for the default (usually 100%).
- speed: How fast to pulse in ms (one byte).
- ramp: How to change to the color (0=default, 1=ramp up/down, 2=no ramping).
- period: How fast to perform the effect in ms (two bytes).
- form: The form of the breathe effect.
- bytes: The raw bytes of other effects.
The possible effects and the fields the use are:
- 0x0: Disable - No fields.
- 0x1: Fixed color - color, whether to ramp.
- 0x2: Pulse a color - color, speed.
- 0x3 Cycle through the spectrum - period, intensity, form.
- 0x8; A boot effect - No fields.
- 0x9: A demo effect - NO fields.
- 0xa: breathe a color (like pulse) - color, period.
- 0xb: Ripple - color, period.
- null: An unknown effect.
Only effects supported by the device can be used.
To set up profiles, first run `solaar profiles <device name>`, which will output a YAML dump of the profiles on the device. Then store the YAML dump into a file and edit the file to make changes. Finally run `solaar profiles <device name> <file name>` to load the profiles back onto the device. Profiles are stored in flash memory and persist when the device is inactive or turned off. When loading profiles Solaar is careful to only write the flash memory sectors that need to be changed. Solaar also checks for correct profile version and device name before loading a profile into a device.
Keep a copy of the initial dump of profiles so that it can be loaded back to the device if problems are encountered with the edited profiles. The safest changes are to take an unused or unenabled profile sector and create a new profile in it, likely mostly copying parts of another profile.
## System Tray
Solaar's GUI normally uses an icon in the system tray.
@ -283,7 +182,7 @@ This allows users to close Solaar and reopen from the tray.
This aspect of Solaar depends on having an active system tray which may
require some special setup when using Gnome, particularly under Wayland.
If you are running Gnome, you most likely need the
If you are running gnome, you most likely need the
`gnome-shell-extension-appindicator` package installed.
In Fedora, this can be done by running
```
@ -300,7 +199,7 @@ You may have to log out and log in again before the system tray shows up.
For many devices, Solaar shows the approximate battery level via icons that
show up in both the main window and the system tray. In previous versions
several heuristics determined which icon names to use for this purpose,
several heuristics to determine which icon names to use for this purpose,
but as more and more battery icon schemes have been developed this has
become impossible to do well. Solaar now uses the eleven standard
battery icon names `battery-{full,good,low,critical,empty}[-charging]` and

View File

@ -11,26 +11,72 @@ Solaar supports most Logitech Nano, Unifying, and Bolt receivers.
Solaar supports some Lightspeed receivers.
See the receiver table below for the list of currently supported receivers.
Solaar supports all Logitech devices (keyboards, mice, trackballs, touchpads, and headsets)
Solaar supports most recent and many older Logitech devices
(keyboards, mice, trackballs, touchpads, and headsets)
that can connect to supported receivers.
Solaar supports all Logitech devices that can connect via a USB cable or via Bluetooth,
as long as the device uses the HID++ protocol.
Solaar supports many recent Logitech devices that can connect via a USB cable,
but some such Logitech devices are not suited for use in Solaar because they do not use the HID++ protocol.
One example is the MX518 Gaming Mouse.
Solaar supports most recent Logitech devices that can connect via Bluetooth.
The best way to determine whether Solaar supports a device is to run Solaar while the device is connected.
If the device is supported, it will show up in the Solaar main window.
If it is not, and there is no issue about the device in the Solaar GitHub repository,
open an enhancement issue requesting that it be supported.
The directory <https://github.com/pwr-Solaar/Solaar/tree/master/docs/devices> contains edited output
of `solaar show` on many devices and can be used to see what Solaar can do with the device.
The directory https://github.com/pwr-Solaar/Solaar/tree/master/docs/devices contains edited output
of `solaar show` on many devices and can be used to see what Solaar can do with the the device.
## Supporting old devices
## Adding new devices
Some old Logitech devices use an old version of HID++.
For Solaar to support these devices well, Solaar needs some information about them.
Most new HID++ devices do not need to be known to Solaar to work.
You should be able to just run Solaar and the device will show up
If you have an old Logitech device that shows up in Solaar but has no settings
and you feel that Solaar should be able to do more with the device you can
open an enhancement request for Solaar to better support the device.
If your device does not show up,
either it doesn't use HID++ or the interface it uses isn't the one Solaar normally uses.
To start the process of support for a Logitech device open an enhancement issue for Solaar and
follow these steps:
1. Make sure the receiver or device is connected and active.
2. Look at output of `grep -H . /sys/class/hidraw/hidraw*/device/uevent` to find
where information about the device is kept.
You are looking for a line like `/sys/class/hidraw/hidrawN/device/uevent:HID_NAME=<NAME>`
where <NAME> is the name of your receiver or device.
`N` is the current HID raw number of your receiver or device.
3. Provide the contents of the file `/sys/class/hidraw/hidrawN/device/uevent` where N was found
above.
4. Also attach contents of the file `/sys/class/hidraw/hidrawN/device/report_descriptor`
to the enhancement request.
You will have to copy the contents to a file with txt extension before attaching it.
Or, better, install hidrd-convert and attach output of
`hidrd-convert -o spec /sys/class/hidraw/hidrawN/device/report_descriptor`
(To install hidrd on Fedora use `sudo dnf install hidrd`.)
5. If your device or receiver connects via USB, look at the output of `lsusb`
to find the ID of device or receiver and also provide output of
`lsusb -vv -d xxxx:yyyy` where xxxx:yyyy is ID of device or receiver.
If your device can connect in multiple ways - via a receiver, via USB (not just charging via a USB cable),
via Bluetooth - do this for each way it can connect.
### Adding information about a new device to the Solaar code
The _D function in `../lib/logitech_receiver/descriptors.py` makes a device known to Solaar.
The usual arguments to the _D function are the device's long name, its short name
(codename), and its HID++ protocol version.
Devices that use HID++ 1.0 need a tuple of known registers (registers) and settings (settings).
Settings can be provided for Devices that use HID++ 2.0 or later,
but Solaar can determine these from the device.
If the device can connect to a receiver, provide its wireless product ID (wpid),
If the device can connect via Bluetooth, provide its Bluetooth product ID (btid).
If the device can connect via a USB cable, provide its USB product ID (usbid),
and the interface it uses to send and receiver HID++ messages (interface - default 2).
The use of a non-default USB interface is the main reason for requiring information about
modern devices to be added to Solaar.
## Adding new receivers
@ -74,25 +120,22 @@ to be specified. Then add the receiver to the tuple of receivers (ALL).
| 17ef:6042 | Nano | 1 |
Some Nano receivers are only partly supported
as they do not implement the full HID++ 1.0 protocol.
Some Nano receivers are not supported as they do not implement the HID++ protocol at all.
as they do not fully implement the full HID++ 1.0 protocol.
Some Nano receivers are not supported at all as they do not implement the HID++ protocol.
Receivers with USB ID 046d:c542 fall into this category.
The receiver with USB ID 046d:c517 is an old 27 MHz receiver, supporting only
a subset of the HID++ 1.0 protocol. Only hardware pairing is supported.
subset of HID++ 1.0 protocol. Only hardware pairing is supported.
## Supported Devices (Historical Interest Only)
## Supported Devices
The device tables below provide a list of some of the devices that Solaar supports,
giving their product name, WPID product number, and HID++ protocol information.
The tables concentrate on older devices that have explicit support information in Solaar
and are not being updated for new devices that are supported by Solaar.
Note that Logitech has the annoying habit of reusing Device names (e.g., M185)
so what is important for support is the USB WPID or Bluetooth model ID.
### Keyboards (Unifying)
| Device | WPID | HID++ |
@ -159,7 +202,6 @@ so what is important for support is the USB WPID or Bluetooth model ID.
| MX Master | 4041 | 2.0 |
| MX Master 2S | 4069 | 2.0 |
| Cube | | 2.0 |
| MX Vertical | 407B | 2.0 |
### Mice (Nano)
@ -209,7 +251,6 @@ so what is important for support is the USB WPID or Bluetooth model ID.
| Device | WPID | HID++ |
|------------------------------|------|-------|
| G604 Wireless Gaming Mouse | 4085 | 4.2 |
| PRO X Superlight Wireless | 4093 | 4.2 |
### Trackballs (Unifying)
@ -242,4 +283,4 @@ so what is important for support is the USB WPID or Bluetooth model ID.
| EX100 keyboard | 0065 | 1.0 |
| EX100 mouse | 003f | 1.0 |
* The EX100 is an old, pre-Unifying receiver and device set, supporting only some HID++ 1.0 features
* The EX100 is an old, preunifying receiver and device set, supporting only part of HID++ 1.0 features

View File

@ -1,59 +0,0 @@
solaar version 1.1.8
1: G304 Lightspeed Wireless Gaming Mouse
Device path : /dev/hidraw6
WPID : 4074
Codename : G304
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 8 ms (125Hz)
Serial number: B2D05D23
Model ID: 407400000000
Unit ID: EB490C63
Bootloader: BOT 69.02.B0021
Firmware: RQM 68.02.B0021
The power switch is located on the base.
Supports 27 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Bootloader BOT 69.02.B0021 4074452F3940
Firmware: Firmware RQM 68.02.B0021 4074452F3940
Unit ID: EB490C63 Model ID: 407400000000 Transport IDs: {'wpid': '4074'}
3: DEVICE NAME {0005} V0
Name: G304 Lightspeed Wireless Gaming Mouse
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: BATTERY STATUS {1000} V0
Battery: 90%, discharging, next level 50%.
6: COLOR LED EFFECTS {8070} V6
7: ONBOARD PROFILES {8100} V0
Device Mode: Host
Onboard Profiles (saved): Disable
Onboard Profiles : Disable
8: MOUSE BUTTON SPY {8110} V0
9: REPORT RATE {8060} V0
Polling Rate (ms): 8
Polling Rate (ms) (saved): 8
Polling Rate (ms) : 8
10: MODE STATUS {8090} V1
11: DFUCONTROL SIGNED {00C2} V0
12: DEVICE RESET {1802} V0 internal, hidden
13: unknown:1803 {1803} V0 internal, hidden
14: CONFIG DEVICE PROPS {1806} V4 internal, hidden
15: unknown:1811 {1811} V0 internal, hidden
16: OOBSTATE {1805} V0 internal, hidden
17: unknown:1830 {1830} V0 internal, hidden
18: unknown:1890 {1890} V0 internal, hidden
19: unknown:1DF3 {1DF3} V0 internal, hidden
20: unknown:1E00 {1E00} V0 hidden
21: unknown:1EB0 {1EB0} V0 internal, hidden
22: unknown:1861 {1861} V0 internal, hidden
23: unknown:18B1 {18B1} V0 internal, hidden
24: unknown:1E22 {1E22} V0 internal, hidden
25: unknown:1801 {1801} V0 internal, hidden
26: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 2200
Sensitivity (DPI) : 2200
Battery: 90%, discharging, next level 50%.

View File

@ -1,58 +0,0 @@
solaar version 1.1.10
1: G305 Lightspeed Wireless Gaming Mouse
Device path : /dev/hidraw7
WPID : 4074
Codename : G305
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number: ED5E9515
Model ID: 407400000000
Unit ID: F074D567
Bootloader: BOT 69.02.B0021
Firmware: RQM 68.02.B0021
The power switch is located on the base.
Supports 27 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Bootloader BOT 69.02.B0021 4074452F3940
Firmware: Firmware RQM 68.02.B0021 4074452F3940
Unit ID: F074D567 Model ID: 407400000000 Transport IDs: {'wpid': '4074'}
3: DEVICE NAME {0005} V0
Name: G305 Lightspeed Wireless Gaming Mouse
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: BATTERY STATUS {1000} V0
Battery: 50%, discharging, next level 30%.
6: COLOR LED EFFECTS {8070} V6
7: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Enable
Onboard Profiles : Enable
8: MOUSE BUTTON SPY {8110} V0
9: REPORT RATE {8060} V0
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
10: MODE STATUS {8090} V1
11: DFUCONTROL SIGNED {00C2} V0
12: DEVICE RESET {1802} V0 internal, hidden
13: unknown:1803 {1803} V0 internal, hidden
14: CONFIG DEVICE PROPS {1806} V4 internal, hidden
15: unknown:1811 {1811} V0 internal, hidden
16: OOBSTATE {1805} V0 internal, hidden
17: unknown:1830 {1830} V0 internal, hidden
18: unknown:1890 {1890} V0 internal, hidden
19: unknown:1DF3 {1DF3} V0 internal, hidden
20: unknown:1E00 {1E00} V0 hidden
21: unknown:1EB0 {1EB0} V0 internal, hidden
22: unknown:1861 {1861} V0 internal, hidden
23: unknown:18B1 {18B1} V0 internal, hidden
24: unknown:1E22 {1E22} V0 internal, hidden
25: unknown:1801 {1801} V0 internal, hidden
26: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 1600
Sensitivity (DPI) : 1600
Battery: 50%, discharging, next level 30%.

View File

@ -1,18 +1,18 @@
solaar version 1.1.12rc1
Solaar version 1.1.7
1: G502 Gaming Mouse
Device path : /dev/hidraw20
Device path : /dev/hidraw4
WPID : 407F
Codename : G502
Kind : mouse
Protocol : HID++ 4.2
Report Rate : 1ms
Serial number: DDDAADBC
Polling rate : 1 ms (1000Hz)
Serial number: 1F2DBC7E
Model ID: 407FC08D0000
Unit ID: DDDAADBC
1: BOT 92.00.B0008
0: MPM 17.00.B0008
3:
Unit ID: 1F2DBC7E
Bootloader: BOT 92.00.B0008
Firmware: MPM 17.00.B0008
Other:
The power switch is located on the base.
Supports 30 HID++ 2.0 features:
0: ROOT {0000} V0
@ -21,34 +21,28 @@ solaar version 1.1.12rc1
Firmware: Bootloader BOT 92.00.B0008 AAEF21F1FA5F
Firmware: Firmware MPM 17.00.B0008 407F21F1FA5F
Firmware: Other
Unit ID: DDDAADBC Model ID: 407FC08D0000 Transport IDs: {'wpid': '407F', 'usbid': 'C08D'}
Unit ID: 1F2DBC7E Model ID: 407FC08D0000 Transport IDs: {'wpid': '407F', 'usbid': 'C08D'}
3: DEVICE NAME {0005} V0
Name: G502 LIGHTSPEED Wireless Gaming Mouse
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
Configuration: 11000000000000000000000000000000
5: RESET {0020} V0
6: BATTERY VOLTAGE {1001} V2
Battery: 90% 4166mV , discharging.
Battery: 70% 3978mV , discharging.
7: COLOR LED EFFECTS {8070} V4
LED Control (saved): Device
LED Control : Device
LEDs Primary (saved): !LEDEffectSetting {ID: 1, color: 16711680, intensity: 0, period: 100, ramp: 0, speed: 0}
LEDs Primary : None
LEDs Logo : None
8: LED CONTROL {1300} V0
9: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Profile 1
Onboard Profiles : Profile 1
Onboard Profiles (saved): Enable
Onboard Profiles : Enable
10: MOUSE BUTTON SPY {8110} V0
11: REPORT RATE {8060} V0
Report Rate: 1ms
Report Rate (saved): 1ms
Report Rate : 1ms
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
12: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 900
Sensitivity (DPI) : 900
Sensitivity (DPI) (saved): 800
Sensitivity (DPI) : 800
13: DEVICE RESET {1802} V0 internal, hidden
14: unknown:1803 {1803} V0 internal, hidden
15: OOBSTATE {1805} V0 internal, hidden
@ -69,12 +63,12 @@ solaar version 1.1.12rc1
Multiplier: 8
Has invert: Normal wheel motion
Has ratchet switch: Normal wheel mode
High resolution mode
Low resolution mode
HID notification
Scroll Wheel Direction (saved): False
Scroll Wheel Direction : False
Scroll Wheel Resolution (saved): True
Scroll Wheel Resolution : True
Scroll Wheel Resolution (saved): False
Scroll Wheel Resolution : False
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
Battery: 90% 4166mV , discharging.
Battery: 70% 3978mV , discharging.

View File

@ -1,49 +0,0 @@
solaar version 1.1.9
2: G502 Proteus Spectrum Optical Mouse
Device path : /dev/hidraw4
USB id : 046d:C332
Codename : G502 Proteus Spectrum
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number:
Model ID: C33200000000
Unit ID: 31374706
Firmware: U1 03.02.B0012
Bootloader: BOT 14.00.B0007
Supports 20 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: COLOR LED EFFECTS {8070} V3
3: DEVICE FW VERSION {0003} V1
Firmware: Firmware U1 03.02.B0012 C332
Firmware: Bootloader BOT 14.00.B0007 AABF
Unit ID: 31374706 Model ID: C33200000000 Transport IDs: {'usbid': 'C332'}
4: DEVICE NAME {0005} V0
Name: Tunable RGB Gaming Mouse G502
Kind: mouse
5: LED CONTROL {1300} V0
6: unknown:18A1 {18A1} V0 internal, hidden
7: unknown:1E00 {1E00} V0 hidden
8: unknown:1E20 {1E20} V0
9: unknown:1EB0 {1EB0} V0 internal, hidden
10: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 7000
Sensitivity (DPI) : 7000
11: ANGLE SNAPPING {2230} V0
12: SURFACE TUNING {2240} V0
13: REPORT RATE {8060} V0
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
14: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Enable
Onboard Profiles : Enable
15: MOUSE BUTTON SPY {8110} V0
16: unknown:1850 {1850} V0 internal, hidden
17: DFUCONTROL UNSIGNED {00C1} V0
18: unknown:1801 {1801} V0 internal, hidden
19: DEVICE RESET {1802} V0 internal, hidden
Battery status unavailable.

View File

@ -1,44 +0,0 @@
1: G502 SE Hero Gaming Mouse
Device path : /dev/hidraw7
USB id : 046d:C08B
Codename : G502 Hero
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number:
Model ID: C08B00000000
Unit ID: 30324703
Firmware: U1 27.03.B0010
Bootloader: BOT 81.00.B0002
Supports 19 HID++ 2.0 features:
0: ROOT {0000}
1: FEATURE SET {0001}
2: COLOR LED EFFECTS {8070}
3: DEVICE FW VERSION {0003}
Firmware: Firmware U1 27.03.B0010 C08B
Firmware: Bootloader BOT 81.00.B0002 AAE6
Unit ID: 30324703 Model ID: C08B00000000 Transport IDs: {'usbid': 'C08B'}
4: DEVICE NAME {0005}
Name: G502 HERO Gaming Mouse
Kind: mouse
5: LED CONTROL {1300}
6: unknown:18A1 {18A1} internal, hidden
7: unknown:1E00 {1E00} hidden
8: unknown:1E22 {1E22} internal, hidden
9: unknown:1EB0 {1EB0} internal, hidden
10: ADJUSTABLE DPI {2201}
Sensitivity (DPI) (saved): 2400
Sensitivity (DPI) : 2400
11: REPORT RATE {8060}
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
12: ONBOARD PROFILES {8100}
Device Mode: Host
13: MOUSE BUTTON SPY {8110}
14: DFUCONTROL SIGNED {00C2}
15: unknown:1801 {1801} internal, hidden
16: DEVICE RESET {1802} internal, hidden
17: CONFIG DEVICE PROPS {1806} internal, hidden
18: unknown:18B1 {18B1} internal, hidden
Battery status unavailable.

View File

@ -1,29 +0,0 @@
solaar version 1.1.8
USB and Bluetooth Devices
1: G535 Wireless Gaming Headset
Device path : /dev/hidraw2
USB id : 046d:0AC4
Codename : G535
Kind : ?
Protocol : HID++ 4.2
Serial number:
Model ID: 000000000AC4
Unit ID: FFFFFFFF
Firmware: U1 90.00.B0200
Supports 6 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Firmware U1 90.00.B0200 0AC4
Unit ID: FFFFFFFF Model ID: 000000000AC4 Transport IDs: {'btid': '0000', 'btleid': '0000'}
3: DEVICE NAME {0005} V0
Name: G535 Wireless Gaming Headset
Kind: None
4: SIDETONE {8300} V0
Sidetone (saved): 0
Sidetone : 0
5: ADC MEASUREMENT {1F20} V0
Battery: 60% 3920mV , discharging.
Battery: 60% 3920mV , discharging.

View File

@ -1,84 +0,0 @@
solaar version 03cfa128
1: G604 Wireless Gaming Mouse
Device path : /dev/hidraw6
WPID : 4085
Codename : G604
Kind : mouse
Protocol : HID++ 4.2
Report Rate : 1ms
Serial number: XXXXXXXX
Model ID: B02440850000
Unit ID: XXXXXXXX
1: BL1 04.01.B0014
0: MPM 21.01.B0014
3:
The power switch is located on the base.
Supports 33 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: 1 BL1 04.01.B0014 0000B01B3067
Firmware: 0 MPM 21.01.B0014 4085B01B3067
Firmware: 3
Unit ID: XXXXXXXX Model ID: B02440850000 Transport IDs: {'btleid': 'B024', 'wpid': '4085'}
3: DEVICE NAME {0005} V0
Name: G604 Wireless Gaming Mouse
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
Configuration: 00000000000000000000000000000000
6: BATTERY STATUS {1000} V0
Battery: 30%, BatteryStatus.DISCHARGING, next level 15%.
7: COLOR LED EFFECTS {8070} V4
LED Control (saved): Device
LED Control : Device
LEDs Primary : None
8: LED CONTROL {1300} V0
9: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Profile 1
Onboard Profiles : Profile 1
10: MOUSE BUTTON SPY {8110} V0
11: REPORT RATE {8060} V0
Report Rate: 1ms
Report Rate (saved): 1ms
Report Rate : 1ms
12: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 800
Sensitivity (DPI) : 800
13: DFUCONTROL SIGNED {00C2} V0
14: DEVICE RESET {1802} V0
15: unknown:1803 {0318} V0 internal, hidden
16: OOBSTATE {1805} V0
17: CONFIG DEVICE PROPS {1806} V4
18: unknown:1813 {1318} V0 internal, hidden
19: unknown:1830 {3018} V0 internal, hidden
20: unknown:1890 {9018} V0 internal, hidden
21: unknown:1891 {9118} V0 internal, hidden
22: unknown:1861 {6118} V0 internal, hidden
23: unknown:1801 {0118} V0 internal, hidden
24: unknown:18B1 {B118} V0 internal, hidden
25: unknown:1DF3 {F31D} V0 internal, hidden
26: unknown:1E00 {001E} V0 hidden
27: unknown:1EB0 {B01E} V0 internal, hidden
28: unknown:1E22 {221E} V0 internal, hidden
29: HIRES WHEEL {2121} V0
Multiplier: 8
Has invert: Normal wheel motion
Has ratchet switch: Normal wheel mode
High resolution mode
HID notification
Scroll Wheel Direction (saved): False
Scroll Wheel Direction : False
Scroll Wheel Resolution (saved): True
Scroll Wheel Resolution : True
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
30: unknown:18C0 {C018} V0 internal, hidden
31: CHANGE HOST {1814} V1
Change Host : 1:host1
32: HOSTS INFO {1815} V1
Host 0 (unpaired): host1
Host 1 (paired):
Battery: 30%, BatteryStatus.DISCHARGING, next level 15%.

View File

@ -1,42 +0,0 @@
solaar version 1.1.11
G733 Gaming Headset
Device path : /dev/hidraw3
USB id : 046d:0AFE
Codename : G733 Headset New
Kind : headset
Protocol : HID++ 4.2
Serial number:
Model ID: 0AFE00000000
Unit ID: FFFFFFFF
Firmware: U2 00.06
Supports 9 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Firmware U2 00.06 0AFE
Unit ID: FFFFFFFF Model ID: 0AFE00000000 Transport IDs: {'usbid': '0AFE'}
3: DEVICE NAME {0005} V0
Name: G733 Gaming Headset
Kind: None
4: COLOR LED EFFECTS {8070} V3
LED Control (saved): Device
LED Control : Device
LEDs Logo (saved): !LEDEffectSetting {ID: 0x0}
LEDs Logo : !LEDEffectSetting {ID: 0}
LEDs Primary (saved): !LEDEffectSetting {ID: 0x1, color: 0x0, ramp: 0x0}
LEDs Primary : !LEDEffectSetting {ID: 1, color: 0x10000, ramp: 0x0}
5: GKEY {8010} V0
Divert G and M Keys (saved): False
Divert G and M Keys : False
6: EQUALIZER {8310} V1
Equalizer (saved): {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
Equalizer : {0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 7: 0, 8: 0, 9: 0}
7: SIDETONE {8300} V0
Sidetone (saved): 0
Sidetone : 0
8: ADC MEASUREMENT {1F20} V4
Battery: 60% 3867mV , discharging.
Power Management (saved): 0
Power Management : 0
Battery: 60% 3867mV , discharging.

View File

@ -1,63 +0,0 @@
solaar version 1.1.9
1: G815 Mechanical Keyboard
Device path : /dev/hidraw2
USB id : 046d:C33F
Codename : G815
Kind : keyboard
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number:
Model ID: C33F00000000
Unit ID: 35304716
Bootloader: BOT 84.00.B0003
Firmware: U1 31.02.B0018
Other:
Other:
Other:
Supports 24 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Bootloader BOT 84.00.B0003 AAEA
Firmware: Firmware U1 31.02.B0018 C33F
Firmware: Other
Firmware: Other
Firmware: Other
Unit ID: 35304716 Model ID: C33F00000000 Transport IDs: {'usbid': 'C33F'}
3: DEVICE NAME {0005} V0
Name: G815 RGB MECHANICAL GAMING KEYBOARD
Kind: keyboard
4: CONFIG CHANGE {0020} V0
5: DFUCONTROL SIGNED {00C2} V0
6: DFU {00D0} V0
7: REPORT HID USAGE {1BC0} V0
8: KEYBOARD DISABLE BY USAGE {4522} V0
9: KEYBOARD LAYOUT 2 {4540} V0
10: GKEY {8010} V0
Divert G Keys (saved): True
Divert G Keys : False
11: MKEYS {8020} V0
M-Key LEDs (saved): {M1:False, M2:False, M3:False}
M-Key LEDs : {M1:False, M2:False, M3:False}
12: MR {8030} V0
MR-Key LED (saved): False
MR-Key LED : False
13: BRIGHTNESS CONTROL {8040} V0
14: REPORT RATE {8060} V0
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
15: RGB EFFECTS {8071} V0
16: PER KEY LIGHTING V2 {8081} V2
17: ONBOARD PROFILES {8100} V0
Device Mode: Host
Onboard Profiles (saved): Disable
Onboard Profiles : Disable
18: unknown:1801 {1801} V0 internal, hidden
19: DEVICE RESET {1802} V0 internal, hidden
20: CONFIG DEVICE PROPS {1806} V5 internal, hidden
21: unknown:18B0 {18B0} V0 internal, hidden
22: unknown:1E00 {1E00} V0 hidden
23: unknown:1EB0 {1EB0} V0 internal, hidden
Battery status unavailable.

View File

@ -1,105 +0,0 @@
solaar version 1.1.8rc3+git1940-4e7b6b3
1: G903 LIGHTSPEED Wireless Gaming Mouse w/ HERO
Device path : /dev/hidraw13
WPID : 4087
Codename : G903 LS
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number: 3EF038B9
Model ID: 4087C0910000
Unit ID: 3EF038B9
Bootloader: BL1 06.01.B0013
Firmware: MPM 23.01.B0013
Other:
The power switch is located on the base.
Supports 31 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Bootloader BL1 06.01.B0013 0000047072FE
Firmware: Firmware MPM 23.01.B0013 4087047072FE
Firmware: Other
Unit ID: 3EF038B9 Model ID: 4087C0910000 Transport IDs: {'wpid': '4087', 'usbid': 'C091'}
3: DEVICE NAME {0005} V0
Name: G903 LIGHTSPEED Wireless Gaming Mouse w/ HERO
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: BATTERY VOLTAGE {1001} V2
Battery: 90% 4079mV , discharging.
7: RGB EFFECTS {8071} V0
8: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Enable
Onboard Profiles : Enable
9: MOUSE BUTTON SPY {8110} V0
10: REPORT RATE {8060} V0
Polling Rate (ms): 1
Polling Rate (ms) (saved): 1
Polling Rate (ms) : 1
11: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 6400
Sensitivity (DPI) : 6400
12: DFUCONTROL SIGNED {00C2} V0
13: DEVICE RESET {1802} V0 internal, hidden
14: unknown:1803 {1803} V0 internal, hidden
15: OOBSTATE {1805} V0 internal, hidden
16: CONFIG DEVICE PROPS {1806} V4 internal, hidden
17: unknown:1811 {1811} V0 internal, hidden
18: unknown:1830 {1830} V0 internal, hidden
19: unknown:1890 {1890} V4 internal, hidden
20: unknown:1891 {1891} V4 internal, hidden
21: unknown:18A1 {18A1} V0 internal, hidden
22: unknown:1801 {1801} V0 internal, hidden
23: unknown:18B1 {18B1} V0 internal, hidden
24: unknown:1DF3 {1DF3} V0 internal, hidden
25: unknown:1E00 {1E00} V0 hidden
26: unknown:1EB0 {1EB0} V0 internal, hidden
27: unknown:1863 {1863} V0 internal, hidden
28: unknown:1E22 {1E22} V0 internal, hidden
29: HIRES WHEEL {2121} V0
Multiplier: 8
Has invert: Normal wheel motion
Has ratchet switch: Normal wheel mode
Low resolution mode
HID notification
Scroll Wheel Direction (saved): False
Scroll Wheel Direction : False
Scroll Wheel Resolution (saved): False
Scroll Wheel Resolution : False
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
30: unknown:18C0 {18C0} V0 internal, hidden
Battery: 90% 4079mV , discharging.
7: Candy companion chip
Device path : /dev/hidraw14
Codename : Candy
Kind : touchpad
Protocol : HID++ 4.2
Serial number: 4E4E9946
Model ID: 405F00000000
Unit ID: 34304713
Firmware: CC 07.00.B0010
Bootloader: BOT 32.00.B0010
Supports 12 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Firmware CC 07.00.B0010 405F
Firmware: Bootloader BOT 32.00.B0010 405F
Unit ID: 34304713 Model ID: 405F00000000 Transport IDs: {'wpid': '405F'}
3: DEVICE NAME {0005} V0
Name: Candy companion chip
Kind: touchpad
4: unknown:18A1 {18A1} V0 internal, hidden
5: unknown:1E00 {1E00} V0 hidden
6: unknown:1EB0 {1EB0} V0 internal, hidden
7: DFUCONTROL SIGNED {00C2} V0
8: unknown:1801 {1801} V0 internal, hidden
9: DEVICE RESET {1802} V0 internal, hidden
10: unknown:1803 {1803} V0 internal, hidden
11: COLOR LED EFFECTS {8070} V4
Battery status unavailable.

View File

@ -1,103 +0,0 @@
solaar version 1.1.12rc1
1: G915 WIRELESS RGB MECHANICAL GAMING KEYBOARD
Device path : None
WPID : 407C
Codename : G915 KEYBOARD
Kind : keyboard
Protocol : HID++ 4.2
Report Rate : 1ms
Serial number: A502B0E1
Model ID: B354407CC33E
Unit ID: A502B0E1
1: BOT 77.02.B0039
3:
0: MPK 09.03.B0041
3:
3:
The power switch is located on the top left corner.
Supports 38 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BOT 77.02.B0039 0000EC44D534
Firmware: Other
Firmware: Firmware MPK 09.03.B0041 407C3791543D
Firmware: Other
Firmware: Other
Unit ID: A502B0E1 Model ID: B354407CC33E Transport IDs: {'btleid': 'B354', 'wpid': '407C', 'usbid': 'C33E'}
3: DEVICE NAME {0005} V0
Name: G915 WIRELESS RGB MECHANICAL GAMING KEYBOARD
Kind: keyboard
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
Configuration: 11000000000000000000000000000000
6: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: G915 KEYBOARD<52>
7: BATTERY VOLTAGE {1001} V3
Battery: 80% 3998mV , discharging.
8: CHANGE HOST {1814} V1
Change Host : 1:Yon
9: HOSTS INFO {1815} V1
Host 0 (paired): Yon
Host 1 (paired):
10: RGB EFFECTS {8071} V0
RGB Control (saved): Device
RGB Control : Device
LEDs Logo (saved): !LEDEffectSetting {ID: 1, color: 11546720, intensity: 0, period: 100, ramp: 0, speed: 0}
LEDs Logo : HID++ error {'number': 1, 'request': 2799, 'error': 7, 'params': b'\x00'}
LEDs Primary (saved): !LEDEffectSetting {ID: 1, color: 16776960, intensity: 0, period: 100, ramp: 0, speed: 0}
LEDs Primary : HID++ error {'number': 1, 'request': 2796, 'error': 7, 'params': b'\x01'}
11: PER KEY LIGHTING V2 {8081} V2
Per-key Lighting (saved): {A:white, B:red, C:white, D:white, E:white, F:white, G:white, H:white, I:white, J:white, K:white, L:white, M:white, N:white, O:white, P:white, Q:white, R:white, S:white, T:white, U:white, V:white, W:white, X:white, Y:white, Z:white, 1:white, 2:white, 3:white, 4:white, 5:white, 6:white, 7:white, 8:white, 9:white, 0:white, ENTER:white, ESC:white, BACKSPACE:white, TAB:white, SPACE:white, -:white, =:white, [:white, \:white, KEY 46:white, ~:white, ;:white, ':white, `:white, ,:white, .:white, /:white, CAPS LOCK:white, F1:white, F2:white, F3:white, F4:white, F5:white, F6:white, F7:white, F8:white, F9:white, F10:white, F11:white, F12:white, PRINT:white, SCROLL LOCK:white, PASTE:white, INSERT:white, HOME:white, PAGE UP:white, DELETE:white, END:white, PAGE DOWN:white, RIGHT:white, LEFT:white, DOWN:white, UP:white, NUMLOCK:white, KEYPAD /:white, KEYPAD *:white, KEYPAD -:white, KEYPAD +:white, KEYPAD ENTER:white, KEYPAD 1:white, KEYPAD 2:white, KEYPAD 3:white, KEYPAD 4:white, KEYPAD 5:white, KEYPAD 6:white, KEYPAD 7:white, KEYPAD 8:white, KEYPAD 9:white, KEYPAD 0:white, KEYPAD .:white, KEY 97:white, COMPOSE:white, POWER:white, KEY 100:white, KEY 101:white, KEY 102:white, KEY 103:white, LEFT CTRL:white, LEFT SHIFT:white, LEFT ALT:white, LEFT WINDOWS:white, RIGHT CTRL:white, RIGHT SHIFT:white, RIGHT ALTGR:white, RIGHT WINDOWS:white, BRIGHTNESS:white, PAUSE:white, MUTE:white, NEXT:white, PREVIOUS:white, G1:white, G2:white, G3:white, G4:white, G5:white, LOGO:white}
Per-key Lighting : {A:white, B:white, C:white, D:white, E:white, F:white, G:white, H:white, I:white, J:white, K:white, L:white, M:white, N:white, O:white, P:white, Q:white, R:white, S:white, T:white, U:white, V:white, W:white, X:white, Y:white, Z:white, 1:white, 2:white, 3:white, 4:white, 5:white, 6:white, 7:white, 8:white, 9:white, 0:white, ENTER:white, ESC:white, BACKSPACE:white, TAB:white, SPACE:white, -:white, =:white, [:white, \:white, KEY 46:white, ~:white, ;:white, ':white, `:white, ,:white, .:white, /:white, CAPS LOCK:white, F1:white, F2:white, F3:white, F4:white, F5:white, F6:white, F7:white, F8:white, F9:white, F10:white, F11:white, F12:white, PRINT:white, SCROLL LOCK:white, PASTE:white, INSERT:white, HOME:white, PAGE UP:white, DELETE:white, END:white, PAGE DOWN:white, RIGHT:white, LEFT:white, DOWN:white, UP:white, NUMLOCK:white, KEYPAD /:white, KEYPAD *:white, KEYPAD -:white, KEYPAD +:white, KEYPAD ENTER:white, KEYPAD 1:white, KEYPAD 2:white, KEYPAD 3:white, KEYPAD 4:white, KEYPAD 5:white, KEYPAD 6:white, KEYPAD 7:white, KEYPAD 8:white, KEYPAD 9:white, KEYPAD 0:white, KEYPAD .:white, KEY 97:white, COMPOSE:white, POWER:white, KEY 100:white, KEY 101:white, KEY 102:white, KEY 103:white, LEFT CTRL:white, LEFT SHIFT:white, LEFT ALT:white, LEFT WINDOWS:white, RIGHT CTRL:white, RIGHT SHIFT:white, RIGHT ALTGR:white, RIGHT WINDOWS:white, BRIGHTNESS:white, PAUSE:white, MUTE:white, NEXT:white, PREVIOUS:white, G1:white, G2:white, G3:white, G4:white, G5:white, LOGO:white}
12: REPROG CONTROLS V4 {1B04} V4
Key/Button Diversion (saved): {Host Switch Channel 1:Regular, Host Switch Channel 2:Regular}
Key/Button Diversion : {Host Switch Channel 1:Regular, Host Switch Channel 2:Regular}
13: REPORT HID USAGE {1BC0} V1
14: ENCRYPTION {4100} V0
15: KEYBOARD DISABLE BY USAGE {4522} V0
16: KEYBOARD LAYOUT 2 {4540} V0
17: GKEY {8010} V0
Divert G and M Keys (saved): False
Divert G and M Keys : False
18: MKEYS {8020} V0
M-Key LEDs (saved): {M1:False, M2:False, M3:False}
M-Key LEDs : {M1:False, M2:False, M3:False}
19: MR {8030} V0
MR-Key LED (saved): False
MR-Key LED : False
20: BRIGHTNESS CONTROL {8040} V0
Brightness Control (saved): 12
Brightness Control : 12
21: ONBOARD PROFILES {8100} V0
Device Mode: Host
Onboard Profiles (saved): Disabled
Onboard Profiles : Disabled
22: REPORT RATE {8060} V0
Report Rate: 1ms
Report Rate (saved): 1ms
Report Rate : 1ms
23: DFUCONTROL SIGNED {00C2} V0
24: DFU {00D0} V3
25: DEVICE RESET {1802} V0 internal, hidden
26: unknown:1803 {1803} V0 internal, hidden
27: CONFIG DEVICE PROPS {1806} V8 internal, hidden
28: unknown:1813 {1813} V0 internal, hidden
29: OOBSTATE {1805} V0 internal, hidden
30: unknown:1830 {1830} V0 internal, hidden
31: unknown:1890 {1890} V5 internal, hidden
32: unknown:1891 {1891} V5 internal, hidden
33: unknown:18A1 {18A1} V0 internal, hidden
34: unknown:1E00 {1E00} V0 hidden
35: unknown:1EB0 {1EB0} V0 internal, hidden
36: unknown:1861 {1861} V0 internal, hidden
37: unknown:18B0 {18B0} V0 internal, hidden
Has 2 reprogrammable keys:
0: Host Switch Channel 1 , default: HostSwitch Channel 1 => HostSwitch Channel 1
divertable, persistently divertable, pos:1, group:0, group mask:empty
reporting: default
1: Host Switch Channel 2 , default: HostSwitch Channel 2 => HostSwitch Channel 2
divertable, persistently divertable, pos:2, group:0, group mask:empty
reporting: default
Battery: 80% 3998mV , discharging.

View File

@ -1,91 +0,0 @@
solaar version 1.1.10
USB and Bluetooth Devices
1: G915 WIRELESS RGB MECHANICAL GAMING KEYBOARD
Device path : /dev/hidraw13
USB id : 046d:C33E
Codename : G915
Kind : ?
Protocol : HID++ 4.2
Polling rate : 1 ms (1000Hz)
Serial number:
Model ID: B354407CC33E
Unit ID: 8816D0DF
Bootloader: BOT 77.03.B0041
Other:
Firmware: MPK 09.04.B0042
Other:
Other:
Supports 37 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BOT 77.03.B0041 00003791543D
Firmware: Other
Firmware: Firmware MPK 09.04.B0042 C33E8A23A76B
Firmware: Other
Firmware: Other
Unit ID: 8816D0DF Model ID: B354407CC33E Transport IDs: {'btleid': 'B354', 'wpid': '407C', 'usbid': 'C33E'}
3: DEVICE NAME {0005} V0
Name: G915 WIRELESS RGB MECHANICAL GAMING KEYBOARD
Kind: keyboard
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: BATTERY VOLTAGE {1001} V3
Battery: 70% 3965mV , recharging.
7: CHANGE HOST {1814} V1
Changer d'hôte : 1:stagcrown
8: HOSTS INFO {1815} V1
Host 0 (paired): stagcrown
Host 1 (paired):
9: RGB EFFECTS {8071} V0
10: PER KEY LIGHTING V2 {8081} V2
11: REPROG CONTROLS V4 {1B04} V4
Interception des boutons/touches (saved): {Host Switch Channel 1:Interception, Host Switch Channel 2:Interception}
Interception des boutons/touches : {Host Switch Channel 1:Interception, Host Switch Channel 2:Interception}
12: REPORT HID USAGE {1BC0} V1
13: ENCRYPTION {4100} V0
14: KEYBOARD DISABLE BY USAGE {4522} V0
15: KEYBOARD LAYOUT 2 {4540} V0
16: GKEY {8010} V0
Définir les touches G (saved): True
Définir les touches G : False
17: MKEYS {8020} V0
LEDs de touche M (saved): {M1:False, M2:False, M3:False}
LEDs de touche M : {M1:False, M2:False, M3:False}
18: MR {8030} V0
LED de touche MR (saved): False
LED de touche MR : False
19: BRIGHTNESS CONTROL {8040} V0
20: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Profils embarqués (saved): Enable
Profils embarqués : Enable
21: REPORT RATE {8060} V0
Polling Rate (ms): 1
Taux de scrutation (ms) (saved): 1
Taux de scrutation (ms) : 1
22: DFUCONTROL SIGNED {00C2} V0
23: DFU {00D0} V3
24: DEVICE RESET {1802} V0 internal, hidden
25: unknown:1803 {1803} V0 internal, hidden
26: CONFIG DEVICE PROPS {1806} V8 internal, hidden
27: unknown:1813 {1813} V0 internal, hidden
28: OOBSTATE {1805} V0 internal, hidden
29: unknown:1830 {1830} V0 internal, hidden
30: unknown:1890 {1890} V9 internal, hidden
31: unknown:1891 {1891} V9 internal, hidden
32: unknown:18A1 {18A1} V0 internal, hidden
33: unknown:1E00 {1E00} V0 hidden
34: unknown:1EB0 {1EB0} V0 internal, hidden
35: unknown:1861 {1861} V0 internal, hidden
36: unknown:18B0 {18B0} V0 internal, hidden
Has 2 reprogrammable keys:
0: Host Switch Channel 1 , default: HostSwitch Channel 1 => HostSwitch Channel 1
divertable, persistently divertable, pos:1, group:0, group mask:empty
reporting: diverted
1: Host Switch Channel 2 , default: HostSwitch Channel 2 => HostSwitch Channel 2
divertable, persistently divertable, pos:2, group:0, group mask:empty
reporting: diverted
Battery: 70% 3965mV , recharging.

View File

@ -1,13 +0,0 @@
solaar version 1.1.8-29-g0ae14c7
1: Illuminated Keyboard
Device path : /dev/hidraw1
USB id : 046d:C318
Codename : Illuminated
Kind : keyboard
Protocol : HID++ 1.0
Serial number:
Firmware: 55.01.B0025
Notifications: (none).
Features: (none)
Battery status unavailable.

View File

@ -1,92 +0,0 @@
solaar version 1.1.8
2: LIFT For Business
Device path : None
WPID : B033
Codename : LIFT B
Kind : mouse
Protocol : HID++ 4.5
Serial number: A67F904D
Model ID: B03300000000
Unit ID: A67F904D
Bootloader: BL1 56.01.B0010
Firmware: RBM 21.01.B0010
Other:
The power switch is located on the (unknown).
Supports 32 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BL1 56.01.B0010 B033B0706FCD
Firmware: Firmware RBM 21.01.B0010 B033B0706FCD
Firmware: Other
Unit ID: A67F904D Model ID: B03300000000 Transport IDs: {'btleid': 'B033'}
3: DEVICE NAME {0005} V0
Name: LIFT For Business
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: CRYPTO ID {0021} V1
7: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: LIFT B
8: UNIFIED BATTERY {1004} V3
Battery: 100%, discharging.
9: REPROG CONTROLS V4 {1B04} V5
Key/Button Actions (saved): {Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, DPI Switch:DPI Switch}
Key/Button Actions : {Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, DPI Switch:DPI Switch}
Key/Button Diversion (saved): {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, DPI Switch:Regular}
Key/Button Diversion : {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, DPI Switch:Regular}
10: CHANGE HOST {1814} V1
Change Host : 1:feathora
11: HOSTS INFO {1815} V2
Host 0 (paired): feathora
Host 1 (unpaired):
Host 2 (unpaired):
12: XY STATS {2250} V1
13: LOWRES WHEEL {2130} V0
Wheel Reports: HID
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
14: ADJUSTABLE DPI {2201} V2
Sensitivity (DPI) (saved): 1600
Sensitivity (DPI) : 1600
15: DFUCONTROL {00C3} V0
16: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
17: unknown:1803 {1803} V0 internal, hidden, unknown:000010
18: CONFIG DEVICE PROPS {1806} V8 internal, hidden, unknown:000010
19: unknown:1816 {1816} V0 internal, hidden, unknown:000010
20: OOBSTATE {1805} V0 internal, hidden
21: unknown:1830 {1830} V0 internal, hidden, unknown:000010
22: unknown:1891 {1891} V7 internal, hidden, unknown:000008
23: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
24: unknown:1E00 {1E00} V0 hidden
25: unknown:1E02 {1E02} V0 internal, hidden
26: unknown:1E22 {1E22} V1 internal, hidden, unknown:000010
27: unknown:1602 {1602} V0
28: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
29: unknown:1861 {1861} V1 internal, hidden, unknown:000010
30: unknown:18B1 {18B1} V0 internal, hidden, unknown:000010
31: unknown:920A {920A} V0 internal, hidden, unknown:000010
Has 7 reprogrammable keys:
0: Left Button , default: Left Click => Left Click
mse, analytics key events, pos:0, group:1, group mask:empty
reporting: default
1: Right Button , default: Right Click => Right Click
mse, analytics key events, pos:0, group:1, group mask:empty
reporting: default
2: Middle Button , default: Mouse Middle Button => Mouse Middle Button
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:3, group mask:g1,g2,g3
reporting: default
3: Back Button , default: Mouse Back Button => Mouse Back Button
mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
reporting: default
4: Forward Button , default: Mouse Forward Button => Mouse Forward Button
mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
reporting: default
5: DPI Switch , default: DPI Switch => DPI Switch
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:3, group mask:g1,g2,g3
reporting: default
6: Virtual Gesture Button , default: Virtual Gesture Button => Virtual Gesture Button
divertable, virtual, raw XY, force raw XY, pos:0, group:4, group mask:empty
reporting: default
Battery: 100%, discharging.

View File

@ -9,5 +9,5 @@ Lightspeed Receiver
Notifications: wireless, software present (0x000900)
Device activity counters: (empty)
Seen as part of a G PowerPlay Wireless Mouse Pad with a Candy companion chip paired a number 7
Seen as part of a G PowerPlay Wireless Mouse Pad.
Seen paired with a G502 Gaming Mouse 407F.

View File

@ -1,10 +0,0 @@
Lightspeed Receiver
Device path : /dev/hidraw3
USB id : 046d:C53F
Serial :
Firmware : 44.01.B0005
Bootloader : 00.02
Other : AA.DE
Has 0 paired device(s) out of a maximum of 1.
Notifications: wireless, software present (0x000900)
Device activity counters: (empty)

View File

@ -1,137 +0,0 @@
solaar version 1.1.8
Bolt Receiver
Device path : /dev/hidraw2
USB id : 046d:C548
Serial : 31454343464242444143334635323035
Has 1 paired device(s) out of a maximum of 6.
Notifications: wireless, software present (0x000900)
Device activity counters: 1=28
1: Logi POP Keys
Device path : None
WPID : B365
Codename : Logi POP Keys
Kind : keyboard
Protocol : HID++ 4.5
Serial number: D1F99582
Model ID: B36500000000
Unit ID: D1F99582
Bootloader: BL1 44.01.B0008
Firmware: RBK 69.01.B0008
Other:
The power switch is located on the (unknown).
Supports 31 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BL1 44.01.B0008 B3652BE8BAF4
Firmware: Firmware RBK 69.01.B0008 B3652BE8BAF4
Firmware: Other
Unit ID: D1F99582 Model ID: B36500000000 Transport IDs: {'btleid': 'B365'}
3: DEVICE NAME {0005} V0
Name: Logi POP Keys
Kind: keyboard
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: Logi POP Keys
7: UNIFIED BATTERY {1004} V3
Battery: 100%, discharging.
8: REPROG CONTROLS V4 {1B04} V5
Key/Button Diversion (saved): {Show Desktop:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, Voice Dictation:Regular, Emoji Smiley Heart Eyes:Regular, Emoji Crying Face:Regular, Emoji Smiley:Regular, Emoji Smilie With Tears:Regular, Open Emoji Panel:Regular, Snipping Tool:Regular, Mute Microphone:Regular}
Key/Button Diversion : {Show Desktop:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, Voice Dictation:Regular, Emoji Smiley Heart Eyes:Regular, Emoji Crying Face:Regular, Emoji Smiley:Regular, Emoji Smilie With Tears:Regular, Open Emoji Panel:Regular, Snipping Tool:Regular, Mute Microphone:Regular}
9: CHANGE HOST {1814} V1
Change Host : 1:astra
10: HOSTS INFO {1815} V2
Host 0 (paired): astra
Host 1 (unpaired):
Host 2 (unpaired):
11: K375S FN INVERSION {40A3} V0
Swap Fx function (saved): False
Swap Fx function : False
12: LOCK KEY STATE {4220} V0
13: KEYBOARD DISABLE KEYS {4521} V0
Disable keys (saved): {Caps Lock:False, Insert:False, Win:False}
Disable keys : {Caps Lock:False, Insert:False, Win:False}
14: MULTIPLATFORM {4531} V1
Set OS (saved): Windows
Set OS : Windows
15: KEYBOARD LAYOUT 2 {4540} V0
16: DFUCONTROL {00C3} V0
17: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
18: unknown:1803 {1803} V0 internal, hidden, unknown:000010
19: CONFIG DEVICE PROPS {1806} V8 internal, hidden, unknown:000010
20: unknown:1816 {1816} V0 internal, hidden, unknown:000010
21: OOBSTATE {1805} V0 internal, hidden
22: unknown:1830 {1830} V0 internal, hidden, unknown:000010
23: unknown:1891 {1891} V7 internal, hidden, unknown:000008
24: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
25: unknown:1E00 {1E00} V0 hidden
26: unknown:1E02 {1E02} V0 internal, hidden
27: unknown:1602 {1602} V0
28: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
29: unknown:1861 {1861} V1 internal, hidden, unknown:000010
30: unknown:18B0 {18B0} V0 internal, hidden, unknown:000010
Has 20 reprogrammable keys:
0: Host Switch Channel 1 , default: HostSwitch Channel 1 => HostSwitch Channel 1
is FN, FN sensitive, analytics key events, pos:1, group:0, group mask:empty
reporting: default
1: Host Switch Channel 2 , default: HostSwitch Channel 2 => HostSwitch Channel 2
is FN, FN sensitive, analytics key events, pos:2, group:0, group mask:empty
reporting: default
2: Host Switch Channel 3 , default: HostSwitch Channel 3 => HostSwitch Channel 3
is FN, FN sensitive, analytics key events, pos:3, group:0, group mask:empty
reporting: default
3: Show Desktop , default: Show Desktop => Show Desktop
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:4, group:0, group mask:empty
reporting: default
4: Snipping Tool , default: Snipping Tool => Snipping Tool
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:5, group:0, group mask:empty
reporting: default
5: Mute Microphone , default: Mute Microphone => Mute Microphone
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:6, group:0, group mask:empty
reporting: default
6: Previous Fn , default: Previous => Previous
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:7, group:0, group mask:empty
reporting: default
7: Play/Pause Fn , default: Play/Pause => Play/Pause
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:8, group:0, group mask:empty
reporting: default
8: Next Fn , default: Next => Next
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:9, group:0, group mask:empty
reporting: default
9: Mute Fn , default: Mute => Mute
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:10, group:0, group mask:empty
reporting: default
10: Volume Down Fn , default: Volume Down => Volume Down
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:11, group:0, group mask:empty
reporting: default
11: Volume Up Fn , default: Volume Up => Volume Up
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:12, group:0, group mask:empty
reporting: default
12: Voice Dictation , default: Voice Dictation => Voice Dictation
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
13: Emoji Smiley Heart Eyes , default: Emoji Smiling Face With Heart Shaped Eyes => Emoji Smiling Face With Heart Shaped Eyes
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
14: Emoji Crying Face , default: Emoji Loudly Crying Face => Emoji Loudly Crying Face
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
15: Emoji Smiley , default: Emoji Smiley => Emoji Smiley
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
16: Emoji Smilie With Tears , default: Emoji Smiley With Tears => Emoji Smiley With Tears
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
17: Open Emoji Panel , default: Open Emoji Panel => Open Emoji Panel
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
18: F Lock , default: Do Nothing One => Do Nothing One
is FN, analytics key events, pos:0, group:0, group mask:empty
reporting: default
19: FN Key , default: Do Nothing One => Do Nothing One
nonstandard, analytics key events, pos:0, group:0, group mask:empty
reporting: default
Battery: 100%, discharging.

View File

@ -1,100 +0,0 @@
solaar version 1.1.14
1: MX Anywhere 3 for Business
Device path : None
WPID : B02D
Codename : MX Anywhere 3
Kind : mouse
Protocol : HID++ 4.5
Serial number: 00000000
Model ID: B02D00000000
Unit ID: 00000000
1: BL1 36.01.B0011
0: RBM 15.01.B0011
3:
The power switch is located on the (unknown).
Supports 35 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: 1 BL1 36.01.B0011 B02D1EEFD8F8
Firmware: 0 RBM 15.01.B0011 B02D1EEFD8F8
Firmware: 3
Unit ID: 00000000 Model ID: B02D00000000 Transport IDs: {'btleid': 'B02D'}
3: DEVICE NAME {0005} V0
Name: MX Anywhere 3 for Business
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
Configuration: 11000000000000000000000000000000
6: CRYPTO ID {0021} V1
7: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: MX Anywhere 3B
8: UNIFIED BATTERY {1004} V3
Battery: 75%, 0.
9: REPROG CONTROLS V4 {1B04} V5
Key/Button Actions : {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Smart Shift:Smart Shift}
Key/Button Diversion : {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, Smart Shift:Diverted}
10: CHANGE HOST {1814} V1
Change Host : 2:archlinux
11: HOSTS INFO {1815} V2
Host 0 (paired): archlinux
Host 1 (paired): archlinux
Host 2 (unpaired):
12: XY STATS {2250} V1
13: ADJUSTABLE DPI {2201} V2
Sensitivity (DPI) : 1000
14: SMART SHIFT ENHANCED {2111} V0
Scroll Wheel Ratcheted : Ratcheted
Scroll Wheel Ratchet Speed : 15
15: HIRES WHEEL {2121} V1
Multiplier: 15
Has invert: Normal wheel motion
Has ratchet switch: Normal wheel mode
Low resolution mode
HID notification
Scroll Wheel Direction : False
Scroll Wheel Resolution : False
Scroll Wheel Diversion : False
16: WHEEL STATS {2251} V0
17: DFUCONTROL {00C3} V0
18: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
19: unknown:1803 {1803} V0 internal, hidden, unknown:000010
20: CONFIG DEVICE PROPS {1806} V8 internal, hidden, unknown:000010
21: unknown:1816 {1816} V0 internal, hidden, unknown:000010
22: OOBSTATE {1805} V0 internal, hidden
23: unknown:1830 {1830} V0 internal, hidden, unknown:000010
24: unknown:1891 {1891} V7 internal, hidden, unknown:000008
25: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
26: unknown:1E00 {1E00} V0 hidden
27: unknown:1E02 {1E02} V0 internal, hidden
28: unknown:1602 {1602} V0
29: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
30: unknown:1861 {1861} V1 internal, hidden, unknown:000010
31: unknown:9300 {9300} V1 internal, hidden, unknown:000010
32: unknown:9001 {9001} V0 internal, hidden, unknown:000010
33: unknown:1E22 {1E22} V0 internal, hidden, unknown:000010
34: unknown:9205 {9205} V0 internal, hidden, unknown:000010
Has 7 reprogrammable keys:
0: Left Button , default: Left Click => Left Click
mse, analytics key events, pos:0, group:1, group mask:g1
reporting: default
1: Right Button , default: Right Click => Right Click
mse, analytics key events, pos:0, group:1, group mask:g1
reporting: default
2: Middle Button , default: Mouse Middle Button => Mouse Middle Button
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
3: Back Button , default: Mouse Back Button => Mouse Back Button
mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
reporting: default
4: Forward Button , default: Mouse Forward Button => Mouse Forward Button
mse, reprogrammable, divertable, raw XY, analytics key events, unknown:000800, pos:0, group:2, group mask:g1,g2
reporting: default
5: Smart Shift , default: Smart Shift => Smart Shift
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: diverted, raw XY diverted
6: Virtual Gesture Button , default: Virtual Gesture Button => Virtual Gesture Button
divertable, virtual, raw XY, force raw XY, pos:0, group:3, group mask:empty
reporting: default
Battery: 75%, 0.

View File

@ -1,137 +0,0 @@
solaar version 1.1.10
1: MX Keys S
Device path : None
WPID : B378
Codename : MX KEYS S
Kind : keyboard
Protocol : HID++ 4.5
Serial number: 48548420
Model ID: B37800000000
Unit ID: 48548420
Bootloader: BL1 88.00.B0013
Firmware: RBK 81.00.B0013
Other:
The power switch is located on the (unknown).
Supports 34 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BL1 88.00.B0013 B37851DB9520
Firmware: Firmware RBK 81.00.B0013 B37851DB9520
Firmware: Other
Unit ID: 48548420 Model ID: B37800000000 Transport IDs: {'btleid': 'B378'}
3: DEVICE NAME {0005} V0
Name: MX Keys S
Kind: keyboard
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: MX KEYS S
7: unknown:0011 {0011} V0
8: UNIFIED BATTERY {1004} V3
Battery: 75%, discharging.
9: REPROG CONTROLS V4 {1B04} V5
Key/Button Diversion (saved): {Calculator:Regular, Lock PC:Regular, Brightness Down:Regular, Brightness Up:Regular, Backlight Down:Regular, Backlight Up:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, App Contextual Menu/Right Click:Regular, Voice Dictation:Regular, Open Emoji Panel:Regular, Snipping Tool:Diverted, Mute Microphone:Regular}
Key/Button Diversion : {Calculator:Regular, Lock PC:Regular, Brightness Down:Regular, Brightness Up:Regular, Backlight Down:Regular, Backlight Up:Regular, Previous Fn:Regular, Play/Pause Fn:Regular, Next Fn:Regular, Mute Fn:Regular, Volume Down Fn:Regular, Volume Up Fn:Regular, App Contextual Menu/Right Click:Regular, Voice Dictation:Regular, Open Emoji Panel:Regular, Snipping Tool:Diverted, Mute Microphone:Regular}
10: CHANGE HOST {1814} V1
Change Host : 1:vs
11: HOSTS INFO {1815} V2
Host 0 (paired): vs
Host 1 (paired): DEV
Host 2 (unpaired):
12: BACKLIGHT2 {1982} V3
Backlight (saved): False
Backlight : True
13: K375S FN INVERSION {40A3} V0
Swap Fx function (saved): False
Swap Fx function : False
14: LOCK KEY STATE {4220} V0
15: KEYBOARD DISABLE KEYS {4521} V0
Disable keys (saved): {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
Disable keys : {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
16: MULTIPLATFORM {4531} V1
Set OS (saved): Linux
Set OS : Linux
17: KEYBOARD LAYOUT 2 {4540} V0
18: DFUCONTROL {00C3} V0
19: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
20: unknown:1803 {1803} V0 internal, hidden, unknown:000010
21: unknown:1807 {1807} V0 internal, hidden, unknown:000010
22: unknown:1816 {1816} V0 internal, hidden, unknown:000010
23: OOBSTATE {1805} V0 internal, hidden
24: unknown:1830 {1830} V0 internal, hidden, unknown:000010
25: unknown:1891 {1891} V7 internal, hidden, unknown:000008
26: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
27: unknown:1E00 {1E00} V0 hidden
28: unknown:1E02 {1E02} V0 internal, hidden
29: unknown:1602 {1602} V0
30: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
31: unknown:1861 {1861} V1 internal, hidden, unknown:000010
32: unknown:1A20 {1A20} V1 internal, hidden, unknown:000010
33: unknown:18B0 {18B0} V1 internal, hidden, unknown:000010
Has 21 reprogrammable keys:
0: Brightness Down , default: Brightness Down => Brightness Down
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:1, group:0, group mask:empty
reporting: default
1: Brightness Up , default: Brightness Up => Brightness Up
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:2, group:0, group mask:empty
reporting: default
2: Backlight Down , default: Backlight Down => Backlight Down
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:3, group:0, group mask:empty
reporting: default
3: Backlight Up , default: Backlight Up => Backlight Up
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:4, group:0, group mask:empty
reporting: default
4: Voice Dictation , default: Voice Dictation => Voice Dictation
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:5, group:0, group mask:empty
reporting: default
5: Open Emoji Panel , default: Open Emoji Panel => Open Emoji Panel
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:6, group:0, group mask:empty
reporting: default
6: Mute Microphone , default: Mute Microphone => Mute Microphone
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:7, group:0, group mask:empty
reporting: default
7: Previous Fn , default: Previous => Previous
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:8, group:0, group mask:empty
reporting: default
8: Play/Pause Fn , default: Play/Pause => Play/Pause
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:9, group:0, group mask:empty
reporting: default
9: Next Fn , default: Next => Next
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:10, group:0, group mask:empty
reporting: default
10: Mute Fn , default: Mute => Mute
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:11, group:0, group mask:empty
reporting: default
11: Volume Down Fn , default: Volume Down => Volume Down
is FN, FN sensitive, reprogrammable, divertable, analytics key events, pos:12, group:0, group mask:empty
reporting: default
12: Volume Up Fn , default: Volume Up => Volume Up
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
13: Calculator , default: Calculator => Calculator
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
14: Snipping Tool , default: Snipping Tool => Snipping Tool
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: diverted
15: App Contextual Menu/Right Click, default: Right Click/App Contextual Menu => Right Click/App Contextual Menu
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
16: Lock PC , default: WindowsLock => WindowsLock
nonstandard, reprogrammable, divertable, analytics key events, pos:0, group:0, group mask:empty
reporting: default
17: Host Switch Channel 1 , default: HostSwitch Channel 1 => HostSwitch Channel 1
nonstandard, analytics key events, pos:0, group:0, group mask:empty
reporting: default
18: Host Switch Channel 2 , default: HostSwitch Channel 2 => HostSwitch Channel 2
nonstandard, analytics key events, pos:0, group:0, group mask:empty
reporting: default
19: Host Switch Channel 3 , default: HostSwitch Channel 3 => HostSwitch Channel 3
nonstandard, analytics key events, pos:0, group:0, group mask:empty
reporting: default
20: F Lock , default: Do Nothing One => Do Nothing One
analytics key events, pos:0, group:0, group mask:empty
reporting: default
Battery: 75%, discharging.

View File

@ -1,14 +1,14 @@
solaar version 1.1.8
Solaar version 1.1.5
1: MX Master 3 for Business
2: MX Master 3 for Business
Device path : None
WPID : B028
Codename : MX Master 3 B
Kind : mouse
Protocol : HID++ 4.5
Serial number: 18F3413B
Serial number: 12617690
Model ID: B02800000000
Unit ID: 18F3413B
Unit ID: 12617690
Bootloader: BL1 41.00.B0009
Firmware: RBM 14.00.B0009
Other:
@ -20,92 +20,29 @@ solaar version 1.1.8
Firmware: Bootloader BL1 41.00.B0009 B0281D13EFC0
Firmware: Firmware RBM 14.00.B0009 B0281D13EFC0
Firmware: Other
Unit ID: 18F3413B Model ID: B02800000000 Transport IDs: {'btleid': 'B028'}
Unit ID: 12617690 Model ID: B02800000000 Transport IDs: {'btleid': 'B028'}
3: DEVICE NAME {0005} V0
Name: MX Master 3 for Business
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
5: RESET {0020} V0
6: CRYPTO ID {0021} V1
7: DEVICE FRIENDLY NAME {0007} V0
Friendly Name: MX Master 3 B
8: UNIFIED BATTERY {1004} V2
Battery: 95%, discharging.
Battery: 80%, recharging.
9: REPROG CONTROLS V4 {1B04} V5
Key/Button Actions (saved): {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Mouse Gesture Button:Gesture Button Navigation, Smart Shift:Smart Shift}
Key/Button Actions : {Left Button:Left Click, Right Button:Right Click, Middle Button:Mouse Middle Button, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Mouse Gesture Button:Gesture Button Navigation, Smart Shift:Smart Shift}
Key/Button Diversion (saved): {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, Mouse Gesture Button:Regular, Smart Shift:Regular}
Key/Button Diversion : {Middle Button:Regular, Back Button:Regular, Forward Button:Regular, Mouse Gesture Button:Regular, Smart Shift:Regular}
10: CHANGE HOST {1814} V1
Change Host : 1:bork
11: XY STATS {2250} V1
12: ADJUSTABLE DPI {2201} V2
Sensitivity (DPI) (saved): 1000
Sensitivity (DPI) : 1000
13: SMART SHIFT {2110} V0
Scroll Wheel Ratcheted (saved): Freespinning
Scroll Wheel Ratcheted : Freespinning
Scroll Wheel Ratchet Speed (saved): 1
Scroll Wheel Ratchet Speed : 1
14: HIRES WHEEL {2121} V1
Multiplier: 15
Has invert: Normal wheel motion
Has ratchet switch: Free wheel mode
Low resolution mode
HID notification
Scroll Wheel Direction (saved): False
Scroll Wheel Direction : False
Scroll Wheel Resolution (saved): False
Scroll Wheel Resolution : False
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
15: THUMB WHEEL {2150} V0
Thumb Wheel Direction (saved): False
Thumb Wheel Direction : False
Thumb Wheel Diversion (saved): False
Thumb Wheel Diversion : False
16: WHEEL STATS {2251} V0
17: DFUCONTROL {00C3} V0
18: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
19: unknown:1803 {1803} V0 internal, hidden, unknown:000010
20: CONFIG DEVICE PROPS {1806} V8 internal, hidden, unknown:000010
21: unknown:1816 {1816} V0 internal, hidden, unknown:000010
22: OOBSTATE {1805} V0 internal, hidden
23: unknown:1830 {1830} V0 internal, hidden, unknown:000010
24: unknown:1891 {1891} V6 internal, hidden, unknown:000008
25: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
26: unknown:1E00 {1E00} V0 hidden
27: unknown:1E02 {1E02} V0 internal, hidden
28: unknown:1602 {1602} V0
29: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
30: unknown:1861 {1861} V0 internal, hidden, unknown:000010
31: unknown:9300 {9300} V0 internal, hidden, unknown:000010
32: unknown:9001 {9001} V0 internal, hidden, unknown:000010
33: unknown:1E22 {1E22} V0 internal, hidden, unknown:000010
34: unknown:9205 {9205} V0 internal, hidden, unknown:000010
Has 8 reprogrammable keys:
0: Left Button , default: Left Click => Left Click
mse, analytics key events, pos:0, group:1, group mask:g1
reporting: default
1: Right Button , default: Right Click => Right Click
mse, analytics key events, pos:0, group:1, group mask:g1
reporting: default
2: Middle Button , default: Mouse Middle Button => Mouse Middle Button
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
3: Back Button , default: Mouse Back Button => Mouse Back Button
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
4: Forward Button , default: Mouse Forward Button => Mouse Forward Button
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
5: Mouse Gesture Button , default: Gesture Button Navigation => Gesture Button Navigation
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
6: Smart Shift , default: Smart Shift => Smart Shift
mse, reprogrammable, divertable, raw XY, analytics key events, pos:0, group:2, group mask:g1,g2
reporting: default
7: Virtual Gesture Button , default: Virtual Gesture Button => Virtual Gesture Button
divertable, virtual, raw XY, force raw XY, pos:0, group:3, group mask:empty
reporting: default
Battery: 95%, discharging.
solaar: error: Traceback (most recent call last):
File "/usr/lib/python3.10/site-packages/solaar/cli/__init__.py", line 210, in run
m.run(c, args, _find_receiver, _find_device)
File "/usr/lib/python3.10/site-packages/solaar/cli/show.py", line 296, in run
_print_device(dev)
File "/usr/lib/python3.10/site-packages/solaar/cli/show.py", line 232, in _print_device
v = setting.val_to_string(setting._device.persister.get(setting.name))
File "/usr/lib/python3.10/site-packages/logitech_receiver/settings.py", line 238, in val_to_string
return self._validator.to_string(value)
File "/usr/lib/python3.10/site-packages/logitech_receiver/settings.py", line 1086, in to_string
return '{' + ', '.join([element_to_string(k, value[k]) for k in sorted(value)]) + '}'
TypeError: '<' not supported between instances of 'str' and 'int'

View File

@ -1,17 +0,0 @@
solaar version 1.1.8
3: Number Pad N545
Device path : /dev/hidraw3
WPID : 2006
Codename : N545
Kind : numpad
Protocol : HID++ 1.0
Polling rate : 20 ms (50Hz)
Serial number: 900A4D98
Firmware: 13.00.B0037
Bootloader: 02.03
Other: 00.01
The power switch is located on the base.
Notifications: battery status (0x100000).
Features: (none)
Battery: full, discharging.

View File

@ -1,68 +0,0 @@
solaar version 1.1.10
Receiver
Device path : /dev/hidraw3
USB id : 046d:C54D
Serial : 8FF3BF7B
Firmware : 07.00.B0008
Bootloader : 00.08
Other : C1.53
Has 1 paired device(s) out of a maximum of 2.
Notifications: (none)
Device activity counters: 1=51
1: PRO X 2
Device path : None
WPID : 40A9
Codename : PRO X 2
Kind : mouse
Protocol : HID++ 4.2
Polling rate : 8 ms (125Hz)
Serial number: <nope>
Model ID: 40A9C09B0000
Unit ID: <nope>
Bootloader: BL1 71.00.B0012
Firmware: MPM 32.00.B0012
Supports 32 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V4
Firmware: Bootloader BL1 71.00.B0012 AB1CDBC0A7D9
Firmware: Firmware MPM 32.00.B0012 40A9DBC0A7D9
Unit ID: <nope> Model ID: 40A9C09B0000 Transport IDs: {'wpid': '40A9', 'usbid': 'C09B'}
3: DEVICE NAME {0005} V2
Name: PRO X 2
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: UNIFIED BATTERY {1004} V3
Battery: 96%, discharging.
7: XY STATS {2250} V1
8: WHEEL STATS {2251} V0
9: unknown:2202 {2202} V0 EXTENDED_ADJUSTABLE_DPI
10: MODE STATUS {8090} V2
11: unknown:8061 {8061} V0 EXTENDED_ADJUSTABLE_REPORT_RATE
12: ONBOARD PROFILES {8100} V0
Device Mode: On-Board
Onboard Profiles (saved): Enable
Onboard Profiles : Enable
13: MOUSE BUTTON SPY {8110} V0
14: unknown:1500 {1500} V0 FORCE_PAIRING
15: unknown:1801 {1801} V0 internal, hidden, unknown:000010
16: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
17: unknown:1803 {1803} V0 internal, hidden, unknown:000010
18: CONFIG DEVICE PROPS {1806} V8 internal, hidden, unknown:000010
19: unknown:1817 {1817} V0 internal, hidden, unknown:000010
20: OOBSTATE {1805} V0 internal, hidden
21: unknown:1830 {1830} V0 internal, hidden, unknown:000010
22: unknown:1875 {1875} V0 internal, hidden, unknown:000010
23: unknown:1861 {1861} V1 internal, hidden, unknown:000010
24: unknown:1890 {1890} V9 internal, hidden, unknown:000008
25: unknown:18A1 {18A1} V0 internal, hidden, unknown:000010
26: unknown:1E00 {1E00} V0 hidden
27: unknown:1E02 {1E02} V0 internal, hidden
28: unknown:1E22 {1E22} V1 internal, hidden, unknown:000010
29: unknown:1602 {1602} V0
30: unknown:1EB0 {1EB0} V0 internal, hidden, unknown:000010
31: unknown:18B1 {18B1} V0 internal, hidden, unknown:000010
Battery: 96%, discharging.

View File

@ -40,7 +40,8 @@ Solaar version 1.1.3
Key/Button Diversion : {Middle Button:Regular, Back Button:Regular, Forward Button:Regular}
9: HOSTS INFO {1815}
Host 0 (paired): legion15
10: XY STATS 11: LOWRES WHEEL {2130}
10: XY STATS {2250}
11: LOWRES WHEEL {2130}
Wheel Reports: HID
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False

View File

@ -1,61 +0,0 @@
Solaar version 1.1.7
1: Wireless Keyboard
Device path : /dev/hidraw6
WPID : 4075
Codename :
Kind : keyboard
Protocol : HID++ 4.5
Polling rate : 20 ms (50Hz)
Serial number: 00000000
Model ID: 000000000000
Unit ID: 00000000
Firmware: RQK 71.00.B0002
Supports 20 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V0
Firmware: Firmware RQK 71.00.B0002 4075
Unit ID: 00000000 Model ID: 000000000000 Transport IDs: {}
3: DEVICE NAME {0005} V0
Name: Wireless Keyboard
Kind: keyboard
4: RESET {0020} V0
5: BATTERY STATUS {1000} V0
Battery: 30%, discharging, next level 5%.
6: REPROG CONTROLS V4 {1B04} V2
Key/Button Diversion (saved): {Calculator:Regular, Mail:Regular, My Home:Regular, Search:Regular}
Key/Button Diversion : {Calculator:Regular, Mail:Regular, My Home:Regular, Search:Regular}
7: WIRELESS DEVICE STATUS {1D4B} V0
8: NEW FN INVERSION {40A2} V0
Fn-swap: disabled
Fn-swap default: disabled
Swap Fx function (saved): False
Swap Fx function : False
9: ENCRYPTION {4100} V0
10: LOCK KEY STATE {4220} V0
11: KEYBOARD DISABLE KEYS {4521} V0
Disable keys (saved): {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
Disable keys : {Caps Lock:False, Num Lock:False, Scroll Lock:False, Insert:False, Win:False}
12: unknown:1810 {1810} V0 internal, hidden
13: unknown:1830 {1830} V0 internal, hidden
14: unknown:1890 {1890} V0 internal, hidden
15: unknown:18A0 {18A0} V0 internal, hidden
16: unknown:18B0 {18B0} V0 internal, hidden
17: unknown:1DF3 {1DF3} V0 internal, hidden
18: unknown:1E00 {1E00} V0 hidden
19: unknown:1868 {1868} V0 internal, hidden
Has 4 reprogrammable keys:
0: My Home , default: HomePage => HomePage
is FN, FN sensitive, reprogrammable, divertable, pos:1, group:0, group mask:empty
reporting: default
1: Mail , default: Email => Email
is FN, FN sensitive, reprogrammable, divertable, pos:2, group:0, group mask:empty
reporting: default
2: Search , default: Search Files => Search Files
is FN, FN sensitive, reprogrammable, divertable, pos:3, group:0, group mask:empty
reporting: default
3: Calculator , default: Calculator => Calculator
is FN, FN sensitive, reprogrammable, divertable, pos:4, group:0, group mask:empty
reporting: default
Battery: 30%, discharging, next level 5%.

View File

@ -1,95 +0,0 @@
solaar version 1.1.9
1: Wireless Mobile Mouse MX Anywhere 2S
Device path : /dev/hidraw1
USB id : 046d:B01A
Codename : Wireless
Kind : mouse
Protocol : HID++ 4.5
Serial number:
Model ID: B01A406A0000
Unit ID: 3F714CA3
Bootloader: BOT 57.00.B0003
Firmware: MPM 13.00.B0003
Firmware: MPM 13.00.B0003
Other:
Supports 24 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V2
Firmware: Bootloader BOT 57.00.B0003 406AD22DCF4D01
Firmware: Firmware MPM 13.00.B0003 B01AD22DCF4D01
Firmware: Firmware MPM 13.00.B0003 406AD22DCF4D01
Firmware: Other
Unit ID: 3F714CA3 Model ID: B01A406A0000 Transport IDs: {'btleid': 'B01A', 'wpid': '406A'}
3: DEVICE NAME {0005} V0
Name: Wireless Mobile Mouse MX Anywhere 2S
Kind: mouse
4: WIRELESS DEVICE STATUS {1D4B} V0
5: CONFIG CHANGE {0020} V0
6: BATTERY STATUS {1000} V0
Battery: 90%, discharging, next level 50%.
7: CONFIG DEVICE PROPS {1806} V0 internal, hidden
8: CHANGE HOST {1814} V1
Change Host : 2:mburcheri2
9: REPROG CONTROLS V4 {1B04} V3
Key/Button Actions (saved): {Left Button:Left Click, Right Button:Right Click, Middle Button:Gesture Button Navigation, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Left Tilt:Mouse Scroll Left Button , Right Tilt:Mouse Scroll Right Button}
Key/Button Actions : {Left Button:Left Click, Right Button:Right Click, Middle Button:Gesture Button Navigation, Back Button:Mouse Back Button, Forward Button:Mouse Forward Button, Left Tilt:Mouse Scroll Left Button , Right Tilt:Mouse Scroll Right Button}
Key/Button Diversion (saved): {Middle Button:Regular, Back Button:Sliding DPI, Forward Button:Regular, Left Tilt:Regular, Right Tilt:Regular}
Key/Button Diversion : {Middle Button:Regular, Back Button:Diverted, Forward Button:Regular, Left Tilt:Regular, Right Tilt:Regular}
10: ADJUSTABLE DPI {2201} V1
Sensitivity (DPI) (saved): 3400
Sensitivity (DPI) : 3400
11: VERTICAL SCROLLING {2100} V0
Roller type: 3G
Ratchet per turn: 24
Scroll lines: 0
12: HIRES WHEEL {2121} V0
Multiplier: 8
Has invert: Normal wheel motion
Has ratchet switch: Free wheel mode
Low resolution mode
HID notification
Scroll Wheel Direction (saved): False
Scroll Wheel Direction : False
Scroll Wheel Resolution (saved): False
Scroll Wheel Resolution : False
Scroll Wheel Diversion (saved): False
Scroll Wheel Diversion : False
13: unknown:1813 {1813} V0 internal, hidden
14: unknown:1830 {1830} V0 internal, hidden
15: unknown:18A1 {18A1} V0 internal, hidden
16: unknown:18C0 {18C0} V0 internal, hidden
17: unknown:1DF3 {1DF3} V0 internal, hidden
18: unknown:1E00 {1E00} V0 hidden
19: unknown:1EB0 {1EB0} V0 internal, hidden
20: unknown:1803 {1803} V0 internal, hidden
21: unknown:1861 {1861} V0 internal, hidden
22: unknown:9001 {9001} V0 internal, hidden
23: OOBSTATE {1805} V0 internal, hidden
Has 8 reprogrammable keys:
0: Left Button , default: Left Click => Left Click
mse, pos:0, group:1, group mask:g1
reporting: default
1: Right Button , default: Right Click => Right Click
mse, pos:0, group:1, group mask:g1
reporting: default
2: Middle Button , default: Gesture Button Navigation => Gesture Button Navigation
mse, reprogrammable, divertable, raw XY, pos:0, group:2, group mask:g1,g2,g4
reporting: default
3: Back Button , default: Mouse Back Button => Mouse Back Button
mse, reprogrammable, divertable, raw XY, pos:0, group:3, group mask:g1,g2,g3,g4
reporting: diverted, raw XY diverted
4: Forward Button , default: Mouse Forward Button => Mouse Forward Button
mse, reprogrammable, divertable, raw XY, pos:0, group:3, group mask:g1,g2,g3,g4
reporting: default
5: Left Tilt , default: Mouse Scroll Left Button => Mouse Scroll Left Button
mse, reprogrammable, divertable, raw XY, pos:0, group:3, group mask:g1,g2,g3,g4
reporting: default
6: Right Tilt , default: Mouse Scroll Right Button => Mouse Scroll Right Button
mse, reprogrammable, divertable, raw XY, pos:0, group:3, group mask:g1,g2,g3,g4
reporting: default
7: Virtual Gesture Button , default: Virtual Gesture Button => Virtual Gesture Button
divertable, virtual, raw XY, force raw XY, pos:0, group:4, group mask:empty
reporting: default
Battery: 90%, discharging, next level 50%.

View File

@ -1,64 +0,0 @@
solaar show
rules cannot access modifier keys in Wayland, accessing process only works on GNOME with Solaar Gnome extension installed
solaar version 1.1.14-2
Unifying Receiver
Device path : /dev/hidraw1
USB id : 046d:C52B
Serial : EC219AC2
C Pending : ff
0 : 12.11.B0032
1 : 04.16
3 : AA.AA
Has 2 paired device(s) out of a maximum of 6.
Notifications: wireless (0x000100)
Device activity counters: 1=195, 2=74
1: Wireless Mouse M175
Device path : /dev/hidraw2
WPID : 4008
Codename : M175
Kind : mouse
Protocol : HID++ 2.0
Report Rate : 8ms
Serial number: 16E46E8C
Model ID: 000000000000
Unit ID: 00000000
0: RQM 40.00.B0016
The power switch is located on the base.
Supports 21 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V0
Firmware: 0 RQM 40.00.B0016 4008
Unit ID: 00000000 Model ID: 000000000000 Transport IDs: {}
3: DEVICE NAME {0005} V0
Name: Wireless Mouse M185
Kind: mouse
4: BATTERY STATUS {1000} V0
Battery: 70%, 0, next level 5%.
5: unknown:1830 {1830} V0 internal, hidden
6: unknown:1850 {1850} V0 internal, hidden
7: unknown:1860 {1860} V0 internal, hidden
8: unknown:1890 {1890} V0 internal, hidden
9: unknown:18A0 {18A0} V0 internal, hidden
10: unknown:18C0 {18C0} V0 internal, hidden
11: WIRELESS DEVICE STATUS {1D4B} V0
12: unknown:1DF3 {1DF3} V0 internal, hidden
13: REPROG CONTROLS {1B00} V0
14: REMAINING PAIRING {1DF0} V0 hidden
Remaining Pairings: 117
15: unknown:1E00 {1E00} V0 hidden
16: unknown:1E80 {1E80} V0 internal, hidden
17: unknown:1E90 {1E90} V0 internal, hidden
18: unknown:1F03 {1F03} V0 internal, hidden
19: VERTICAL SCROLLING {2100} V0
Roller type: standard
Ratchet per turn: 24
Scroll lines: 0
20: MOUSE POINTER {2200} V0
DPI: 1000
Acceleration: low
Override OS ballistics
No vertical tuning, standard mice
Battery: 70%, 0, next level 5%.

View File

@ -1,55 +1,3 @@
1: Wireless Mouse M325
Device path : /dev/hidraw4
WPID : 400A
Codename : M325
Kind : mouse
Protocol : HID++ 2.0
Polling rate : 8 ms (125Hz)
Serial number: D72D97E9
Model ID: 000000000000
Unit ID: 00000000
Firmware: RQM 40.01.B0018
The power switch is located on the base.
Supports 22 HID++ 2.0 features:
0: ROOT {0000} V0
1: FEATURE SET {0001} V0
2: DEVICE FW VERSION {0003} V0
Firmware: Firmware RQM 40.01.B0018 400A
Unit ID: 00000000 Model ID: 000000000000 Transport IDs: {}
3: DEVICE NAME {0005} V0
Name: Wireless Mouse M325
Kind: mouse
4: BATTERY STATUS {1000} V0
Battery: 70%, discharging, next level 5%.
5: unknown:1830 {1830} V0 internal, hidden
6: unknown:1850 {1850} V0 internal, hidden
7: unknown:1860 {1860} V0 internal, hidden
8: unknown:1890 {1890} V0 internal, hidden
9: unknown:18A0 {18A0} V0 internal, hidden
10: unknown:18C0 {18C0} V0 internal, hidden
11: WIRELESS DEVICE STATUS {1D4B} V0
12: unknown:1DF3 {1DF3} V0 internal, hidden
13: REPROG CONTROLS {1B00} V0
14: REMAINING PAIRING {1DF0} V0 hidden
Remaining Pairings: 117
15: unknown:1E00 {1E00} V0 hidden
16: unknown:1E80 {1E80} V0 internal, hidden
17: unknown:1E90 {1E90} V0 internal, hidden
18: unknown:1F03 {1F03} V0 internal, hidden
19: VERTICAL SCROLLING {2100} V0
Roller type: micro
Ratchet per turn: 36
Scroll lines: 0
20: MOUSE POINTER {2200} V0
DPI: 800
Acceleration: low
Override OS ballistics
No vertical tuning, standard mice
21: unknown:18B0 {18B0} V0 internal, hidden
Battery: 70%, discharging, next level 5%.
Wireless Mouse M325
Codename : M325
Kind : mouse

View File

@ -1,128 +1,3 @@
solaar version 1.1.11-80-gdea496f
EX100 Receiver 27 Mhz
Device path : /dev/hidraw2
USB id : 046d:C517
Serial : None
Has 2 paired device(s) out of a maximum of 4.
Notifications: wireless (0x000100)
1: Wireless Mouse EX100
Device path : /dev/hidraw3
WPID : 003F
Codename : EX100m
Kind : mouse
Protocol : HID++ 1.0
Serial number:
The power switch is located on the (unknown).
Notifications: roller V, mouse extra buttons, battery status, roller H (0x3C0000).
Battery: good, discharging.
3: Wireless Keyboard EX100
Device path : /dev/hidraw6
WPID : 0065
Codename : EX100
Kind : keyboard
Protocol : HID++ 1.0
Serial number:
The power switch is located on the (unknown).
Notifications: keyboard multimedia raw, battery status (0x110000).
Battery: good, discharging.
Register Dump
Notifications 0x00: 0x000100
Connection State 0x02: 0x000100
Device Activity 0xb3: None
Pairing Register 0xb5 0x00: None
Pairing Register 0xb5 0x01: None
Pairing Register 0xb5 0x02: None
Pairing Register 0xb5 0x03: None
Pairing Register 0xb5 0x04: None
Pairing Register 0xb5 0x05: None
Pairing Register 0xb5 0x06: None
Pairing Register 0xb5 0x07: None
Pairing Register 0xb5 0x08: None
Pairing Register 0xb5 0x09: None
Pairing Register 0xb5 0x0a: None
Pairing Register 0xb5 0x0b: None
Pairing Register 0xb5 0x0c: None
Pairing Register 0xb5 0x0d: None
Pairing Register 0xb5 0x0e: None
Pairing Register 0xb5 0x0f: None
Pairing Register 0xb5 0x10: None
Pairing Register 0xb5 0x20: None
Pairing Register 0xb5 0x30: None
Pairing Register 0xb5 0x50: None
Pairing Name 0xb5 0x40: None
Pairing Name 0xb5 0x60 0x1: 0 None
Pairing Name 0xb5 0x60 0x2: 0 None
Pairing Name 0xb5 0x60 0x3: 0 None
Pairing Register 0xb5 0x11: None
Pairing Register 0xb5 0x21: None
Pairing Register 0xb5 0x31: None
Pairing Register 0xb5 0x51: None
Pairing Name 0xb5 0x41: None
Pairing Name 0xb5 0x61 0x1: 0 None
Pairing Name 0xb5 0x61 0x2: 0 None
Pairing Name 0xb5 0x61 0x3: 0 None
Pairing Register 0xb5 0x12: None
Pairing Register 0xb5 0x22: None
Pairing Register 0xb5 0x32: None
Pairing Register 0xb5 0x52: None
Pairing Name 0xb5 0x42: None
Pairing Name 0xb5 0x62 0x1: 0 None
Pairing Name 0xb5 0x62 0x2: 0 None
Pairing Name 0xb5 0x62 0x3: 0 None
Pairing Register 0xb5 0x13: None
Pairing Register 0xb5 0x23: None
Pairing Register 0xb5 0x33: None
Pairing Register 0xb5 0x53: None
Pairing Name 0xb5 0x43: None
Pairing Name 0xb5 0x63 0x1: 0 None
Pairing Name 0xb5 0x63 0x2: 0 None
Pairing Name 0xb5 0x63 0x3: 0 None
Pairing Register 0xb5 0x14: None
Pairing Register 0xb5 0x24: None
Pairing Register 0xb5 0x34: None
Pairing Register 0xb5 0x54: None
Pairing Name 0xb5 0x44: None
Pairing Name 0xb5 0x64 0x1: 0 None
Pairing Name 0xb5 0x64 0x2: 0 None
Pairing Name 0xb5 0x64 0x3: 0 None
Pairing Register 0xb5 0x15: None
Pairing Register 0xb5 0x25: None
Pairing Register 0xb5 0x35: None
Pairing Register 0xb5 0x55: None
Pairing Name 0xb5 0x45: None
Pairing Name 0xb5 0x65 0x1: 0 None
Pairing Name 0xb5 0x65 0x2: 0 None
Pairing Name 0xb5 0x65 0x3: 0 None
Pairing Register 0xb5 0x16: None
Pairing Register 0xb5 0x26: None
Pairing Register 0xb5 0x36: None
Pairing Register 0xb5 0x56: None
Pairing Name 0xb5 0x46: None
Pairing Name 0xb5 0x66 0x1: 0 None
Pairing Name 0xb5 0x66 0x2: 0 None
Pairing Name 0xb5 0x66 0x3: 0 None
Firmware 0xf1 0x00: None
Firmware 0xf1 0x01: None
Firmware 0xf1 0x02: None
Firmware 0xf1 0x03: None
Firmware 0xf1 0x04: None
Register Short 0x00 0x00: 0x000100
Register Long 0x00 0x00: invalid SubID/command
...
Register Long 0x00 0xfe: invalid SubID/command
Register Short 0x01 0x00: 0x000200
Register Long 0x01 0x00: invalid SubID/command
Register Long 0x01 0x01: invalid SubID/command
Register Long 0x01 0x02: invalid SubID/command
...
./scan-registers.sh ff /dev/hidraw4
# Old notification flags: 000100
>> ( 0.035) [10 FF 8100 000100] '\x10\xff\x81\x00\x00\x01\x00'
@ -192,7 +67,52 @@ Fn pressed
>> ( 1652.170) [10 03 0300 000000] '\x10\x03\x03\x00\x00\x00\x00'
$ bin/solaar probe
Nano Receiver
Device path : /dev/hidraw3
USB id : 046d:c517
Serial : None
Has 2 paired device(s) out of a maximum of 6.
Notifications: wireless (0x000100)
Register Dump
Notification Register 0x00: 0x000100
Connection State 0x02: 0x000200
Device Activity 0xb3: None
Pairing Register 0xb5 0x00: None
Pairing Register 0xb5 0x10: None
Pairing Register 0xb5 0x20: None
Pairing Register 0xb5 0x30: None
Pairing Name 0xb5 0x40: None
Pairing Register 0xb5 0x01: None
Pairing Register 0xb5 0x11: None
Pairing Register 0xb5 0x21: None
Pairing Register 0xb5 0x31: None
Pairing Name 0xb5 0x41: None
Pairing Register 0xb5 0x02: None
Pairing Register 0xb5 0x12: None
Pairing Register 0xb5 0x22: None
Pairing Register 0xb5 0x32: None
Pairing Name 0xb5 0x42: None
Pairing Register 0xb5 0x03: None
Pairing Register 0xb5 0x13: None
Pairing Register 0xb5 0x23: None
Pairing Register 0xb5 0x33: None
Pairing Name 0xb5 0x43: None
Pairing Register 0xb5 0x04: None
Pairing Register 0xb5 0x14: None
Pairing Register 0xb5 0x24: None
Pairing Register 0xb5 0x34: None
Pairing Name 0xb5 0x44: None
Pairing Register 0xb5 0x05: None
Pairing Register 0xb5 0x15: None
Pairing Register 0xb5 0x25: None
Pairing Register 0xb5 0x35: None
Pairing Name 0xb5 0x45: None
Firmware 0xf1 0x00: None
Firmware 0xf1 0x01: None
Firmware 0xf1 0x02: None
Firmware 0xf1 0x03: None
Firmware 0xf1 0x04: None
Battery status:
1.9V critical

View File

@ -39,8 +39,8 @@ Feature | ID | Status | Notes
`CONFIG_DEVICE_PROPS` | `0x1806` | Unsupported |
`CHANGE_HOST` | `0x1814` | Supported | `ChangeHost`
`HOSTS_INFO` | `0x1815` | Partial Support | `get_host_names`, partial listing only
`BACKLIGHT` | `0x1981` | Supported | `Backlight`
`BACKLIGHT2` | `0x1982` | Supported | `Backlight2`, ...
`BACKLIGHT` | `0x1981` | Unsupported |
`BACKLIGHT2` | `0x1982` | Supported | `Backlight2`
`BACKLIGHT3` | `0x1983` | Unsupported |
`PRESENTER_CONTROL` | `0x1A00` | Unsupported |
`SENSOR_3D` | `0x1A01` | Unsupported |
@ -54,7 +54,7 @@ Feature | ID | Status | Notes
`WIRELESS_DEVICE_STATUS` | `0x1D4B` | Read only | status reporting from device
`REMAINING_PAIRING` | `0x1DF0` | Unsupported |
`FIRMWARE_PROPERTIES` | `0x1F1F` | Unsupported |
`ADC_MEASUREMENT` | `0x1F20` | Supported | `ADCPower`
`ADC_MEASUREMENT` | `0x1F20` | Unsupported |
`LEFT_RIGHT_SWAP` | `0x2001` | Unsupported |
`SWAP_BUTTON_CANCEL` | `0x2005` | Unsupported |
`POINTER_AXIS_ORIENTATION` | `0x2006` | Unsupported |
@ -97,22 +97,22 @@ Feature | ID | Status | Notes
`GESTURE` | `0x6500` | Unsupported |
`GESTURE_2` | `0x6501` | Partial Support | `Gesture2Gestures`, `Gesture2Params`
`GKEY` | `0x8010` | Partial Support | `DivertGkeys`
`MKEYS` | `0x8020` | Supported | `MkeyLEDs`
`MR` | `0x8030` | Supported | `MRKeyLED`
`BRIGHTNESS_CONTROL` | `0x8040` | Supported | `BrightnessControl`
`REPORT_RATE` | `0x8060` | Supported | `ReportRate`
`COLOR_LED_EFFECTS` | `0x8070` | Supported | `LEDControl`, `LEDZoneSetting`
`RGB_EFFECTS` | `0X8071` | Supported | `RGBControl`, `RGBEffectSetting`
`MKEYS` | `0x8020` | Unsupported |
`MR` | `0x8030` | Unsupported |
`BRIGHTNESS_CONTROL` | `0x8040` | Unsupported |
`REPORT_RATE` | `0x8060` | Supported | `ReportRate`
`COLOR_LED_EFFECTS` | `0x8070` | Unsupported |
`RGB_EFFECTS` | `0X8071` | Unsupported |
`PER_KEY_LIGHTING` | `0x8080` | Unsupported |
`PER_KEY_LIGHTING_V2` | `0x8081` | Supported | `PerKeyLighting`
`PER_KEY_LIGHTING_V2` | `0x8081` | Unsupported |
`MODE_STATUS` | `0x8090` | Unsupported |
`ONBOARD_PROFILES` | `0x8100` | Supported |
`ONBOARD_PROFILES` | `0x8100` | Unsupported |
`MOUSE_BUTTON_SPY` | `0x8110` | Unsupported |
`LATENCY_MONITORING` | `0x8111` | Unsupported |
`GAMING_ATTACHMENTS` | `0x8120` | Unsupported |
`FORCE_FEEDBACK` | `0x8123` | Unsupported |
`SIDETONE` | `0x8300` | Supported | `Sidetone`
`EQUALIZER` | `0x8310` | Supported | `Equalizer`
`SIDETONE` | `0x8300` | Unsupported |
`EQUALIZER` | `0x8310` | Unsupported |
`HEADSET_OUT` | `0x8320` | Unsupported |
A “read only” note means the feature is a read-only feature.
@ -120,9 +120,9 @@ A “read only” note means the feature is a read-only feature.
## Implementing a feature
Features are implemented as settable features in
`lib/logitech_receiver/settings_templates.py`.
Some features also have direct implementation in
`lib/logitech_receiver/hidpp20.py`.
lib/logitech_receiver/settings_templates.py
some features also have direct implementation in
lib/logitech_receiver/hidpp20.py
In most cases it should suffice to only implement the settable feature
interface for each setting in the feature. That will add one or more
@ -202,4 +202,4 @@ device implements the feature it does not usefully support the setting.
Settings need to be added to the `SETTINGS` list so that setting discovery can be done.
For more information on implementing feature settings
see the comments in `lib/logitech_receiver/settings_templates.py`.
see the comments in lib/logitech_receiver/settings_templates.py.

View File

@ -5,12 +5,12 @@ layout: page
# Translating Solaar
First, make sure you have installed the `gettext` package. Also, you would need to install language pack for Gnome for your language, e.g. `language-pack-gnome-XX-base` for Debian/Ubuntu.
First, make sure you have installed the `gettext` package.
Here are the steps to add/update a translation (you should run all scripts from
the source root):
1. Get an up-to-date copy of the source files. Preferably, make a fork on
1. Get an up-to-date copy of the source files. Preferably, make a clone on
GitHub and clone it locally on your machine; this way you can later make a
pull request to the main project.
@ -22,7 +22,7 @@ the source root):
the translation (msgstr); if you leave msgstr empty, the string will remain
untranslated.
Alternatively, you can use the excellent [Poedit](https://poedit.net/) or [Lokalize](https://apps.kde.org/lokalize/).
Alternatively, you can use the excellent `poedit`.
4. Run `./tools/po-compile.sh`. It will bring up-to-date all the compiled
language files, necessary at runtime.
@ -31,7 +31,7 @@ the source root):
from your environment; to start it in another language, run
`LANGUAGE=<language> ./bin/solaar`.
To edit the translation iteratively, just repeat from step 3.
You can edit the translation iteratively, just repeat from step 3.
If the upstream changes, do a `git pull` and then repeat from step 2.
Before opening a pull request, please run `./tools/po-update.sh <language>` again. This will
@ -43,43 +43,26 @@ a translation.
Some of the languages Solaar has been translated to are listed below. A full list of available translations can be obtained by checking the `/po` folder for translation files.
- Chinese (Simplified): [Rongrong][Rongronggg9]
- Chinese (Taiwan): Peter Dave Hello
- Czech: Marián Kyral
- Croatian: gogo
- Danish: John Erling Blad
- Dutch: Heimen Stoffels
- Français: [Papoteur][papoteur], [David Geiger][david-geiger], [Damien Lallement][damsweb]
- Finnish: Tomi Leppänen
- German: Daniel Frost
- Greek: Vangelis Skarmoutsos
- Indonesia: [Ferdina Kusumah][feku]
- Italiano: [Michele Olivo][micheleolivo], Lorenzo
- Japanese: Ryunosuke Toda
- Français: [Papoteur][papoteur], [David Geiger][david-geiger],
[Damien Lallement][damsweb]
- Italiano: [Michele Olivo][micheleolivo]
- Norsk (Bokmål): [John Erling Blad][jeblad]
- Norsk (Nynorsk): [John Erling Blad][jeblad]
- Polski: [Adrian Piotrowicz][nexces], Matthaiks
- Portuguese: Américo Monteiro
- Portuguese-BR: [Drovetto][drovetto], [Josenivaldo Benito Jr.][jrbenito], Vinícius
- Polski: [Adrian Piotrowicz][nexces]
- Portuguese-BR: [Drovetto][drovetto], [Josenivaldo Benito Jr.][jrbenito]
- Română: Daniel Pavel
- Russian: [Dimitriy Ryazantcev][DJm00n], Anton Soroko
- Serbian: [Renato Kaurić][renatoka]
- Russian: [Dimitriy Ryazantcev][DJm00n]
- Slovak: [Jose Riha][jose1711]
- Spanish, Castilian: Jose Luis Tirado
- Swedish: John Erling Blad, [Daniel Zippert][zipperten], Emelie Snecker, Jonatan Nyberg
- Turkish: Osman Karagöz
- Ukrainian: Олександр Афанасьєв
- Svensk: [Daniel Zippert][zipperten], Emelie Snecker
[Rongronggg9]: https://github.com/Rongronggg9
[papoteur]: https://github.com/papoteur
[david-geiger]: https://github.com/david-geiger
[damsweb]: https://github.com/damsweb
[papoteur]: http://github.com/papoteur
[david-geiger]: http://github.com/david-geiger
[damsweb]: http://github.com/damsweb
[DJm00n]: https://github.com/DJm00n
[jose1711]: https://github.com/jose1711
[nexces]: https://github.com/nexces
[zipperten]: https://github.com/zipperten
[micheleolivo]: https://github.com/micheleolivo
[nexces]: http://github.com/nexces
[zipperten]: http://github.com/zipperten
[micheleolivo]: http://github.com/micheleolivo
[drovetto]: https://github.com/drovetto
[jrbenito]: https://github.com/jrbenito
[jeblad]: https://github.com/jeblad
[feku]: https://github.com/FerdinaKusumah
[renatoka]: https://github.com/renatoka
[jrbenito]: https://github.com/jrbenito/
[jeblad]: https://github.com/jeblad/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,204 +0,0 @@
---
title: Solaar Implementation
layout: page
---
# Solaar Implementation
Solaar has three main components: code mostly about receivers and devices, code for the command line interface, and code for the graphical user interface.
The following graph shows the main components of Solaar and how they interact.
```mermaid
graph TD
subgraph User interface
U[UI]
C[CLI]
end
subgraph Core
U --> S{Solaar}
C --> S
S --> L[Logitech receiver]
L --> R[Receiver]
L --> D[Device]
S --> B[dbus]
end
subgraph Hardware interface
R --> A
D --> A
A[hidapi]--> P[hid parser]
end
subgraph Peripherals
P <-.-> M[Logitech mouse]
P <-.-> K[Logitech keyboard]
end
```
## Receivers and Devices
The code in `logitech_receiver` is responsible for creating and maintaining receiver (`receiver/Receiver`) and device (`device/Device`) objects for each device on the computer that uses the Logitech HID++ protocol. These objects are discovered in Linux by interacting with the Linux `udev` system using code in `hidapi`.
The code in `logitech_receiver/receiver' is responsible for receiver objects.
...
The code in `logitech_receiver/device' is responsible for device objects.
... the complex device setup process
A device object stores the currrent value of many aspects of the device. It provides methods for retrieving and setting these aspects. The setters generally store the new value and call an hidpp10 or hidpp20 function to modify the device accordingly. The retrievers generally check whether the value is cached on the device if so just returning the cached value and if not calling an hidpp10 or hidpp20 function to retrieve the value and returning the value after caching it.
...
Not all communication with a device is done through the `Device` class. Some is done directly from settings.
....
### HID++
#### HID++ 2.0
The code in `logitech_receiver/hidpp20' interacts with devices using the HID++ 2.0 (and greater) protocol. Many of the functions in this module send messages to devices to modify their internal state, for example setting a host name stored in the device. Many other functions send messages to devices that query their internal state and interpret the response, for example returning how often a mouse sends movement reports. The result of these latter functions are generally cached in device objects.
A few of these functions create and return a large structure or a class object.
The HID++ 2.0 protocol is built around a number of features, each with its own functionality. One of the features, that is required to be implemented by all devices supporting the protocol, provide information on which features the device provides. The `hidpp20` module provides a class (`FeaturesArray`) to store information on what features are provided by a device and how to access them. Each device that implements the HID++ 2.0 protocol has an instance of this class. The heavily used function `feature_request` creates an HID++ 2.0 message using this information to help determine what data to put into the message.
Many devices allow reprogramming some keys or buttons. One the main reasons for reprogramming a key or device is to cause it to produce an HID++ message instead of its normal HID message, this is referred to as diverting the key (to HID++). The `ReprogrammableKey` class stores information about the reprogramming of one key for one version of this capability, with methods to access and update this information. The `PersistentRemappableAction` class does the same for another version. The `KeysArray` class stores information about the reprogramming of a collection of keys, with methods to access this information. Functions in the Device class request `KeysArray` information for a device when appropriate and store it on the device.
Many pointing devices provide a facility for recognizing gestures and sending an HID message for the gesture. The `Gesture` class stores inforation for one gesture and the `Gestures` class stores information for all the gestures on a device. Functions in the Device class request `KeysArray` information and store it on devices. Functions in the Device class request `Gestures` information for a device when appropriate and store it on the device.
Many gaming devices provide an interface to controlling their LEDs by zone. The `LEDEffectSetting` class stores the current state of one zone of LEDs. This information can come directly from an LED feature but is also part of Onboard Profiles so this class provides a byte string interface. Solaar stores this information in YAML so this class provides a YAML interface. The `LEDEffectsInfo` class stores information about what LED zones are on a device and what effects they can perform and provides a method that builds an object by querying a device.
Many gaming devices can be controlled by selecting one of their Onboard Profiles. An Onboard Profile sets up the rate at which the device reports movement, a set of sensitivites of its movement detector, a set of actions to be performed by mouse buttons or G and M keys, and effects for up to two LED zones. The `Button` class stores information about a button or key action. The `OnboardProfile` class stores a single profile, using the `LEDEffectSetting` and `Button` classes. Because retrieving and changing a profile is complex, this class provides a byte string interface. Because Solaar dumps profiles from devices as YAML documents and loads them into devices from YAML documents, this class provides a YAML interface. The `OnboardProfiles` class class stores the entire profiles information for a device. It provides an interface to construct an `OnboardProfiles` object by querying a device.
Because Solaar dumps profiles from devices as YAML documents and loads them into devices from YAML documents, these classes also provide a YAML interface.
#### HID++ 1.0
The code in `logitech_receiver/hidpp10' interacts with devices using the HID++ 1.0 protocol.
...
### Low Level Information and Access
The module `descriptors` sets up information on device models for which Solaar needs information to support. Solaar can determine all this information for most modern devices so it is only needed for older devices or devices that are unusual in some way. The information may include the name of the device model, short name of the device model, the HID++ protocol used by the device model, HID++ registers supported by the device model, various identifiers for the device model, and the USB interface that the device model uses for HID++ messages. It used to include the HID++-based settings for the device model but this information is now added in `setting_templates`. The information about a device model can be retrieved in several ways.
The module `base_usb` sets up information for most of the receiver models that Solaar supports, including USB id, USB interface used for HID++ messages, what kind of receiver model it is, and some capabilities of the receiver model. Solaar can now support other receivers as long as they are not too unusual. The module lso sets up lists of device models by USB ID and Bluetooth ID and provides a function to determine whether a USB ID or Bluetooth ID is an HID++ device model
The module `base` provides functions that call discovery to enumerate all current receivers and devices and to set up a callback for when new receivers or devices are discovered. It provides functions to open and close I/O channels to receivers and devices, write HID++ messages to receivers and devices, and read HID++ messages from receivers and devices. It provides a function to turn an HID++ message into a notification.
The module provides a function to send an HID++ message to a receiver or device, constructing the message from parameters to the function, and optionally waiting for and returning a response. The function checks messages from the receiver or device, only terminating at timeout or when a message that appears to be the response is seen. Other messages are turned into notifications if appropriate and ignoreed otherwise. A separate function sends a ping message and waits for a reply to the ping.
### Notifications and Status
HID++ devices not only respond to commands but can spontaneously emit HID++ messages, such as when their movement sensitivity changes or when a diverted key is pressed. These spontaneous messages are called notifications and if software is well behaved can be distinguished from messages that are responses to commands. (The Linux HIDPP driver was not well behaved at some time and still may not be well behaved, resulting in it causing devices to send responses that cannot be distinguished from notifications.)
The `listener` module provides a class to set up a thread that listens to all the HID++ messages that come from a given device or receiver, convert the message that are notifications to a Solaar notification structure, and invoke a callback on the notification.
The 'notifications` module provides a function to take a notification from a receiver or device and initiate processing required for that notification. For receivers notifications are used to signal the progress of pairing attempts. For devices some notifications are for pairing, some signal device connection and diconnection from a receiver, some are other parts of the HID++ 1.0 protocol, and some are for the HID++ 2.0 protocol. Devices can provide a callback for special handling of notifications. This facility is used for two special kinds of Solaar settings.
The module contains code that determines the meaning of a notification based on fields in the notification and the status and HID++ 2.0 features of the device if appropriate and updates the device and its status accordingly. Updates to device status can trigger updates to the Solaar user interface. The processing of some notifications also directly runs a function to update the Solaar user interface.
After this processing HID++ 2.0 notifications are sent to the `diversion` module where they initiate Solaar rule processing.
The `status` module provides the `DeviceStatus` class to record the battery status of a device. It also provides an interface to signal changes to the connection status of the device that can invoke a callback. This callback is used to update the Solaar user interface when the status changes.
### Settings
The Solaar GUI is based around settings.
A setting contains all the information needed to store the value of some aspect of a device, read it from the device, write it to the device, and record its state in a dictionary. A setting also contains information to display and manipulate a setting, namely what kind of user interface element to use, what values are permissable, a label to use for the setting, and a tooltip to provide additional information for the setting. Settings can be either based on HID++ 1.0, using an HID++ 1.0 register that the device provides, or based on HID++ 2.0, using an HID++ 2.0 feature that the device provides. The module `settings` provides classes and methods to create and support a setting. The module `setting_templates` contains all the settings that Solaar supports as well as functions to determine what feature-based settings a device can support.
A simple boolean setting can be set up as follows:
```
class HiresSmoothInvert(_Setting):
name = 'hires-smooth-invert'
label = _('Scroll Wheel Direction')
description = _('Invert direction for vertical scroll with wheel.')
feature = _F.HIRES_WHEEL
rw_options = {'read_fnid': 0x10, 'write_fnid': 0x20}
validator_options = {'true_value': 0x04, 'mask': 0x04}
```
The setting is a boolean setting, the default for settings.
`name` is the dictionary key for recording the state of the setting.
`label` is the label to be shown for the setting in a user interface and `description` is the tooltip.
`feature` is the HID++ 2.0 feature that is used to read the current state of the setting from a device and write it back to a device.
`rw_options` contains options used when reading or writing the state of the setting, here to use feature command 0x10 to read the value and feature command 0x20 to write the value.
`validator_options` contains options to turn setting values into bytes and bytes into setting values. The options here to take a single byte (the default) and mask it with 0x04 to get a value with a result of 0x04 being true and anything else being false. They also say to use 0x04 when writing a true value and 0x00 (the default) when writing a false value. Because this is a boolean setting and the mask masks off part of a byte the value to be written is or'ed with the byte read for the setting before writing to the device.
A simple choice setting can be set up as follows:
```
class Backlight(_Setting):
name = 'backlight-qualitative'
label = _('Backlight')
description = _('Set illumination time for keyboard.')
feature = _F.BACKLIGHT
choices_universe = _NamedInts(Off=0, Varying=2, VeryShort=5, Short=10, Medium=20, Long=60, VeryLong=180)
validator_class = _ChoicesV
validator_options = {'choices': choices_universe}
```
This is a choice setting because of the value for `validator_class`.
`choices_universe` is all the possible stored values for the setting along with how they are to be displayed in a user interface.
`validator_options` provides the current permissable choices, here always are the same as all the choices.
The Solaar GUI takes these settings and constructs an interface for displaying and changing the setting.
This setup allows for very quick implementation of simple settings but it bypasses the data stored in a device object.
### Solaar Rules
The `diversion` module (so-named because it initially mostly handled diverted key notifications) implements Solaar rules.
...
### Utility Functions, Structures, and Classes
The module `common.py` provides utility functions, structures, and classes.
`crc16` is a function to compute checksums used in profiles.
`NamedInt`, `NamedInts`, and `UnsortedNamedInts` provide integers and sets of integers with attached names.
`FirmwareInfo` provides information about device firmware.
`BATTERY_APPROX` provides named integers used for approximate battery levels of devices.
`i18n.py` provides a few strings that need translations and might not otherwise be visible to translation software.
`special_keys.py` provides named integers for various collections of key codes and colors.
## Discovery of HID++ Receivers and Devices and I/O
The code in `hidapi` is responsible for discovery of receivers and devices that use the HID++ protocol. The module used in Linux is `hidapi/udev` which is a modification of some old Python code that provides an interface to the Linux `udev` system.
The code originally was only for receivers that used USB and devices paired with them. It identifies HID++ receivers by their USB ids, based on a list of Logitech HID++ receivers with their USB ids. It then added all devices that were paired with them and that were in a list of HID++ devices with their WPID. A WPID is used to identify the device type for devices paired with HID++ receivers. This code now also adds all devices paired with HID++ receivers whether they are in this list or not.
The code now also identifies HID ++ devices that are directly connected via either USB or Bluetooth. These devices are recognized by several means: the internal list of HID++ devices for elements of the list that have either a USB IS or a Bluetooth ID, any device with a USB ID or Bluetooth ID that falls in one of several ranges of IDs that are known to support HID++, or any device that has an HID protocol descriptor that claims support for HID++. This last method requires an external Pyshon module to decipher HID protocol descriptors that is not always present.
Device and receiver discovery is performed when Solaar starts. While the Solaar GUI is running the `udev` code also listens for connections of new hardware using facilities from `GLib`.
This code is also responsible for actual writing data to devices and receivers and reading data from them.
## Solaar
### Startup and Commonalities
__init__.py
configuration.py
gtk.py*
i18n.py
listener.py
tasks.py
upower.py
The files `version` and `commit` contain data about the current version and git commit of Solaar.
### Solaar Command Line Interface
solaar/cli
### Solaar (Graphical) User Interface
solaar/ui

View File

@ -17,7 +17,7 @@ Solaar runs as a regular user process, albeit with direct access to the Linux in
that lets it directly communicate with the Logitech devices it manages using special
Logitech-proprietary (HID++) commands.
Each Logitech device implements a different subset of these commands.
Solaar is thus only able to make the changes that a particular device supports.
Solaar is thus only able to make the changes to devices that devices implement.
Solaar is not a device driver and does not process normal input from devices.
It is thus unable to fix problems that arise from incorrect handling of
@ -27,7 +27,7 @@ Solaar can be used as a GUI application, the usual case, or via its command-line
The Solaar GUI is meant to run continuously in the background,
monitoring devices, making changes to them, and responding to some messages they emit.
To this end, it is useful to have Solaar start at user login so that
changes made to devices by Solaar are applied at login and throughout the user's session.
changes made to devices by Solaar are applied at login and through out the user's session.
Both Solaar interfaces are able to list the connected devices and
show information about each device, often including battery status.
@ -46,8 +46,8 @@ and for more information on its capabilities see
Solaar's GUI normally uses an icon in the system tray and starts with its main window visible.
This aspect of Solaar depends on having an active system tray, which is not the default
situation for recent versions of Gnome. For information on how to set up a system tray under
Gnome see [the capabilities page](https://pwr-solaar.github.io/Solaar/capabilities).
situation for recent versions of Gnome. For information on to set up a system tray under Gnome see
[the capabilities page](https://pwr-solaar.github.io/Solaar/capabilities).
Solaar's GUI can be started in several ways
@ -75,7 +75,7 @@ Please report such experiences by creating an issue in
Solaar will detect all devices paired with supported Unifying, Bolt, Lightspeed, or Nano
receivers, and at the very least display some basic information about them.
Solaar will detect many Logitech devices that connect via a USB cable or Bluetooth.
Solaar will detect some Logitech devices that connect via a USB cable or Bluetooth.
Solaar can pair and unpair a Logitech device showing the Unifying logo
(Solaar's version of the [logo][logo])
@ -84,7 +84,7 @@ and pair and unpair a Logitech device showing the Bolt logo
with any Bolt receiver,
and
can pair and unpair Lightspeed devices with Lightspeed receivers for the same model.
Solaar can pair some Logitech devices with Logitech Nano receivers, but not all Logitech
Solaar can pair some Logitech devices with Logitech Nano receivers but not all Logitech
devices can be paired with Nano receivers.
Logitech devices without a Unifying or Bolt logo
generally cannot be paired with Unifying or Bolt receivers.
@ -95,36 +95,37 @@ which is done using the usual Bluetooth mechanisms.
For a partial list of supported devices
and their features, see [the devices page](https://pwr-solaar.github.io/Solaar/devices).
[logo]: https://pwr-solaar.github.io/Solaar/img/solaar.svg
[logo]: https://pwr-solaar.github.io/Solaar/assets/solaar.svg
## Prebuilt packages
Up-to-date prebuilt packages are available for some Linux distros
(e.g., Fedora 33+) in their standard repositories.
If a recent version of Solaar is not
available from the standard repositories for your distribution, you can try
available from the standard repositories for your distribution you can try
one of these packages.
- Arch solaar package in the [extra repository][arch]
- Ubuntu/Kubuntu package in [Solaar stable ppa][ppa2]
- NixOS Flake package in [Svenum/Solaar-Flake][nix flake]
- Arch solaar package in the [community repository][arch]
- Ubuntu/Kubuntu stable packages: use the [Solaar stable ppa][ppa2], courtesy of [gogo][ppa4]
- Ubuntu/Kubuntu git build packages: use the [Solaar git ppa][ppa1], courtesy of [gogo][ppa4]
Solaar is available from some other repositories
but they may be several versions behind the current version.
but they are several versions behind the current version.
- for Ubuntu/Kubuntu 16.04+: the solaar package from [universe repository][universe repository]
- a [Gentoo package][gentoo], courtesy of Carlos Silva and Tim Harder
- a [Mageia package][mageia], courtesy of David Geiger
Solaar uses a standard system tray implementation; solaar-gnome3 is no longer required for Gnome or Unity integration.
Solaar uses a standard system tray implementation; solaar-gnome3 is no longer required for gnome or unity integration.
[ppa4]: https://launchpad.net/~trebelnik-stefina
[ppa2]: https://launchpad.net/~solaar-unifying/+archive/ubuntu/stable
[arch]: https://www.archlinux.org/packages/extra/any/solaar/
[ppa1]: https://launchpad.net/~solaar-unifying/+archive/ubuntu/ppa
[ppa]: http://launchpad.net/~daniel.pavel/+archive/solaar
[arch]: https://www.archlinux.org/packages/community/any/solaar/
[gentoo]: https://packages.gentoo.org/packages/app-misc/solaar
[mageia]: http://mageia.madb.org/package/show/release/cauldron/application/0/name/solaar
[universe repository]: http://packages.ubuntu.com/search?keywords=solaar&searchon=names&suite=all&section=all
[nix flake]: https://github.com/Svenum/Solaar-Flake
## Manual installation
@ -133,38 +134,32 @@ for the step-by-step procedure for manual installation.
## Known Issues
- Onboard Profiles, when active, can prevent changes to other settings, such as Polling Rate, DPI, and various LED settings. Which settings are affected depends on the device. To make changes to affected settings, disable Onboard Profiles. If Onboard Profiles are later enabled the affected settings may change to the value in the profile.
- Solaar version 1.1.12 has a bug resulting in devices remaining in their default configuration after a system resume. This is fixed in 1.1.13.
- Bluez 5.73 does not remove Bluetooth devices when they disconnect.
Solaar 1.1.12 processes the DBus disconnection and connection messages from Bluez and does re-initialize devices when they reconnect.
The HID++ driver does not re-initialize devices, which causes problems with smooth scrolling.
Until the problem is resolved having Scroll Wheel Resolution set to true (and not ignored) may be helpful.
- The Linux HID++ driver modifies the Scroll Wheel Resolution setting to
implement smooth scrolling. If Solaar changes this setting, scrolling
can be either very fast or very slow. To fix this problem
click on the icon at the right edge of the setting to set it to
"Ignore this setting", which is the default for new devices.
The mouse has to be reset (e.g., by turning it off and on again) before this fix will take effect.
- Solaar expects that it has exclusive control over settings that are not ignored.
Running other programs that modify these settings, such as logiops,
will likely result in unexpected device behavior.
- The Linux HID++ driver modifies the setting Scroll Wheel Resolution to
implement smooth scrolling. If Solaar later changes this setting scrolling
can be either very fast or very slow. To fix this problem
click on the icon at the right edge of the setting to set it to
"Ignore this setting".
The mouse has to be reset (e.g., by turning it off and on again) before this fix will take effect.
- The driver also sets the scrolling direction to its normal setting when implementing smooth scrolling.
This can interfere with the Scroll Wheel Direction setting, requiring flipping this setting back and forth
to restore reversed scrolling.
- The driver sends messages to devices that do not conform with the Logitech HID++ specification
resulting in responses being sent back that look like other messages. For some devices this causes
resulting in reponses being sent back that look like other messages. For some devices this causes
Solaar to report incorrect battery levels.
- Solaar normally uses icon names for its icons, which in some system tray implementations
- If the Python hid-parser package is not available Solaar will not recognize some devices.
Use pip to install hid-parser.
- Solaar normally uses icon names for its icons, which in some system tray implementatations
results in missing or wrong-sized icons.
The `--tray-icon-size` option forces Solaar to use icon files of appropriate size
for tray icons instead, which produces better results in some system tray implementations.
for tray icons instead, which produces better results in some system tray implementatations.
To use icon files close to 32 pixels in size use `--tray-icon-size=32`.
- The icon in the system tray can show up as 'black on black' in dark
@ -172,14 +167,19 @@ for the step-by-step procedure for manual installation.
in some system tray implementations. Changing to a different theme may help.
The `--battery-icons=symbolic` option can be used to force symbolic icons.
- Many gaming mice and keyboards have the ONBOARD PROFILES feature.
This feature can override other features, including polling rate and key lighting.
To make the Polling Rate and M-Key LEDs settings effective the Onboard Profiles setting has to be disabled.
This may have other effects, such as turning off backlighting.
- Solaar will try to use uinput to simulate input from rules under Wayland or if Xtest is not available
but this needs write permission on /dev/uinput.
For more information see [the rules page](https://pwr-solaar.github.io/Solaar/rules).
- Diverted keys remain diverted and so do not have their normal behavior when Solaar terminates
or a device disconnects from a host that is running Solaar. If necessary, their normal behavior
- Diverted keys remain diverted and so do not have their normal behaviour when Solaar terminates
or a device disconnects from a host that is running Solaar. If necessary, their normal behaviour
can be reestablished by turning the device off and on again. This is most important to restore
the host switching behavior of a host switch key that was diverted, for example to switch away
the host switching behaviour of a host switch key that was diverted, for example to switch away
from a host that crashed or was turned off.
- When a receiver-connected device changes hosts Solaar remembers which diverted keys were down on it.
@ -187,14 +187,9 @@ for the step-by-step procedure for manual installation.
realize that the key was newly depressed. For this reason Solaar rules that can change hosts should
trigger on key releasing.
## License
This software is distributed under the terms of the
[GNU Public License, v2](LICENSE.txt), or later.
## Contributing to Solaar
Contributions to Solaar are very welcome.
Conributions to Solaaar are very welcome.
Solaar has complete or partial translations of its GUI strings in several languages.
If you want to update a translation or add a new one see [the translation page](https://pwr-solaar.github.io/Solaar/i18n) for more information.
@ -204,21 +199,28 @@ If you find a bug, please check first if it has already been reported. If yes, p
If you want to add a new feature to Solaar, feel free to open a feature request issue to discuss your proposal.
There are also usually several open issues for enhancements that have already been requested.
## Contributors
## License
This software is distributed under the terms of the
[GNU Public License, v2](COPYING).
## Thanks
This project began as a third-hand clone of [Noah K. Tilton](https://github.com/noah)'s
logitech-solar-k750 project on GitHub (no longer available). It was developed
further thanks to the contributions of many other people, including:
further thanks to the diggings in Logitech's HID++ protocol done by many other
people:
- [Daniel Pavel](https://github.com/pwr)
- [Filipe Lains](https://github.com/FFY00)
- [Peter Wu](https://github.com/Lekensteyn), who also did some [reverse engineering on pairing](https://lekensteyn.nl/logitech-unifying.html)
- Julien Danjou
- [Julien Danjou](http://julien.danjou.info/blog/2012/logitech-k750-linux-support),
who also provided some internal
[Logitech documentation](http://julien.danjou.info/blog/2012/logitech-unifying-upower)
- [Lars-Dominik Braun](http://6xq.net/git/lars/lshidpp.git)
- [Alexander Hofbauer](http://derhofbauer.at/blog/blog/2012/08/28/logitech-performance-mx)
- [Clach04](https://github.com/clach04)
- [Peter F. Patel-Schneider](https://github.com/pfps)
- [Clach04](http://bitbucket.org/clach04/logitech-unifying-receiver-tools)
- [Peter Wu](https://lekensteyn.nl/logitech-unifying.html)
- [Nestor Lopez Casado](http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28)
provided some more Logitech specifications for the HID++ protocol
Thanks go to Nestor Lopez Casado, who
provided [public Logitech specifications for the HID++ protocol](http://drive.google.com/folderview?id=0BxbRzx7vEV7eWmgwazJ3NUFfQ28).
Also, thanks to Douglas Wagner, Julien Gascard, and others for helping with application testing and supporting new devices.
Also, thanks to Douglas Wagner, Julien Gascard, and Peter Wu for helping with
application testing and supporting new devices.

View File

@ -5,75 +5,44 @@ layout: page
# Installing from PyPI
An easy way to install the most recent release version of Solaar is from the PyPI repository.
An easy way to install the most recent release version of Solaar is from the PyPI repository.
First install pip, and then run
`pip install --user solaar` or `pipx install --system-site-packages solaar` or
If you are using pipx add the `` flag.
`pip install --user 'solaar[report-descriptor,git-commit]'`.
This will not install the Solaar udev rule, which you will need to install manually by copying
`~/.local/lib/udev/rules.d/42-logitech-unify-permissions.rules`
This will not install the Solaar udev rule, which you will need to copy from
`~/.local/share/solaar/udev-rules.d/42-logitech-unify-permissions.rules`
to `/etc/udev/rules.d` as root.
If you want Solaar rules to simulate input you will have to instead install Solaar's uinput udev rule
from the GitHub repository.
## Installing in macOS
Solaar has limited support for macOS. You can use it to pair devices and configure settings
but the rule system and diversion will not work.
After installing Solaar via pip use homebrew to install the needed libraries:
```
brew update
brew install hidapi gtk+3 pygobject3
```
# Installating from GitHub
# Manual installation from GitHub
## Downloading
Clone Solaar from GitHub by `git clone https://github.com/pwr-Solaar/Solaar.git`.
## Installing using the Makefile
Solaar has a makefile that can be used to easily install Solaar after cloning the repository.
First, install the needed system packages by `make install_apt`
or `make install_dnf` or `make install_brew`.
These might not install all needed packages in older versions of your distribution.
Next, install the Solaar rule via `make install_udev`.
If you are using Wayland instead of X11 you may want to instead `make install_udev_uinput`
so that Solaar rules can simulate input in Wayland.
Finally, install Solaar via `make install_pip` or `make install_pipx`.
Parts of the installation process require sudo privileges so you may be asked for your password.
## Running from the download directory
To run Solaar from the download directory, just cd to there and run `bin/solaar` for the GUI
or `bin/solaar <command> <arguments>` for the CLI.
## Requirements for Solaar
This is only relevant if you have problems with the easier methods above.
If you have previously successfully installed a recent version of Solaar from a repository
you should be able to skip this section.
Solaar needs a reasonably new kernel with kernel modules `hid-logitech-dj` and `hid-logitech-hidpp` loaded.
The kernel option CONFIG_HIDRAW also needs to be enabled.
Solaar needs a reasonably new kernel with kernel modules `hid-logitech-dj`
and `hid-logitech-hidpp` loaded.
Most of Solaar should work fine with any kernel more recent than 5.2,
but newer kernels might be needed for some devices to be correctly recognized and handled.
The `udev` package must be installed and its daemon running.
Solaar requires Python 3.7+ and requires several packages to be installed.
If you are running the system version of Python you should have the
`python3-pyudev`, `python3-psutil`, `python3-xlib`, `python3-evdev`, `python3-typing-extensions`, `dbus-python`
or `python3-dbus`, and `python3-yaml` or `python3-pyyaml` packages installed.
`python3-pyudev`, `python3-psutil`, `python3-xlib`, `python3-evdev`, `python3-typing-extensions`,
and `python3-yaml` or `python3-pyyaml` packages installed.
To run the GUI Solaar also requires Gtk3 and its GObject introspection bindings.
If you are running the system version of Python in Debian/Ubuntu you should have the
`python3-gi` and `gir1.2-gtk-3.0` packages installed.
In Fedora you need `gtk3` and `python3-gobject`.
If you are running the system version of Python
the Debian/Ubuntu packages you should have
`python3-gi` and `gir1.2-gtk-3.0` installed.
in Fedora you need `gtk3` and `python3-gobject`.
You may have to install `gcc` and the Python development package (`python3-dev` or `python3-devel`,
depending on your distribution).
Other system packages may be required depending on your distribution, such as `python-gobject-common-devel` and `python-typing-extensions'.
Although the Solaar CLI does not require Gtk3,
`solaar config` does use Gtk3 capabilities to determine whether the Solaar GUI is running
and thus should tell the Solaar GUI to update its information about settings
@ -92,11 +61,10 @@ If desktop notifications bindings are also installed
(`gir1.2-notify-0.7` for Debian/Ubuntu),
you will also see desktop notifications when devices come online and go offline.
Solaar includes its own version of `hid_parser` because the version that is in PyPi
(at https://pypi.org/project/hid-parser/) does not have some changes that are in
https://github.com/usb-tools/python-hid-parser and are needed for some devices.
Do not use pip to install hid_parser!
Some distributions (e.g., Fedora) may separately package this code.
If the `hid_parser` Python package is available, Solaar parses HID report descriptors
and can control more HID++ devices that do not use a receiver.
This package may not be available in some distributions but can be installed using pip
via `pip install --user hid-parser`.
If the `gitinfo` Python package is available, Solaar shows better information
about which version of Solaar is running.
@ -113,16 +81,54 @@ which requires installation of the X11 development package.
Solaar will run under Wayland but some parts of Solaar rules will not work.
For more information see [the rules page](https://pwr-solaar.github.io/Solaar/rules).
## Installing Solaar's udev rule manually
### Installing Solaar's udev rule
You can install Solaar's udev rule manually by copying the file
`rules.d/42-logitech-unify-permissions.rules`
as root from the Solaar repository to `/etc/udev/rules.d`.
In Wayland you may want to instead copy
`rules.d-uinput/42-logitech-unify-permissions.rules`.
Let udev reload its rules by running `sudo udevadm control --reload-rules`.
Solaar needs to write to HID devices for receivers and devices.
To be able to do this without running as root requires a udev rule
that gives seated users write access to the HID devices for Logitech receiver and devices.
# Solaar in other languages
You can install this rule by copying, as root,
`rules.d/42-logitech-unify-permissions.rules` from Solaar to
`/etc/udev/rules.d`.
You will probably also have to tell udev to reload its rule via
`sudo udevadm control --reload-rules`.
For this rule to set up the correct permissions for your receivers and devices
you will then need to either disconnect your receivers and
any USB-connected or Bluetooth-connected devices and
re-connect them or reboot your computer.
## Running from the download directory
To run Solaar from the download directory, first install the Solaar udev rule if necessary.
Then cd to the solaar directory and run `bin/solaar` for the GUI
or `bin/solaar <command> <arguments>` for the CLI.
Do not run Solaar as root, you may encounter problems with X11 integration and with the system tray.
## Installing Solaar from the download directory using Pip
Python programs are usually installed using [pip][pip].
The pip instructions for Solaar are in `setup.py`, the standard place to put such instructions.
To install Solaar for yourself only run
`pip install --user '.[report-descriptor,git-commit]'`.
from the download directory.
This tells pip to install into your `.local` directory, but does not install Solaar's udev rule.
(See above for installing the udev rule.)
Once the udev rule has been installed you can then run Solaar as `~/.local/bin/solaar`.
Installing python programs to system directories using pip is generally frowned on both
because this runs arbitrary code as root and because this can override existing python libraries
that other users or even the system depend on. If you want to install solaar to /usr/local run
`sudo bash -c 'umask 022 ; pip install .'` in the solaar directory.
(The umask is needed so that the created files and directories can be read and executed by everyone.)
Then solaar can be run as /usr/local/bin/solaar.
You will also have to install the udev rule.
[pip]: https://en.wikipedia.org/wiki/Pip_(package_manager)
## Solaar in other languages
If you want to have Solaar's user messages in some other language you need to run
`tools/po-compile.sh` to create the translation files before running or installing Solaar
@ -132,6 +138,6 @@ and set the LANGUAGE environment variable appropriately when running Solaar.
Distributions can cause Solaar can be run automatically at user login by installing a desktop file at
`/etc/xdg/autostart/solaar.desktop`. An example of this file content can be seen in the repository at
[`share/autostart/solaar.desktop`](https://github.com/pwr-Solaar/Solaar/blob/master/share/autostart/solaar.desktop).
[share/autostart/solaar.desktop](https://github.com/pwr-Solaar/Solaar/blob/master/share/autostart/solaar.desktop).
If you install Solaar yourself you may need to create or modify this file or install a startup file under your home directory.

View File

@ -3,16 +3,15 @@ title: Rule Processing of HID++ Notifications
layout: page
---
# Rule Processing of HID++ Notifications
Creating and editing most rules can be done in the Solaar GUI, by pressing the 'Rule Editor' button in the
Solaar main window.
Note that rule processing only fully works under X11.
Rule processing is an experimental feature. Significant changes might be made in response to problems.
*Note that rule processing only fully works under X11.
When running under Wayland with X11 libraries loaded some features will not be available.
When running under Wayland without X11 libraries loaded even more features will not be available.
Rule features known not to work under Wayland include process and mouse process conditions,
although on GNOME desktop under Wayland, you can use those with the Solaar Gnome extension installed,
You can install it from `https://extensions.gnome.org/extension/6162/solaar-extension`.
Rule features known not to work under Wayland include process and mouse process conditions.
Under Wayland using keyboard groups may result in incorrect symbols being input for simulated input.
Under Wayland simulating inputs when modifier keys are pressed may result in incorrect symbols being sent.
Simulated input uses Xtest if available under X11 or uinput if the user has write access to /dev/uinput.
@ -21,11 +20,10 @@ The easiest way to maintain write access to /dev/uinput is to use Solaar's alter
and copying it as root into the `/etc/udev/rules.d` directory.
You may have to reboot your system for the write permission to be set up.
Another way to get write access to /dev/uinput is to run `sudo setfacl -m u:${USER}:rw /dev/uinput`
but this needs to be done every time the system is rebooted.
but this needs to be done every time the system is rebooted.*
## HID++ notifications and diversion
Logitech devices that use HID++ version 2.0 or greater, produce feature-based
notifications that Solaar can process using a simple rule language. For
Logitech devices that use HID++ version 2.0 or greater produce feature-based
notifications that Solaar can process using a simple rule language. For
example, using rules Solaar can emulate an `XF86_MonBrightnessDown` key tap
in response to the pressing of the `Brightness Down` key on Craft keyboards,
which normally does not produce any input at all when the keyboard is in
@ -34,7 +32,7 @@ Windows mode.
Solaar's rules only trigger on HID++ notifications so device actions that
normally produce HID output have to be first be set (diverted) to
produce HID++ notifications instead of their normal behavior.
Currently, Solaar can divert some mouse scroll wheels, some
Currently Solaar can divert some mouse scroll wheels, some
mouse thumb wheels, the crown of Craft keyboards, and some keys and buttons.
If the scroll wheel, thumb wheel, crown, key, or button is
not diverted by setting the appropriate setting then no HID++ notification is
@ -42,62 +40,49 @@ generated and rules will not be triggered by manipulating the wheel, crown, key,
Look for `HID++` or `Diversion` settings to see what
diversion can be done with your devices.
### Show notifications
Running Solaar with the `-ddd`
option will show information about notifications, including their feature
name, report number, and data.
In response to a feature-based HID++ notification Solaar runs a sequence of
rules. A `Rule` is a sequence of components, which are either sub-rules,
conditions, or actions. Conditions and actions are dictionaries with one
rules. A `Rule` is a sequence of components, which are either sub-rules,
conditions, or actions. Conditions and actions are dictionaries with one
entry whose key is the name of the condition or action and whose value is
the argument of the action.
If the last thing that a rule does is execute an action, no more rules are
processed for the notification.
Rules are evaluated by evaluating each of their components in order. The
Rules are evaluated by evaluating each of their components in order. The
evaluation of a rule is terminated early if a condition component evaluates
to false or the last evaluated subcomponent of a component is an action. A
rule is false if its last evaluated component evaluates to false.
to false or the last evaluated sub-component of a component is an action. A
rule is false if its last evaluated component evaluates to a false value.
## Conditions
### Not
`Not` conditions take a single component and are true if their component
evaluates to a false value.
### Or
`Or` conditions take a sequence of components and are evaluated by
evaluating each of their components in order.
An Or condition is terminated early if a component evaluates to true or the
last evaluated subcomponent of a component is an action.
last evaluated sub-component of a component is an action.
A Or condition is true if its last evaluated component evaluates to a true
value. `And` conditions take a sequence of components which are evaluated the same
value. `And` conditions take a sequence of components are evaluated the same
as rules.
### Feature
`Feature` conditions are true if the name of the feature of the current
`Feature` conditions are if true if the name of the feature of the current
notification is their string argument.
`Report` conditions are true if the report number in the current
`Report` conditions are if true if the report number in the current
notification is their integer argument.
### Key
`Key` conditions are true if the Logitech name of the current **diverted** key or button being pressed is their
string argument. Alternatively, if the argument is a list `[name, action]` where `action`
`Key` conditions are true if the Logitech name of the last diverted key or button pressed is their
string argument. Alternatively, if the argument is a list `[name, action]` where `action`
is either `'pressed'` or `'released'`, the key down or key up events of `name` argument are
matched, respectively. Logitech key and button names are shown in the `Key/Button Diversion`
setting. These names are also shown in the output of `solaar show` in the 'Reprogrammable keys'
section. Only keys or buttons that have 'Divertable' in their report can be diverted.
Some keyboards have 'Gn', 'Mn', or 'MR' keys, which are diverted using the 'Divert G Keys' setting.
matched, respectively. Logitech key and button names are shown in the `Key/Button Diversion`
setting. These names are also shown in the output of `solaar show` in the 'reprogrammable keys'
section. Only keys or buttons that have 'divertable' in their report can be diverted.
Some keyboards have Gn, Mn, or MR keys, which are diverted using the 'Divert G Keys' setting.
### Key is down
`KeyIsDown` conditions are true if the **diverted** key or button that is their string argument is currently down.
Note that this only works for **diverted** keys or buttons, including diverted Gn, Mn, and MR keys.
### Key and button diversion
Solaar can also create special notifications in response to mouse movements on some mice.
Setting `Key/Button Diversion` for a key or button to Mouse Gestures causes the key or button to create a `Mouse Gesture`
Setting `Key/Button Diversion` for a key or button causes the key or button to create a `Mouse Gesture`
notification for the period that the key or button is depressed.
Moving the mouse creates a mouse movement event.
Stopping the mouse for a little while and moving it again creates another mouse movement event.
@ -105,47 +90,34 @@ Pressing a diverted key creates a key event.
When the key is released the sequence of events is sent as a synthetic notification
that can be matched with `Mouse Gesture` conditions.
### Mouse gestures
`Mouse Gesture` conditions are true if the actions (mouse movements and diverted key presses) taken while a mouse gestures button is held down match the arguments of the condition.
Mouse gestures buttons can be set using the 'Key/Button Diversion' setting, by changing the value to `Mouse Gestures`.
The arguments of a Mouse Gesture condition can be a direction, i.e., `Mouse Up`, `Mouse Down`, `Mouse Left`, `Mouse Right`, `Mouse Up-Left`, `Mouse Up-Right`, `Mouse Down-Left`, or `Mouse Down-Right`, or the Logitech name of a key.
Mouse gestures buttons can be set using the 'Key/Button Diversion' setting, by changing the value to `Mouse Gestures'.
The arguments of a Mouse Gesture condition can be a direction, i.e., `Mouse Up`, `Mouse Down`, `Mouse Left`, `Mouse Right`, `Mouse Up-left`, `Mouse Up-Right`, `Mouse Down-left`, or `Mouse Down-right`, or the Logitech name of a key.
If the first argument is the Logitech name of a key then that argument is matched against the button that was held down to initiate mouse gesture processing.
For example, a Mouse Gesture condition of `Mouse Up` -> `Mouse Up` would match pressing any Mouse Gestures button, moving the mouse upwards, pausing momentarily, moving the mouse upwards again, and releasing the button.
The condition `Smart Shift` -> `Mouse Down` -> `Back Button` would match pressing the Smart Shift button (provided that it is a Mouse Gestures button!), moving the mouse downwards, clicking the Back button (provided that it is diverted!), and then releasing the Smart Shift button.
So, for example, a Mouse Gesture condition of `Mouse Up` -> `Mouse Up` would match pressing any Mouse Gestures button, moving the mouse upwards, pausing momentarily, moving the mouse upwards again, and releasing the button.
The condition `Smart Shift` -> 'Mouse Down` -> `Back Button` would match pressing the Smart Shift button (provided that it is a Mouse Gestures button!) moving the mouse downwards, clicking the Back button (provided that it is diverted!), and then releasing the Smart Shift button.
Directions and buttons can be mixed and chained together however you like.
It's possible to create a `No-op` gesture by clicking 'Delete' on the initial Action when you first create the rule. This gesture will trigger when you simply click a Mouse Gestures button.
### Key modifiers
`Modifiers` conditions take either a string or a sequence of strings, which
can only be `Shift`, `Control`, `Alt`, and `Super`.
Modifiers conditions are true if their argument is the current keyboard
modifiers.
### Process focused
`Process` conditions are true if the process for the focused input window
`Process` conditions are true if the process for focus input window
or the window's Window manager class or instance name starts with their string argument.
### Window under cursor
`MouseProcess` conditions are true if the process for the window under the mouse
or the window's Window manager class or instance name starts with their string argument.
### Device notification and device active
`Device` conditions are true if a particular device originated the notification.
`Active` conditions are true if a particular device is active.
`Device` and `Active` conditions take one argument, which is the serial number or unit ID of a device,
as shown in Solaar's detail pane, or either of its names, as shown by Solaar.
Some older devices do not have a useful serial number or unit ID and so cannot
distinguished from other devices with the same names.
`Active` conditions take one argument, which is the Serial number or Unit ID of a device,
as shown in Solaar's detail pane.
### Host
`Host` conditions are true if the computers hostname starts with the condition's argument.
### Solaar device setting
`Setting` conditions check the value of a Solaar setting on a device.
`Setting` conditions take three or four arguments, depending on the setting:
the Serial number or Unit ID of a device, as shown in Solaar's detail pane,
or null for the device that initiated rule processing;
the internal name of a setting (which can be found from solaar config \<device\>);
the internal name of a setting (which can be found from solaar config <device>);
one or two arguments for the setting.
For settings that use keys or buttons as an argument the Logtech name can be used
as shown in the Solaar main window for these settings,
@ -155,28 +127,16 @@ which can be found in the GESTURE2_GESTURES_LABELS structure in lib/logitech_rec
For settings that need one of a set of names as an argument the name can be used or its internal integer value,
as used in the Solaar config file.
`Setting` conditions check device settings of devices, provided the device is on-line.
The first arguments to the condition are the Serial number or Unit ID of a device, as shown in Solaar's detail pane,
or null for the device that initiated rule processing; and
the internal name of a setting (which can be found from solaar config \<device\>).
Most simple settings take one extra argument, the value to check the setting value against.
Range setting can also take two arguments, which form an inclusive range to check against.
Other settings take two arguments, a key indicating which sub-setting to check and the value to check it against.
For settings that use gestures as an argument the internal name of the gesture is used,
which can be found in the GESTURE2_GESTURES_LABELS structure in lib/logitech_receiver/settings_templates.
For boolean settings '~' can be used to toggle the setting.
### Test and TestBytes
`Test` and `TestBytes` conditions are true if their test evaluates to true on the feature,
report and data of the current notification.
`TestBytes` conditions can return a number instead of a boolean.
report, and data of the current notification.
`TestBytes` conditions can return a number instead of a boolean.
`TestBytes` conditions consist of a sequence of three or four integers and use the first
two to select bytes of the notification data.
Writing this kind of test condition is not trivial.
Three-element `TestBytes` conditions are true if the selected bytes bit-wise AND
Three-element `TestBytes` conditions are true if the selected bytes bit-wise anded
with its third element is non-zero.
The value of these test conditions is the result of the AND.
The value of these test conditions is the result of the and.
Four-element `TestBytes` conditions are true if the selected bytes form a signed
integer between the third and fourth elements.
The value of these conditions is the signed value of the selected bytes
@ -204,25 +164,33 @@ This displacement is reset when the thumb wheel is inactive.
With a parameter the test is only true if the current thumb wheel displacement is greater than the parameter.
The displacement is then lessened by the amount of the parameter.
## Actions
`Setting` conditions check device settings of devices, provided the device is on-line.
The first arguments to the condition are the Serial number or Unit ID of a device, as shown in Solaar's detail pane,
or null for the device that initiated rule processing; and
the internal name of a setting (which can be found from solaar config <device>).
Most simple settings take one extra argument, the value to check the setting value against.
Range setting can also take two arguments, which form an inclusive range to check against.
Other settings take two arguments, a key indicating which sub-setting to check and the value to check it against.
For settings that use gestures as an argument the internal name of the gesture is used,
which can be found in the GESTURE2_GESTURES_LABELS structure in lib/logitech_receiver/settings_templates.
For boolean settings '~' can be used to toggle the setting.
### Key press
A `KeyPress` action takes either the name of an X11 key symbol, such as "a",
a list of X11 key symbols, such as "a" or "CTRL + A",
a list of X11 key symbols, such as "a" or "Control+a",
or a two-element list with the first element as above
and the second element one of `'click'`, `'depress'`, or `'release'`
and the second element one of 'click', 'depress', or 'release'
and executes key actions on a simulated keyboard to produce these symbols.
Use separate `KeyPress` actions for multiple characters,
Use separate `KeyPress` actions for multiple characters,
i.e., don't use a single `KeyPress` like 'a+b'.
The `KeyPress` action normally both depresses and releases (clicks) the keys,
but can also just depress the keys or just release the keys.
Use the depress or release options with extreme care,
ensuring that the depressed keys are later released,
otherwise it may become difficult to use your system.
ensuring that the depressed keys are later released.
Otherwise it may become difficult to use your system.
The keys are depressed in forward order and released in reverse order.
If a key symbol can only be produced by a shfited or level 3 keypress, e.g., "A",
then Solaar will add keypresses to produce that key symbol,
then Solaar will add keypresses to produce that keysymbol,
e.g., simulating a left shift keypress to get "A" instead of "a".
If a key symbol is not available in the current keymap or needs other shift-like keys,
then Solaar cannot simulate it.
@ -238,42 +206,36 @@ simulate inputting a key symbol.
Unfortunately, this determination can go wrong in several ways and is more likely
to go wrong under Wayland than under X11.
### Mouse scroll
A `MouseScroll` action takes a sequence of two numbers and simulates a horizontal and vertical mouse scroll of these amounts.
If the previous condition in the parent rule returns a number the scroll amounts are multiplied by this number.
### Mouse click
A `MouseClick` action takes a mouse button name (`left`, `middle` or `right`) and a positive number or 'click', 'depress', or 'release'.
The action simulates that number of clicks of the specified button or just one click, depress, or release of the button.
A `MouseClick` action takes a mouse button name (`left`, `middle` or `right`) and a positive number, and simulates that number of clicks of the specified button.
An `Execute` action takes a program and arguments and executes it asynchronously.
### Set setting
A `Set` action changes a Solaar setting for a device, provided that the device is on-line.
`Set` actions take three or four arguments, depending on the setting.
The first two are the Serial number or Unit ID of a device, as shown in Solaar's detail pane,
or null for the device that initiated rule processing; and
the internal name of a setting (which can be found from `solaar config <device>`).
the internal name of a setting (which can be found from solaar config <device>).
Simple settings take one extra argument, the value to set the setting to.
For boolean settings `~` can be used to toggle the setting.
For boolean settings '~' can be used to toggle the setting.
Other simple settings take two extra arguments, a key indicating which sub-setting to set and the value to set it to.
For settings that use gestures as an argument the internal name of the gesture is used,
which can be found in the GESTURE2_GESTURES_LABELS structure in `lib/logitech_receiver/settings_templates`.
which can be found in the GESTURE2_GESTURES_LABELS structure in lib/logitech_receiver/settings_templates.
All settings are supported.
### Later
A `Later` action executes rule components later.
`Later` actions take an integer delay in seconds between 1 and 100 followed by zero or more rule components that will be executed later.
`Later` actions take an integer delay in seconds between 1 and 100 followed by a zero or more rule components that will be executed later.
Processing of the rest of the rule continues immediately.
## Built-in Rules
Solaar has a built-in rule, which is run after user-created rules and so can be overridden by user-created rules.
This rule turns
Solaar has several built-in rules, which are run after user-created rules and so can be overridden by user-created rules.
One rule turns
`Brightness Down` key press notifications into `XF86_MonBrightnessDown` key taps
and `Brightness Up` key press notifications into `XF86_MonBrightnessUp` key taps.
## Example Solaar Rule File
Another rule makes Craft crown ratchet movements move between tabs when the crown is pressed
and up and down otherwise.
A third rule turns Craft crown ratchet movements into `XF86_AudioNext` or `XF86_AudioPrev` key taps when the crown is pressed and `XF86_AudioRaiseVolume` or `XF86_AudioLowerVolume` otherwise.
A fourth rule doubles the speed of `THUMB WHEEL` movements unless the `Control` modifier is on.
All of these rules are only active if the key or feature is diverted, of course.
Solaar reads rules from a YAML configuration file (normally `~/.config/solaar/rules.yaml`).
This file contains zero or more documents, each a rule.
@ -321,11 +283,10 @@ Here is a file with six rules:
...
```
## Button diversion example
Here is an example showing how to divert the Back Button on an MX Master 3 so that pressing
the button will initiate rule processing and a rule that triggers on this notification and
switches the mouse to host 3 after popping up a simple notification.
![Solaar-divert-back](screenshots/Solaar-main-window-back-divert.png)
![Solaar-divert-back](Solaar-main-window-back-divert.png)
![Solaar-rule-back-host](screenshots/Solaar-rule-editor.png)
![Solaar-rule-back-host](Solaar-rule-editor.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

View File

@ -1,40 +0,0 @@
---
title: Uninstalling Solaar
layout: page
---
# Uninstalling Solaar
## Uninstalling from Debian systems
If you installed Solaar using `apt`, you can remove it by running:
```bash
sudo apt remove --purge solaar
```
## Uninstalling from GitHub
If you cloned and installed Solaar from GitHub manually, navigate to the cloned directory and run:
```bash
sudo make uninstall
```
## Removing Configuration Files
Solaar may leave behind configuration files in your home directory. To delete them, run:
```bash
rm -rf ~/.config/solaar
```
## Verifying Uninstallation
To confirm that Solaar is fully removed, try running:
```bash
which solaar
```
If no output is returned, Solaar has been successfully uninstalled.

View File

@ -22,7 +22,7 @@ The following is an image of the Solaar menu and the icon (the battery
symbol is in the system tray at the left of the image). The icon can
also be other battery icons or versions of the Logitech Unifying icon.
![Solaar-menu](screenshots/Solaar-menu.png)
![Solaar-menu](Solaar-menu.png)
Clicking on “Quit” in the Solaar menu terminates the program.
Clicking on “About Solaar” pops up a window with further information about Solaar.
@ -32,13 +32,10 @@ Clicking on “About Solaar” pops up a window with further information about S
There are several options that affect how the Solaar GUI behaves:
* `--help` shows a help message and then quits
* `--version` shows the version of Solaar and then quits
* `--window=show` starts Solaar with the main window showing
* `--window=hide` starts Solaar with the main window not showing
* `--window=only` starts Solaar with no system tray icon and the main window showing
* `--battery-icons=regular` uses regular icons for battery levels
* `--battery-icons=symbolic` uses symbolic icons for battery levels
* `--battery-icons=solaar` uses only the Solaar icon in the system tray
## Solaar main window
@ -64,7 +61,7 @@ To pair with a Bolt receiver you have to type a passcode followed by enter
or click the left and right buttons in the correct sequence followed by
clicking both buttons simultaneously.
![Solaar-main-window-receiver](screenshots/Solaar-main-window-receiver.png)
![Solaar-main-window-receiver](Solaar-main-window-receiver.png)
When a device is selected you can unpair the device if your receiver supports
unpairing. To unpair the device, just click on the “Unpair” button and
@ -93,26 +90,26 @@ You can also see and change the settings of devices.
Changing settings is performed by clicking on buttons,
moving sliders, or selecting from alternatives.
![Solaar-main-window-keyboard](screenshots/Solaar-main-window-keyboard.png)
![Solaar-main-window-keyboard](Solaar-main-window-keyboard.png)
![Solaar-main-window-mouse](screenshots/Solaar-main-window-mouse.png)
![Solaar-main-window-mouse](Solaar-main-window-mouse.png)
Device settings now have a clickable icon that determines whether the
setting can be changed and whether the setting is ignored.
![Solaar-divert-back](screenshots/Solaar-main-window-back-divert.png)
![Solaar-divert-back](Solaar-main-window-back-divert.png)
If the selected device that is paired with a receiver is powered down or
otherwise disconnected its settings cannot be changed
but it still can be unpaired if its receiver allows unpairing.
![Solaar-main-window-offline](screenshots/Solaar-main-window-offline.png)
![Solaar-main-window-offline](Solaar-main-window-offline.png)
If a device is paired with a receiver but directly connected via USB or Bluetooth
the receiver pairing will show up as well as the direct connection.
The device can only be manipulated using the direct connection.
![Solaar-main-window-multiple](screenshots/Solaar-main-window-multiple.png)
![Solaar-main-window-multiple](Solaar-main-window-multiple.png)
#### Remapping key and button actions
@ -123,11 +120,11 @@ devices can be remapped and they can only be remapped to a limited
number of actions. The remapping is done by selecting a key
or button in the left-hand box on the “Action” setting line and then
selecting the action to be performed in the right-hand box. The default
action is always the one shown first in the list. As with all settings,
action is always the shown first in the list. As with all settings,
Solaar will remember past action settings and restore them on the device
from then on.
![Solaar-main-window-actions](screenshots/Solaar-main-window-button-actions.png)
![Solaar-main-window-actions](Solaar-main-window-button-actions.png)
The names of the keys, buttons, and actions are mostly taken from Logitech
documentation and may not be completely obvious.
@ -136,9 +133,9 @@ It is possible to end up with an unusable system, for example by having no
way to do a mouse left click, so exercise caution when remapping keys or
buttons that are needed to operate your system.
## Solaar command-line interface
## Solaar command line interface
Solaar also has a command-line interface that can do most of what can be
Solaar also has a command line interface that can do most of what can be
done using the main window. For more information on the
command line interface, run `solaar --help` to see the commands and
then `solaar <command> --help` to see the arguments to any of the commands.

BIN
jekyll/images/bg_hr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API."""
from hidapi.udev import close # noqa: F401
from hidapi.udev import enumerate # noqa: F401
from hidapi.udev import find_paired_node # noqa: F401
from hidapi.udev import find_paired_node_wpid # noqa: F401
from hidapi.udev import get_manufacturer # noqa: F401
from hidapi.udev import get_product # noqa: F401
from hidapi.udev import get_serial # noqa: F401
from hidapi.udev import monitor_glib # noqa: F401
from hidapi.udev import open # noqa: F401
from hidapi.udev import open_path # noqa: F401
from hidapi.udev import read # noqa: F401
from hidapi.udev import write # noqa: F401
__version__ = '0.9'

View File

@ -1,20 +0,0 @@
from __future__ import annotations
import dataclasses
@dataclasses.dataclass
class DeviceInfo:
path: str
bus_id: str | None
vendor_id: str
product_id: str
interface: str | None
driver: str | None
manufacturer: str | None
product: str | None
serial: str | None
release: str | None
isDevice: bool
hidpp_short: str | None
hidpp_long: str | None

View File

@ -1,538 +0,0 @@
## Copyright (C) 2012-2013 Daniel Pavel
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API.
This provides a python interface to libusb's hidapi library which,
unlike udev, is available for non-linux platforms.
See https://github.com/libusb/hidapi for how to obtain binaries.
Parts of this code are adapted from https://github.com/apmorton/pyhidapi
which is MIT licensed.
"""
from __future__ import annotations
import atexit
import ctypes
import logging
import platform
import typing
from threading import Thread
from time import sleep
from typing import Any
from typing import Callable
from hidapi.common import DeviceInfo
if typing.TYPE_CHECKING:
import gi
gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__)
ACTION_ADD = "add"
ACTION_REMOVE = "remove"
# Global handle to hidapi
_hidapi = None
# hidapi binary names for various platforms
_library_paths = (
"libhidapi-hidraw.so",
"libhidapi-hidraw.so.0",
"libhidapi-libusb.so",
"libhidapi-libusb.so.0",
"libhidapi-iohidmanager.so",
"libhidapi-iohidmanager.so.0",
"libhidapi.dylib",
"hidapi.dll",
"libhidapi-0.dll",
)
for lib in _library_paths:
try:
_hidapi = ctypes.cdll.LoadLibrary(lib)
break
except OSError:
pass
else:
raise ImportError(f"Unable to load hidapi library, tried: {' '.join(_library_paths)}")
# Retrieve version of hdiapi library
class _cHidApiVersion(ctypes.Structure):
_fields_ = [
("major", ctypes.c_int),
("minor", ctypes.c_int),
("patch", ctypes.c_int),
]
_hidapi.hid_version.argtypes = []
_hidapi.hid_version.restype = ctypes.POINTER(_cHidApiVersion)
_hid_version = _hidapi.hid_version()
# Construct device info struct based on API version
class _cDeviceInfo(ctypes.Structure):
def as_dict(self):
return {name: getattr(self, name) for name, _t in self._fields_ if name != "next"}
# Low level hdiapi device info struct
# See https://github.com/libusb/hidapi/blob/master/hidapi/hidapi.h#L143
_cDeviceInfo_fields = [
("path", ctypes.c_char_p),
("vendor_id", ctypes.c_ushort),
("product_id", ctypes.c_ushort),
("serial_number", ctypes.c_wchar_p),
("release_number", ctypes.c_ushort),
("manufacturer_string", ctypes.c_wchar_p),
("product_string", ctypes.c_wchar_p),
("usage_page", ctypes.c_ushort),
("usage", ctypes.c_ushort),
("interface_number", ctypes.c_int),
("next", ctypes.POINTER(_cDeviceInfo)),
]
if _hid_version.contents.major >= 0 and _hid_version.contents.minor >= 13:
_cDeviceInfo_fields.append(("bus_type", ctypes.c_int))
_cDeviceInfo._fields_ = _cDeviceInfo_fields
# Set up hidapi functions
_hidapi.hid_init.argtypes = []
_hidapi.hid_init.restype = ctypes.c_int
_hidapi.hid_exit.argtypes = []
_hidapi.hid_exit.restype = ctypes.c_int
_hidapi.hid_enumerate.argtypes = [ctypes.c_ushort, ctypes.c_ushort]
_hidapi.hid_enumerate.restype = ctypes.POINTER(_cDeviceInfo)
_hidapi.hid_free_enumeration.argtypes = [ctypes.POINTER(_cDeviceInfo)]
_hidapi.hid_free_enumeration.restype = None
_hidapi.hid_open.argtypes = [ctypes.c_ushort, ctypes.c_ushort, ctypes.c_wchar_p]
_hidapi.hid_open.restype = ctypes.c_void_p
_hidapi.hid_open_path.argtypes = [ctypes.c_char_p]
_hidapi.hid_open_path.restype = ctypes.c_void_p
_hidapi.hid_write.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
_hidapi.hid_write.restype = ctypes.c_int
_hidapi.hid_read_timeout.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int]
_hidapi.hid_read_timeout.restype = ctypes.c_int
_hidapi.hid_read.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
_hidapi.hid_read.restype = ctypes.c_int
_hidapi.hid_get_input_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
_hidapi.hid_get_input_report.restype = ctypes.c_int
_hidapi.hid_set_nonblocking.argtypes = [ctypes.c_void_p, ctypes.c_int]
_hidapi.hid_set_nonblocking.restype = ctypes.c_int
_hidapi.hid_send_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int]
_hidapi.hid_send_feature_report.restype = ctypes.c_int
_hidapi.hid_get_feature_report.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_size_t]
_hidapi.hid_get_feature_report.restype = ctypes.c_int
_hidapi.hid_close.argtypes = [ctypes.c_void_p]
_hidapi.hid_close.restype = None
_hidapi.hid_get_manufacturer_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t]
_hidapi.hid_get_manufacturer_string.restype = ctypes.c_int
_hidapi.hid_get_product_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t]
_hidapi.hid_get_product_string.restype = ctypes.c_int
_hidapi.hid_get_serial_number_string.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_size_t]
_hidapi.hid_get_serial_number_string.restype = ctypes.c_int
_hidapi.hid_get_indexed_string.argtypes = [ctypes.c_void_p, ctypes.c_int, ctypes.c_wchar_p, ctypes.c_size_t]
_hidapi.hid_get_indexed_string.restype = ctypes.c_int
_hidapi.hid_error.argtypes = [ctypes.c_void_p]
_hidapi.hid_error.restype = ctypes.c_wchar_p
# Initialize hidapi
_hidapi.hid_init()
atexit.register(_hidapi.hid_exit)
# Solaar opens the same device more than once which will fail unless we
# allow non-exclusive opening. On windows opening with shared access is
# the default, for macOS we need to set it explicitly.
if platform.system() == "Darwin":
_hidapi.hid_darwin_set_open_exclusive.argtypes = [ctypes.c_int]
_hidapi.hid_darwin_set_open_exclusive.restype = None
_hidapi.hid_darwin_set_open_exclusive(0)
class HIDError(Exception):
pass
def _enumerate_devices():
"""Returns all HID devices which are potentially useful to us"""
devices = []
c_devices = _hidapi.hid_enumerate(0, 0)
p = c_devices
while p:
devices.append(p.contents.as_dict())
p = p.contents.next
_hidapi.hid_free_enumeration(c_devices)
unique_devices = {}
for device in devices:
# hidapi returns separate entries for each usage page of a device.
# Deduplicate by path to only keep one device entry.
if device["path"] not in unique_devices:
unique_devices[device["path"]] = device
unique_devices = unique_devices.values()
# print("Unique devices:\n" + '\n'.join([f"{dev}" for dev in unique_devices]))
return unique_devices
# Use a separate thread to check if devices have been removed or connected
class _DeviceMonitor(Thread):
def __init__(self, device_callback, polling_delay=5.0):
self.device_callback = device_callback
self.polling_delay = polling_delay
self.prev_devices = None
# daemon threads are automatically killed when main thread exits
super().__init__(daemon=True)
def run(self):
# Populate initial set of devices so startup doesn't cause any callbacks
self.prev_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()}
# Continously enumerate devices and raise callback for changes
while True:
current_devices = {tuple(dev.items()): dev for dev in _enumerate_devices()}
for key, device in self.prev_devices.items():
if key not in current_devices:
self.device_callback(ACTION_REMOVE, device)
for key, device in current_devices.items():
if key not in self.prev_devices:
self.device_callback(ACTION_ADD, device)
self.prev_devices = current_devices
sleep(self.polling_delay)
def _match(
action: str,
device: dict[str, Any],
filter_func: Callable[[int, int, int, bool, bool], dict[str, Any]],
):
"""
The filter_func is used to determine whether this is a device of
interest to Solaar. It is given the bus id, vendor id, and product
id and returns a dictionary with the required hid_driver and
usb_interface and whether this is a receiver or device.
"""
vid = device["vendor_id"]
pid = device["product_id"]
hid_bus_type = device["bus_type"]
# Translate hidapi bus_type to the bus_id values Solaar expects
if device.get("bus_type") == 0x01:
bus_id = 0x03 # USB
elif device.get("bus_type") == 0x02:
bus_id = 0x05 # Bluetooth
else:
bus_id = None
logger.info(f"Device {device['path']} has an unsupported bus type {hid_bus_type:02X}")
return None
# Skip unlikely devices with all-zero VID PID or unsupported bus IDs
if vid == 0 and pid == 0:
logger.info(f"Device {device['path']} has all-zero VID and PID")
logger.info(f"Skipping unlikely device {device['path']} ({bus_id}/{vid:04X}/{pid:04X})")
return None
# Check for hidpp support
device["hidpp_short"] = False
device["hidpp_long"] = False
device_handle = None
def check_hidpp_short():
report = _get_input_report(device_handle, 0x10, 32)
if len(report) == 1 + 6 and report[0] == 0x10:
device["hidpp_short"] = True
def check_hidpp_long():
report = _get_input_report(device_handle, 0x11, 32)
if len(report) == 1 + 19 and report[0] == 0x11:
device["hidpp_long"] = True
try:
device_handle = open_path(device["path"])
for check_func in (check_hidpp_short, check_hidpp_long):
try:
check_func()
except HIDError as e:
logger.info(
f"Error while {check_func.__name__}"
f"on device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}"
)
except HIDError as e:
logger.info(f"Error opening device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}")
finally:
if device_handle:
close(device_handle)
logger.info(
"Found device BID %s VID %04X PID %04X HID++ SHORT %s LONG %s",
bus_id,
vid,
pid,
device["hidpp_short"],
device["hidpp_long"],
)
if not device["hidpp_short"] and not device["hidpp_long"]:
return None
filtered_result = filter_func(bus_id, vid, pid, device["hidpp_short"], device["hidpp_long"])
if not filtered_result:
return
is_device = filtered_result.get("isDevice")
if action == ACTION_ADD:
d_info = DeviceInfo(
path=device["path"].decode(),
bus_id=bus_id,
vendor_id=f"{vid:04X}", # noqa
product_id=f"{pid:04X}", # noqa
interface=None,
driver=None,
manufacturer=device["manufacturer_string"],
product=device["product_string"],
serial=device["serial_number"],
release=device["release_number"],
isDevice=is_device,
hidpp_short=device["hidpp_short"],
hidpp_long=device["hidpp_long"],
)
return d_info
elif action == ACTION_REMOVE:
d_info = DeviceInfo(
path=device["path"].decode(),
bus_id=None,
vendor_id=f"{vid:04X}", # noqa
product_id=f"{pid:04X}", # noqa
interface=None,
driver=None,
manufacturer=None,
product=None,
serial=None,
release=None,
isDevice=is_device,
hidpp_short=None,
hidpp_long=None,
)
return d_info
logger.info(f"Finished checking HIDPP support for device {device['path']} ({bus_id}/{vid:04X}/{pid:04X})")
def find_paired_node(receiver_path: str, index: int, timeout: int):
"""Find the node of a device paired with a receiver"""
return None
def find_paired_node_wpid(receiver_path: str, index: int):
"""Find the node of a device paired with a receiver, get wpid from udev"""
return None
def monitor_glib(
glib: GLib,
callback: Callable,
filter_func: Callable[[int, int, int, bool, bool], dict[str, Any]],
) -> None:
"""Monitor GLib.
Parameters
----------
glib
GLib instance.
callback
Called when device found.
filter_func
Filter devices callback.
"""
def device_callback(action: str, device):
if action == ACTION_ADD:
d_info = _match(action, device, filter_func)
if d_info:
glib.idle_add(callback, action, d_info)
elif action == ACTION_REMOVE:
# Removed devices will be detected by Solaar directly
pass
monitor = _DeviceMonitor(device_callback=device_callback)
monitor.start()
def enumerate(filter_func) -> DeviceInfo:
"""Enumerate the HID Devices.
List all the HID devices attached to the system, optionally filtering by
vendor_id, product_id, and/or interface_number.
:returns: a list of matching ``DeviceInfo`` tuples.
"""
for device in _enumerate_devices():
d_info = _match(ACTION_ADD, device, filter_func)
if d_info:
yield d_info
def open(vendor_id, product_id, serial=None):
"""Open a HID device by its Vendor ID, Product ID and optional serial number.
If no serial is provided, the first device with the specified IDs is opened.
:returns: an opaque device handle, or ``None``.
"""
if serial is not None:
serial = ctypes.create_unicode_buffer(serial)
device_handle = _hidapi.hid_open(vendor_id, product_id, serial)
if device_handle is None:
raise HIDError(_hidapi.hid_error(None))
return device_handle
def open_path(device_path: str) -> int:
"""Open a HID device by its path name.
:param device_path: the path of a ``DeviceInfo`` tuple returned by enumerate().
:returns: an opaque device handle, or ``None``.
"""
if not isinstance(device_path, bytes):
device_path = device_path.encode()
device_handle = _hidapi.hid_open_path(device_path)
if device_handle is None:
raise HIDError(_hidapi.hid_error(None))
return device_handle
def close(device_handle) -> None:
"""Close a HID device.
:param device_handle: a device handle returned by open() or open_path().
"""
assert device_handle
_hidapi.hid_close(device_handle)
def write(device_handle: int, data: bytes) -> int:
"""Write an Output report to a HID device.
:param device_handle: a device handle returned by open() or open_path().
:param data: the data bytes to send including the report number as the
first byte.
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. For example,
if a hid report is 16 bytes long, 17 bytes must be passed to
hid_write(), the Report ID (or 0x0, for devices with a
single report), followed by the report data (16 bytes). In
this example, the length passed in would be 17.
write() will send the data on the first OUT endpoint, if
one exists. If it does not, it will send the data through
the Control Endpoint (Endpoint 0).
"""
assert device_handle
assert data
assert isinstance(data, bytes), (repr(data), type(data))
bytes_written = _hidapi.hid_write(device_handle, data, len(data))
if bytes_written < 0:
raise HIDError(_hidapi.hid_error(device_handle))
return bytes_written
def read(device_handle, bytes_count, timeout_ms=None):
"""Read an Input report from a HID device.
:param device_handle: a device handle returned by open() or open_path().
:param bytes_count: maximum number of bytes to read.
:param timeout_ms: can be -1 (default) to wait for data indefinitely, 0 to
read whatever is in the device's input buffer, or a positive integer to
wait that many milliseconds.
Input reports are returned to the host through the INTERRUPT IN endpoint.
The first byte will contain the Report number if the device uses numbered
reports.
:returns: the data packet read, an empty bytes string if a timeout was
reached, or None if there was an error while reading.
"""
assert device_handle
data = ctypes.create_string_buffer(bytes_count)
if timeout_ms is None or timeout_ms < 0:
bytes_read = _hidapi.hid_read(device_handle, data, bytes_count)
else:
bytes_read = _hidapi.hid_read_timeout(device_handle, data, bytes_count, timeout_ms)
if bytes_read < 0:
raise HIDError(_hidapi.hid_error(device_handle))
return data.raw[:bytes_read]
def _get_input_report(device_handle, report_id, size):
assert device_handle
data = ctypes.create_string_buffer(size)
data[0] = bytearray((report_id,))
size = _hidapi.hid_get_input_report(device_handle, data, size)
if size < 0:
raise HIDError(_hidapi.hid_error(device_handle))
return data.raw[:size]
def _readstring(device_handle, func, max_length=255):
assert device_handle
buf = ctypes.create_unicode_buffer(max_length)
ret = func(device_handle, buf, max_length)
if ret < 0:
raise HIDError("Error reading device property")
return buf.value
def get_manufacturer(device_handle):
"""Get the Manufacturer String from a HID device.
:param device_handle: a device handle returned by open() or open_path().
"""
return _readstring(device_handle, _hidapi.get_manufacturer_string)
def get_product(device_handle):
"""Get the Product String from a HID device.
:param device_handle: a device handle returned by open() or open_path().
"""
return _readstring(device_handle, _hidapi.get_product_string)
def get_serial(device_handle):
"""Get the serial number from a HID device.
:param device_handle: a device handle returned by open() or open_path().
"""
return _readstring(device_handle, _hidapi.get_serial_number_string)

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -14,46 +16,47 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import argparse
import os
import os.path
import platform
import readline
import sys
import time
from binascii import hexlify
from binascii import unhexlify
from select import select
from binascii import hexlify, unhexlify
from select import select as _select
from threading import Lock
from threading import Thread
if platform.system() == "Linux":
import hidapi.udev_impl as hidapi
else:
import hidapi.hidapi_impl as hidapi
import hidapi as _hid
LOGITECH_VENDOR_ID = 0x046D
#
#
#
try:
read_packet = raw_input
except NameError:
# Python 3 equivalent of raw_input
read_packet = input
interactive = os.isatty(0)
prompt = "?? Input: " if interactive else ""
prompt = '?? Input: ' if interactive else ''
start_time = time.time()
strhex = lambda d: hexlify(d).decode('ascii').upper()
def strhex(d):
return hexlify(d).decode("ascii").upper()
#
#
#
print_lock = Lock()
del Lock
def _print(marker, data, scroll=False):
t = time.time() - start_time
if isinstance(data, str):
s = f"{marker} {data}"
s = marker + ' ' + data
else:
hexs = strhex(data)
s = "%s (% 8.3f) [%s %s %s %s] %s" % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
s = '%s (% 8.3f) [%s %s %s %s] %s' % (marker, t, hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
with print_lock:
# allow only one thread at a time to write to the console, otherwise
@ -62,18 +65,18 @@ def _print(marker, data, scroll=False):
if interactive and scroll:
# scroll the entire screen above the current line up by 1 line
sys.stdout.write(
"\033[s" # save cursor position
"\033[S" # scroll up
"\033[A" # cursor up
"\033[L" # insert 1 line
"\033[G"
'\033[s' # save cursor position
'\033[S' # scroll up
'\033[A' # cursor up
'\033[L' # insert 1 line
'\033[G'
) # move cursor to column 1
sys.stdout.write(s)
if interactive and scroll:
# restore cursor position
sys.stdout.write("\033[u")
sys.stdout.write('\033[u')
else:
sys.stdout.write("\n")
sys.stdout.write('\n')
# flush stdout manually...
# because trying to open stdin/out unbuffered programmatically
@ -82,99 +85,107 @@ def _print(marker, data, scroll=False):
def _error(text, scroll=False):
_print("!!", text, scroll)
_print('!!', text, scroll)
def _continuous_read(handle, timeout=2000):
while True:
try:
reply = hidapi.read(handle, 128, timeout)
reply = _hid.read(handle, 128, timeout)
except OSError as e:
_error(f"Read failed, aborting: {str(e)}", True)
_error('Read failed, aborting: ' + str(e), True)
break
assert reply is not None
if reply:
_print(">>", reply, True)
_print('>>', reply, True)
def _validate_input(line, hidpp=False):
try:
data = unhexlify(line.encode("ascii"))
data = unhexlify(line.encode('ascii'))
except Exception as e:
_error(f"Invalid input: {str(e)}")
_error('Invalid input: ' + str(e))
return None
if hidpp:
if len(data) < 4:
_error("Invalid HID++ request: need at least 4 bytes")
_error('Invalid HID++ request: need at least 4 bytes')
return None
if data[:1] not in b"\x10\x11":
_error("Invalid HID++ request: first byte must be 0x10 or 0x11")
if data[:1] not in b'\x10\x11':
_error('Invalid HID++ request: first byte must be 0x10 or 0x11')
return None
if data[1:2] not in b"\xff\x00\x01\x02\x03\x04\x05\x06\x07":
_error("Invalid HID++ request: second byte must be 0xFF or one of 0x00..0x07")
if data[1:2] not in b'\xFF\x01\x02\x03\x04\x05\x06':
_error('Invalid HID++ request: second byte must be 0xFF or one of 0x01..0x06')
return None
if data[:1] == b"\x10":
if data[:1] == b'\x10':
if len(data) > 7:
_error("Invalid HID++ request: maximum length of a 0x10 request is 7 bytes")
_error('Invalid HID++ request: maximum length of a 0x10 request is 7 bytes')
return None
while len(data) < 7:
data = (data + b"\x00" * 7)[:7]
elif data[:1] == b"\x11":
data = (data + b'\x00' * 7)[:7]
elif data[:1] == b'\x11':
if len(data) > 20:
_error("Invalid HID++ request: maximum length of a 0x11 request is 20 bytes")
_error('Invalid HID++ request: maximum length of a 0x11 request is 20 bytes')
return None
while len(data) < 20:
data = (data + b"\x00" * 20)[:20]
data = (data + b'\x00' * 20)[:20]
return data
def _open(args):
def matchfn(bid, vid, pid, _a, _b):
if vid == LOGITECH_VENDOR_ID:
return {"vid": vid}
def matchfn(bid, vid, pid):
if vid == 0x046d:
return {'vid': 0x046d}
device = args.device
if args.hidpp and not device:
for d in hidapi.enumerate(matchfn):
if d.driver == "logitech-djreceiver":
for d in _hid.enumerate(matchfn):
if d.driver == 'logitech-djreceiver':
device = d.path
break
if not device:
sys.exit("!! No HID++ receiver found.")
sys.exit('!! No HID++ receiver found.')
if not device:
sys.exit("!! Device path required.")
sys.exit('!! Device path required.')
print(".. Opening device", device)
handle = hidapi.open_path(device)
print('.. Opening device', device)
handle = _hid.open_path(device)
if not handle:
sys.exit(f"!! Failed to open {device}, aborting.")
sys.exit('!! Failed to open %s, aborting.' % device)
print(
".. Opened handle %r, vendor %r product %r serial %r."
% (handle, hidapi.get_manufacturer(handle), hidapi.get_product(handle), hidapi.get_serial(handle))
'.. Opened handle %r, vendor %r product %r serial %r.' %
(handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle))
)
if args.hidpp:
if hidapi.get_manufacturer(handle) is not None and hidapi.get_manufacturer(handle) != b"Logitech":
sys.exit("!! Only Logitech devices support the HID++ protocol.")
print(".. HID++ validation enabled.")
if _hid.get_manufacturer(handle) != b'Logitech':
sys.exit('!! Only Logitech devices support the HID++ protocol.')
print('.. HID++ validation enabled.')
else:
if hidapi.get_manufacturer(handle) == b"Logitech" and b"Receiver" in hidapi.get_product(handle):
if (_hid.get_manufacturer(handle) == b'Logitech' and b'Receiver' in _hid.get_product(handle)):
args.hidpp = True
print(".. Logitech receiver detected, HID++ validation enabled.")
print('.. Logitech receiver detected, HID++ validation enabled.')
return handle
#
#
#
def _parse_arguments():
import argparse
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("--history", help="history file (default ~/.hidconsole-history)")
arg_parser.add_argument("--hidpp", action="store_true", help="ensure input data is a valid HID++ request")
arg_parser.add_argument('--history', help='history file (default ~/.hidconsole-history)')
arg_parser.add_argument('--hidpp', action='store_true', help='ensure input data is a valid HID++ request')
arg_parser.add_argument(
"device",
nargs="?",
help="linux device to connect to (/dev/hidrawX); "
"may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver",
'device',
nargs='?',
help='linux device to connect to (/dev/hidrawX); '
'may be omitted if --hidpp is given, in which case it looks for the first Logitech receiver'
)
return arg_parser.parse_args()
@ -184,10 +195,12 @@ def main():
handle = _open(args)
if interactive:
print(".. Press ^C/^D to exit, or type hex bytes to write to the device.")
print('.. Press ^C/^D to exit, or type hex bytes to write to the device.')
import readline
if args.history is None:
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
import os.path
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
try:
readline.read_history_file(args.history)
except Exception:
@ -195,17 +208,18 @@ def main():
pass
try:
t = Thread(target=_continuous_read, args=(handle,))
from threading import Thread
t = Thread(target=_continuous_read, args=(handle, ))
t.daemon = True
t.start()
if interactive:
# move the cursor at the bottom of the screen
sys.stdout.write("\033[300B") # move cusor at most 300 lines down, don't scroll
sys.stdout.write('\033[300B') # move cusor at most 300 lines down, don't scroll
while t.is_alive():
line = input(prompt)
line = line.strip().replace(" ", "")
line = read_packet(prompt)
line = line.strip().replace(' ', '')
# print ("line", line)
if not line:
continue
@ -214,12 +228,12 @@ def main():
if data is None:
continue
_print("<<", data)
hidapi.write(handle, data)
_print('<<', data)
_hid.write(handle, data)
# wait for some kind of reply
if args.hidpp and not interactive:
rlist, wlist, xlist = select([handle], [], [], 1)
if data[1:2] == b"\xff":
rlist, wlist, xlist = _select([handle], [], [], 1)
if data[1:2] == b'\xFF':
# the receiver will reply very fast, in a few milliseconds
time.sleep(0.010)
else:
@ -227,16 +241,16 @@ def main():
time.sleep(0.700)
except EOFError:
if interactive:
print("")
print('')
else:
time.sleep(1)
finally:
print(f".. Closing handle {handle!r}")
hidapi.close(handle)
print('.. Closing handle %r' % handle)
_hid.close(handle)
if interactive:
readline.write_history_file(args.history)
if __name__ == "__main__":
if __name__ == '__main__':
main()

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -13,7 +15,6 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Generic Human Interface Device API.
It is currently a partial pure-Python implementation of the native HID API
@ -23,37 +24,47 @@ The docstrings are mostly copied from the hidapi API header, with changes where
necessary.
"""
from __future__ import annotations
import errno
import logging
import os
import typing
import warnings
import errno as _errno
import os as _os
import warnings as _warnings
# the tuple object we'll expose when enumerating devices
from select import select
from collections import namedtuple
from logging import INFO as _INFO
from logging import getLogger
from select import select as _select
from time import sleep
from time import time
from typing import Callable
from time import time as _timestamp
import pyudev
from hidapi.common import DeviceInfo
if typing.TYPE_CHECKING:
import gi
gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402
logger = logging.getLogger(__name__)
from pyudev import Context as _Context
from pyudev import Device as _Device
from pyudev import DeviceNotFoundError
from pyudev import Devices as _Devices
from pyudev import Monitor as _Monitor
_log = getLogger(__name__)
del getLogger
native_implementation = 'udev'
fileopen = open
ACTION_ADD = "add"
ACTION_REMOVE = "remove"
DeviceInfo = namedtuple(
'DeviceInfo', [
'path',
'bus_id',
'vendor_id',
'product_id',
'interface',
'driver',
'manufacturer',
'product',
'serial',
'release',
'isDevice',
'hidpp_short',
'hidpp_long',
]
)
del namedtuple
#
# exposed API
@ -79,34 +90,31 @@ def exit():
return True
def _match(action: str, device, filter_func: typing.Callable[[int, int, int, bool, bool], dict[str, typing.Any]]):
"""
The filter_func is used to determine whether this is a device of
interest to Solaar. It is given the bus id, vendor id, and product
id and returns a dictionary with the required hid_driver and
usb_interface and whether this is a receiver or device."""
logger.debug(f"Dbus event {action} {device}")
hid_device = device.find_parent("hid")
if hid_device is None: # only HID devices are of interest to Solaar
# The filterfn is used to determine whether this is a device of interest to Solaar.
# It is given the bus id, vendor id, and product id and returns a dictionary
# with the required hid_driver and usb_interface and whether this is a receiver or device.
def _match(action, device, filterfn):
hid_device = device.find_parent('hid')
if not hid_device: # only HID devices are of interest to Solaar
return
hid_id = hid_device.properties.get("HID_ID")
hid_id = hid_device.get('HID_ID')
if not hid_id:
return # there are reports that sometimes the id isn't set up right so be defensive
bid, vid, pid = hid_id.split(":")
hid_hid_device = hid_device.find_parent("hid")
if hid_hid_device is not None:
bid, vid, pid = hid_id.split(':')
hid_hid_device = hid_device.find_parent('hid')
if hid_hid_device:
return # these are devices connected through a receiver so don't pick them up here
try: # if report descriptor does not indicate HID++ capabilities then this device is not of interest to Solaar
from hid_parser import ReportDescriptor
from hid_parser import ReportDescriptor as _ReportDescriptor
# from hid_parser import Usage as _Usage
hidpp_short = hidpp_long = False
devfile = "/sys" + hid_device.properties.get("DEVPATH") + "/report_descriptor"
with fileopen(devfile, "rb") as fd:
with warnings.catch_warnings():
warnings.simplefilter("ignore")
rd = ReportDescriptor(fd.read())
devfile = '/sys' + hid_device.get('DEVPATH') + '/report_descriptor'
with fileopen(devfile, 'rb') as fd:
with _warnings.catch_warnings():
_warnings.simplefilter('ignore')
rd = _ReportDescriptor(fd.read())
hidpp_short = 0x10 in rd.input_report_ids and 6 * 8 == int(rd.get_input_report_size(0x10))
# and _Usage(0xFF00, 0x0001) in rd.get_input_items(0x10)[0].usages # be more permissive
hidpp_long = 0x11 in rd.input_report_ids and 19 * 8 == int(rd.get_input_report_size(0x11))
@ -114,42 +122,37 @@ def _match(action: str, device, filter_func: typing.Callable[[int, int, int, boo
if not hidpp_short and not hidpp_long:
return
except Exception as e: # if can't process report descriptor fall back to old scheme
hidpp_short = None
hidpp_long = None
logger.info(
"Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s",
device.device_node,
bid,
vid,
pid,
e,
)
hidpp_short = hidpp_long = None
_log.warn('Report Descriptor not processed for BID %s VID %s PID %s: %s', bid, vid, pid, e)
filtered_result = filter_func(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long)
if not filtered_result:
filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long)
if not filter:
return
interface_number = filtered_result.get("usb_interface")
isDevice = filtered_result.get("isDevice")
hid_driver = filter.get('hid_driver')
interface_number = filter.get('usb_interface')
isDevice = filter.get('isDevice')
if action == ACTION_ADD:
hid_driver_name = hid_device.properties.get("DRIVER")
intf_device = device.find_parent("usb", "usb_interface")
usb_interface = None if intf_device is None else intf_device.attributes.asint("bInterfaceNumber")
if action == 'add':
hid_driver_name = hid_device.get('DRIVER')
# print ("** found hid", action, device, "hid:", hid_device, hid_driver_name)
if hid_driver:
if isinstance(hid_driver, tuple):
if hid_driver_name not in hid_driver:
return
elif hid_driver_name != hid_driver:
return
intf_device = device.find_parent('usb', 'usb_interface')
usb_interface = None if intf_device is None else intf_device.attributes.asint('bInterfaceNumber')
# print('*** usb interface', action, device, 'usb_interface:', intf_device, usb_interface, interface_number)
logger.info(
"Found device %s BID %s VID %s PID %s HID++ %s %s USB %s %s",
device.device_node,
bid,
vid,
pid,
hidpp_short,
hidpp_long,
usb_interface,
interface_number,
)
if _log.isEnabledFor(_INFO):
_log.info(
'Found device BID %s VID %s PID %s HID++ %s %s USB %s %s', bid, vid, pid, hidpp_short, hidpp_long,
usb_interface, interface_number
)
if not (hidpp_short or hidpp_long or interface_number is None or interface_number == usb_interface):
return
attrs = intf_device.attributes if intf_device is not None else None
attrs = intf_device.attributes if intf_device else None
d_info = DeviceInfo(
path=device.device_node,
@ -158,17 +161,19 @@ def _match(action: str, device, filter_func: typing.Callable[[int, int, int, boo
product_id=pid[-4:],
interface=usb_interface,
driver=hid_driver_name,
manufacturer=attrs.get("manufacturer") if attrs else None,
product=attrs.get("product") if attrs else None,
serial=hid_device.properties.get("HID_UNIQ"),
release=attrs.get("bcdDevice") if attrs else None,
manufacturer=attrs.get('manufacturer') if attrs else None,
product=attrs.get('product') if attrs else None,
serial=hid_device.get('HID_UNIQ'),
release=attrs.get('bcdDevice') if attrs else None,
isDevice=isDevice,
hidpp_short=hidpp_short,
hidpp_long=hidpp_long,
)
return d_info
elif action == ACTION_REMOVE:
elif action == 'remove':
# print (dict(device), dict(usb_device))
d_info = DeviceInfo(
path=device.device_node,
bus_id=None,
@ -187,41 +192,41 @@ def _match(action: str, device, filter_func: typing.Callable[[int, int, int, boo
return d_info
def find_paired_node(receiver_path: str, index: int, timeout: int):
def find_paired_node(receiver_path, index, timeout):
"""Find the node of a device paired with a receiver"""
context = pyudev.Context()
receiver_phys = pyudev.Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
context = _Context()
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS')
if not receiver_phys:
return None
phys = f"{receiver_phys}:{index}" # noqa: E231
timeout += time()
delta = time()
phys = f'{receiver_phys}:{index}'
timeout += _timestamp()
delta = _timestamp()
while delta < timeout:
for dev in context.list_devices(subsystem="hidraw"):
dev_phys = dev.find_parent("hid").get("HID_PHYS")
for dev in context.list_devices(subsystem='hidraw'):
dev_phys = dev.find_parent('hid').get('HID_PHYS')
if dev_phys and dev_phys == phys:
return dev.device_node
delta = time()
delta = _timestamp()
return None
def find_paired_node_wpid(receiver_path: str, index: int):
def find_paired_node_wpid(receiver_path, index):
"""Find the node of a device paired with a receiver, get wpid from udev"""
context = pyudev.Context()
receiver_phys = pyudev.Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
context = _Context()
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS')
if not receiver_phys:
return None
phys = f"{receiver_phys}:{index}" # noqa: E231
for dev in context.list_devices(subsystem="hidraw"):
dev_phys = dev.find_parent("hid").get("HID_PHYS")
phys = f'{receiver_phys}:{index}'
for dev in context.list_devices(subsystem='hidraw'):
dev_phys = dev.find_parent('hid').get('HID_PHYS')
if dev_phys and dev_phys == phys:
# get hid id like 0003:0000046D:00000065
hid_id = dev.find_parent("hid").get("HID_ID")
hid_id = dev.find_parent('hid').get('HID_ID')
# get wpid - last 4 symbols
udev_wpid = hid_id[-4:]
return udev_wpid
@ -229,48 +234,55 @@ def find_paired_node_wpid(receiver_path: str, index: int):
return None
def monitor_glib(glib: GLib, callback: Callable, filter_func: Callable):
"""Monitor GLib.
def monitor_glib(callback, filterfn):
from gi.repository import GLib
Parameters
----------
glib
GLib instance.
"""
c = pyudev.Context()
m = pyudev.Monitor.from_netlink(c)
m.filter_by(subsystem="hidraw")
c = _Context()
def _process_udev_event(monitor, condition, cb, filter_func):
if condition == glib.IO_IN:
# already existing devices
# for device in c.list_devices(subsystem='hidraw'):
# # print (device, dict(device), dict(device.attributes))
# for filter in device_filters:
# d_info = _match('add', device, *filter)
# if d_info:
# GLib.idle_add(callback, 'add', d_info)
# break
m = _Monitor.from_netlink(c)
m.filter_by(subsystem='hidraw')
def _process_udev_event(monitor, condition, cb, filterfn):
if condition == GLib.IO_IN:
event = monitor.receive_device()
if event:
action, device = event
# print ("***", action, device)
if action == ACTION_ADD:
d_info = _match(action, device, filter_func)
if action == 'add':
d_info = _match(action, device, filterfn)
if d_info:
glib.idle_add(cb, action, d_info)
elif action == ACTION_REMOVE:
GLib.idle_add(cb, action, d_info)
elif action == 'remove':
# the GLib notification does _not_ match!
pass
return True
try:
# io_add_watch_full may not be available...
glib.io_add_watch_full(m, glib.PRIORITY_LOW, glib.IO_IN, _process_udev_event, callback, filter_func)
GLib.io_add_watch_full(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, filterfn)
# print ("did io_add_watch_full")
except AttributeError:
try:
# and the priority parameter appeared later in the API
glib.io_add_watch(m, glib.PRIORITY_LOW, glib.IO_IN, _process_udev_event, callback, filter_func)
GLib.io_add_watch(m, GLib.PRIORITY_LOW, GLib.IO_IN, _process_udev_event, callback, filterfn)
# print ("did io_add_watch with priority")
except Exception:
glib.io_add_watch(m, glib.IO_IN, _process_udev_event, callback, filter_func)
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, filterfn)
# print ("did io_add_watch")
logger.debug("Starting dbus monitoring")
m.start()
def enumerate(filter_func: typing.Callable[[int, int, int, bool, bool], dict[str, typing.Any]]):
def enumerate(filterfn):
"""Enumerate the HID Devices.
List all the HID devices attached to the system, optionally filtering by
@ -279,9 +291,8 @@ def enumerate(filter_func: typing.Callable[[int, int, int, bool, bool], dict[str
:returns: a list of matching ``DeviceInfo`` tuples.
"""
logger.debug("Starting dbus enumeration")
for dev in pyudev.Context().list_devices(subsystem="hidraw"):
dev_info = _match(ACTION_ADD, dev, filter_func)
for dev in _Context().list_devices(subsystem='hidraw'):
dev_info = _match('add', dev, filterfn)
if dev_info:
yield dev_info
@ -310,29 +321,17 @@ def open_path(device_path):
:returns: an opaque device handle, or ``None``.
"""
assert device_path
assert device_path.startswith("/dev/hidraw")
logger.info("OPEN PATH %s", device_path)
retrycount = 0
while retrycount < 3:
retrycount += 1
try:
return os.open(device_path, os.O_RDWR | os.O_SYNC)
except OSError as e:
logger.info("OPEN PATH FAILED %s ERROR %s %s", device_path, e.errno, e)
if e.errno == errno.EACCES:
sleep(0.1)
else:
raise e
assert device_path.startswith('/dev/hidraw')
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
def close(device_handle) -> None:
def close(device_handle):
"""Close a HID device.
:param device_handle: a device handle returned by open() or open_path().
"""
assert device_handle
os.close(device_handle)
_os.close(device_handle)
def write(device_handle, data):
@ -361,17 +360,17 @@ def write(device_handle, data):
assert isinstance(data, bytes), (repr(data), type(data))
retrycount = 0
bytes_written = 0
while retrycount < 3:
while (retrycount < 3):
try:
retrycount += 1
bytes_written = os.write(device_handle, data)
bytes_written = _os.write(device_handle, data)
except OSError as e:
if e.errno == errno.EPIPE:
if e.errno == _errno.EPIPE:
sleep(0.1)
else:
break
if bytes_written != len(data):
raise OSError(errno.EIO, f"written {int(bytes_written)} bytes out of expected {len(data)}")
raise OSError(_errno.EIO, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
def read(device_handle, bytes_count, timeout_ms=-1):
@ -392,26 +391,26 @@ def read(device_handle, bytes_count, timeout_ms=-1):
"""
assert device_handle
timeout = None if timeout_ms < 0 else timeout_ms / 1000.0
rlist, wlist, xlist = select([device_handle], [], [device_handle], timeout)
rlist, wlist, xlist = _select([device_handle], [], [device_handle], timeout)
if xlist:
assert xlist == [device_handle]
raise OSError(errno.EIO, f"exception on file descriptor {int(device_handle)}")
raise OSError(_errno.EIO, 'exception on file descriptor %d' % device_handle)
if rlist:
assert rlist == [device_handle]
data = os.read(device_handle, bytes_count)
data = _os.read(device_handle, bytes_count)
assert data is not None
assert isinstance(data, bytes), (repr(data), type(data))
return data
else:
return b""
return b''
_DEVICE_STRINGS = {
0: "manufacturer",
1: "product",
2: "serial",
0: 'manufacturer',
1: 'product',
2: 'serial',
}
@ -456,22 +455,22 @@ def get_indexed_string(device_handle, index):
return None
assert device_handle
stat = os.fstat(device_handle)
stat = _os.fstat(device_handle)
try:
dev = pyudev.Devices.from_device_number(pyudev.Context(), "char", stat.st_rdev)
except (pyudev.DeviceNotFoundError, ValueError):
dev = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
except (DeviceNotFoundError, ValueError):
return None
hid_dev = dev.find_parent("hid")
hid_dev = dev.find_parent('hid')
if hid_dev:
assert "HID_ID" in hid_dev
bus, _ignore, _ignore = hid_dev["HID_ID"].split(":")
assert 'HID_ID' in hid_dev
bus, _ignore, _ignore = hid_dev['HID_ID'].split(':')
if bus == "0003": # USB
usb_dev = dev.find_parent("usb", "usb_device")
if bus == '0003': # USB
usb_dev = dev.find_parent('usb', 'usb_device')
assert usb_dev
return usb_dev.attributes.get(key)
elif bus == "0005": # BLUETOOTH
elif bus == '0005': # BLUETOOTH
# TODO
pass

View File

@ -1,39 +1,39 @@
#!/usr/bin/env python3
"""Extract key symbol encodings from X11 header files."""
from pathlib import Path
from pprint import pprint
from re import findall
from subprocess import run
from tempfile import TemporaryDirectory
repo = "https://gitlab.freedesktop.org/xorg/proto/xorgproto.git"
pattern = r"#define XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?"
xf86pattern = r"#define XF86XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?"
repo = 'https://github.com/freedesktop/xorg-proto-x11proto.git'
pattern = r'#define XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?'
xf86pattern = r'#define XF86XK_(\w+)\s+0x(\w+)(?:\s+/\*\s+U\+(\w+))?'
def main():
keysymdef = {}
keysym_files = [
("include/X11/keysymdef.h", pattern, ""),
("include/X11/XF86keysym.h", xf86pattern, "XF86_"),
]
with TemporaryDirectory() as temp:
run(["git", "clone", repo, "."], cwd=temp)
run(['git', 'clone', repo, '.'], cwd=temp)
text = Path(temp, 'keysymdef.h').read_text()
for name, sym, uni in findall(pattern, text):
sym = int(sym, 16)
uni = int(uni, 16) if uni else None
if keysymdef.get(name, None):
print('KEY DUP', name)
keysymdef[name] = sym
text = Path(temp, 'XF86keysym.h').read_text()
for name, sym, uni in findall(xf86pattern, text):
sym = int(sym, 16)
uni = int(uni, 16) if uni else None
if keysymdef.get('XF86_' + name, None):
print('KEY DUP', 'XF86_' + name)
keysymdef['XF86_' + name] = sym
for filename, extraction_pattern, prefix in keysym_files:
text = Path(temp, filename).read_text()
for name, sym, _ in findall(extraction_pattern, text):
sym = int(sym, 16)
if keysymdef.get(f"{prefix}{name}", None):
print(f"KEY DUP {prefix}{name}")
keysymdef[f"{prefix}{name}"] = sym
with open("keysymdef.py", "w") as f:
f.write("# flake8: noqa\nkey_symbols = \\\n")
with open('keysymdef.py', 'w') as f:
f.write('# flake8: noqa\nkeysymdef = \\\n')
pprint(keysymdef, f)
if __name__ == "__main__":
if __name__ == '__main__':
main()

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -13,12 +15,35 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Low-level interface for devices using Logitech HID++ protocol.
"""Low-level interface for devices connected through a Logitech Universal
Receiver (UR).
Uses the HID api exposed through hidapi_impl.py, a Python thin layer over a native
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
implementation.
Incomplete. Based on a bit of documentation, trial-and-error, and guesswork.
References:
http://julien.danjou.info/blog/2012/logitech-k750-linux-support
http://6xq.net/git/lars/lshidpp.git/plain/doc/
"""
import logging
logger = logging.getLogger(__name__)
from . import listener, status # noqa: F401
from .base import DeviceUnreachable, NoReceiver, NoSuchDevice # noqa: F401
from .common import strhex # noqa: F401
from .device import Device # noqa: F401
from .hidpp20 import FeatureCallError, FeatureNotSupported # noqa: F401
from .receiver import Receiver # noqa: F401
_DEBUG = logging.DEBUG
_log = logging.getLogger(__name__)
_log.setLevel(logging.root.level)
# if logging.root.level > logging.DEBUG:
# _log.addHandler(logging.NullHandler())
# _log.propagate = 0
del logging
__version__ = '0.9'

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -14,80 +16,38 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Base low-level functions as API for upper layers."""
# Base low-level functions used by the API proper.
# Unlikely to be used directly unless you're expanding the API.
from __future__ import annotations
import dataclasses
import logging
import platform
import struct
import threading
import typing
import threading as _threading
from collections import namedtuple
from contextlib import contextmanager
from random import getrandbits
from time import time
from typing import Any
from typing import Callable
from logging import DEBUG as _DEBUG
from logging import INFO as _INFO
from logging import getLogger
from random import getrandbits as _random_bits
from struct import pack as _pack
from time import time as _timestamp
from . import base_usb
from . import common
from . import descriptors
from . import exceptions
from .common import LOGITECH_VENDOR_ID
from .common import BusID
from .hidpp10_constants import ErrorCode as Hidpp10ErrorCode
from .hidpp20_constants import ErrorCode as Hidpp20ErrorCode
import hidapi as _hid
if typing.TYPE_CHECKING:
import gi
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from .base_usb import ALL as _RECEIVER_USB_IDS
from .base_usb import DEVICES as _DEVICE_IDS
from .base_usb import other_device_check as _other_device_check
from .common import KwException as _KwException
from .common import strhex as _strhex
from hidapi.common import DeviceInfo
_log = getLogger(__name__)
del getLogger
gi.require_version("Gdk", "3.0")
from gi.repository import GLib # NOQA: E402
#
#
#
if platform.system() == "Linux":
import hidapi.udev_impl as hidapi
else:
import hidapi.hidapi_impl as hidapi
logger = logging.getLogger(__name__)
class HIDProtocol(typing.Protocol):
def find_paired_node_wpid(self, receiver_path: str, index: int):
...
def find_paired_node(self, receiver_path: str, index: int, timeout: int):
...
def open(self, vendor_id, product_id, serial=None):
...
def open_path(self, path) -> int:
...
def enumerate(self, filter_func: Callable[[int, int, int, bool, bool], dict[str, typing.Any]]) -> DeviceInfo:
...
def monitor_glib(
self, glib: GLib, callback: Callable, filter_func: Callable[[int, int, int, bool, bool], dict[str, typing.Any]]
) -> None:
...
def read(self, device_handle, bytes_count, timeout_ms):
...
def write(self, device_handle: int, data: bytes) -> int:
...
def close(self, device_handle) -> None:
...
SHORT_MESSAGE_SIZE = 7
_SHORT_MESSAGE_SIZE = 7
_LONG_MESSAGE_SIZE = 20
_MEDIUM_MESSAGE_SIZE = 15
_MAX_READ_SIZE = 32
@ -96,7 +56,13 @@ HIDPP_SHORT_MESSAGE_ID = 0x10
HIDPP_LONG_MESSAGE_ID = 0x11
DJ_MESSAGE_ID = 0x20
# mapping from report_id to message length
report_lengths = {
HIDPP_SHORT_MESSAGE_ID: _SHORT_MESSAGE_SIZE,
HIDPP_LONG_MESSAGE_ID: _LONG_MESSAGE_SIZE,
DJ_MESSAGE_ID: _MEDIUM_MESSAGE_SIZE,
0x21: _MAX_READ_SIZE
}
"""Default timeout on read (in seconds)."""
DEFAULT_TIMEOUT = 4
# the receiver itself should reply very fast, within 500ms
@ -106,156 +72,84 @@ _DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
# when pinging, be extra patient (no longer)
_PING_TIMEOUT = DEFAULT_TIMEOUT
hidapi = typing.cast(HIDProtocol, hidapi)
request_lock = threading.Lock() # serialize all requests
handles_lock = {}
#
# Exceptions that may be raised by this API.
#
@dataclasses.dataclass
class HIDPPNotification:
report_id: int
devnumber: int
sub_id: int
address: int
data: bytes
def __str__(self):
text_as_hex = common.strhex(self.data)
return f"Notification({self.report_id:02x},{self.devnumber},{self.sub_id:02X},{self.address:02X},{text_as_hex})"
class NoReceiver(_KwException):
"""Raised when trying to talk through a previously open handle, when the
receiver is no longer available. Should only happen if the receiver is
physically disconnected from the machine, or its kernel driver module is
unloaded."""
pass
def _usb_device(product_id: int, usb_interface: int) -> dict[str, Any]:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"bus_id": BusID.USB,
"usb_interface": usb_interface,
"isDevice": True,
}
class NoSuchDevice(_KwException):
"""Raised when trying to reach a device number not paired to the receiver."""
pass
def _bluetooth_device(product_id: int) -> dict[str, Any]:
return {"vendor_id": LOGITECH_VENDOR_ID, "product_id": product_id, "bus_id": BusID.BLUETOOTH, "isDevice": True}
class DeviceUnreachable(_KwException):
"""Raised when a request is made to an unreachable (turned off) device."""
pass
KNOWN_DEVICE_IDS = []
for _ignore, d in descriptors.DEVICES.items():
if d.usbid:
usb_interface = d.interface if d.interface else 2
KNOWN_DEVICE_IDS.append(_usb_device(d.usbid, usb_interface))
if d.btid:
KNOWN_DEVICE_IDS.append(_bluetooth_device(d.btid))
#
#
#
def product_information(usb_id: int) -> dict[str, Any]:
"""Returns hardcoded information from USB receiver."""
return base_usb.get_receiver_info(usb_id)
def match(record, bus_id, vendor_id, product_id):
return ((record.get('bus_id') is None or record.get('bus_id') == bus_id)
and (record.get('vendor_id') is None or record.get('vendor_id') == vendor_id)
and (record.get('product_id') is None or record.get('product_id') == product_id))
def filter_receivers(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False):
"""Check that this product is a Logitech receiver and if so return the receiver record for further checking"""
for record in _RECEIVER_USB_IDS: # known receivers
if match(record, bus_id, vendor_id, product_id):
return record
if vendor_id == 0x046D and 0xC500 <= product_id <= 0xC5FF: # unknown receiver
return {'vendor_id': vendor_id, 'product_id': product_id, 'bus_id': bus_id, 'isDevice': False}
def receivers():
"""Enumerate all the receivers attached to the machine."""
yield from hidapi.enumerate(get_known_receiver_info)
yield from _hid.enumerate(filter_receivers)
def filter_products_of_interest(
bus_id: int, vendor_id: int, product_id: int, hidpp_short: bool = False, hidpp_long: bool = False
) -> dict[str, Any] | None:
def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False):
"""Check that this product is of interest and if so return the device record for further checking"""
recv = get_known_receiver_info(bus_id, vendor_id, product_id, hidpp_short, hidpp_long)
if recv: # known or unknown receiver
return recv
device = get_known_device_info(bus_id, vendor_id, product_id)
if device:
return device
if hidpp_short or hidpp_long:
return get_unknown_hid_device_info(bus_id, vendor_id, product_id)
if hidpp_short is None and hidpp_long is None:
return get_unknown_logitech_device_info(bus_id, vendor_id, product_id)
return None
def get_known_device_info(bus_id: int, vendor_id: int, product_id: int) -> dict[str, Any]:
for recv in KNOWN_DEVICE_IDS:
if _match_device(recv, bus_id, vendor_id, product_id):
return recv
def get_unknown_hid_device_info(bus_id: int, vendor_id: int, product_id: int) -> dict[str, Any]:
return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": True}
def get_unknown_logitech_device_info(bus_id: int, vendor_id: int, product_id: int) -> dict[str, Any] | None:
"""Get info from unknown device in Logitech product range.
Check whether product is a Logitech USB-connected or Bluetooth
device based on bus, vendor, and product ID. This allows Solaar to
support receiverless HID++ 2.0 devices that it knows nothing about.
"""
if vendor_id != LOGITECH_VENDOR_ID:
return None
if bus_id == BusID.USB.value and (0xC07D <= product_id <= 0xC094 or 0xC32B <= product_id <= 0xC344):
device_info = _usb_device(product_id, 2)
return device_info
elif bus_id == BusID.BLUETOOTH.value and (0xB012 <= product_id <= 0xB0FF or 0xB317 <= product_id <= 0xB3FF):
device_info = _bluetooth_device(product_id)
return device_info
return None
def _match_device(record: dict[str, Any], bus_id: int, vendor_id: int, product_id: int):
return (
(record.get("bus_id") is None or record.get("bus_id") == bus_id)
and (record.get("vendor_id") is None or record.get("vendor_id") == vendor_id)
and (record.get("product_id") is None or record.get("product_id") == product_id)
)
def get_known_receiver_info(
bus_id: int, vendor_id: int, product_id: int, _hidpp_short: bool = False, _hidpp_long: bool = False
) -> dict[str, Any]:
"""Check that this product is a Logitech receiver and return it.
Filters based on bus_id, vendor_id and product_id.
If so return the receiver record for further checking.
"""
try:
record = base_usb.get_receiver_info(product_id)
if _match_device(record, bus_id, vendor_id, product_id):
record = filter_receivers(bus_id, vendor_id, product_id, hidpp_short, hidpp_long)
if record: # known or unknown receiver
return record
for record in _DEVICE_IDS: # known devices
if match(record, bus_id, vendor_id, product_id):
return record
except ValueError:
pass
if vendor_id == LOGITECH_VENDOR_ID and 0xC500 <= product_id <= 0xC5FF: # unknown receiver
return {"vendor_id": vendor_id, "product_id": product_id, "bus_id": bus_id, "isDevice": False}
return None
if hidpp_short or hidpp_long: # unknown devices that use HID++
return {'vendor_id': vendor_id, 'product_id': product_id, 'bus_id': bus_id, 'isDevice': True}
elif hidpp_short is None and hidpp_long is None: # unknown devices in correct range of IDs
return _other_device_check(bus_id, vendor_id, product_id)
def receivers_and_devices():
"""Enumerate all the receivers and devices directly attached to the machine."""
yield from hidapi.enumerate(filter_products_of_interest)
yield from _hid.enumerate(filter)
def notify_on_receivers_glib(glib: GLib, callback: Callable):
"""Watch for matching devices and notifies the callback on the GLib thread.
Parameters
----------
glib
GLib instance.
"""
return hidapi.monitor_glib(glib, callback, filter_products_of_interest)
def notify_on_receivers_glib(callback):
"""Watch for matching devices and notifies the callback on the GLib thread."""
return _hid.monitor_glib(callback, filter)
def open_path(path) -> int:
#
#
#
def open_path(path):
"""Checks if the given Linux device path points to the right UR device.
:param path: the Linux device path.
@ -268,7 +162,7 @@ def open_path(path) -> int:
:returns: an open receiver handle if this is the right Linux device, or
``None``.
"""
return hidapi.open_path(path)
return _hid.open_path(path)
def open():
@ -287,11 +181,13 @@ def close(handle):
if handle:
try:
if isinstance(handle, int):
hidapi.close(handle)
_hid.close(handle)
else:
handle.close()
# _log.info("closed receiver handle %r", handle)
return True
except Exception:
# _log.exception("closing receiver handle %r", handle)
pass
return False
@ -314,26 +210,19 @@ def write(handle, devnumber, data, long_message=False):
assert data is not None
assert isinstance(data, bytes), (repr(data), type(data))
if long_message or len(data) > SHORT_MESSAGE_SIZE - 2 or data[:1] == b"\x82":
wdata = struct.pack("!BB18s", HIDPP_LONG_MESSAGE_ID, devnumber, data)
if long_message or len(data) > _SHORT_MESSAGE_SIZE - 2 or data[:1] == b'\x82':
wdata = _pack('!BB18s', HIDPP_LONG_MESSAGE_ID, devnumber, data)
else:
wdata = struct.pack("!BB5s", HIDPP_SHORT_MESSAGE_ID, devnumber, data)
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
"(%s) <= w[%02X %02X %s %s]",
handle,
ord(wdata[:1]),
devnumber,
common.strhex(wdata[2:4]),
common.strhex(wdata[4:]),
)
wdata = _pack('!BB5s', HIDPP_SHORT_MESSAGE_ID, devnumber, data)
if _log.isEnabledFor(_DEBUG):
_log.debug('(%s) <= w[%02X %02X %s %s]', handle, ord(wdata[:1]), devnumber, _strhex(wdata[2:4]), _strhex(wdata[4:]))
try:
hidapi.write(int(handle), wdata)
_hid.write(int(handle), wdata)
except Exception as reason:
logger.error("write failed, assuming handle %r no longer available", handle)
_log.error('write failed, assuming handle %r no longer available', handle)
close(handle)
raise exceptions.NoReceiver(reason=reason) from reason
raise NoReceiver(reason=reason)
def read(handle, timeout=DEFAULT_TIMEOUT):
@ -354,31 +243,19 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
return reply
def _is_relevant_message(data: bytes) -> bool:
"""Checks if given id is a HID++ or DJ message.
Applies sanity checks on message report ID and message size.
"""
# sanity checks on message report id and size
def check_message(data):
assert isinstance(data, bytes), (repr(data), type(data))
# mapping from report_id to message length
report_lengths = {
HIDPP_SHORT_MESSAGE_ID: SHORT_MESSAGE_SIZE,
HIDPP_LONG_MESSAGE_ID: _LONG_MESSAGE_SIZE,
DJ_MESSAGE_ID: _MEDIUM_MESSAGE_SIZE,
0x21: _MAX_READ_SIZE,
}
report_id = ord(data[:1])
if report_id in report_lengths:
if report_id in report_lengths: # is this an HID++ or DJ message?
if report_lengths.get(report_id) == len(data):
return True
else:
logger.warning(f"unexpected message size: report_id {report_id:02X} message {common.strhex(data)}")
_log.warn('unexpected message size: report_id %02X message %s' % (report_id, _strhex(data)))
return False
def _read(handle, timeout) -> tuple[int, int, bytes]:
def _read(handle, timeout):
"""Read an incoming packet from the receiver.
:returns: a tuple of (report_id, devnumber, data), or `None`.
@ -390,72 +267,105 @@ def _read(handle, timeout) -> tuple[int, int, bytes]:
try:
# convert timeout to milliseconds, the hidapi expects it
timeout = int(timeout * 1000)
data = hidapi.read(int(handle), _MAX_READ_SIZE, timeout)
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
except Exception as reason:
logger.warning("read failed, assuming handle %r no longer available", handle)
_log.warn('read failed, assuming handle %r no longer available', handle)
close(handle)
raise exceptions.NoReceiver(reason=reason) from reason
raise NoReceiver(reason=reason)
if data and _is_relevant_message(data): # ignore messages that fail check
if data and check_message(data): # ignore messages that fail check
report_id = ord(data[:1])
devnumber = ord(data[1:2])
if logger.isEnabledFor(logging.DEBUG) and (
report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10
): # ignore DJ input messages
logger.debug(
"(%s) => r[%02X %02X %s %s]",
handle,
report_id,
devnumber,
common.strhex(data[2:4]),
common.strhex(data[4:]),
)
if _log.isEnabledFor(_DEBUG) and (report_id != DJ_MESSAGE_ID or ord(data[2:3]) > 0x10): # ignore DJ input messages
_log.debug('(%s) => r[%02X %02X %s %s]', handle, report_id, devnumber, _strhex(data[2:4]), _strhex(data[4:]))
return report_id, devnumber, data[2:]
def make_notification(report_id: int, devnumber: int, data: bytes) -> HIDPPNotification | None:
#
#
#
def _skip_incoming(handle, ihandle, notifications_hook):
"""Read anything already in the input buffer.
Used by request() and ping() before their write.
"""
while True:
try:
# read whatever is already in the buffer, if any
data = _hid.read(ihandle, _MAX_READ_SIZE, 0)
except Exception as reason:
_log.error('read failed, assuming receiver %s no longer available', handle)
close(handle)
raise NoReceiver(reason=reason)
if data:
if check_message(data): # only process messages that pass check
# report_id = ord(data[:1])
if notifications_hook:
n = make_notification(ord(data[:1]), ord(data[1:2]), data[2:])
if n:
notifications_hook(n)
else:
# nothing in the input buffer, we're done
return
def make_notification(report_id, devnumber, data):
"""Guess if this is a notification (and not just a request reply), and
return a Notification if it is."""
return a Notification tuple if it is."""
sub_id = ord(data[:1])
if sub_id & 0x80 == 0x80:
# this is either a HID++1.0 register r/w, or an error reply
return None
return
# DJ input records are not notifications
if report_id == DJ_MESSAGE_ID and (sub_id < 0x10):
return None
return
address = ord(data[1:2])
if sub_id == 0x00 and (address & 0x0F == 0x00):
# this is a no-op notification - don't do anything with it
return None
return
if (
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
(sub_id >= 0x40) # noqa: E131
or
(sub_id >= 0x40) or # noqa: E131
# custom HID++1.0 battery events, where SubId is 0x07/0x0D
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b"\x00")
or
(sub_id in (0x07, 0x0D) and len(data) == 5 and data[4:5] == b'\x00') or
# custom HID++1.0 illumination event, where SubId is 0x17
(sub_id == 0x17 and len(data) == 5)
or
(sub_id == 0x17 and len(data) == 5) or
# HID++ 2.0 feature notifications have the SoftwareID 0
(address & 0x0F == 0x00)
): # noqa: E129
return HIDPPNotification(report_id, devnumber, sub_id, address, data[2:])
return None
return _HIDPP_Notification(report_id, devnumber, sub_id, address, data[2:])
_HIDPP_Notification = namedtuple('_HIDPP_Notification', ('report_id', 'devnumber', 'sub_id', 'address', 'data'))
_HIDPP_Notification.__str__ = lambda self: 'Notification(%02x,%d,%02X,%02X,%s)' % (
self.report_id, self.devnumber, self.sub_id, self.address, _strhex(self.data)
)
del namedtuple
#
#
#
request_lock = _threading.Lock() # serialize all requests
handles_lock = {}
def handle_lock(handle):
with request_lock:
if handles_lock.get(handle) is None:
if logger.isEnabledFor(logging.INFO):
logger.info("New lock %s", repr(handle))
handles_lock[handle] = threading.Lock() # Serialize requests on the handle
if _log.isEnabledFor(_INFO):
_log.info('New lock %s', repr(handle))
handles_lock[handle] = _threading.Lock() # Serialize requests on the handle
return handles_lock[handle]
@ -465,37 +375,15 @@ def acquire_timeout(lock, handle, timeout):
result = lock.acquire(timeout=timeout)
try:
if not result:
logger.error("lock on handle %d not acquired, probably due to timeout", int(handle))
_log.error('lock on handle %d not acquired, probably due to timeout', int(handle))
yield result
finally:
if result:
lock.release()
def find_paired_node(receiver_path: str, index: int, timeout: int):
"""Find the node of a device paired with a receiver."""
return hidapi.find_paired_node(receiver_path, index, timeout)
def find_paired_node_wpid(receiver_path: str, index: int):
"""Find the node of a device paired with a receiver.
Get wpid from udev.
"""
return hidapi.find_paired_node_wpid(receiver_path, index)
# a very few requests (e.g., host switching) do not expect a reply, but use no_reply=True with extreme caution
def request(
handle,
devnumber,
request_id: int,
*params,
no_reply: bool = False,
return_error: bool = False,
long_message: bool = False,
protocol: float = 1.0,
):
def request(handle, devnumber, request_id, *params, no_reply=False, return_error=False, long_message=False, protocol=1.0):
"""Makes a feature call to a device and waits for a matching reply.
:param handle: an open UR handle.
:param devnumber: attached device number.
@ -503,14 +391,19 @@ def request(
:param params: parameters for the feature call, 3 to 16 bytes.
:returns: the reply data, or ``None`` if some error occurred. or no reply expected
"""
with acquire_timeout(handle_lock(handle), handle, 10.0):
# import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack()))
with acquire_timeout(handle_lock(handle), handle, 10.):
assert isinstance(request_id, int)
if (devnumber != 0xFF or protocol >= 2.0) and request_id < 0x8000:
# Always set the most significant bit (8) in SoftwareId,
# to make notifications easier to distinguish from request replies.
# For HID++ 2.0 feature requests, randomize the SoftwareId to make it
# easier to recognize the reply for this request. also, always set the
# most significant bit (8) in SoftwareId, to make notifications easier
# to distinguish from request replies.
# This only applies to peripheral requests, ofc.
sw_id = _get_next_sw_id()
request_id = (request_id & 0xFFF0) | sw_id # was 0x08 | getrandbits(3)
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
# be extra patient on long register read
@ -518,17 +411,19 @@ def request(
timeout *= 2
if params:
params = b"".join(struct.pack("B", p) if isinstance(p, int) else p for p in params)
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
else:
params = b""
request_data = struct.pack("!H", request_id) + params
params = b''
# if _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) device %d request_id {%04X} params [%s]", handle, devnumber, request_id, _strhex(params))
request_data = _pack('!H', request_id) + params
ihandle = int(handle)
notifications_hook = getattr(handle, "notifications_hook", None)
notifications_hook = getattr(handle, 'notifications_hook', None)
try:
_read_input_buffer(handle, ihandle, notifications_hook)
except exceptions.NoReceiver:
logger.warning("device or receiver disconnected")
_skip_incoming(handle, ihandle, notifications_hook)
except NoReceiver:
_log.warn('device or receiver disconnected')
return None
write(ihandle, devnumber, request_data, long_message)
@ -536,48 +431,33 @@ def request(
return None
# we consider timeout from this point
request_started = time()
request_started = _timestamp()
delta = 0
while delta < timeout:
reply = _read(handle, timeout)
if reply:
report_id, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber or reply_devnumber == devnumber ^ 0xFF: # BT device returning 0x00
if (
report_id == HIDPP_SHORT_MESSAGE_ID
and reply_data[:1] == b"\x8f"
and reply_data[1:3] == request_data[:2]
):
if reply_devnumber == devnumber:
if report_id == HIDPP_SHORT_MESSAGE_ID and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2
]:
error = ord(reply_data[3:4])
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
"(%s) device 0x%02X error on request {%04X}: %d = %s",
handle,
devnumber,
request_id,
error,
Hidpp10ErrorCode(error),
if _log.isEnabledFor(_DEBUG):
_log.debug(
'(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error,
_hidpp10.ERROR[error]
)
return Hidpp10ErrorCode(error) if return_error else None
if reply_data[:1] == b"\xff" and reply_data[1:3] == request_data[:2]:
return _hidpp10.ERROR[error] if return_error else None
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
# a HID++ 2.0 feature call returned with an error
error = ord(reply_data[3:4])
logger.error(
"(%s) device %d error on feature request {%04X}: %d = %s",
handle,
devnumber,
request_id,
error,
Hidpp20ErrorCode(error),
)
raise exceptions.FeatureCallError(
number=devnumber,
request=request_id,
error=error,
params=params,
_log.error(
'(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error,
_hidpp20.ERROR[error]
)
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
if reply_data[:2] == request_data[:2]:
if devnumber == 0xFF:
@ -595,119 +475,94 @@ def request(
else:
# a reply was received, but did not match our request in any way
# reset the timeout starting point
request_started = time()
request_started = _timestamp()
if notifications_hook:
n = make_notification(report_id, reply_devnumber, reply_data)
if n:
notifications_hook(n)
delta = time() - request_started
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
logger.warning(
"timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
delta,
timeout,
devnumber,
request_id,
common.strhex(params),
delta = _timestamp() - request_started
# if _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) still waiting for reply, delta %f", handle, delta)
_log.warn(
'timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]', delta, timeout, devnumber, request_id,
_strhex(params)
)
# raise DeviceUnreachable(number=devnumber, request=request_id)
def ping(handle, devnumber, long_message: bool = False):
def ping(handle, devnumber, long_message=False):
"""Check if a device is connected to the receiver.
:returns: The HID protocol supported by the device, as a floating point number, if the device is active.
"""
if logger.isEnabledFor(logging.DEBUG):
logger.debug("(%s) pinging device %d", handle, devnumber)
with acquire_timeout(handle_lock(handle), handle, 10.0):
notifications_hook = getattr(handle, "notifications_hook", None)
if _log.isEnabledFor(_DEBUG):
_log.debug('(%s) pinging device %d', handle, devnumber)
# import inspect as _inspect
# print ('\n '.join(str(s) for s in _inspect.stack()))
with acquire_timeout(handle_lock(handle), handle, 10.):
# randomize the SoftwareId and mark byte to be able to identify the ping
# reply, and set most significant (0x8) bit in SoftwareId so that the reply
# is always distinguishable from notifications
request_id = 0x0018 | _random_bits(3)
request_data = _pack('!HBBB', request_id, 0, 0, _random_bits(8))
ihandle = int(handle)
notifications_hook = getattr(handle, 'notifications_hook', None)
try:
_read_input_buffer(handle, int(handle), notifications_hook)
except exceptions.NoReceiver:
logger.warning("device or receiver disconnected")
_skip_incoming(handle, ihandle, notifications_hook)
except NoReceiver:
_log.warn('device or receiver disconnected')
return
# randomize the mark byte to be able to identify the ping reply
sw_id = _get_next_sw_id()
request_id = 0x0010 | sw_id # was 0x0018 | getrandbits(3)
request_data = struct.pack("!HBBB", request_id, 0, 0, getrandbits(8))
write(int(handle), devnumber, request_data, long_message)
write(ihandle, devnumber, request_data, long_message)
request_started = time() # we consider timeout from this point
# we consider timeout from this point
request_started = _timestamp()
delta = 0
while delta < _PING_TIMEOUT:
reply = _read(handle, _PING_TIMEOUT)
if reply:
report_id, reply_devnumber, reply_data = reply
if reply_devnumber == devnumber or reply_devnumber == devnumber ^ 0xFF: # BT device returning 0x00
if reply_devnumber == devnumber:
if reply_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
# HID++ 2.0+ device, currently connected
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
if (
report_id == HIDPP_SHORT_MESSAGE_ID
and reply_data[:1] == b"\x8f"
and reply_data[1:3] == request_data[:2]
): # error response
if report_id == HIDPP_SHORT_MESSAGE_ID and reply_data[:1] == b'\x8F' and reply_data[1:3] == request_data[:2
]:
assert reply_data[-1:] == b'\x00'
error = ord(reply_data[3:4])
if error == Hidpp10ErrorCode.INVALID_SUB_ID_COMMAND:
# a valid reply from a HID++ 1.0 device
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
return 1.0
if error in [Hidpp10ErrorCode.RESOURCE_ERROR, Hidpp10ErrorCode.CONNECTION_REQUEST_FAILED]:
return # device unreachable
if error == Hidpp10ErrorCode.UNKNOWN_DEVICE: # no paired device with that number
logger.error("(%s) device %d error on ping request: unknown device", handle, devnumber)
raise exceptions.NoSuchDevice(number=devnumber, request=request_id)
if error == _hidpp10.ERROR.resource_error: # device unreachable
return
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
_log.error('(%s) device %d error on ping request: unknown device', handle, devnumber)
raise NoSuchDevice(number=devnumber, request=request_id)
if notifications_hook:
n = make_notification(report_id, reply_devnumber, reply_data)
if n:
notifications_hook(n)
# elif _log.isEnabledFor(_DEBUG):
# _log.debug("(%s) ignoring reply %02X [%s]", handle, reply_devnumber, _strhex(reply_data))
delta = time() - request_started
delta = _timestamp() - request_started
logger.warning("(%s) timeout (%0.2f/%0.2f) on device %d ping", handle, delta, _PING_TIMEOUT, devnumber)
def _read_input_buffer(handle, ihandle, notifications_hook):
"""Consume anything already in the input buffer.
Used by request() and ping() before their write.
"""
while True:
try:
# read whatever is already in the buffer, if any
data = hidapi.read(ihandle, _MAX_READ_SIZE, 0)
except Exception as reason:
logger.error("read failed, assuming receiver %s no longer available", handle)
close(handle)
raise exceptions.NoReceiver(reason=reason) from reason
if data:
if _is_relevant_message(data): # only process messages that pass check
# report_id = ord(data[:1])
if notifications_hook:
n = make_notification(ord(data[:1]), ord(data[1:2]), data[2:])
if n:
notifications_hook(n)
else:
# nothing in the input buffer, we're done
return
def _get_next_sw_id() -> int:
"""Returns 'random' software ID to separate replies from different devices.
Cycle the HID++ 2.0 software ID from 0x2 to 0xF to separate
results and notifications.
"""
if not hasattr(_get_next_sw_id, "software_id"):
_get_next_sw_id.software_id = 0xF
if _get_next_sw_id.software_id < 0xF:
_get_next_sw_id.software_id += 1
else:
_get_next_sw_id.software_id = 2
return _get_next_sw_id.software_id
_log.warn('(%s) timeout (%0.2f/%0.2f) on device %d ping', handle, delta, _PING_TIMEOUT, devnumber)
# raise DeviceUnreachable(number=devnumber, request=request_id)

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -14,222 +16,235 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Collection of known Logitech product IDs.
## According to Logitech, they use the following product IDs (as of September 2020)
## USB product IDs for receivers: 0xC526 - 0xC5xx
## Wireless PIDs for hidpp10 devices: 0x2006 - 0x2019
## Wireless PIDs for hidpp20 devices: 0x4002 - 0x4097, 0x4101 - 0x4102
## USB product IDs for hidpp20 devices: 0xC07D - 0xC094, 0xC32B - 0xC344
## Bluetooth product IDs (for hidpp20 devices): 0xB012 - 0xB0xx, 0xB32A - 0xB3xx
According to Logitech, they use the following product IDs (as of September 2020)
USB product IDs for receivers: 0xC526 - 0xC5xx
Wireless PIDs for hidpp10 devices: 0x2006 - 0x2019
Wireless PIDs for hidpp20 devices: 0x4002 - 0x4097, 0x4101 - 0x4102
USB product IDs for hidpp20 devices: 0xC07D - 0xC094, 0xC32B - 0xC344
Bluetooth product IDs (for hidpp20 devices): 0xB012 - 0xB0xx, 0xB32A - 0xB3xx
# USB ids of Logitech wireless receivers.
# Only receivers supporting the HID++ protocol can go in here.
USB ids of Logitech wireless receivers.
Only receivers supporting the HID++ protocol can go in here.
"""
from .descriptors import DEVICES as _DEVICES
from .i18n import _
from __future__ import annotations
# max_devices is only used for receivers that do not support reading from _R.receiver_info offset 0x03, default to 1
# may_unpair is only used for receivers that do not support reading from _R.receiver_info offset 0x03, default to False
# unpair is for receivers that do support reading from _R.receiver_info offset 0x03, no default
## should this last be changed so that may_unpair is used for all receivers? writing to _R.receiver_pairing doesn't seem right
# re_pairs determines whether a receiver pairs by replacing existing pairings, default to False
## currently only one receiver is so marked - should there be more?
from typing import Any
_DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver')
from solaar.i18n import _
_bolt_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Bolt Receiver'),
'receiver_kind': 'bolt',
'max_devices': 6,
'may_unpair': True
}
# max_devices is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03, default
# to 1.
# may_unpair is only used for receivers that do not support reading from Registers.RECEIVER_INFO offset 0x03,
# default to False.
# unpair is for receivers that do support reading from Registers.RECEIVER_INFO offset 0x03, no default.
## should this last be changed so that may_unpair is used for all receivers? writing to Registers.RECEIVER_PAIRING
## doesn't seem right
_unifying_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Unifying Receiver'),
'receiver_kind': 'unifying',
'may_unpair': True
}
LOGITECH_VENDOR_ID = 0x046D
_nano_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False,
're_pairs': True
}
_nano_receiver_no_unpair = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False,
'unpair': False,
're_pairs': True
}
def _bolt_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 2,
"name": _("Bolt Receiver"),
"receiver_kind": "bolt",
"max_devices": 6,
"may_unpair": True,
}
_nano_receiver_max2 = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'max_devices': 2,
'may_unpair': False,
're_pairs': True
}
_nano_receiver_maxn = lambda product_id, max: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'max_devices': max,
'may_unpair': False,
're_pairs': True
}
def _unifying_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 2,
"name": _("Unifying Receiver"),
"receiver_kind": "unifying",
"may_unpair": True,
}
_lenovo_receiver = lambda product_id: {
'vendor_id': 0x17ef,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Nano Receiver'),
'receiver_kind': 'nano',
'may_unpair': False
}
_lightspeed_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 2,
'hid_driver': _DRIVER, # noqa: F821
'name': _('Lightspeed Receiver'),
'may_unpair': False
}
def _nano_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 1,
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
"re_pairs": True,
}
def _nano_receiver_no_unpair(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 1,
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
"unpair": False,
"re_pairs": True,
}
def _nano_receiver_max2(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 1,
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"max_devices": 2,
"may_unpair": False,
"re_pairs": True,
}
def _lenovo_receiver(product_id: int) -> dict:
return {
"vendor_id": 6127,
"product_id": product_id,
"usb_interface": 1,
"name": _("Nano Receiver"),
"receiver_kind": "nano",
"may_unpair": False,
}
def _lightspeed_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 2,
"receiver_kind": "lightspeed",
"name": _("Lightspeed Receiver"),
"may_unpair": False,
}
def _ex100_receiver(product_id: int) -> dict:
return {
"vendor_id": LOGITECH_VENDOR_ID,
"product_id": product_id,
"usb_interface": 1,
"name": _("EX100 Receiver 27 Mhz"),
"receiver_kind": "27Mhz",
"max_devices": 4,
"may_unpair": False,
"re_pairs": True,
}
_ex100_receiver = lambda product_id: {
'vendor_id': 0x046d,
'product_id': product_id,
'usb_interface': 1,
'hid_driver': _DRIVER, # noqa: F821
'name': _('EX100 Receiver 27 Mhz'),
'receiver_kind': '27Mhz',
'max_devices': 4,
'may_unpair': False,
're_pairs': True
}
# Receivers added here should also be listed in
# share/solaar/io.github.pwr_solaar.solaar.meta-info.xml
# share/solaar/io.github.pwr_solaar.solaar.metainfo.xml
# Look in https://github.com/torvalds/linux/blob/master/drivers/hid/hid-ids.h
# Bolt receivers (marked with the yellow lightning bolt logo)
BOLT_RECEIVER_C548 = _bolt_receiver(0xC548)
BOLT_RECEIVER_C548 = _bolt_receiver(0xc548)
# standard Unifying receivers (marked with the orange Unifying logo)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xC52B)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xC532)
UNIFYING_RECEIVER_C52B = _unifying_receiver(0xc52b)
UNIFYING_RECEIVER_C532 = _unifying_receiver(0xc532)
# Nano receivers (usually sold with low-end devices)
NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xC52F)
NANO_RECEIVER_C518 = _nano_receiver(0xC518)
NANO_RECEIVER_C51A = _nano_receiver(0xC51A)
NANO_RECEIVER_C51B = _nano_receiver(0xC51B)
NANO_RECEIVER_C521 = _nano_receiver(0xC521)
NANO_RECEIVER_C525 = _nano_receiver(0xC525)
NANO_RECEIVER_C526 = _nano_receiver(0xC526)
NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xC52E)
NANO_RECEIVER_C531 = _nano_receiver(0xC531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xC534)
NANO_RECEIVER_C535 = _nano_receiver(0xC535) # branded as Dell
NANO_RECEIVER_C537 = _nano_receiver(0xC537)
NANO_RECEIVER_ADVANCED = _nano_receiver_no_unpair(0xc52f)
NANO_RECEIVER_C518 = _nano_receiver(0xc518)
NANO_RECEIVER_C51A = _nano_receiver(0xc51a)
NANO_RECEIVER_C51B = _nano_receiver(0xc51b)
NANO_RECEIVER_C521 = _nano_receiver(0xc521)
NANO_RECEIVER_C525 = _nano_receiver(0xc525)
NANO_RECEIVER_C526 = _nano_receiver(0xc526)
NANO_RECEIVER_C52E = _nano_receiver_no_unpair(0xc52e)
NANO_RECEIVER_C531 = _nano_receiver(0xc531)
NANO_RECEIVER_C534 = _nano_receiver_max2(0xc534)
NANO_RECEIVER_C535 = _nano_receiver(0xc535) # branded as Dell
NANO_RECEIVER_C537 = _nano_receiver(0xc537)
# NANO_RECEIVER_C542 = _nano_receiver(0xc542) # does not use HID++
NANO_RECEIVER_6042 = _lenovo_receiver(0x6042)
# Lightspeed receivers (usually sold with gaming devices)
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xC539)
LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xC53A)
LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xC53D)
LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xC53F)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xC541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xC545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xC547)
LIGHTSPEED_RECEIVER_C539 = _lightspeed_receiver(0xc539)
LIGHTSPEED_RECEIVER_C53A = _lightspeed_receiver(0xc53a)
LIGHTSPEED_RECEIVER_C53D = _lightspeed_receiver(0xc53d)
LIGHTSPEED_RECEIVER_C53F = _lightspeed_receiver(0xc53f)
LIGHTSPEED_RECEIVER_C541 = _lightspeed_receiver(0xc541)
LIGHTSPEED_RECEIVER_C545 = _lightspeed_receiver(0xc545)
LIGHTSPEED_RECEIVER_C547 = _lightspeed_receiver(0xc547)
# EX100 old style receiver pre-unifying protocol
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517)
# EX100_27MHZ_RECEIVER_C50C = _ex100_receiver(0xc50C) # in hid/hid-ids.h
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xc517)
# EX100_27MHZ_RECEIVER_C51B = _ex100_receiver(0xc51B) # in hid/hid-ids.h
KNOWN_RECEIVERS = {
0xC548: BOLT_RECEIVER_C548,
0xC52B: UNIFYING_RECEIVER_C52B,
0xC532: UNIFYING_RECEIVER_C532,
0xC52F: NANO_RECEIVER_ADVANCED,
0xC518: NANO_RECEIVER_C518,
0xC51A: NANO_RECEIVER_C51A,
0xC51B: NANO_RECEIVER_C51B,
0xC521: NANO_RECEIVER_C521,
0xC525: NANO_RECEIVER_C525,
0xC526: NANO_RECEIVER_C526,
0xC52E: NANO_RECEIVER_C52E,
0xC531: NANO_RECEIVER_C531,
0xC534: NANO_RECEIVER_C534,
0xC535: NANO_RECEIVER_C535,
0xC537: NANO_RECEIVER_C537,
0x6042: NANO_RECEIVER_6042,
0xC539: LIGHTSPEED_RECEIVER_C539,
0xC53A: LIGHTSPEED_RECEIVER_C53A,
0xC53D: LIGHTSPEED_RECEIVER_C53D,
0xC53F: LIGHTSPEED_RECEIVER_C53F,
0xC541: LIGHTSPEED_RECEIVER_C541,
0xC545: LIGHTSPEED_RECEIVER_C545,
0xC547: LIGHTSPEED_RECEIVER_C547,
0xC517: EX100_27MHZ_RECEIVER_C517,
ALL = (
BOLT_RECEIVER_C548,
UNIFYING_RECEIVER_C52B,
UNIFYING_RECEIVER_C532,
NANO_RECEIVER_ADVANCED,
NANO_RECEIVER_C518,
NANO_RECEIVER_C51A,
NANO_RECEIVER_C51B,
NANO_RECEIVER_C521,
NANO_RECEIVER_C525,
NANO_RECEIVER_C526,
NANO_RECEIVER_C52E,
NANO_RECEIVER_C531,
NANO_RECEIVER_C534,
NANO_RECEIVER_C535,
NANO_RECEIVER_C537,
# NANO_RECEIVER_C542, # does not use HID++
NANO_RECEIVER_6042,
LIGHTSPEED_RECEIVER_C539,
LIGHTSPEED_RECEIVER_C53A,
LIGHTSPEED_RECEIVER_C53D,
LIGHTSPEED_RECEIVER_C53F,
LIGHTSPEED_RECEIVER_C541,
LIGHTSPEED_RECEIVER_C545,
LIGHTSPEED_RECEIVER_C547,
EX100_27MHZ_RECEIVER_C517,
)
_wired_device = lambda product_id, interface: {
'vendor_id': 0x046d,
'product_id': product_id,
'bus_id': 0x3,
'usb_interface': interface,
'isDevice': True
}
_bt_device = lambda product_id: {'vendor_id': 0x046d, 'product_id': product_id, 'bus_id': 0x5, 'isDevice': True}
def get_receiver_info(product_id: int) -> dict[str, Any]:
"""Returns hardcoded information about a Logitech receiver.
DEVICES = []
Parameters
----------
product_id
Product ID (pid) of the receiver, e.g. 0xC548 for a Logitech
Bolt receiver.
for _ignore, d in _DEVICES.items():
if d.usbid:
DEVICES.append(_wired_device(d.usbid, d.interface if d.interface else 2))
if d.btid:
DEVICES.append(_bt_device(d.btid))
Returns
-------
dict[str, Any]
Receiver info with mandatory fields:
- vendor_id
- product_id
Raises
------
ValueError
If the product ID is unknown.
"""
try:
return KNOWN_RECEIVERS[product_id]
except KeyError:
pass
def other_device_check(bus_id, vendor_id, product_id):
"""Check whether product is a Logitech USB-connected or Bluetooth device based on bus, vendor, and product IDs
This allows Solaar to support receiverless HID++ 2.0 devices that it knows nothing about"""
if vendor_id != 0x46d: # Logitech
return
if bus_id == 0x3: # USB
if (product_id >= 0xC07D and product_id <= 0xC094 or product_id >= 0xC32B and product_id <= 0xC344):
return _wired_device(product_id, 2)
elif bus_id == 0x5: # Bluetooth
if (product_id >= 0xB012 and product_id <= 0xB0FF or product_id >= 0xB317 and product_id <= 0xB3FF):
return _bt_device(product_id)
raise ValueError(f"Unknown product ID '0x{product_id:02X}'")
def product_information(usb_id):
if isinstance(usb_id, str):
usb_id = int(usb_id, 16)
for r in ALL:
if usb_id == r.get('product_id'):
return r
return {}
del _DRIVER, _unifying_receiver, _nano_receiver, _lenovo_receiver, _lightspeed_receiver

View File

@ -1,5 +1,6 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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
@ -14,297 +15,17 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations
import binascii
import dataclasses
import typing
# Some common functions and types.
from enum import Flag
from enum import IntEnum
from typing import Generator
from typing import Iterable
from typing import Optional
from typing import Union
from binascii import hexlify as _hexlify
from collections import namedtuple
import yaml
is_string = lambda d: isinstance(d, str)
from solaar.i18n import _
if typing.TYPE_CHECKING:
from logitech_receiver.hidpp20_constants import FirmwareKind
LOGITECH_VENDOR_ID = 0x046D
def crc16(data: bytes):
"""
CRC-16 (CCITT) implemented with a precomputed lookup table
"""
table = [
0x0000,
0x1021,
0x2042,
0x3063,
0x4084,
0x50A5,
0x60C6,
0x70E7,
0x8108,
0x9129,
0xA14A,
0xB16B,
0xC18C,
0xD1AD,
0xE1CE,
0xF1EF,
0x1231,
0x0210,
0x3273,
0x2252,
0x52B5,
0x4294,
0x72F7,
0x62D6,
0x9339,
0x8318,
0xB37B,
0xA35A,
0xD3BD,
0xC39C,
0xF3FF,
0xE3DE,
0x2462,
0x3443,
0x0420,
0x1401,
0x64E6,
0x74C7,
0x44A4,
0x5485,
0xA56A,
0xB54B,
0x8528,
0x9509,
0xE5EE,
0xF5CF,
0xC5AC,
0xD58D,
0x3653,
0x2672,
0x1611,
0x0630,
0x76D7,
0x66F6,
0x5695,
0x46B4,
0xB75B,
0xA77A,
0x9719,
0x8738,
0xF7DF,
0xE7FE,
0xD79D,
0xC7BC,
0x48C4,
0x58E5,
0x6886,
0x78A7,
0x0840,
0x1861,
0x2802,
0x3823,
0xC9CC,
0xD9ED,
0xE98E,
0xF9AF,
0x8948,
0x9969,
0xA90A,
0xB92B,
0x5AF5,
0x4AD4,
0x7AB7,
0x6A96,
0x1A71,
0x0A50,
0x3A33,
0x2A12,
0xDBFD,
0xCBDC,
0xFBBF,
0xEB9E,
0x9B79,
0x8B58,
0xBB3B,
0xAB1A,
0x6CA6,
0x7C87,
0x4CE4,
0x5CC5,
0x2C22,
0x3C03,
0x0C60,
0x1C41,
0xEDAE,
0xFD8F,
0xCDEC,
0xDDCD,
0xAD2A,
0xBD0B,
0x8D68,
0x9D49,
0x7E97,
0x6EB6,
0x5ED5,
0x4EF4,
0x3E13,
0x2E32,
0x1E51,
0x0E70,
0xFF9F,
0xEFBE,
0xDFDD,
0xCFFC,
0xBF1B,
0xAF3A,
0x9F59,
0x8F78,
0x9188,
0x81A9,
0xB1CA,
0xA1EB,
0xD10C,
0xC12D,
0xF14E,
0xE16F,
0x1080,
0x00A1,
0x30C2,
0x20E3,
0x5004,
0x4025,
0x7046,
0x6067,
0x83B9,
0x9398,
0xA3FB,
0xB3DA,
0xC33D,
0xD31C,
0xE37F,
0xF35E,
0x02B1,
0x1290,
0x22F3,
0x32D2,
0x4235,
0x5214,
0x6277,
0x7256,
0xB5EA,
0xA5CB,
0x95A8,
0x8589,
0xF56E,
0xE54F,
0xD52C,
0xC50D,
0x34E2,
0x24C3,
0x14A0,
0x0481,
0x7466,
0x6447,
0x5424,
0x4405,
0xA7DB,
0xB7FA,
0x8799,
0x97B8,
0xE75F,
0xF77E,
0xC71D,
0xD73C,
0x26D3,
0x36F2,
0x0691,
0x16B0,
0x6657,
0x7676,
0x4615,
0x5634,
0xD94C,
0xC96D,
0xF90E,
0xE92F,
0x99C8,
0x89E9,
0xB98A,
0xA9AB,
0x5844,
0x4865,
0x7806,
0x6827,
0x18C0,
0x08E1,
0x3882,
0x28A3,
0xCB7D,
0xDB5C,
0xEB3F,
0xFB1E,
0x8BF9,
0x9BD8,
0xABBB,
0xBB9A,
0x4A75,
0x5A54,
0x6A37,
0x7A16,
0x0AF1,
0x1AD0,
0x2AB3,
0x3A92,
0xFD2E,
0xED0F,
0xDD6C,
0xCD4D,
0xBDAA,
0xAD8B,
0x9DE8,
0x8DC9,
0x7C26,
0x6C07,
0x5C64,
0x4C45,
0x3CA2,
0x2C83,
0x1CE0,
0x0CC1,
0xEF1F,
0xFF3E,
0xCF5D,
0xDF7C,
0xAF9B,
0xBFBA,
0x8FD9,
0x9FF8,
0x6E17,
0x7E36,
0x4E55,
0x5E74,
0x2E93,
0x3EB2,
0x0ED1,
0x1EF0,
]
crc = 0xFFFF
for byte in data:
crc = (crc << 8) ^ table[(crc >> 8) ^ byte]
crc &= 0xFFFF # important, crc must stay 16bits all the way through
return crc
#
#
#
class NamedInt(int):
@ -314,7 +35,7 @@ class NamedInt(int):
(case-insensitive)."""
def __new__(cls, value, name):
assert isinstance(name, str)
assert is_string(name)
obj = int.__new__(cls, value)
obj.name = str(name)
return obj
@ -323,17 +44,15 @@ class NamedInt(int):
return int2bytes(self, count)
def __eq__(self, other):
if other is None:
return False
if isinstance(other, NamedInt):
return int(self) == int(other) and self.name == other.name
if isinstance(other, int):
return int(self) == int(other)
if isinstance(other, str):
if is_string(other):
return self.name.lower() == other.lower()
# this should catch comparisons with bytes in Py3
if other is not None:
raise TypeError(f"Unsupported type {str(type(other))}")
raise TypeError('Unsupported type ' + str(type(other)))
def __ne__(self, other):
return not self.__eq__(other)
@ -345,20 +64,7 @@ class NamedInt(int):
return self.name
def __repr__(self):
return f"NamedInt({int(self)}, {self.name!r})"
@classmethod
def from_yaml(cls, loader, node):
args = loader.construct_mapping(node)
return cls(value=args["value"], name=args["name"])
@classmethod
def to_yaml(cls, dumper, data):
return dumper.represent_mapping("!NamedInt", {"value": int(data), "name": data.name}, flow_style=True)
yaml.SafeLoader.add_constructor("!NamedInt", NamedInt.from_yaml)
yaml.add_representer(NamedInt, NamedInt.to_yaml)
return 'NamedInt(%d, %r)' % (int(self), self.name)
class NamedInts:
@ -374,15 +80,17 @@ class NamedInts:
if the value already exists in the set (int or string), ValueError will be
raised.
"""
__slots__ = ('__dict__', '_values', '_indexed', '_fallback', '_is_sorted')
__slots__ = ("__dict__", "_values", "_indexed", "_fallback", "_is_sorted")
def __init__(self, dict=None, **kwargs):
def __init__(self, dict_=None, **kwargs):
def _readable_name(n):
return n.replace("__", "/").replace("_", " ")
if not is_string(n):
raise TypeError('expected string, got ' + str(type(n)))
return n.replace('__', '/').replace('_', ' ')
# print (repr(kwargs))
elements = dict_ if dict_ else kwargs
elements = dict if dict else kwargs
values = {k: NamedInt(v, _readable_name(k)) for (k, v) in elements.items()}
self.__dict__ = values
self._is_sorted = False
@ -406,13 +114,13 @@ class NamedInts:
def flag_names(self, value):
unknown_bits = value
for k in self._indexed:
assert bin(k).count("1") == 1
assert bin(k).count('1') == 1
if k & value == k:
unknown_bits &= ~k
yield str(self._indexed[k])
if unknown_bits:
yield f"unknown:{unknown_bits:06X}"
yield 'unknown:%06X' % unknown_bits
def _sort_values(self):
self._values = sorted(self._values)
@ -430,10 +138,10 @@ class NamedInts:
self._sort_values()
return value
elif isinstance(index, str):
elif is_string(index):
if index in self.__dict__:
return self.__dict__[index]
return next((x for x in self._values if str(x) == index), None)
return (next((x for x in self._values if str(x) == index), None))
elif isinstance(index, slice):
values = self._values if self._is_sorted else sorted(self._values)
@ -467,17 +175,17 @@ class NamedInts:
def __setitem__(self, index, name):
assert isinstance(index, int), type(index)
if isinstance(name, NamedInt):
assert int(index) == int(name), f"{repr(index)} {repr(name)}"
assert int(index) == int(name), repr(index) + ' ' + repr(name)
value = name
elif isinstance(name, str):
elif is_string(name):
value = NamedInt(index, name)
else:
raise TypeError("name must be a string")
raise TypeError('name must be a string')
if str(value) in self.__dict__:
raise ValueError(f"{value} ({int(value)}) already known")
raise ValueError('%s (%d) already known' % (value, int(value)))
if int(value) in self._indexed:
raise ValueError(f"{int(value)} ({value}) already known")
raise ValueError('%d (%s) already known' % (int(value), value))
self._values.append(value)
self._is_sorted = False
@ -490,7 +198,7 @@ class NamedInts:
return self[value] == value
elif isinstance(value, int):
return value in self._indexed
elif isinstance(value, str):
elif is_string(value):
return value in self.__dict__ or value in self._values
def __iter__(self):
@ -500,41 +208,14 @@ class NamedInts:
return len(self._values)
def __repr__(self):
return f"NamedInts({', '.join(repr(v) for v in self._values)})"
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
def __or__(self, other):
return NamedInts(**self.__dict__, **other.__dict__)
def __eq__(self, other):
return isinstance(other, self.__class__) and self._values == other._values
def flag_names(enum_class: Iterable, value: int) -> Generator[str]:
"""Extracts single bit flags from a (binary) number.
Parameters
----------
enum_class
Enum class to extract flags from.
value
Number to extract binary flags from.
"""
indexed = {item.value: item.name for item in enum_class}
unknown_bits = value
for k in indexed:
# Ensure that the key (flag value) is a power of 2 (a single bit flag)
assert bin(k).count("1") == 1
if k & value == k:
unknown_bits &= ~k
yield indexed[k].lower()
# Yield any remaining unknown bits
if unknown_bits != 0:
yield f"unknown:{unknown_bits:06X}"
class UnsortedNamedInts(NamedInts):
def _sort_values(self):
pass
@ -546,18 +227,18 @@ class UnsortedNamedInts(NamedInts):
def strhex(x):
assert x is not None
"""Produce a hex-string representation of a sequence of bytes."""
return binascii.hexlify(x).decode("ascii").upper()
return _hexlify(x).decode('ascii').upper()
def bytes2int(x, signed=False):
return int.from_bytes(x, signed=signed, byteorder="big")
return int.from_bytes(x, signed=signed, byteorder='big')
def int2bytes(x, count=None, signed=False):
if count:
return x.to_bytes(length=count, byteorder="big", signed=signed)
return x.to_bytes(length=count, byteorder='big', signed=signed)
else:
return x.to_bytes(length=8, byteorder="big", signed=signed).lstrip(b"\x00")
return x.to_bytes(length=8, byteorder='big', signed=signed).lstrip(b'\x00')
class KwException(Exception):
@ -575,102 +256,9 @@ class KwException(Exception):
return self.args[0].get(k) # was self.args[0][k]
class FirmwareKind(IntEnum):
Firmware = 0x00
Bootloader = 0x01
Hardware = 0x02
Other = 0x03
"""Firmware information."""
FirmwareInfo = namedtuple('FirmwareInfo', ['kind', 'name', 'version', 'extras'])
BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90)
@dataclasses.dataclass
class FirmwareInfo:
kind: FirmwareKind
name: str
version: str
extras: str | None
class BatteryStatus(Flag):
DISCHARGING = 0x00
RECHARGING = 0x01
ALMOST_FULL = 0x02
FULL = 0x03
SLOW_RECHARGE = 0x04
INVALID_BATTERY = 0x05
THERMAL_ERROR = 0x06
class BatteryLevelApproximation(IntEnum):
EMPTY = 0
CRITICAL = 5
LOW = 20
GOOD = 50
FULL = 90
@dataclasses.dataclass
class Battery:
"""Information about the current state of a battery"""
ATTENTION_LEVEL = 5
level: Optional[Union[BatteryLevelApproximation, int]]
next_level: Optional[Union[NamedInt, int]]
status: Optional[BatteryStatus]
voltage: Optional[int]
light_level: Optional[int] = None # light level for devices with solaar recharging
def __post_init__(self):
if self.level is None: # infer level from status if needed and possible
if self.status == BatteryStatus.FULL:
self.level = BatteryLevelApproximation.FULL
elif self.status in (BatteryStatus.ALMOST_FULL, BatteryStatus.RECHARGING):
self.level = BatteryLevelApproximation.GOOD
elif self.status == BatteryStatus.SLOW_RECHARGE:
self.level = BatteryLevelApproximation.LOW
def ok(self) -> bool:
return self.status not in (BatteryStatus.INVALID_BATTERY, BatteryStatus.THERMAL_ERROR) and (
self.level is None or self.level > Battery.ATTENTION_LEVEL
)
def charging(self) -> bool:
return self.status in (
BatteryStatus.RECHARGING,
BatteryStatus.ALMOST_FULL,
BatteryStatus.FULL,
BatteryStatus.SLOW_RECHARGE,
)
def to_str(self) -> str:
if isinstance(self.level, BatteryLevelApproximation):
level = self.level.name.lower()
status = self.status.name.lower().replace("_", " ") if self.status is not None else "Unknown"
return _("Battery: %(level)s (%(status)s)") % {"level": _(level), "status": _(status)}
elif isinstance(self.level, int):
status = self.status.name.lower().replace("_", " ") if self.status is not None else "Unknown"
return _("Battery: %(percent)d%% (%(status)s)") % {"percent": self.level, "status": _(status)}
return ""
class Alert(IntEnum):
NONE = 0x00
NOTIFICATION = 0x01
SHOW_WINDOW = 0x02
ATTENTION = 0x04
ALL = 0xFF
class Notification(IntEnum):
NO_OPERATION = 0x00
CONNECT_DISCONNECT = 0x40
DJ_PAIRING = 0x41
CONNECTED = 0x42
RAW_INPUT = 0x49
PAIRING_LOCK = 0x4A
POWER = 0x4B
class BusID(IntEnum):
USB = 0x03
BLUETOOTH = 0x05
del namedtuple

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -14,44 +16,30 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# Devices (not receivers) known to Solaar.
# Solaar can handle many recent devices without having any entry here.
# An entry should only be added to fix problems, such as
# - the device's device ID or WPID falls outside the range that Solaar searches
# - the device uses a USB interface other than 2
# - the name or codename should be different from what the device reports
"""Devices (not receivers) known to Solaar.
from collections import namedtuple
Solaar can handle many recent devices without having any entry here.
An entry should only be added to fix problems, such as
- the device's device ID or WPID falls outside the range that Solaar searches
- the device uses a USB interface other than 2
- the name or codename should be different from what the device reports
"""
from . import settings_templates as _ST
from .common import NamedInts as _NamedInts
from .hidpp10 import DEVICE_KIND as _DK
from .hidpp10 import REGISTERS as _R
from .hidpp10_constants import DEVICE_KIND
from .hidpp10_constants import Registers as Reg
class _DeviceDescriptor:
def __init__(
self,
name=None,
kind=None,
wpid=None,
codename=None,
protocol=None,
registers=None,
usbid=None,
interface=None,
btid=None,
):
self.name = name
self.kind = kind
self.wpid = wpid
self.codename = codename
self.protocol = protocol
self.registers = registers
self.usbid = usbid
self.interface = interface
self.btid = btid
self.settings = None
#
#
#
_DeviceDescriptor = namedtuple(
'_DeviceDescriptor',
('name', 'kind', 'wpid', 'codename', 'protocol', 'registers', 'settings', 'usbid', 'interface', 'btid')
)
del namedtuple
DEVICES_WPID = {}
DEVICES = {}
@ -69,35 +57,31 @@ def _D(
interface=None,
btid=None,
):
assert name
if kind is None:
kind = (
DEVICE_KIND.mouse
if "Mouse" in name
else DEVICE_KIND.keyboard
if "Keyboard" in name
else DEVICE_KIND.numpad
if "Number Pad" in name
else DEVICE_KIND.touchpad
if "Touchpad" in name
else DEVICE_KIND.trackball
if "Trackball" in name
else None
_DK.mouse if 'Mouse' in name else _DK.keyboard if 'Keyboard' in name else _DK.numpad
if 'Number Pad' in name else _DK.touchpad if 'Touchpad' in name else _DK.trackball if 'Trackball' in name else None
)
assert kind is not None, f"descriptor for {name} does not have kind set"
assert kind is not None, 'descriptor for %s does not have kind set' % name
# heuristic: the codename is the last word in the device name
if codename is None and ' ' in name:
codename = name.split(' ')[-1]
assert codename is not None, 'descriptor for %s does not have codename set' % name
if protocol is not None:
if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid,):
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
if protocol > 1.0:
assert w[0:1] == "4", f"{name} has protocol {protocol:0.1f}, wpid {w}"
assert w[0:1] == '4', '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
else:
if w[0:1] == "1":
assert kind == DEVICE_KIND.mouse, f"{name} has protocol {protocol:0.1f}, wpid {w}"
elif w[0:1] == "2":
assert kind in (
DEVICE_KIND.keyboard,
DEVICE_KIND.numpad,
), f"{name} has protocol {protocol:0.1f}, wpid {w}"
if w[0:1] == '1':
assert kind == _DK.mouse, '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
elif w[0:1] == '2':
assert kind in (_DK.keyboard, _DK.numpad), '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
device_descriptor = _DeviceDescriptor(
name=name,
@ -106,25 +90,25 @@ def _D(
codename=codename,
protocol=protocol,
registers=registers,
settings=settings,
usbid=usbid,
interface=interface,
btid=btid,
btid=btid
)
if usbid:
found = get_usbid(usbid)
assert found is None, f"duplicate usbid in device descriptors: {found}"
assert found is None, 'duplicate usbid in device descriptors: %s' % (found, )
if btid:
found = get_btid(btid)
assert found is None, f"duplicate btid in device descriptors: {found}"
assert found is None, 'duplicate btid in device descriptors: %s' % (found, )
assert codename not in DEVICES, f"duplicate codename in device descriptors: {DEVICES[codename]}"
if codename:
DEVICES[codename] = device_descriptor
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
DEVICES[codename] = device_descriptor
if wpid:
for w in wpid if isinstance(wpid, tuple) else (wpid,):
assert w not in DEVICES_WPID, f"duplicate wpid in device descriptors: {DEVICES_WPID[w]}"
for w in wpid if isinstance(wpid, tuple) else (wpid, ):
assert w not in DEVICES_WPID, 'duplicate wpid in device descriptors: %s' % (DEVICES_WPID[w], )
DEVICES_WPID[w] = device_descriptor
@ -179,288 +163,156 @@ def get_btid(btid):
# The 'protocol' and 'wpid' fields are optional (they can be discovered at
# runtime), but specifying them here speeds up device discovery and reduces the
# USB traffic Solaar has to do to fully identify peripherals.
# Same goes for HID++ 2.0 feature settings (like _feature_fn_swap).
#
# The 'registers' field indicates read-only registers, specifying a state. These
# are valid (AFAIK) only to HID++ 1.0 devices.
# The 'settings' field indicates a read/write register; based on them Solaar
# generates, at runtime, the settings controls in the device panel.
# Solaar now sets up this field in settings_templates.py to eliminate a imports loop.
# HID++ 1.0 devices may only have register-based settings; HID++ 2.0 devices may only have
# generates, at runtime, the settings controls in the device panel. HID++ 1.0
# devices may only have register-based settings; HID++ 2.0 devices may only have
# feature-based settings.
# Devices are organized by kind
# Within kind devices are sorted by wpid, then by usbid, then by btid, with missing values sorted later
# yapf: disable
# Keyboards
_D("Wireless Keyboard EX110", codename="EX110", protocol=1.0, wpid="0055", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Keyboard S510", codename="S510", protocol=1.0, wpid="0056", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Wave Keyboard K550", codename="K550", protocol=1.0, wpid="0060", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Keyboard EX100", codename="EX100", protocol=1.0, wpid="0065", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Keyboard MK300", codename="MK300", protocol=1.0, wpid="0068", registers=(Reg.BATTERY_STATUS,))
_D("Number Pad N545", codename="N545", protocol=1.0, wpid="2006", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Compact Keyboard K340", codename="K340", protocol=1.0, wpid="2007", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Keyboard MK700", codename="MK700", protocol=1.0, wpid="2008", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Wave Keyboard K350", codename="K350", protocol=1.0, wpid="200A", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Keyboard MK320", codename="MK320", protocol=1.0, wpid="200F", registers=(Reg.BATTERY_STATUS,))
_D(
"Wireless Illuminated Keyboard K800",
codename="K800",
protocol=1.0,
wpid="2010",
registers=(Reg.BATTERY_STATUS, Reg.THREE_LEDS),
)
_D("Wireless Keyboard K520", codename="K520", protocol=1.0, wpid="2011", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Solar Keyboard K750", codename="K750", protocol=2.0, wpid="4002")
_D("Wireless Keyboard K270 (unifying)", codename="K270", protocol=2.0, wpid="4003")
_D("Wireless Keyboard K360", codename="K360", protocol=2.0, wpid="4004")
_D("Wireless Keyboard K230", codename="K230", protocol=2.0, wpid="400D")
_D("Wireless Touch Keyboard K400", codename="K400", protocol=2.0, wpid=("400E", "4024"))
_D("Wireless Keyboard MK270", codename="MK270", protocol=2.0, wpid="4023")
_D("Illuminated Living-Room Keyboard K830", codename="K830", protocol=2.0, wpid="4032")
_D("Wireless Touch Keyboard K400 Plus", codename="K400 Plus", protocol=2.0, wpid="404D")
_D("Wireless Multi-Device Keyboard K780", codename="K780", protocol=4.5, wpid="405B")
_D("Wireless Keyboard K375s", codename="K375s", protocol=2.0, wpid="4061")
_D("Craft Advanced Keyboard", codename="Craft", protocol=4.5, wpid="4066", btid=0xB350)
_D("Wireless Illuminated Keyboard K800 new", codename="K800 new", protocol=4.5, wpid="406E")
_D("Wireless Keyboard K470", codename="K470", protocol=4.5, wpid="4075")
_D("MX Keys Keyboard", codename="MX Keys", protocol=4.5, wpid="408A", btid=0xB35B)
_D(
"G915 TKL LIGHTSPEED Wireless RGB Mechanical Gaming Keyboard",
codename="G915 TKL",
protocol=4.2,
wpid="408E",
usbid=0xC343,
)
_D("Illuminated Keyboard", codename="Illuminated", protocol=1.0, usbid=0xC318, interface=1)
_D("G213 Prodigy Gaming Keyboard", codename="G213", usbid=0xC336, interface=1)
_D("G512 RGB Mechanical Gaming Keyboard", codename="G512", usbid=0xC33C, interface=1)
_D("G815 Mechanical Keyboard", codename="G815", usbid=0xC33F, interface=1)
_D("diNovo Edge Keyboard", codename="diNovo", protocol=1.0, wpid="C714")
_D("K845 Mechanical Keyboard", codename="K845", usbid=0xC341, interface=3)
_D('Wireless Keyboard S510', codename='S510', protocol=1.0, wpid='0056', registers=(_R.battery_status, ))
_D('Wireless Keyboard EX100', codename='EX100', protocol=1.0, wpid='0065', registers=(_R.battery_status, ))
_D('Wireless Keyboard MK300', protocol=1.0, wpid='0068', registers=(_R.battery_status, ))
_D('Number Pad N545', protocol=1.0, wpid='2006', registers=(_R.battery_status, ))
_D('Wireless Compact Keyboard K340', protocol=1.0, wpid='2007', registers=(_R.battery_status, ))
_D('Wireless Keyboard MK700', protocol=1.0, wpid='2008', registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap])
_D('Wireless Wave Keyboard K350', protocol=1.0, wpid='200A', registers=(_R.battery_status, ))
_D('Wireless Keyboard MK320', protocol=1.0, wpid='200F', registers=(_R.battery_status, ))
_D('Wireless Illuminated Keyboard K800', protocol=1.0, wpid='2010',
registers=(_R.battery_status, _R.three_leds), settings=[_ST.RegisterFnSwap, _ST.RegisterHandDetection])
_D('Wireless Keyboard K520', protocol=1.0, wpid='2011', registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap])
_D('Wireless Solar Keyboard K750', protocol=2.0, wpid='4002', settings=[_ST.FnSwap])
_D('Wireless Keyboard K270 (unifying)', protocol=2.0, wpid='4003')
_D('Wireless Keyboard K360', protocol=2.0, wpid='4004', settings=[_ST.FnSwap])
_D('Wireless Keyboard K230', protocol=2.0, wpid='400D')
_D('Wireless Touch Keyboard K400', protocol=2.0, wpid=('400E', '4024'), settings=[_ST.FnSwap])
_D('Wireless Keyboard MK270', protocol=2.0, wpid='4023', settings=[_ST.FnSwap])
_D('Illuminated Living-Room Keyboard K830', protocol=2.0, wpid='4032', settings=[_ST.NewFnSwap])
_D('Wireless Touch Keyboard K400 Plus', codename='K400 Plus', protocol=2.0, wpid='404D')
_D('Wireless Multi-Device Keyboard K780', protocol=4.5, wpid='405B', settings=[_ST.NewFnSwap])
_D('Wireless Keyboard K375s', protocol=2.0, wpid='4061', settings=[_ST.K375sFnSwap])
_D('Craft Advanced Keyboard', codename='Craft', protocol=4.5, wpid='4066', btid=0xB350)
_D('Wireless Illuminated Keyboard K800 new', codename='K800 new', protocol=4.5, wpid='406E', settings=[_ST.FnSwap])
_D('MX Keys Keyboard', codename='MX Keys', protocol=4.5, wpid='408A', btid=0xB35B)
_D('G915 TKL LIGHTSPEED Wireless RGB Mechanical Gaming Keyboard', codename='G915 TKL', protocol=4.2, wpid='408E', usbid=0xC343)
_D('G213 Prodigy Gaming Keyboard', codename='G213', usbid=0xc336, interface=1)
_D('G512 RGB Mechanical Gaming Keyboard', codename='G512', usbid=0xc33c, interface=1)
_D('G815 Mechanical Keyboard', codename='G815', usbid=0xc33f, interface=1)
# Mice
_D("LX5 Cordless Mouse", codename="LX5", protocol=1.0, wpid="0036", registers=(Reg.BATTERY_STATUS,))
_D("LX7 Cordless Laser Mouse", codename="LX7", protocol=1.0, wpid="0039", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Wave Mouse M550", codename="M550", protocol=1.0, wpid="003C", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Mouse EX100", codename="EX100m", protocol=1.0, wpid="003F", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Mouse M30", codename="M30", protocol=1.0, wpid="0085", registers=(Reg.BATTERY_STATUS,))
_D("MX610 Laser Cordless Mouse", codename="MX610", protocol=1.0, wpid="1001", registers=(Reg.BATTERY_STATUS,))
_D("G7 Cordless Laser Mouse", codename="G7", protocol=1.0, wpid="1002", registers=(Reg.BATTERY_STATUS,))
_D("V400 Laser Cordless Mouse", codename="V400", protocol=1.0, wpid="1003", registers=(Reg.BATTERY_STATUS,))
_D("MX610 Left-Handled Mouse", codename="MX610L", protocol=1.0, wpid="1004", registers=(Reg.BATTERY_STATUS,))
_D("V450 Laser Cordless Mouse", codename="V450", protocol=1.0, wpid="1005", registers=(Reg.BATTERY_STATUS,))
_D(
"VX Revolution",
codename="VX Revolution",
kind=DEVICE_KIND.mouse,
protocol=1.0,
wpid=("1006", "100D", "0612"),
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"MX Air",
codename="MX Air",
protocol=1.0,
kind=DEVICE_KIND.mouse,
wpid=("1007", "100E"),
registers=Reg.BATTERY_CHARGE,
)
_D(
"MX Revolution",
codename="MX Revolution",
protocol=1.0,
kind=DEVICE_KIND.mouse,
wpid=("1008", "100C"),
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"MX620 Laser Cordless Mouse",
codename="MX620",
protocol=1.0,
wpid=("100A", "1016"),
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"VX Nano Cordless Laser Mouse",
codename="VX Nano",
protocol=1.0,
wpid=("100B", "100F"),
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"V450 Nano Cordless Laser Mouse",
codename="V450 Nano",
protocol=1.0,
wpid="1011",
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"V550 Nano Cordless Laser Mouse",
codename="V550 Nano",
protocol=1.0,
wpid="1013",
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"MX 1100 Cordless Laser Mouse",
codename="MX 1100",
protocol=1.0,
kind=DEVICE_KIND.mouse,
wpid="1014",
registers=(Reg.BATTERY_CHARGE,),
)
_D("Anywhere Mouse MX", codename="Anywhere MX", protocol=1.0, wpid="1017", registers=(Reg.BATTERY_CHARGE,))
_D(
"Performance Mouse MX",
codename="Performance MX",
protocol=1.0,
wpid="101A",
registers=(Reg.BATTERY_STATUS, Reg.THREE_LEDS),
)
_D(
"Marathon Mouse M705 (M-R0009)",
codename="M705 (M-R0009)",
protocol=1.0,
wpid="101B",
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"Wireless Mouse M350",
codename="M350",
protocol=1.0,
wpid="101C",
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"Wireless Mouse M505",
codename="M505/B605",
protocol=1.0,
wpid="101D",
registers=(Reg.BATTERY_CHARGE,),
)
_D(
"Wireless Mouse M305",
codename="M305",
protocol=1.0,
wpid="101F",
registers=(Reg.BATTERY_STATUS,),
)
_D(
"Wireless Mouse M215",
codename="M215",
protocol=1.0,
wpid="1020",
)
_D(
"G700 Gaming Mouse",
codename="G700",
protocol=1.0,
wpid="1023",
usbid=0xC06B,
interface=1,
registers=(
Reg.BATTERY_STATUS,
Reg.THREE_LEDS,
),
)
_D("Wireless Mouse M310", codename="M310", protocol=1.0, wpid="1024", registers=(Reg.BATTERY_STATUS,))
_D("Wireless Mouse M510", codename="M510", protocol=1.0, wpid="1025", registers=(Reg.BATTERY_STATUS,))
_D("Fujitsu Sonic Mouse", codename="Sonic", protocol=1.0, wpid="1029")
_D(
"G700s Gaming Mouse",
codename="G700s",
protocol=1.0,
wpid="102A",
usbid=0xC07C,
interface=1,
registers=(
Reg.BATTERY_STATUS,
Reg.THREE_LEDS,
),
)
_D("Couch Mouse M515", codename="M515", protocol=2.0, wpid="4007")
_D("Wireless Mouse M175", codename="M175", protocol=2.0, wpid="4008")
_D("Wireless Mouse M325", codename="M325", protocol=2.0, wpid="400A")
_D("Wireless Mouse M525", codename="M525", protocol=2.0, wpid="4013")
_D("Wireless Mouse M345", codename="M345", protocol=2.0, wpid="4017")
_D("Wireless Mouse M187", codename="M187", protocol=2.0, wpid="4019")
_D("Touch Mouse M600", codename="M600", protocol=2.0, wpid="401A")
_D("Wireless Mouse M150", codename="M150", protocol=2.0, wpid="4022")
_D("Wireless Mouse M185", codename="M185", protocol=2.0, wpid="4038")
_D("Wireless Mouse MX Master", codename="MX Master", protocol=4.5, wpid="4041", btid=0xB012)
_D("Anywhere Mouse MX 2", codename="Anywhere MX 2", protocol=4.5, wpid="404A")
_D("Wireless Mouse M510", codename="M510v2", protocol=2.0, wpid="4051")
_D("Wireless Mouse M185 new", codename="M185n", protocol=4.5, wpid="4054")
_D("Wireless Mouse M185/M235/M310", codename="M185/M235/M310", protocol=4.5, wpid="4055")
_D("Wireless Mouse MX Master 2S", codename="MX Master 2S", protocol=4.5, wpid="4069", btid=0xB019)
_D("Multi Device Silent Mouse M585/M590", codename="M585/M590", protocol=4.5, wpid="406B")
_D(
"Marathon Mouse M705 (M-R0073)",
codename="M705 (M-R0073)",
protocol=4.5,
wpid="406D",
)
_D("MX Vertical Wireless Mouse", codename="MX Vertical", protocol=4.5, wpid="407B", btid=0xB020, usbid=0xC08A)
_D("Wireless Mouse Pebble M350", codename="Pebble", protocol=2.0, wpid="4080")
_D("MX Master 3 Wireless Mouse", codename="MX Master 3", protocol=4.5, wpid="4082", btid=0xB023)
_D("PRO X Wireless", kind="mouse", codename="PRO X", wpid="4093", usbid=0xC094)
_D('LX5 Cordless Mouse', codename='LX5', protocol=1.0, wpid='0036', registers=(_R.battery_status, ))
_D('Wireless Mouse EX100', codename='EX100m', protocol=1.0, wpid='003F', registers=(_R.battery_status, ))
_D('Wireless Mouse M30', codename='M30', protocol=1.0, wpid='0085', registers=(_R.battery_status, ))
_D('MX610 Laser Cordless Mouse', codename='MX610', protocol=1.0, wpid='1001', registers=(_R.battery_status, ))
_D('G7 Cordless Laser Mouse', codename='G7', protocol=1.0, wpid='1002', registers=(_R.battery_status, ))
_D('V400 Laser Cordless Mouse', codename='V400', protocol=1.0, wpid='1003', registers=(_R.battery_status, ))
_D('MX610 Left-Handled Mouse', codename='MX610L', protocol=1.0, wpid='1004', registers=(_R.battery_status, ))
_D('V450 Laser Cordless Mouse', codename='V450', protocol=1.0, wpid='1005', registers=(_R.battery_status, ))
_D('VX Revolution', codename='VX Revolution', kind=_DK.mouse, protocol=1.0, wpid=('1006', '100D', '0612'),
registers=(_R.battery_charge, ))
_D('MX Air', codename='MX Air', protocol=1.0, kind=_DK.mouse, wpid=('1007', '100E'), registers=(_R.battery_charge, ))
_D('MX Revolution', codename='MX Revolution', protocol=1.0, kind=_DK.mouse, wpid=('1008', '100C'),
registers=(_R.battery_charge, ))
_D('MX620 Laser Cordless Mouse', codename='MX620', protocol=1.0, wpid=('100A', '1016'), registers=(_R.battery_charge, ))
_D('VX Nano Cordless Laser Mouse', codename='VX Nano', protocol=1.0, wpid=('100B', '100F'),
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('V450 Nano Cordless Laser Mouse', codename='V450 Nano', protocol=1.0, wpid='1011', registers=(_R.battery_charge, ))
_D('V550 Nano Cordless Laser Mouse', codename='V550 Nano', protocol=1.0, wpid='1013',
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll, ])
_D('MX 1100 Cordless Laser Mouse', codename='MX 1100', protocol=1.0, kind=_DK.mouse, wpid='1014',
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Anywhere Mouse MX', codename='Anywhere MX', protocol=1.0, wpid='1017',
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D("G9 Laser Mouse", codename="G9", usbid=0xC048, interface=1, protocol=1.0)
_D("G9x Laser Mouse", codename="G9x", usbid=0xC066, interface=1, protocol=1.0)
_D("G502 Gaming Mouse", codename="G502", usbid=0xC07D, interface=1)
_D("G402 Gaming Mouse", codename="G402", usbid=0xC07E, interface=1)
_D("G900 Chaos Spectrum Gaming Mouse", codename="G900", usbid=0xC081)
_D("G403 Gaming Mouse", codename="G403", usbid=0xC082)
_D("G903 Lightspeed Gaming Mouse", codename="G903", usbid=0xC086)
_D("G703 Lightspeed Gaming Mouse", codename="G703", usbid=0xC087)
_D("GPro Gaming Mouse", codename="GPro", usbid=0xC088)
_D("G502 SE Hero Gaming Mouse", codename="G502 Hero", usbid=0xC08B, interface=1)
_D("G502 Lightspeed Gaming Mouse", codename="G502 Lightspeed", usbid=0xC08D)
_D("MX518 Gaming Mouse", codename="MX518", usbid=0xC08E, interface=1)
_D("G703 Hero Gaming Mouse", codename="G703 Hero", usbid=0xC090)
_D("G903 Hero Gaming Mouse", codename="G903 Hero", usbid=0xC091)
_D(None, kind=DEVICE_KIND.mouse, usbid=0xC092, interface=1) # two mice share this ID
_D("M500S Mouse", codename="M500S", usbid=0xC093, interface=1)
class _PerformanceMXDpi(_ST.RegisterDpi):
choices_universe = _NamedInts.range(0x81, 0x8F, lambda x: str((x - 0x80) * 100))
validator_options = {'choices': choices_universe}
_D('Performance Mouse MX', codename='Performance MX', protocol=1.0, wpid='101A',
registers=(_R.battery_status, _R.three_leds),
settings=[_PerformanceMXDpi, _ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Marathon Mouse M705 (M-R0009)', codename='M705 (M-R0009)', protocol=1.0, wpid='101B',
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Wireless Mouse M350', protocol=1.0, wpid='101C', registers=(_R.battery_charge, ))
_D('Wireless Mouse M505', codename='M505/B605', protocol=1.0, wpid='101D',
registers=(_R.battery_charge, ), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Wireless Mouse M305', protocol=1.0, wpid='101F', registers=(_R.battery_status, ), settings=[_ST.RegisterSideScroll])
_D('Wireless Mouse M215', protocol=1.0, wpid='1020')
_D('G700 Gaming Mouse', codename='G700', protocol=1.0, wpid='1023', usbid=0xc06b, interface=1,
registers=(_R.battery_status, _R.three_leds,), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Wireless Mouse M310', protocol=1.0, wpid='1024', registers=(_R.battery_status, ))
_D('Wireless Mouse M510', protocol=1.0, wpid='1025', registers=(_R.battery_status, ), settings=[_ST.RegisterSideScroll])
_D('Fujitsu Sonic Mouse', codename='Sonic', protocol=1.0, wpid='1029')
_D('G700s Gaming Mouse', codename='G700s', protocol=1.0, wpid='102A', usbid=0xc07c, interface=1,
registers=(_R.battery_status, _R.three_leds,), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('Couch Mouse M515', protocol=2.0, wpid='4007')
_D('Wireless Mouse M175', protocol=2.0, wpid='4008')
_D('Wireless Mouse M325', protocol=2.0, wpid='400A', settings=[_ST.HiResScroll])
_D('Wireless Mouse M525', protocol=2.0, wpid='4013')
_D('Wireless Mouse M345', protocol=2.0, wpid='4017')
_D('Wireless Mouse M187', protocol=2.0, wpid='4019')
_D('Touch Mouse M600', protocol=2.0, wpid='401A')
_D('Wireless Mouse M150', protocol=2.0, wpid='4022')
_D('Wireless Mouse M185', protocol=2.0, wpid='4038')
_D('Wireless Mouse MX Master', codename='MX Master', protocol=4.5, wpid='4041', btid=0xb012)
_D('Anywhere Mouse MX 2', codename='Anywhere MX 2', protocol=4.5, wpid='404A', settings=[_ST.HiresSmoothInvert])
_D('Wireless Mouse M510', protocol=2.0, wpid='4051', codename='M510v2')
_D('Wireless Mouse M185 new', codename='M185n', protocol=4.5, wpid='4054')
_D('Wireless Mouse M185/M235/M310', codename='M185/M235/M310', protocol=4.5, wpid='4055')
_D('Wireless Mouse MX Master 2S', codename='MX Master 2S', protocol=4.5, wpid='4069', btid=0xb019,
settings=[_ST.HiresSmoothInvert])
_D('Multi Device Silent Mouse M585/M590', codename='M585/M590', protocol=4.5, wpid='406B')
_D('Marathon Mouse M705 (M-R0073)', codename='M705 (M-R0073)', protocol=4.5, wpid='406D',
settings=[_ST.HiresSmoothInvert, _ST.PointerSpeed])
_D('MX Vertical Wireless Mouse', codename='MX Vertical', protocol=4.5, wpid='407B', btid=0xb020, usbid=0xc08a)
_D('Wireless Mouse Pebble M350', protocol=2.0, wpid='4080', codename='Pebble')
_D('MX Master 3 Wireless Mouse', codename='MX Master 3', protocol=4.5, wpid='4082', btid=0xb023)
_D('PRO X Wireless', kind='mouse', codename='PRO X', wpid='4093', usbid=0xc094)
_D('G9 Laser Mouse', codename='G9', usbid=0xc048, interface=1, protocol=1.0,
settings=[_PerformanceMXDpi, _ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
_D('G502 Gaming Mouse', codename='G502', usbid=0xc07d, interface=1)
_D('G402 Gaming Mouse', codename='G402', usbid=0xc07e, interface=1)
_D('G900 Chaos Spectrum Gaming Mouse', codename='G900', usbid=0xc081)
_D('G403 Gaming Mouse', codename='G403', usbid=0xc082)
_D('G903 Lightspeed Gaming Mouse', codename='G903', usbid=0xc086)
_D('G703 Lightspeed Gaming Mouse', codename='G703', usbid=0xc087)
_D('GPro Gaming Mouse', codename='GPro', usbid=0xc088)
_D('G502 SE Hero Gaming Mouse', codename='G502 Hero', usbid=0xc08b, interface=1)
_D('G502 Lightspeed Gaming Mouse', codename='G502 Lightspeed', usbid=0xc08d)
_D('MX518 Gaming Mouse', codename='MX518', usbid=0xc08e, interface=1)
_D('G703 Hero Gaming Mouse', codename='G703 Hero', usbid=0xc090)
_D('G903 Hero Gaming Mouse', codename='G903 Hero', usbid=0xc091)
_D('G102 Lightsync Mouse', codename='G102', usbid=0xc092, interface=1)
_D('M500S Mouse', codename='M500S', usbid=0xc093, interface=1)
# _D('G600 Gaming Mouse', codename='G600 Gaming', usbid=0xc24a, interface=1) # not an HID++ device
_D("G500s Gaming Mouse", codename="G500s Gaming", usbid=0xC24E, interface=1, protocol=1.0)
_D("G502 Proteus Spectrum Optical Mouse", codename="G502 Proteus Spectrum", usbid=0xC332, interface=1)
_D("Logitech PRO Gaming Keyboard", codename="PRO Gaming Keyboard", usbid=0xC339, interface=1)
_D("Logitech MX Revolution Mouse M-RCL 124", codename="M-RCL 124", btid=0xB007, interface=1)
_D('G502 Proteus Spectrum Optical Mouse', codename='G502 Proteus Spectrum', usbid=0xc332, interface=1)
_D('Logitech PRO Gaming Keyboard', codename='PRO Gaming Keyboard', usbid=0xc339, interface=1)
# Trackballs
_D("Wireless Trackball M570", codename="M570")
_D('Wireless Trackball M570')
# Touchpads
_D("Wireless Touchpad", codename="Wireless Touch", protocol=2.0, wpid="4011")
_D("Wireless Rechargeable Touchpad T650", codename="T650", protocol=2.0, wpid="4101")
_D(
"G Powerplay", codename="Powerplay", protocol=2.0, kind=DEVICE_KIND.touchpad, wpid="405F"
) # To override self-identification
_D('Wireless Touchpad', codename='Wireless Touch', protocol=2.0, wpid='4011')
_D('Wireless Rechargeable Touchpad T650', protocol=2.0, wpid='4101')
# Headset
_D("G533 Gaming Headset", codename="G533 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0A66)
_D("G535 Gaming Headset", codename="G535 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0AC4)
_D("G935 Gaming Headset", codename="G935 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0A87)
_D("G733 Gaming Headset", codename="G733 Headset", protocol=2.0, interface=3, kind=DEVICE_KIND.headset, usbid=0x0AB5)
_D(
"G733 Gaming Headset",
codename="G733 Headset New",
protocol=2.0,
interface=3,
kind=DEVICE_KIND.headset,
usbid=0x0AFE,
)
_D(
"PRO X Wireless Gaming Headset",
codename="PRO Headset",
protocol=2.0,
interface=3,
kind=DEVICE_KIND.headset,
usbid=0x0ABA,
)
_D('G533 Gaming Headset', codename='G533 Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0a66)
_D('G935 Gaming Headset', codename='G935 Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0a87)
_D('G733 Gaming Headset', codename='G733 Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0ab5)
_D('PRO X Wireless Gaming Headset', codename='PRO Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0aba)

View File

@ -1,129 +0,0 @@
## Copyright (C) 2024 Solaar contributors
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""Implements the desktop notification service."""
import importlib
import logging
logger = logging.getLogger(__name__)
def notifications_available():
"""Checks if notification service is available."""
notifications_supported = False
try:
import gi
gi.require_version("Notify", "0.7")
gi.require_version("Gtk", "3.0")
importlib.util.find_spec("gi.repository.GLib")
importlib.util.find_spec("gi.repository.Gtk")
importlib.util.find_spec("gi.repository.Notify")
notifications_supported = True
except ValueError as e:
logger.warning(f"Notification service is not available: {e}")
return notifications_supported
available = notifications_available()
if available:
from gi.repository import GLib
from gi.repository import Gtk
from gi.repository import Notify
# cache references to shown notifications here to allow reuse
_notifications = {}
_ICON_LISTS = {}
def init():
"""Initialize desktop notifications."""
global available
if available:
if not Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info("starting desktop notifications")
try:
return Notify.init("solaar") # replace with better name later
except Exception:
logger.exception("initializing desktop notifications")
available = False
return available and Notify.is_initted()
def uninit():
"""Stop desktop notifications."""
if available and Notify.is_initted():
if logger.isEnabledFor(logging.INFO):
logger.info("stopping desktop notifications")
_notifications.clear()
Notify.uninit()
def show(dev, message: str, icon=None):
"""Show a notification with title and text."""
if available and (Notify.is_initted() or init()):
summary = dev.name
n = _notifications.get(summary) # reuse notification of same name
if n is None:
n = _notifications[summary] = Notify.Notification()
icon_name = device_icon_name(dev.name, dev.kind) if icon is None else icon
n.update(summary, message, icon_name)
n.set_urgency(Notify.Urgency.NORMAL)
n.set_hint("desktop-entry", GLib.Variant("s", "solaar")) # replace with better name late
try:
return n.show()
except Exception:
logger.exception(f"showing {n}")
def device_icon_list(name="_", kind=None):
icon_list = _ICON_LISTS.get(name)
if icon_list is None:
# names of possible icons, in reverse order of likelihood
# the theme will hopefully pick up the most appropriate
icon_list = ["preferences-desktop-peripherals"]
kind = str(kind)
if kind:
if kind == "numpad":
icon_list += ("input-keyboard", "input-dialpad")
elif kind == "touchpad":
icon_list += ("input-mouse", "input-tablet")
elif kind == "trackball":
icon_list += ("input-mouse",)
elif kind == "headset":
icon_list += ("audio-headphones", "audio-headset")
icon_list += (f"input-{kind}",)
_ICON_LISTS[name] = icon_list
return icon_list
def device_icon_name(name, kind=None):
_default_theme = Gtk.IconTheme.get_default()
icon_list = device_icon_list(name, kind)
for n in reversed(icon_list):
if _default_theme.has_icon(n):
return n
else:
def init():
return False
def uninit():
return None
def show(dev, reason=None):
return None

View File

@ -1,222 +1,187 @@
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import errno as _errno
import threading as _threading
from __future__ import annotations
import errno
import logging
import threading
import time
import typing
from typing import Callable
from logging import INFO as _INFO
from logging import getLogger
from typing import Optional
from typing import Protocol
from solaar import configuration
import hidapi as _hid
import solaar.configuration as _configuration
from . import descriptors
from . import exceptions
from . import hidpp10
from . import hidpp10_constants
from . import hidpp20
from . import settings
from . import settings_templates
from .common import Alert
from .common import Battery
from .hidpp10_constants import NotificationFlag
from .hidpp20_constants import SupportedFeature
from . import base as _base
from . import descriptors as _descriptors
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from .common import strhex as _strhex
from .settings_templates import check_feature_settings as _check_feature_settings
if typing.TYPE_CHECKING:
from logitech_receiver import common
_log = getLogger(__name__)
del getLogger
logger = logging.getLogger(__name__)
_R = _hidpp10.REGISTERS
_IR = _hidpp10.INFO_SUBREGISTERS
_hidpp10 = hidpp10.Hidpp10()
_hidpp20 = hidpp20.Hidpp20()
KIND_MAP = {kind: _hidpp10.DEVICE_KIND[str(kind)] for kind in _hidpp20.DEVICE_KIND}
class LowLevelInterface(Protocol):
def open_path(self, path) -> int:
...
def find_paired_node(self, receiver_path: str, index: int, timeout: int):
...
def ping(self, handle, number, long_message: bool):
...
def request(self, handle, devnumber, request_id, *params, **kwargs):
...
def close(self, handle, *args, **kwargs) -> bool:
...
def create_device(low_level: LowLevelInterface, device_info, setting_callback=None):
"""Opens a Logitech Device found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or None.
"""
try:
handle = low_level.open_path(device_info.path)
if handle:
# a direct connected device might not be online (as reported by user)
return Device(
low_level,
None,
None,
None,
handle=handle,
device_info=device_info,
setting_callback=setting_callback,
)
except OSError as e:
logger.exception("open %s", device_info)
if e.errno == errno.EACCES:
raise e
except Exception as e:
logger.exception("open %s", device_info)
raise e
#
#
#
class Device:
instances = []
read_register: Callable = hidpp10.read_register
write_register: Callable = hidpp10.write_register
read_register = _hidpp10.read_register
write_register = _hidpp10.write_register
def __init__(
self,
low_level: LowLevelInterface,
receiver,
number,
online,
pairing_info=None,
handle=None,
device_info=None,
setting_callback=None,
):
assert receiver or device_info
if receiver:
assert 0 < number <= 15 # some receivers have devices past their max # of devices
self.low_level = low_level
self.number = number # will be None at this point for directly connected devices
self.online = online # is the device online? - gates many atempts to contact the device
self.descriptor = None
self.isDevice = True # some devices act as receiver so we need a property to distinguish them
self.may_unpair = False
def __init__(self, receiver, number, link_notification=None, info=None, path=None, handle=None):
assert receiver or info
Device.instances.append(self)
self.receiver = receiver
self.may_unpair = False
self.isDevice = True # some devices act as receiver so we need a property to distinguish them
self.path = path
self.handle = handle
self.path = device_info.path if device_info else None
self.product_id = device_info.product_id if device_info else None
self.hidpp_short = device_info.hidpp_short if device_info else None
self.hidpp_long = device_info.hidpp_long if device_info else None
self.bluetooth = device_info.bus_id == 0x0005 if device_info else False # Bluetooth needs long messages
self.hid_serial = device_info.serial if device_info else None
self.setting_callback = setting_callback # for changes to settings
self.status_callback = None # for changes to other potentially visible aspects
self.wpid = pairing_info["wpid"] if pairing_info else None # the Wireless PID is unique per device model
self._kind = pairing_info["kind"] if pairing_info else None # mouse, keyboard, etc (see hidpp10.DEVICE_KIND)
self._serial = pairing_info["serial"] if pairing_info else None # serial number (an 8-char hex string)
self._polling_rate = pairing_info["polling"] if pairing_info else None
self._power_switch = pairing_info["power_switch"] if pairing_info else None
self._name = None # the full name of the model
self._codename = None # Unifying peripherals report a codename.
self._protocol = None # HID++ protocol version, 1.0 or 2.0
self._unitId = None # unit id (distinguishes within a model - generally the same as serial)
self._modelId = None # model id (contains identifiers for the transports of the device)
self._tid_map = None # map from transports to product identifiers
self._persister = None # persister holds settings
self._led_effects = self._firmware = self._keys = self._remap_keys = self._gestures = None
self._profiles = self._backlight = self._settings = None
self.registers = []
self.notification_flags = None
self.battery_info = None
self.link_encrypted = None
self._active = None # lags self.online - is used to help determine when to setup devices
self.present = True # used for devices that are integral with their receiver but that separately be disconnected
self.product_id = None
self.hidpp_short = info.hidpp_short if info else None
self.hidpp_long = info.hidpp_long if info else None
if receiver:
assert number > 0 and number <= 15 # some receivers have devices past their max # of devices
self.number = number # will be None at this point for directly connected devices
# 'device active' flag; requires manual management.
self.online = None
# the Wireless PID is unique per device model
self.wpid = None
self.descriptor = None
# Bluetooth connections need long messages
self.bluetooth = False
# mouse, keyboard, etc (see _hidpp10.DEVICE_KIND)
self._kind = None
# Unifying peripherals report a codename.
self._codename = None
# the full name of the model
self._name = None
# HID++ protocol version, 1.0 or 2.0
self._protocol = None
# serial number (an 8-char hex string)
self._serial = None
# unit id (distinguishes within a model - the same as serial)
self._unitId = None
# model id (contains identifiers for the transports of the device)
self._modelId = None
# map from transports to product identifiers
self._tid_map = None
# persister holds settings
self._persister = None
self._firmware = None
self._keys = None
self._remap_keys = None
self._gestures = None
self._gestures_lock = _threading.Lock()
self._registers = None
self._settings = None
self._feature_settings_checked = False
self._gestures_lock = threading.Lock()
self._settings_lock = threading.Lock()
self._persister_lock = threading.Lock()
self._notification_handlers = {} # See `add_notification_handler`
self.cleanups = [] # functions to run on the device when it is closed
self._settings_lock = _threading.Lock()
# Misc stuff that's irrelevant to any functionality, but may be
# displayed in the UI and caching it here helps.
self._polling_rate = None
self._power_switch = None
# See `add_notification_handler`
self._notification_handlers = {}
# if _log.isEnabledFor(_DEBUG):
# _log.debug("new Device(%s, %s, %s)", receiver, number, link_notification)
if not self.path:
self.path = self.low_level.find_paired_node(receiver.path, number, 1) if receiver else None
self.path = _hid.find_paired_node(receiver.path, number, 1) if receiver else info.path
if not self.handle:
try:
self.handle = self.low_level.open_path(self.path) if self.path else None
self.handle = _base.open_path(self.path) if self.path else None
except Exception: # maybe the device wasn't set up
try:
import time
time.sleep(1)
self.handle = self.low_level.open_path(self.path) if self.path else None
self.handle = _base.open_path(self.path) if self.path else None
except Exception: # give up
self.handle = None # should this give up completely?
self.handle = None
if receiver:
if not self.wpid:
raise exceptions.NoSuchDevice(
number=number, receiver=receiver, error="no wpid for device connected to receiver"
)
self.descriptor = descriptors.get_wpid(self.wpid)
if link_notification is not None:
self.online = not bool(ord(link_notification.data[0:1]) & 0x40)
self.wpid = _strhex(link_notification.data[2:3] + link_notification.data[1:2])
# assert link_notification.address == (0x04 if unifying else 0x03)
kind = ord(link_notification.data[0:1]) & 0x0F
# get 27Mhz wpid and set kind based on index
if receiver.receiver_kind == '27Mhz': # 27 Mhz receiver
self.wpid = '00' + _strhex(link_notification.data[2:3])
kind = self.get_kind_from_index(number, receiver)
self._kind = _hidpp10.DEVICE_KIND[kind]
else:
# Not a notification, force a reading of pairing information
self.online = True
self.update_pairing_information()
self.update_extended_pairing_information()
if not self.wpid and not self._serial: # if neither then the device almost certainly wasn't found
raise _base.NoSuchDevice(number=number, receiver=receiver, error='no wpid or serial')
# the wpid is set to None on this object when the device is unpaired
assert self.wpid is not None, 'failed to read wpid: device %d of %s' % (number, receiver)
self.descriptor = _descriptors.get_wpid(self.wpid)
if self.descriptor is None:
codename = self.receiver.device_codename(self.number) # Last chance to get a descriptor, may fail
# Last chance to correctly identify the device; many Nano receivers do not support this call.
codename = self.receiver.device_codename(self.number)
if codename:
self._codename = codename
self.descriptor = descriptors.get_codename(self._codename)
self.descriptor = _descriptors.get_codename(self._codename)
else:
self.descriptor = (
descriptors.get_btid(self.product_id) if self.bluetooth else descriptors.get_usbid(self.product_id)
)
self.online = None # a direct connected device might not be online (as reported by user)
self.product_id = info.product_id
self.bluetooth = info.bus_id == 0x0005
self.descriptor = _descriptors.get_btid(self.product_id) if self.bluetooth else \
_descriptors.get_usbid(self.product_id)
if self.number is None: # for direct-connected devices get 'number' from descriptor protocol else use 0xFF
if self.descriptor and self.descriptor.protocol and self.descriptor.protocol < 2.0:
number = 0x00
else:
number = 0xFF
self.number = number
self.ping() # determine whether a direct-connected device is online
self.number = 0x00 if self.descriptor and self.descriptor.protocol and self.descriptor.protocol < 2.0 else 0xFF
if self.descriptor:
self._name = self.descriptor.name
if self.descriptor.protocol:
self._protocol = self.descriptor.protocol
if self._codename is None:
self._codename = self.descriptor.codename
if self._kind is None:
self._kind = self.descriptor.kind
self._protocol = self.descriptor.protocol if self.descriptor.protocol else None
self.registers = self.descriptor.registers if self.descriptor.registers else []
if self._protocol is not None:
self.features = None if self._protocol < 2.0 else hidpp20.FeaturesArray(self)
self.features = None if self._protocol < 2.0 else _hidpp20.FeaturesArray(self)
else:
self.features = hidpp20.FeaturesArray(self) # may be a 2.0 device; if not, it will fix itself later
# may be a 2.0 device; if not, it will fix itself later
self.features = _hidpp20.FeaturesArray(self)
Device.instances.append(self)
def find(self, id): # find a device by serial number or unit ID or name or codename
assert id, "need id to find a device"
@classmethod
def find(self, serial):
assert serial, 'need serial number or unit ID to find a device'
result = None
for device in Device.instances:
if device.online and (device.unitId == id or device.serial == id or device.name == id or device.codename == id):
return device
if device.online and (device.unitId == serial or device.serial == serial):
result = device
return result
@property
def protocol(self):
if not self._protocol:
self.ping()
if not self._protocol and self.online:
self._protocol = _base.ping(
self.handle or self.receiver.handle, self.number, long_message=self.bluetooth or self.hidpp_short is False
)
# if the ping failed, the peripheral is (almost) certainly offline
self.online = self._protocol is not None
# if _log.isEnabledFor(_DEBUG):
# _log.debug("device %d protocol %s", self.number, self._protocol)
return self._protocol or 0
@property
@ -225,28 +190,28 @@ class Device:
if self.online and self.protocol >= 2.0:
self._codename = _hidpp20.get_friendly_name(self)
if not self._codename:
self._codename = self.name.split(" ", 1)[0] if self.name else None
if not self._codename and self.receiver:
self._codename = self.name.split(' ', 1)[0] if self.name else None
elif self.receiver:
codename = self.receiver.device_codename(self.number)
if codename:
self._codename = codename
elif self.protocol < 2.0:
self._codename = "? (%s)" % (self.wpid or self.product_id)
return self._codename or f"?? ({self.wpid or self.product_id})"
self._codename = '? (%s)' % (self.wpid or self.product_id)
return self._codename if self._codename else '?? (%s)' % (self.wpid or self.product_id)
@property
def name(self):
if not self._name:
if self.online and self.protocol >= 2.0:
self._name = _hidpp20.get_name(self)
return self._name or self._codename or f"Unknown device {self.wpid or self.product_id}"
return self._name or self._codename or ('Unknown device %s' % (self.wpid or self.product_id))
def get_ids(self):
ids = _hidpp20.get_ids(self)
if ids:
self._unitId, self._modelId, self._tid_map = ids
if logger.isEnabledFor(logging.INFO) and self._serial and self._serial != self._unitId:
logger.info("%s: unitId %s does not match serial %s", self, self._unitId, self._serial)
if _log.isEnabledFor(_INFO) and self._serial and self._serial != self._unitId:
_log.info('%s: unitId %s does not match serial %s', self, self._unitId, self._serial)
@property
def unitId(self):
@ -266,14 +231,35 @@ class Device:
self.get_ids()
return self._tid_map
@property
def kind(self):
if not self._kind and self.online and self.protocol >= 2.0:
self._kind = _hidpp20.get_kind(self)
return self._kind or "?"
def update_pairing_information(self):
if self.receiver and (not self.wpid or self._kind is None or self._polling_rate is None):
wpid, kind, polling_rate = self.receiver.device_pairing_information(self.number)
if not self.wpid:
self.wpid = wpid
if not self._kind:
self._kind = kind
if not self._polling_rate:
self._polling_rate = polling_rate
def update_extended_pairing_information(self):
if self.receiver:
serial, power_switch = self.receiver.device_extended_pairing_information(self.number)
if not self._serial:
self._serial = serial
if not self._power_switch:
self._power_switch = power_switch
@property
def firmware(self) -> tuple[common.FirmwareInfo]:
def kind(self):
if not self._kind:
self.update_pairing_information()
if not self._kind and self.protocol >= 2.0:
kind = _hidpp20.get_kind(self)
self._kind = KIND_MAP[kind] if kind else None
return self._kind or '?'
@property
def firmware(self):
if self._firmware is None and self.online:
if self.protocol >= 2.0:
self._firmware = _hidpp20.get_firmware(self)
@ -283,32 +269,32 @@ class Device:
@property
def serial(self):
return self._serial or ""
if not self._serial:
self.update_extended_pairing_information()
return self._serial or ''
@property
def id(self):
if not self.serial:
if self.persister and self.persister.get('_serial', None):
self._serial = self.persister.get('_serial', None)
return self.unitId or self.serial
@property
def power_switch_location(self):
if not self._power_switch:
self.update_extended_pairing_information()
return self._power_switch
@property
def polling_rate(self):
if self.online and self.protocol >= 2.0:
if not self._polling_rate:
self.update_pairing_information()
if self.protocol >= 2.0:
rate = _hidpp20.get_polling_rate(self)
self._polling_rate = rate if rate else self._polling_rate
return self._polling_rate
@property
def led_effects(self):
if not self._led_effects and self.online and self.protocol >= 2.0:
if SupportedFeature.COLOR_LED_EFFECTS in self.features:
self._led_effects = hidpp20.LEDEffectsInfo(self)
elif SupportedFeature.RGB_EFFECTS in self.features:
self._led_effects = hidpp20.RGBEffectsInfo(self)
return self._led_effects
@property
def keys(self):
if not self._keys:
@ -333,33 +319,13 @@ class Device:
return self._gestures
@property
def backlight(self):
if self._backlight is None:
if self.online and self.protocol >= 2.0:
self._backlight = _hidpp20.get_backlight(self)
return self._backlight
@property
def profiles(self):
if self._profiles is None:
if self.online and self.protocol >= 2.0:
self._profiles = _hidpp20.get_profiles(self)
return self._profiles
def set_configuration(self, configuration_, no_reply=False):
if self.online and self.protocol >= 2.0:
_hidpp20.config_change(self, configuration_, no_reply=no_reply)
def reset(self, no_reply=False):
self.set_configuration(0, no_reply)
@property
def persister(self):
if not self._persister:
with self._persister_lock:
if not self._persister:
self._persister = configuration.persister(self)
return self._persister
def registers(self):
if not self._registers:
if self.descriptor and self.descriptor.registers:
self._registers = list(self.descriptor.registers)
else:
self._registers = []
return self._registers
@property
def settings(self):
@ -381,87 +347,37 @@ class Device:
if not self._feature_settings_checked:
with self._settings_lock:
if not self._feature_settings_checked:
self._feature_settings_checked = settings_templates.check_feature_settings(self, self._settings)
self._feature_settings_checked = _check_feature_settings(self, self._settings)
return self._settings
def set_configuration(self, configuration, no_reply=False):
if self.online and self.protocol >= 2.0:
_hidpp20.config_change(self, configuration, no_reply=no_reply)
def reset(self, no_reply=False):
self.set_configuration(0, no_reply)
@property
def persister(self):
if not self._persister:
self._persister = _configuration.persister(self)
return self._persister
def battery(self): # None or level, next, status, voltage
if self.protocol < 2.0:
return _hidpp10.get_battery(self)
else:
battery_feature = self.persister.get("_battery", None) if self.persister else None
battery_feature = self.persister.get('_battery', None) if self.persister else None
if battery_feature != 0:
result = _hidpp20.get_battery(self, battery_feature)
try:
feature, battery = result
feature, level, next, status, voltage = result
if self.persister and battery_feature is None:
self.persister["_battery"] = feature.value
return battery
self.persister['_battery'] = feature
return level, next, status, voltage
except Exception:
if self.persister and battery_feature is None and result is not None:
self.persister["_battery"] = result.value
def set_battery_info(self, info):
"""Update battery information for device, calling changed callback if necessary"""
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%s: battery %s, %s", self, info.level, info.status)
if info.level is None and self.battery_info: # use previous level if missing from new information
info.level = self.battery_info.level
changed = self.battery_info != info
self.battery_info, old_info = info, self.battery_info
if old_info is None:
old_info = Battery(None, None, None, None)
alert, reason = Alert.NONE, None
if not info.ok():
logger.warning("%s: battery %d%%, ALERT %s", self, info.level, info.status)
if old_info.status != info.status:
alert = Alert.NOTIFICATION | Alert.ATTENTION
reason = info.to_str()
if changed or reason:
# update the leds on the device, if any
_hidpp10.set_3leds(self, info.level, charging=info.charging(), warning=bool(alert))
self.changed(active=True, alert=alert, reason=reason)
# Retrieve and regularize battery status
def read_battery(self):
if self.online:
battery = self.battery()
self.set_battery_info(battery if battery is not None else Battery(None, None, None, None))
def changed(self, active=None, alert=Alert.NONE, reason=None, push=False):
"""The status of the device had changed, so invoke the status callback.
Also push notifications and settings to the device when necessary."""
if logger.isEnabledFor(logging.DEBUG):
logger.debug("device %d changing: active=%s %s present=%s", self.number, active, self._active, self.present)
if active is not None:
self.online = active
was_active, self._active = self._active, active
if active:
# Push settings for new devices when devices request software reconfiguration
# and when devices become active if they don't have wireless device status feature,
if (
was_active is None
or not was_active
or push
and (not self.features or SupportedFeature.WIRELESS_DEVICE_STATUS not in self.features)
):
if logger.isEnabledFor(logging.INFO):
logger.info("%s pushing device settings %s", self, self.settings)
settings.apply_all_settings(self)
if not was_active:
if self.protocol < 2.0: # Make sure to set notification flags on the device
self.notification_flags = self.enable_connection_notifications()
else:
self.set_configuration(0x11) # signal end of configuration
self.read_battery() # battery information may have changed so try to read it now
elif was_active and self.receiver: # need to set configuration pending flag in receiver
hidpp10.set_configuration_pending_flags(self.receiver, 0xFF)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("device %d changed: active=%s %s", self.number, self._active, self.battery_info)
if self.status_callback is not None:
self.status_callback(self, alert, reason)
if self.persister and battery_feature is None:
self.persister['_battery'] = result
def enable_connection_notifications(self, enable=True):
"""Enable or disable device (dis)connection notifications on this
@ -470,21 +386,22 @@ class Device:
return False
if enable:
set_flag_bits = NotificationFlag.BATTERY_STATUS | NotificationFlag.UI | NotificationFlag.CONFIGURATION_COMPLETE
set_flag_bits = (
_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.keyboard_illumination
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present
)
else:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
if not ok:
logger.warning("%s: failed to %s device notifications", self, "enable" if enable else "disable")
_log.warn('%s: failed to %s device notifications', self, 'enable' if enable else 'disable')
flag_bits = _hidpp10.get_notification_flags(self)
if logger.isEnabledFor(logging.INFO):
if flag_bits is None:
flag_names = None
else:
flag_names = hidpp10_constants.NotificationFlag.flag_names(flag_bits)
is_enabled = "enabled" if enable else "disabled"
logger.info(f"{self}: device notifications {is_enabled} {flag_names}")
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
if _log.isEnabledFor(_INFO):
_log.info('%s: device notifications %s %s', self, 'enabled' if enable else 'disabled', flag_names)
return flag_bits if ok else None
def add_notification_handler(self, id: str, fn):
@ -504,8 +421,8 @@ class Device:
def remove_notification_handler(self, id: str):
"""Unregisters the notification handler under name `id`."""
if id not in self._notification_handlers and logger.isEnabledFor(logging.INFO):
logger.info(f"Tried to remove nonexistent notification handler {id} from device {self}.")
if id not in self._notification_handlers and _log.isEnabledFor(_INFO):
_log.info(f'Tried to remove nonexistent notification handler {id} from device {self}.')
else:
del self._notification_handlers[id]
@ -518,53 +435,29 @@ class Device:
def request(self, request_id, *params, no_reply=False):
if self:
long = self.hidpp_long is True or (
self.hidpp_long is None and (self.bluetooth or self._protocol is not None and self._protocol >= 2.0)
)
return self.low_level.request(
return _base.request(
self.handle or self.receiver.handle,
self.number,
request_id,
*params,
no_reply=no_reply,
long_message=long,
protocol=self.protocol,
long_message=self.bluetooth or self.hidpp_short is False or self.protocol >= 2.0,
protocol=self.protocol
)
def feature_request(self, feature, function=0x00, *params, no_reply=False):
if self.protocol >= 2.0:
return hidpp20.feature_request(self, feature, function, *params, no_reply=no_reply)
return _hidpp20.feature_request(self, feature, function, *params, no_reply=no_reply)
def ping(self):
"""Checks if the device is online and present, returns True of False.
Some devices are integral with their receiver but may not be present even if the receiver responds to ping."""
long = self.hidpp_long is True or (
self.hidpp_long is None and (self.bluetooth or self._protocol is not None and self._protocol >= 2.0)
)
handle = self.handle or self.receiver.handle
try:
protocol = self.low_level.ping(handle, self.number, long_message=long)
except exceptions.NoReceiver: # if ping fails, device is offline
protocol = None
self.online = protocol is not None and self.present
"""Checks if the device is online, returns True of False"""
long = self.bluetooth or self._protocol is not None and self._protocol >= 2.0
protocol = _base.ping(self.handle or self.receiver.handle, self.number, long_message=long)
self.online = protocol is not None
if protocol:
self._protocol = protocol
if logger.isEnabledFor(logging.DEBUG):
logger.debug("pinged %s: online %s protocol %s present %s", self.number, self.online, protocol, self.present)
return self.online
def notify_devices(self): # no need to notify, as there are none
pass
def close(self):
handle, self.handle = self.handle, None
if self in Device.instances:
Device.instances.remove(self)
if hasattr(self, "cleanups"):
for cleanup in self.cleanups:
cleanup(self)
return handle and self.low_level.close(handle)
def __index__(self):
return self.number
@ -584,17 +477,37 @@ class Device:
__nonzero__ = __bool__
def status_string(self):
return self.battery_info.to_str() if self.battery_info is not None else ""
def __str__(self):
try:
name = self._name or self._codename or "?"
except exceptions.NoSuchDevice:
name = "name not available"
return f"<Device({int(self.number)},{self.wpid or self.product_id},{name},{self.serial})>"
return '<Device(%d,%s,%s,%s)>' % (
self.number, self.wpid or self.product_id, self.name or self.codename or '?', self.serial
)
__repr__ = __str__
def notify_devices(self): # no need to notify, as there are none
pass
@classmethod
def open(self, device_info):
"""Opens a Logitech Device found attached to the machine, by Linux device path.
:returns: An open file handle for the found receiver, or ``None``.
"""
try:
handle = _base.open_path(device_info.path)
if handle:
return Device(None, None, info=device_info, handle=handle, path=device_info.path)
except OSError as e:
_log.exception('open %s', device_info)
if e.errno == _errno.EACCES:
raise
except Exception:
_log.exception('open %s', device_info)
def close(self):
handle, self.handle = self.handle, None
if self in Device.instances:
Device.instances.remove(self)
return (handle and _base.close(handle))
def __del__(self):
self.close()

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +0,0 @@
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from .common import KwException
"""Exceptions that may be raised by this API."""
class NoReceiver(KwException):
"""Raised when trying to talk through a previously open handle, when the
receiver is no longer available. Should only happen if the receiver is
physically disconnected from the machine, or its kernel driver module is
unloaded."""
pass
class NoSuchDevice(KwException):
"""Raised when trying to reach a device number not paired to the receiver."""
pass
class DeviceUnreachable(KwException):
"""Raised when a request is made to an unreachable (turned off) device."""
pass
class FeatureNotSupported(KwException):
"""Raised when trying to request a feature not supported by the device."""
pass
class FeatureCallError(KwException):
"""Raised if the device replied to a feature call with an error."""
pass

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -13,273 +15,382 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations
import logging
from logging import getLogger # , DEBUG as _DEBUG
from typing import Any
from .common import BATTERY_APPROX as _BATTERY_APPROX
from .common import FirmwareInfo as _FirmwareInfo
from .common import NamedInts as _NamedInts
from .common import bytes2int as _bytes2int
from .common import int2bytes as _int2bytes
from .common import strhex as _strhex
from .hidpp20 import BATTERY_STATUS, FIRMWARE_KIND
from typing_extensions import Protocol
_log = getLogger(__name__)
del getLogger
from . import common
from .common import Battery
from .common import BatteryLevelApproximation
from .common import BatteryStatus
from .common import FirmwareKind
from .hidpp10_constants import NotificationFlag
from .hidpp10_constants import Registers
#
# Constants - most of them as defined by the official Logitech HID++ 1.0
# documentation, some of them guessed.
#
logger = logging.getLogger(__name__)
DEVICE_KIND = _NamedInts(
unknown=0x00,
keyboard=0x01,
mouse=0x02,
numpad=0x03,
presenter=0x04,
remote=0x07,
trackball=0x08,
touchpad=0x09,
headset=0x0D, # not from Logitech documentation
remote_control=0x0E, # for compatibility with HID++ 2.0
receiver=0x0F # for compatibility with HID++ 2.0
)
POWER_SWITCH_LOCATION = _NamedInts(
base=0x01,
top_case=0x02,
edge_of_top_right_corner=0x03,
top_left_corner=0x05,
bottom_left_corner=0x06,
top_right_corner=0x07,
bottom_right_corner=0x08,
top_edge=0x09,
right_edge=0x0A,
left_edge=0x0B,
bottom_edge=0x0C
)
# Some flags are used both by devices and receivers. The Logitech documentation
# mentions that the first and last (third) byte are used for devices while the
# second is used for the receiver. In practise, the second byte is also used for
# some device-specific notifications (keyboard illumination level). Do not
# simply set all notification bits if the software does not support it. For
# example, enabling keyboard_sleep_raw makes the Sleep key a no-operation unless
# the software is updated to handle that event.
# Observations:
# - wireless and software present were seen on receivers, reserved_r1b4 as well
# - the rest work only on devices as far as we can tell right now
# In the future would be useful to have separate enums for receiver and device notification flags,
# but right now we don't know enough.
# additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
NOTIFICATION_FLAG = _NamedInts(
numpad_numerical_keys=0x800000,
f_lock_status=0x400000,
roller_H=0x200000,
battery_status=0x100000, # send battery charge notifications (0x07 or 0x0D)
mouse_extra_buttons=0x080000,
roller_V=0x040000,
keyboard_sleep_raw=0x020000, # system control keys such as Sleep
keyboard_multimedia_raw=0x010000, # consumer controls such as Mute and Calculator
# reserved_r1b4= 0x001000, # unknown, seen on a unifying receiver
reserved5=0x008000,
reserved4=0x004000,
reserved3=0x002000,
reserved2=0x001000,
software_present=0x000800, # .. no idea
reserved1=0x000400,
keyboard_illumination=0x000200, # illumination brightness level changes (by pressing keys)
wireless=0x000100, # notify when the device wireless goes on/off-line
mx_air_3d_gesture=0x000001,
)
ERROR = _NamedInts(
invalid_SubID__command=0x01,
invalid_address=0x02,
invalid_value=0x03,
connection_request_failed=0x04,
too_many_devices=0x05,
already_exists=0x06,
busy=0x07,
unknown_device=0x08,
resource_error=0x09,
request_unavailable=0x0A,
unsupported_parameter_value=0x0B,
wrong_pin_code=0x0C
)
PAIRING_ERRORS = _NamedInts(device_timeout=0x01, device_not_supported=0x02, too_many_devices=0x03, sequence_timeout=0x06)
BOLT_PAIRING_ERRORS = _NamedInts(device_timeout=0x01, failed=0x02)
"""Known registers.
Devices usually have a (small) sub-set of these. Some registers are only
applicable to certain device kinds (e.g. smooth_scroll only applies to mice."""
REGISTERS = _NamedInts(
# only apply to receivers
receiver_connection=0x02,
receiver_pairing=0xB2,
devices_activity=0x2B3,
receiver_info=0x2B5,
bolt_device_discovery=0xC0,
bolt_pairing=0x2C1,
bolt_uniqueId=0x02FB,
# only apply to devices
mouse_button_flags=0x01,
keyboard_hand_detection=0x01,
battery_status=0x07,
keyboard_fn_swap=0x09,
battery_charge=0x0D,
keyboard_illumination=0x17,
three_leds=0x51,
mouse_dpi=0x63,
# apply to both
notifications=0x00,
firmware=0xF1,
# notifications
passkey_request_notification=0x4D,
passkey_pressed_notification=0x4E,
device_discovery_notification=0x4F,
discovery_status_notification=0x53,
pairing_status_notification=0x54,
)
# Subregisters for receiver_info register
INFO_SUBREGISTERS = _NamedInts(
serial_number=0x01, # not found on many receivers
fw_version=0x02,
receiver_information=0x03,
pairing_information=0x20, # 0x2N, by connected device
extended_pairing_information=0x30, # 0x3N, by connected device
device_name=0x40, # 0x4N, by connected device
bolt_pairing_information=0x50, # 0x5N, by connected device
bolt_device_name=0x60, # 0x6N01, by connected device,
)
# Flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
DEVICE_FEATURES = _NamedInts(
reserved1=0x010000,
special_buttons=0x020000,
enhanced_key_usage=0x040000,
fast_fw_rev=0x080000,
reserved2=0x100000,
reserved3=0x200000,
scroll_accel=0x400000,
buttons_control_resolution=0x800000,
inhibit_lock_key_sound=0x000001,
reserved4=0x000002,
mx_air_3d_engine=0x000004,
host_control_leds=0x000008,
reserved5=0x000010,
reserved6=0x000020,
reserved7=0x000040,
reserved8=0x000080,
)
#
# functions
#
class Device(Protocol):
def request(self, request_id, *params):
...
@property
def kind(self) -> Any:
...
@property
def online(self) -> bool:
...
@property
def protocol(self) -> Any:
...
@property
def registers(self) -> list:
...
def read_register(device: Device, register: Registers | int, *params) -> Any:
assert device is not None, f"tried to read register {register:02X} from invalid device {device}"
def read_register(device, register_number, *params):
assert device is not None, 'tried to read register %02X from invalid device %s' % (register_number, device)
# support long registers by adding a 2 in front of the register number
request_id = 0x8100 | (int(register) & 0x2FF)
request_id = 0x8100 | (int(register_number) & 0x2FF)
return device.request(request_id, *params)
def write_register(device: Device, register: Registers | int, *value) -> Any:
assert device is not None, f"tried to write register {register:02X} to invalid device {device}"
def write_register(device, register_number, *value):
assert device is not None, 'tried to write register %02X to invalid device %s' % (register_number, device)
# support long registers by adding a 2 in front of the register number
request_id = 0x8000 | (int(register) & 0x2FF)
request_id = 0x8000 | (int(register_number) & 0x2FF)
return device.request(request_id, *value)
def get_configuration_pending_flags(receiver):
assert not receiver.isDevice
result = read_register(receiver, Registers.DEVICES_CONFIGURATION)
if result is not None:
return ord(result[:1])
def get_battery(device):
assert device is not None
assert device.kind is not None
if not device.online:
return
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
if device.protocol and device.protocol >= 2.0:
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
return
def set_configuration_pending_flags(receiver, devices):
assert not receiver.isDevice
result = write_register(receiver, Registers.DEVICES_CONFIGURATION, devices)
return result is not None
class Hidpp10:
def get_battery(self, device: Device):
assert device is not None
assert device.kind is not None
if not device.online:
return
"""Reads a device's battery level, if provided by the HID++ 1.0 protocol."""
if device.protocol and device.protocol >= 2.0:
# let's just assume HID++ 2.0 devices do not provide the battery info in a register
for r in (REGISTERS.battery_status, REGISTERS.battery_charge):
if r in device.registers:
reply = read_register(device, r)
if reply:
return parse_battery_status(r, reply)
return
for r in (Registers.BATTERY_STATUS, Registers.BATTERY_CHARGE):
if r in device.registers:
reply = read_register(device, r)
if reply:
return parse_battery_status(r, reply)
return
# the descriptor does not tell us which register this device has, try them both
reply = read_register(device, REGISTERS.battery_charge)
if reply:
# remember this for the next time
device.registers.append(REGISTERS.battery_charge)
return parse_battery_status(REGISTERS.battery_charge, reply)
# the descriptor does not tell us which register this device has, try them both
reply = read_register(device, Registers.BATTERY_CHARGE)
if reply:
# remember this for the next time
device.registers.append(Registers.BATTERY_CHARGE)
return parse_battery_status(Registers.BATTERY_CHARGE, reply)
reply = read_register(device, Registers.BATTERY_STATUS)
if reply:
# remember this for the next time
device.registers.append(Registers.BATTERY_STATUS)
return parse_battery_status(Registers.BATTERY_STATUS, reply)
def get_firmware(self, device: Device) -> tuple[common.FirmwareInfo] | None:
assert device is not None
firmware = [None, None, None]
reply = read_register(device, Registers.FIRMWARE, 0x01)
if not reply:
# won't be able to read any of it now...
return
fw_version = common.strhex(reply[1:3])
fw_version = f"{fw_version[0:2]}.{fw_version[2:4]}"
reply = read_register(device, Registers.FIRMWARE, 0x02)
if reply:
fw_version += ".B" + common.strhex(reply[1:3])
fw = common.FirmwareInfo(FirmwareKind.Firmware, "", fw_version, None)
firmware[0] = fw
reply = read_register(device, Registers.FIRMWARE, 0x04)
if reply:
bl_version = common.strhex(reply[1:3])
bl_version = f"{bl_version[0:2]}.{bl_version[2:4]}"
bl = common.FirmwareInfo(FirmwareKind.Bootloader, "", bl_version, None)
firmware[1] = bl
reply = read_register(device, Registers.FIRMWARE, 0x03)
if reply:
o_version = common.strhex(reply[1:3])
o_version = f"{o_version[0:2]}.{o_version[2:4]}"
o = common.FirmwareInfo(FirmwareKind.Other, "", o_version, None)
firmware[2] = o
if any(firmware):
return tuple(f for f in firmware if f)
def set_3leds(self, device: Device, battery_level=None, charging=None, warning=None):
assert device is not None
assert device.kind is not None
if not device.online:
return
if Registers.THREE_LEDS not in device.registers:
return
if battery_level is not None:
if battery_level < BatteryLevelApproximation.CRITICAL:
# 1 orange, and force blink
v1, v2 = 0x22, 0x00
warning = True
elif battery_level < BatteryLevelApproximation.LOW:
# 1 orange
v1, v2 = 0x22, 0x00
elif battery_level < BatteryLevelApproximation.GOOD:
# 1 green
v1, v2 = 0x20, 0x00
elif battery_level < BatteryLevelApproximation.FULL:
# 2 greens
v1, v2 = 0x20, 0x02
else:
# all 3 green
v1, v2 = 0x20, 0x22
if warning:
# set the blinking flag for the leds already set
v1 |= v1 >> 1
v2 |= v2 >> 1
elif charging:
# blink all green
v1, v2 = 0x30, 0x33
elif warning:
# 1 red
v1, v2 = 0x02, 0x00
else:
# turn off all leds
v1, v2 = 0x11, 0x11
write_register(device, Registers.THREE_LEDS, v1, v2)
def get_notification_flags(self, device: Device):
return self._get_register(device, Registers.NOTIFICATIONS)
def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag):
assert device is not None
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flag_bits = sum(int(b.value) for b in flag_bits)
assert flag_bits & 0x00FFFFFF == flag_bits
result = write_register(device, Registers.NOTIFICATIONS, common.int2bytes(flag_bits, 3))
return result is not None
def get_device_features(self, device: Device):
return self._get_register(device, Registers.MOUSE_BUTTON_FLAGS)
def _get_register(self, device: Device, register: Registers | int):
assert device is not None
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flags = read_register(device, register)
if flags is not None:
assert len(flags) == 3
return common.bytes2int(flags)
reply = read_register(device, REGISTERS.battery_status)
if reply:
# remember this for the next time
device.registers.append(REGISTERS.battery_status)
return parse_battery_status(REGISTERS.battery_status, reply)
def parse_battery_status(register: Registers | int, reply) -> Battery | None:
def status_byte_to_charge(status_byte_: int) -> BatteryLevelApproximation:
if status_byte_ == 7:
charge_ = BatteryLevelApproximation.FULL
elif status_byte_ == 5:
charge_ = BatteryLevelApproximation.GOOD
elif status_byte_ == 3:
charge_ = BatteryLevelApproximation.LOW
elif status_byte_ == 1:
charge_ = BatteryLevelApproximation.CRITICAL
else:
# pure 'charging' notifications may come without a status
charge_ = BatteryLevelApproximation.EMPTY
return charge_
def status_byte_to_battery_status(status_byte_: int) -> BatteryStatus:
if status_byte_ == 0x30:
status_text_ = BatteryStatus.DISCHARGING
elif status_byte_ == 0x50:
status_text_ = BatteryStatus.RECHARGING
elif status_byte_ == 0x90:
status_text_ = BatteryStatus.FULL
else:
status_text_ = None
return status_text_
def charging_byte_to_status_text(charging_byte_: int) -> BatteryStatus:
if charging_byte_ == 0x00:
status_text_ = BatteryStatus.DISCHARGING
elif charging_byte_ & 0x21 == 0x21:
status_text_ = BatteryStatus.RECHARGING
elif charging_byte_ & 0x22 == 0x22:
status_text_ = BatteryStatus.FULL
else:
logger.warning("could not parse 0x07 battery status: %02X (level %02X)", charging_byte_, status_byte)
status_text_ = None
return status_text_
if register == Registers.BATTERY_CHARGE:
def parse_battery_status(register, reply):
if register == REGISTERS.battery_charge:
charge = ord(reply[:1])
status_byte = ord(reply[2:3]) & 0xF0
status_text = (
BATTERY_STATUS.discharging if status_byte == 0x30 else
BATTERY_STATUS.recharging if status_byte == 0x50 else BATTERY_STATUS.full if status_byte == 0x90 else None
)
return charge, None, status_text, None
battery_status = status_byte_to_battery_status(status_byte)
return Battery(charge, None, battery_status, None)
if register == Registers.BATTERY_STATUS:
if register == REGISTERS.battery_status:
status_byte = ord(reply[:1])
charging_byte = ord(reply[1:2])
charge = (
_BATTERY_APPROX.full if status_byte == 7 # full
else _BATTERY_APPROX.good if status_byte == 5 # good
else _BATTERY_APPROX.low if status_byte == 3 # low
else _BATTERY_APPROX.critical if status_byte == 1 # critical
# pure 'charging' notifications may come without a status
else _BATTERY_APPROX.empty
)
status_text = charging_byte_to_status_text(charging_byte)
charge = status_byte_to_charge(status_byte)
charging_byte = ord(reply[1:2])
if charging_byte == 0x00:
status_text = BATTERY_STATUS.discharging
elif charging_byte & 0x21 == 0x21:
status_text = BATTERY_STATUS.recharging
elif charging_byte & 0x22 == 0x22:
status_text = BATTERY_STATUS.full
else:
_log.warn('could not parse 0x07 battery status: %02X (level %02X)', charging_byte, status_byte)
status_text = None
if charging_byte & 0x03 and status_byte == 0:
# some 'charging' notifications may come with no battery level information
charge = None
# Return None for next charge level and voltage as these are not in HID++ 1.0 spec
return Battery(charge, None, status_text, None)
return charge, None, status_text, None
def get_firmware(device):
assert device is not None
firmware = [None, None, None]
reply = read_register(device, REGISTERS.firmware, 0x01)
if not reply:
# won't be able to read any of it now...
return
fw_version = _strhex(reply[1:3])
fw_version = '%s.%s' % (fw_version[0:2], fw_version[2:4])
reply = read_register(device, REGISTERS.firmware, 0x02)
if reply:
fw_version += '.B' + _strhex(reply[1:3])
fw = _FirmwareInfo(FIRMWARE_KIND.Firmware, '', fw_version, None)
firmware[0] = fw
reply = read_register(device, REGISTERS.firmware, 0x04)
if reply:
bl_version = _strhex(reply[1:3])
bl_version = '%s.%s' % (bl_version[0:2], bl_version[2:4])
bl = _FirmwareInfo(FIRMWARE_KIND.Bootloader, '', bl_version, None)
firmware[1] = bl
reply = read_register(device, REGISTERS.firmware, 0x03)
if reply:
o_version = _strhex(reply[1:3])
o_version = '%s.%s' % (o_version[0:2], o_version[2:4])
o = _FirmwareInfo(FIRMWARE_KIND.Other, '', o_version, None)
firmware[2] = o
if any(firmware):
return tuple(f for f in firmware if f)
def set_3leds(device, battery_level=None, charging=None, warning=None):
assert device is not None
assert device.kind is not None
if not device.online:
return
if REGISTERS.three_leds not in device.registers:
return
if battery_level is not None:
if battery_level < _BATTERY_APPROX.critical:
# 1 orange, and force blink
v1, v2 = 0x22, 0x00
warning = True
elif battery_level < _BATTERY_APPROX.low:
# 1 orange
v1, v2 = 0x22, 0x00
elif battery_level < _BATTERY_APPROX.good:
# 1 green
v1, v2 = 0x20, 0x00
elif battery_level < _BATTERY_APPROX.full:
# 2 greens
v1, v2 = 0x20, 0x02
else:
# all 3 green
v1, v2 = 0x20, 0x22
if warning:
# set the blinking flag for the leds already set
v1 |= (v1 >> 1)
v2 |= (v2 >> 1)
elif charging:
# blink all green
v1, v2 = 0x30, 0x33
elif warning:
# 1 red
v1, v2 = 0x02, 0x00
else:
# turn off all leds
v1, v2 = 0x11, 0x11
write_register(device, REGISTERS.three_leds, v1, v2)
def get_notification_flags(device):
assert device is not None
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flags = read_register(device, REGISTERS.notifications)
if flags is not None:
assert len(flags) == 3
return _bytes2int(flags)
def set_notification_flags(device, *flag_bits):
assert device is not None
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flag_bits = sum(int(b) for b in flag_bits)
assert flag_bits & 0x00FFFFFF == flag_bits
result = write_register(device, REGISTERS.notifications, _int2bytes(flag_bits, 3))
return result is not None
def get_device_features(device):
assert device is not None
# Avoid a call if the device is not online,
# or the device does not support registers.
if device.kind is not None:
# peripherals with protocol >= 2.0 don't support registers
if device.protocol and device.protocol >= 2.0:
return
flags = read_register(device, REGISTERS.mouse_button_flags)
if flags is not None:
assert len(flags) == 3
return _bytes2int(flags)

View File

@ -1,257 +0,0 @@
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations
from enum import Flag
from enum import IntEnum
from typing import List
from .common import NamedInts
"""HID constants for HID++ 1.0.
Most of them as defined by the official Logitech HID++ 1.0
documentation, some of them guessed.
"""
DEVICE_KIND = NamedInts(
unknown=0x00,
keyboard=0x01,
mouse=0x02,
numpad=0x03,
presenter=0x04,
remote=0x07,
trackball=0x08,
touchpad=0x09,
tablet=0x0A,
gamepad=0x0B,
joystick=0x0C,
headset=0x0D, # not from Logitech documentation
remote_control=0x0E, # for compatibility with HID++ 2.0
receiver=0x0F, # for compatibility with HID++ 2.0
)
class PowerSwitchLocation(IntEnum):
UNKNOWN = 0x00
BASE = 0x01
TOP_CASE = 0x02
EDGE_OF_TOP_RIGHT_CORNER = 0x03
TOP_LEFT_CORNER = 0x05
BOTTOM_LEFT_CORNER = 0x06
TOP_RIGHT_CORNER = 0x07
BOTTOM_RIGHT_CORNER = 0x08
TOP_EDGE = 0x09
RIGHT_EDGE = 0x0A
LEFT_EDGE = 0x0B
BOTTOM_EDGE = 0x0C
@classmethod
def location(cls, loc: int) -> PowerSwitchLocation:
try:
return cls(loc)
except ValueError:
return cls.UNKNOWN
class NotificationFlag(Flag):
"""Some flags are used both by devices and receivers.
The Logitech documentation mentions that the first and last (third)
byte are used for devices while the second is used for the receiver.
In practise, the second byte is also used for some device-specific
notifications (keyboard illumination level). Do not simply set all
notification bits if the software does not support it. For example,
enabling keyboard_sleep_raw makes the Sleep key a no-operation
unless the software is updated to handle that event.
Observations:
- wireless and software present seen on receivers,
reserved_r1b4 as well
- the rest work only on devices as far as we can tell right now
In the future would be useful to have separate enums for receiver
and device notification flags, but right now we don't know enough.
Additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
"""
@classmethod
def flag_names(cls, flag_bits: int) -> List[str]:
"""Extract the names of the flags from the integer."""
indexed = {item.value: item.name for item in cls}
flag_names = []
unknown_bits = flag_bits
for k in indexed:
# Ensure that the key (flag value) is a power of 2 (a single bit flag)
assert bin(k).count("1") == 1
if k & flag_bits == k:
unknown_bits &= ~k
flag_names.append(indexed[k].replace("_", " ").lower())
# Yield any remaining unknown bits
if unknown_bits != 0:
flag_names.append(f"unknown:{unknown_bits:06X}")
return flag_names
NUMPAD_NUMERICAL_KEYS = 0x800000
F_LOCK_STATUS = 0x400000
ROLLER_H = 0x200000
BATTERY_STATUS = 0x100000 # send battery charge notifications (0x07 or 0x0D)
MOUSE_EXTRA_BUTTONS = 0x080000
ROLLER_V = 0x040000
POWER_KEYS = 0x020000 # system control keys such as Sleep
KEYBOARD_MULTIMEDIA_RAW = 0x010000 # consumer controls such as Mute and Calculator
MULTI_TOUCH = 0x001000 # notify on multi-touch changes
SOFTWARE_PRESENT = 0x000800 # software is controlling part of device behaviour
LINK_QUALITY = 0x000400 # notify on link quality changes
UI = 0x000200 # notify on UI changes
WIRELESS = 0x000100 # notify when the device wireless goes on/off-line
CONFIGURATION_COMPLETE = 0x000004
VOIP_TELEPHONY = 0x000002
THREED_GESTURE = 0x000001
def flags_to_str(flag_bits: int | None, fallback: str) -> str:
flag_names = []
if flag_bits is not None:
if flag_bits == 0:
flag_names = (fallback,)
else:
flag_names = NotificationFlag.flag_names(flag_bits)
return f"\n{' ':15}".join(sorted(flag_names))
class ErrorCode(IntEnum):
INVALID_SUB_ID_COMMAND = 0x01
INVALID_ADDRESS = 0x02
INVALID_VALUE = 0x03
CONNECTION_REQUEST_FAILED = 0x04
TOO_MANY_DEVICES = 0x05
ALREADY_EXISTS = 0x06
BUSY = 0x07
UNKNOWN_DEVICE = 0x08
RESOURCE_ERROR = 0x09
REQUEST_UNAVAILABLE = 0x0A
UNSUPPORTED_PARAMETER_VALUE = 0x0B
WRONG_PIN_CODE = 0x0C
class PairingError(IntEnum):
DEVICE_TIMEOUT = 0x01
DEVICE_NOT_SUPPORTED = 0x02
TOO_MANY_DEVICES = 0x03
SEQUENCE_TIMEOUT = 0x06
class BoltPairingError(IntEnum):
DEVICE_TIMEOUT = 0x01
FAILED = 0x02
class Registers(IntEnum):
"""Known HID registers.
Devices usually have a (small) sub-set of these. Some registers are only
applicable to certain device kinds (e.g. smooth_scroll only applies to mice).
"""
# Generally applicable
NOTIFICATIONS = 0x00
FIRMWARE = 0xF1
# only apply to receivers
RECEIVER_CONNECTION = 0x02
RECEIVER_PAIRING = 0xB2
DEVICES_ACTIVITY = 0x2B3
RECEIVER_INFO = 0x2B5
BOLT_DEVICE_DISCOVERY = 0xC0
BOLT_PAIRING = 0x2C1
BOLT_UNIQUE_ID = 0x02FB
# only apply to devices
MOUSE_BUTTON_FLAGS = 0x01
KEYBOARD_HAND_DETECTION = 0x01
DEVICES_CONFIGURATION = 0x03
BATTERY_STATUS = 0x07
KEYBOARD_FN_SWAP = 0x09
BATTERY_CHARGE = 0x0D
KEYBOARD_ILLUMINATION = 0x17
THREE_LEDS = 0x51
MOUSE_DPI = 0x63
# notifications
PASSKEY_REQUEST_NOTIFICATION = 0x4D
PASSKEY_PRESSED_NOTIFICATION = 0x4E
DEVICE_DISCOVERY_NOTIFICATION = 0x4F
DISCOVERY_STATUS_NOTIFICATION = 0x53
PAIRING_STATUS_NOTIFICATION = 0x54
# Subregisters for receiver_info register
class InfoSubRegisters(IntEnum):
SERIAL_NUMBER = 0x01 # not found on many receivers
FW_VERSION = 0x02
RECEIVER_INFORMATION = 0x03
PAIRING_INFORMATION = 0x20 # 0x2N, by connected device
EXTENDED_PAIRING_INFORMATION = 0x30 # 0x3N, by connected device
DEVICE_NAME = 0x40 # 0x4N, by connected device
BOLT_PAIRING_INFORMATION = 0x50 # 0x5N, by connected device
BOLT_DEVICE_NAME = 0x60 # 0x6N01, by connected device
class DeviceFeature(Flag):
"""Features for devices.
Flags taken from
https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
"""
@classmethod
def flag_names(cls, flag_bits: int) -> List[str]:
"""Extract the names of the flags from the integer."""
indexed = {item.value: item.name for item in cls}
flag_names = []
unknown_bits = flag_bits
for k in indexed:
# Ensure that the key (flag value) is a power of 2 (a single bit flag)
assert bin(k).count("1") == 1
if k & flag_bits == k:
unknown_bits &= ~k
flag_names.append(indexed[k].replace("_", " ").lower())
# Yield any remaining unknown bits
if unknown_bits != 0:
flag_names.append(f"unknown:{unknown_bits:06X}")
return flag_names
RESERVED1 = 0x010000
SPECIAL_BUTTONS = 0x020000
ENHANCED_KEY_USAGE = 0x040000
FAST_FW_REV = 0x080000
RESERVED2 = 0x100000
RESERVED3 = 0x200000
SCROLL_ACCEL = 0x400000
BUTTONS_CONTROL_RESOLUTION = 0x800000
INHIBIT_LOCK_KEY_SOUND = 0x000001
RESERVED4 = 0x000002
MX_AIR_3D_ENGINE = 0x000004
HOST_CONTROL_LEDS = 0x000008
RESERVED5 = 0x000010
RESERVED6 = 0x000020
RESERVED7 = 0x000040
RESERVED8 = 0x000080

File diff suppressed because it is too large Load Diff

View File

@ -1,278 +0,0 @@
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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 2 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, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from enum import IntEnum
from enum import IntFlag
from .common import NamedInts
# <FeaturesSupported.xml sed '/LD_FID_/{s/.*LD_FID_/\t/;s/"[ \t]*Id="/=/;s/" \/>/,/p}' | sort -t= -k2
# additional features names taken from https://github.com/cvuchener/hidpp and
# https://github.com/Logitech/cpg-docs/tree/master/hidpp20
"""Possible features available on a Logitech device.
A particular device might not support all these features, and may support other
unknown features as well.
"""
class SupportedFeature(IntEnum):
ROOT = 0x0000
FEATURE_SET = 0x0001
FEATURE_INFO = 0x0002
# Common
DEVICE_FW_VERSION = 0x0003
DEVICE_UNIT_ID = 0x0004
DEVICE_NAME = 0x0005
DEVICE_GROUPS = 0x0006
DEVICE_FRIENDLY_NAME = 0x0007
KEEP_ALIVE = 0x0008
CONFIG_CHANGE = 0x0020
CRYPTO_ID = 0x0021
TARGET_SOFTWARE = 0x0030
WIRELESS_SIGNAL_STRENGTH = 0x0080
DFUCONTROL_LEGACY = 0x00C0
DFUCONTROL_UNSIGNED = 0x00C1
DFUCONTROL_SIGNED = 0x00C2
DFUCONTROL = 0x00C3
DFU = 0x00D0
BATTERY_STATUS = 0x1000
BATTERY_VOLTAGE = 0x1001
UNIFIED_BATTERY = 0x1004
CHARGING_CONTROL = 0x1010
LED_CONTROL = 0x1300
FORCE_PAIRING = 0x1500
GENERIC_TEST = 0x1800
DEVICE_RESET = 0x1802
OOBSTATE = 0x1805
CONFIG_DEVICE_PROPS = 0x1806
CHANGE_HOST = 0x1814
HOSTS_INFO = 0x1815
BACKLIGHT = 0x1981
BACKLIGHT2 = 0x1982
BACKLIGHT3 = 0x1983
ILLUMINATION = 0x1990
PRESENTER_CONTROL = 0x1A00
SENSOR_3D = 0x1A01
REPROG_CONTROLS = 0x1B00
REPROG_CONTROLS_V2 = 0x1B01
REPROG_CONTROLS_V2_2 = 0x1B02 # LogiOptions 2.10.73 features.xml
REPROG_CONTROLS_V3 = 0x1B03
REPROG_CONTROLS_V4 = 0x1B04
REPORT_HID_USAGE = 0x1BC0
PERSISTENT_REMAPPABLE_ACTION = 0x1C00
WIRELESS_DEVICE_STATUS = 0x1D4B
REMAINING_PAIRING = 0x1DF0
FIRMWARE_PROPERTIES = 0x1F1F
ADC_MEASUREMENT = 0x1F20
# Mouse
LEFT_RIGHT_SWAP = 0x2001
SWAP_BUTTON_CANCEL = 0x2005
POINTER_AXIS_ORIENTATION = 0x2006
VERTICAL_SCROLLING = 0x2100
SMART_SHIFT = 0x2110
SMART_SHIFT_ENHANCED = 0x2111
HI_RES_SCROLLING = 0x2120
HIRES_WHEEL = 0x2121
LOWRES_WHEEL = 0x2130
THUMB_WHEEL = 0x2150
MOUSE_POINTER = 0x2200
ADJUSTABLE_DPI = 0x2201
EXTENDED_ADJUSTABLE_DPI = 0x2202
POINTER_SPEED = 0x2205
ANGLE_SNAPPING = 0x2230
SURFACE_TUNING = 0x2240
XY_STATS = 0x2250
WHEEL_STATS = 0x2251
HYBRID_TRACKING = 0x2400
# Keyboard
FN_INVERSION = 0x40A0
NEW_FN_INVERSION = 0x40A2
K375S_FN_INVERSION = 0x40A3
ENCRYPTION = 0x4100
LOCK_KEY_STATE = 0x4220
SOLAR_DASHBOARD = 0x4301
KEYBOARD_LAYOUT = 0x4520
KEYBOARD_DISABLE_KEYS = 0x4521
KEYBOARD_DISABLE_BY_USAGE = 0x4522
DUALPLATFORM = 0x4530
MULTIPLATFORM = 0x4531
KEYBOARD_LAYOUT_2 = 0x4540
CROWN = 0x4600
# Touchpad
TOUCHPAD_FW_ITEMS = 0x6010
TOUCHPAD_SW_ITEMS = 0x6011
TOUCHPAD_WIN8_FW_ITEMS = 0x6012
TAP_ENABLE = 0x6020
TAP_ENABLE_EXTENDED = 0x6021
CURSOR_BALLISTIC = 0x6030
TOUCHPAD_RESOLUTION = 0x6040
TOUCHPAD_RAW_XY = 0x6100
TOUCHMOUSE_RAW_POINTS = 0x6110
TOUCHMOUSE_6120 = 0x6120
GESTURE = 0x6500
GESTURE_2 = 0x6501
# Gaming Devices
GKEY = 0x8010
MKEYS = 0x8020
MR = 0x8030
BRIGHTNESS_CONTROL = 0x8040
REPORT_RATE = 0x8060
EXTENDED_ADJUSTABLE_REPORT_RATE = 0x8061
COLOR_LED_EFFECTS = 0x8070
RGB_EFFECTS = 0x8071
PER_KEY_LIGHTING = 0x8080
PER_KEY_LIGHTING_V2 = 0x8081
MODE_STATUS = 0x8090
ONBOARD_PROFILES = 0x8100
MOUSE_BUTTON_SPY = 0x8110
LATENCY_MONITORING = 0x8111
GAMING_ATTACHMENTS = 0x8120
FORCE_FEEDBACK = 0x8123
# Headsets
SIDETONE = 0x8300
EQUALIZER = 0x8310
HEADSET_OUT = 0x8320
# Fake features for Solaar internal use
MOUSE_GESTURE = 0xFE00
def __str__(self):
return self.name.replace("_", " ")
class FeatureFlag(IntFlag):
"""Single bit flags."""
INTERNAL = 0x20
HIDDEN = 0x40
OBSOLETE = 0x80
DEVICE_KIND = NamedInts(
keyboard=0x00,
remote_control=0x01,
numpad=0x02,
mouse=0x03,
touchpad=0x04,
trackball=0x05,
presenter=0x06,
receiver=0x07,
)
class OnboardMode(IntEnum):
MODE_NO_CHANGE = 0x00
MODE_ONBOARD = 0x01
MODE_HOST = 0x02
class ChargeLevel(IntEnum):
AVERAGE = 50
FULL = 90
CRITICAL = 5
class ChargeType(IntEnum):
STANDARD = 0x00
FAST = 0x01
SLOW = 0x02
class ErrorCode(IntEnum):
UNKNOWN = 0x01
INVALID_ARGUMENT = 0x02
OUT_OF_RANGE = 0x03
HARDWARE_ERROR = 0x04
LOGITECH_ERROR = 0x05
INVALID_FEATURE_INDEX = 0x06
INVALID_FUNCTION = 0x07
BUSY = 0x08
UNSUPPORTED = 0x09
class GestureId(IntEnum):
"""Gesture IDs for feature GESTURE_2."""
TAP_1_FINGER = 1 # task Left_Click
TAP_2_FINGER = 2 # task Right_Click
TAP_3_FINGER = 3
CLICK_1_FINGER = 4 # task Left_Click
CLICK_2_FINGER = 5 # task Right_Click
CLICK_3_FINGER = 6
DOUBLE_TAP_1_FINGER = 10
DOUBLE_TAP_2_FINGER = 11
DOUBLE_TAP_3_FINGER = 12
TRACK_1_FINGER = 20 # action MovePointer
TRACKING_ACCELERATION = 21
TAP_DRAG_1_FINGER = 30 # action Drag
TAP_DRAG_2_FINGER = 31 # action SecondaryDrag
DRAG_3_FINGER = 32
TAP_GESTURES = 33 # group all tap gestures under a single UI setting
FN_CLICK_GESTURE_SUPPRESSION = 34 # suppresses Tap and Edge gestures, toggled by Fn+Click
SCROLL_1_FINGER = 40 # action ScrollOrPageXY / ScrollHorizontal
SCROLL_2_FINGER = 41 # action ScrollOrPageXY / ScrollHorizontal
SCROLL_2_FINGER_HORIZONTAL = 42 # action ScrollHorizontal
SCROLL_2_FINGER_VERTICAL = 43 # action WheelScrolling
SCROLL_2_FINGER_STATELESS = 44
NATURAL_SCROLLING = 45 # affects native HID wheel reporting by gestures, not when diverted
THUMBWHEEL = (46,) # action WheelScrolling
V_SCROLL_INTERTIA = 48
V_SCROLL_BALLISTICS = 49
SWIPE_2_FINGER_HORIZONTAL = 50 # action PageScreen
SWIPE_3_FINGER_HORIZONTAL = 51 # action PageScreen
SWIPE_4_FINGER_HORIZONTAL = 52 # action PageScreen
SWIPE_3_FINGER_VERTICAL = 53
SWIPE_4_FINGER_VERTICAL = 54
LEFT_EDGE_SWIPE_1_FINGER = 60
RIGHT_EDGE_SWIPE_1_FINGER = 61
BOTTOM_EDGE_SWIPE_1_FINGER = 62
TOP_EDGE_SWIPE_1_FINGER = 63
LEFT_EDGE_SWIPE_1_FINGER_2 = 64 # task HorzScrollNoRepeatSet
RIGHT_EDGE_SWIPE_1_FINGER_2 = 65
BOTTOM_EDGE_SWIPE_1_FINGER_2 = 66
TOP_EDGE_SWIPE_1_FINGER_2 = 67
LEFT_EDGE_SWIPE_2_FINGER = 70
RIGHT_EDGE_SWIPE_2_FINGER = 71
BottomEdgeSwipe2Finger = 72
BOTTOM_EDGE_SWIPE_2_FINGER = 72
TOP_EDGE_SWIPE_2_FINGER = 73
ZOOM_2_FINGER = 80 # action Zoom
ZOOM_2_FINGER_PINCH = 81 # ZoomBtnInSet
ZOOM_2_FINGER_SPREAD = 82 # ZoomBtnOutSet
ZOOM_3_FINGER = 83
ZOOM_2_FINGER_STATELESS = 84
TWO_FINGERS_PRESENT = 85
ROTATE_2_FINGER = 87
FINGER_1 = 90
FINGER_2 = 91
FINGER_3 = 92
FINGER_4 = 93
FINGER_5 = 94
FINGER_6 = 95
FINGER_7 = 96
FINGER_8 = 97
FINGER_9 = 98
FINGER_10 = 99
DEVICE_SPECIFIC_RAW_DATA = 100
class ParamId(IntEnum):
"""Param Ids for feature GESTURE_2"""
EXTRA_CAPABILITIES = 1 # not suitable for use
PIXEL_ZONE = 2 # 4 2-byte integers, left, bottom, width, height; pixels
RATIO_ZONE = 3 # 4 bytes, left, bottom, width, height; unit 1/240 pad size
SCALE_FACTOR = 4 # 2-byte integer, with 256 as normal scale

View File

@ -1,3 +1,5 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
##
## This program is free software; you can redistribute it and/or modify
@ -16,65 +18,70 @@
# Translation support for the Logitech receivers library
import gettext
import gettext as _gettext
_ = gettext.gettext
ngettext = gettext.ngettext
_ = _gettext.gettext
ngettext = _gettext.ngettext
# A few common strings, not always accessible as such in the code.
_DUMMY = (
# approximative battery levels
_("empty"),
_("critical"),
_("low"),
_("average"),
_("good"),
_("full"),
_('empty'),
_('critical'),
_('low'),
_('average'),
_('good'),
_('full'),
# battery charging statuses
_("discharging"),
_("recharging"),
_("charging"),
_("not charging"),
_("almost full"),
_("charged"),
_("slow recharge"),
_("invalid battery"),
_("thermal error"),
_("error"),
_("standard"),
_("fast"),
_("slow"),
_('discharging'),
_('recharging'),
_('charging'),
_('not charging'),
_('almost full'),
_('charged'),
_('slow recharge'),
_('invalid battery'),
_('thermal error'),
_('error'),
_('standard'),
_('fast'),
_('slow'),
# pairing errors
_("device timeout"),
_("device not supported"),
_("too many devices"),
_("sequence timeout"),
_('device timeout'),
_('device not supported'),
_('too many devices'),
_('sequence timeout'),
# firmware kinds
_("Firmware"),
_("Bootloader"),
_("Hardware"),
_("Other"),
_('Firmware'),
_('Bootloader'),
_('Hardware'),
_('Other'),
# common button and task names (from special_keys.py)
_("Left Button"),
_("Right Button"),
_("Middle Button"),
_("Back Button"),
_("Forward Button"),
_("Mouse Gesture Button"),
_("Smart Shift"),
_("DPI Switch"),
_("Left Tilt"),
_("Right Tilt"),
_("Left Click"),
_("Right Click"),
_("Mouse Middle Button"),
_("Mouse Back Button"),
_("Mouse Forward Button"),
_("Gesture Button Navigation"),
_("Mouse Scroll Left Button"),
_("Mouse Scroll Right Button"),
_('Left Button'),
_('Right Button'),
_('Middle Button'),
_('Back Button'),
_('Forward Button'),
_('Mouse Gesture Button'),
_('Smart Shift'),
_('DPI Switch'),
_('Left Tilt'),
_('Right Tilt'),
_('Left Click'),
_('Right Click'),
_('Mouse Middle Button'),
_('Mouse Back Button'),
_('Mouse Forward Button'),
_('Gesture Button Navigation'),
_('Mouse Scroll Left Button'),
_('Mouse Scroll Right Button'),
# key/button statuses
_("pressed"),
_("released"),
_('pressed'),
_('released'),
)

View File

@ -1,5 +1,6 @@
# -*- python-mode -*-
## Copyright (C) 2012-2013 Daniel Pavel
## Copyright (C) 2014-2024 Solaar Contributors https://pwr-solaar.github.io/Solaar/
##
## 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
@ -15,22 +16,37 @@
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
import logging
import queue
import threading
import threading as _threading
from . import base
from . import exceptions
from logging import DEBUG as _DEBUG
from logging import INFO as _INFO
from logging import getLogger
logger = logging.getLogger(__name__)
from . import base as _base
# from time import time as _timestamp
# for both Python 2 and 3
try:
from Queue import Queue as _Queue
except ImportError:
from queue import Queue as _Queue
_log = getLogger(__name__)
del getLogger
#
#
#
class _ThreadedHandle:
"""A thread-local wrapper with different open handles for each thread.
Closing a ThreadedHandle will close all handles.
"""
__slots__ = ("path", "_local", "_handles", "_listener")
__slots__ = ('path', '_local', '_handles', '_listener')
def __init__(self, listener, path, handle):
assert listener is not None
@ -40,18 +56,18 @@ class _ThreadedHandle:
self._listener = listener
self.path = path
self._local = threading.local()
self._local = _threading.local()
# take over the current handle for the thread doing the replacement
self._local.handle = handle
self._handles = [handle]
def _open(self):
handle = base.open_path(self.path)
handle = _base.open_path(self.path)
if handle is None:
logger.error("%r failed to open new handle", self)
_log.error('%r failed to open new handle', self)
else:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("%r opened new handle %d", self, handle)
# if _log.isEnabledFor(_DEBUG):
# _log.debug("%r opened new handle %d", self, handle)
self._local.handle = handle
self._handles.append(handle)
return handle
@ -60,16 +76,16 @@ class _ThreadedHandle:
if self._local:
self._local = None
handles, self._handles = self._handles, []
if logger.isEnabledFor(logging.DEBUG):
logger.debug("%r closing %s", self, handles)
if _log.isEnabledFor(_DEBUG):
_log.debug('%r closing %s', self, handles)
for h in handles:
base.close(h)
_base.close(h)
@property
def notifications_hook(self):
if self._listener:
assert isinstance(self._listener, threading.Thread)
if threading.current_thread() == self._listener:
assert isinstance(self._listener, _threading.Thread)
if _threading.current_thread() == self._listener:
return self._listener._notifications_hook
def __del__(self):
@ -92,7 +108,7 @@ class _ThreadedHandle:
return str(int(self))
def __repr__(self):
return f"<_ThreadedHandle({self.path})>"
return '<_ThreadedHandle(%s)>' % self.path
def __bool__(self):
return bool(self._local)
@ -100,59 +116,90 @@ class _ThreadedHandle:
__nonzero__ = __bool__
# How long to wait during a read for the next packet, in seconds.
# Ideally this should be rather long (10s ?), but the read is blocking and this means that when the thread
# is signalled to stop, it would take a while for it to acknowledge it.
# Forcibly closing the file handle on another thread does _not_ interrupt the read on Linux systems.
_EVENT_READ_TIMEOUT = 1.0 # in seconds
#
#
#
# How long to wait during a read for the next packet, in seconds
# Ideally this should be rather long (10s ?), but the read is blocking
# and this means that when the thread is signalled to stop, it would take
# a while for it to acknowledge it.
# Forcibly closing the file handle on another thread does _not_ interrupt the
# read on Linux systems.
_EVENT_READ_TIMEOUT = 1. # in seconds
# After this many reads that did not produce a packet, call the tick() method.
# This only happens if tick_period is enabled (>0) for the Listener instance.
# _IDLE_READS = 1 + int(5 // _EVENT_READ_TIMEOUT) # wait at least 5 seconds between ticks
class EventsListener(threading.Thread):
class EventsListener(_threading.Thread):
"""Listener thread for notifications from the Unifying Receiver.
Incoming packets will be passed to the callback function in sequence.
"""
def __init__(self, receiver, notifications_callback):
try:
path_name = receiver.path.split("/")[2]
except IndexError:
path_name = receiver.path
super().__init__(name=f"{self.__class__.__name__}:{path_name}")
super().__init__(name=self.__class__.__name__ + ':' + receiver.path.split('/')[2])
self.daemon = True
self._active = False
self.receiver = receiver
self._queued_notifications = queue.Queue(16)
self._queued_notifications = _Queue(16)
self._notifications_callback = notifications_callback
# self.tick_period = 0
def run(self):
self._active = True
# replace the handle with a threaded one
self.receiver.handle = _ThreadedHandle(self, self.receiver.path, self.receiver.handle)
if logger.isEnabledFor(logging.INFO):
logger.info("started with %s (%d)", self.receiver, int(self.receiver.handle))
# get the right low-level handle for this thread
ihandle = int(self.receiver.handle)
if _log.isEnabledFor(_INFO):
_log.info('started with %s (%d)', self.receiver, ihandle)
self.has_started()
if self.receiver.isDevice: # ping (wired or BT) devices to see if they are really online
if self.receiver.ping():
self.receiver.changed(active=True, reason="initialization")
# last_tick = 0
# the first idle read -- delay it a bit, and make sure to stagger
# idle reads for multiple receivers
# idle_reads = _IDLE_READS + (ihandle % 5) * 2
while self._active:
if self._queued_notifications.empty():
try:
n = base.read(self.receiver.handle, _EVENT_READ_TIMEOUT)
except exceptions.NoReceiver:
logger.warning("%s disconnected", self.receiver.name)
# _log.debug("read next notification")
n = _base.read(self.receiver.handle, _EVENT_READ_TIMEOUT)
except _base.NoReceiver:
_log.warning('%s disconnected', self.receiver.name)
self.receiver.close()
break
if n:
n = base.make_notification(*n)
n = _base.make_notification(*n)
else:
n = self._queued_notifications.get() # deliver any queued notifications
# deliver any queued notifications
n = self._queued_notifications.get()
if n:
# if _log.isEnabledFor(_DEBUG):
# _log.debug("%s: processing %s", self.receiver, n)
try:
self._notifications_callback(n)
except Exception:
logger.exception("processing %s", n)
_log.exception('processing %s', n)
# elif self.tick_period:
# idle_reads -= 1
# if idle_reads <= 0:
# idle_reads = _IDLE_READS
# now = _timestamp()
# if now - last_tick >= self.tick_period:
# last_tick = now
# self.tick(now)
del self._queued_notifications
self.has_stopped()
@ -170,13 +217,17 @@ class EventsListener(threading.Thread):
"""Called right before the thread stops."""
pass
# def tick(self, timestamp):
# """Called about every tick_period seconds."""
# pass
def _notifications_hook(self, n):
# Only consider unhandled notifications that were sent from this thread,
# i.e. triggered by a callback handling a previous notification.
assert threading.current_thread() == self
if self._active: # and threading.current_thread() == self:
# if logger.isEnabledFor(logging.DEBUG):
# logger.debug("queueing unhandled %s", n)
assert _threading.current_thread() == self
if self._active: # and _threading.current_thread() == self:
# if _log.isEnabledFor(_DEBUG):
# _log.debug("queueing unhandled %s", n)
if not self._queued_notifications.full():
self._queued_notifications.put(n)

Some files were not shown because too many files have changed in this diff Show More