Merge branch 'master' into update_de
|
|
@ -0,0 +1,22 @@
|
|||
[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
|
||||
|
|
@ -8,13 +8,23 @@ 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. -->
|
||||
<!-- Make sure that your issue is not one of the known issues in the
|
||||
Solaar documentation at https://pwr-solaar.github.io/Solaar/ -->
|
||||
<!-- Make sure that Solaar's udev rule is running by executing
|
||||
`ls -l /dev/hidraw*` and looking for + as the last character of the permissions. -->
|
||||
<!-- Do not bother opening an issue for a version older than 1.1.14.
|
||||
Upgrade to the current version and see if your issue persists. -->
|
||||
<!-- If you are not running the current version of Solaar,
|
||||
strongly consider upgrading to the current version. -->
|
||||
<!-- Note that some distributions have very old versions of Solaar
|
||||
as their default version. -->
|
||||
|
||||
- Solaar version (`solaar --version` or `git describe --tags` if cloned from this repository):
|
||||
- Distribution:
|
||||
- Kernel version (ex. `uname -srmo`): `KERNEL VERSION HERE`
|
||||
- Kernel version (ex. `uname -srmo`):
|
||||
- Output of `solaar show`:
|
||||
<!-- To run `solaar show` in 1.1.18 you have to clone Solaar from this repository
|
||||
and `run bin/solaar show` from the download directory. -->
|
||||
|
||||
<details>
|
||||
|
||||
|
|
@ -34,11 +44,11 @@ CONTENTS HERE
|
|||
|
||||
|
||||
- Errors or warrnings from Solaar:
|
||||
<!-- Under normal operation Solaar keeps a log of warning and error messages in ~/.tmp
|
||||
while it is running as a file starting with 'Solaar'.
|
||||
<!-- Under normal operation Solaar keeps a log of warning and error messages
|
||||
in ~/.tmp while it is running, as a file starting with 'Solaar'.
|
||||
If this file is not available or does not have useful information you can
|
||||
run Solaar as `solaar -dd`, after killing any running Solaar processes to
|
||||
have Solaar log informational, warning, and error messages to stdout. -->
|
||||
run Solaar as `solaar -ddd`, after killing any running Solaar processes to
|
||||
have Solaar log debug, informational, warning, and error messages to stdout. -->
|
||||
|
||||
|
||||
**Describe the bug**
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4.3.0
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
|
||||
- name: Run pre-commit
|
||||
uses: pre-commit/action@v3.0.0
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
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
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
name: tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
ubuntu-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [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.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 }}
|
||||
|
|
@ -13,8 +13,19 @@ __pycache__/
|
|||
/deb_dist/
|
||||
/MANIFEST
|
||||
|
||||
.coverage
|
||||
/htmlcov/
|
||||
|
||||
/docs/captures/
|
||||
/share/logitech_icons/
|
||||
/share/locale/
|
||||
|
||||
/po/*.po~
|
||||
|
||||
/.idea/
|
||||
|
||||
.DS_Store
|
||||
._*
|
||||
|
||||
Pipfile
|
||||
Pipfile.lock
|
||||
|
|
|
|||
|
|
@ -8,19 +8,13 @@ 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/pre-commit/mirrors-yapf
|
||||
rev: v0.32.0
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.2.2
|
||||
hooks:
|
||||
- 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']
|
||||
- id: ruff
|
||||
name: ruff lint
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
name: ruff format
|
||||
|
|
|
|||
|
|
@ -1,3 +1,346 @@
|
|||
# 1.1.19
|
||||
|
||||
* New Georgian translation
|
||||
* Remove test that doesn't work in older Pythons
|
||||
* Update help messages for CLI commands
|
||||
* Allow solaar config to change LED settings
|
||||
* Add python3-devel to install-dnf in Makefile
|
||||
* hidconsole can send an HID command non-interactively
|
||||
* Add info about new lightspeed receiver
|
||||
* Update Swedish and zh_TW translation
|
||||
* Fix bug when showing details about direct-connected device
|
||||
* Drop testing of Python before 3.13
|
||||
* Fix crash in solaar show when showing notification flags. (#3070)
|
||||
|
||||
# 1.1.18
|
||||
|
||||
* Fix crash when showing notification flags
|
||||
|
||||
# 1.1.17
|
||||
|
||||
* Add dark icons
|
||||
* Permit onboard profiles version 5
|
||||
* Update Russian and Polish translations
|
||||
* Add onboard profiles warning to sensitivity tooltip
|
||||
* Better error messages for solaar profile
|
||||
* Remove Solaar name for mice with WPID 4008
|
||||
* Prevent lock failure when showing debug messages
|
||||
* Replace color picker (#3028)
|
||||
* Add setting for HAPTIC feature
|
||||
* Add setting to adjust force needed for force-sensing buttons
|
||||
* Expand new settings type
|
||||
* Add new settings type for structure-backed setting
|
||||
* Use PATH instead of hardcoded absolute paths (#3014)
|
||||
* Add scroll ratchet force setting
|
||||
* Fix debug messages for MouseClick rule
|
||||
* Improve debug message for rule evaluation
|
||||
* App wrapper and launch agent scripts for MacOS
|
||||
* Ignore hidden features
|
||||
* Don't pop up window in response to ADC changes
|
||||
* Update bug report template
|
||||
* Fixed malformed doc file by adding closing tag
|
||||
* Fix error in low-level request for device with no recevier
|
||||
* Update documentation files
|
||||
|
||||
# 1.1.16
|
||||
|
||||
* Add new flags for reprogrammable keys feature
|
||||
* Correctly handle missing battery feature
|
||||
|
||||
# 1.1.15
|
||||
|
||||
* Correctly re-raise permissions exception
|
||||
* Add several new special keys and tasks
|
||||
* Update several translations
|
||||
* 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
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
include COPYRIGHT COPYING README.md ChangeLog.md lib/solaar/version lib/solaar/commit
|
||||
include COPYRIGHT LICENSE.txt README.md CHANGELOG.md lib/solaar/version lib/solaar/commit
|
||||
recursive-include rules.d *
|
||||
recursive-include share/locale *
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
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 dnf"
|
||||
sudo dnf install gtk3 python3-devel 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
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
# <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> -
|
||||
<a href="https://pwr-solaar.github.io/Solaar/issues">Known Issues</a>
|
||||
|
||||
|
||||
[](https://codecov.io/gh/pwr-Solaar/Solaar)
|
||||
[](../LICENSE.txt)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://pwr-solaar.github.io/Solaar/screenshots/Solaar-main-window-multiple.png" width="54%"/>
|
||||
 
|
||||
<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%"/>
|
||||
 
|
||||
<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§ion=all
|
||||
[nix flake]: https://github.com/Svenum/Solaar-Flake
|
||||
[debian]: https://packages.debian.org/search?keywords=solaar&searchon=names&suite=all§ion=all
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,175 @@
|
|||
# Solaar installation guide for RHEL, Rocky, AlmaLinux, and CentOS Stream
|
||||
|
||||
This guide covers manual installation and an automated install example for
|
||||
RHEL-family systems using `dnf`.
|
||||
|
||||
## Supported distributions
|
||||
|
||||
- Red Hat Enterprise Linux (RHEL)
|
||||
- Rocky Linux
|
||||
- AlmaLinux
|
||||
- Oracle Linux
|
||||
- CentOS Stream
|
||||
|
||||
The commands assume a minimal CLI system with `sudo` access.
|
||||
|
||||
## 1) Install dependencies
|
||||
|
||||
```bash
|
||||
sudo dnf makecache --refresh
|
||||
sudo dnf install -y \
|
||||
git \
|
||||
gtk3 \
|
||||
python3 \
|
||||
python3-devel \
|
||||
python3-dbus \
|
||||
python3-gobject \
|
||||
python3-pip \
|
||||
python3-psutil \
|
||||
python3-pyudev \
|
||||
python3-setuptools \
|
||||
python3-xlib \
|
||||
python3-yaml
|
||||
```
|
||||
|
||||
Optional troubleshooting helpers:
|
||||
|
||||
```bash
|
||||
sudo dnf install -y \
|
||||
evemu \
|
||||
libinput \
|
||||
usbutils
|
||||
```
|
||||
|
||||
## 2) Clone Solaar
|
||||
|
||||
```bash
|
||||
git clone https://github.com/pwr-Solaar/Solaar.git
|
||||
cd Solaar
|
||||
```
|
||||
|
||||
## 3) Install Solaar
|
||||
|
||||
Install for the current user:
|
||||
|
||||
```bash
|
||||
python3 -m pip install --user .
|
||||
```
|
||||
|
||||
Or install system-wide:
|
||||
|
||||
```bash
|
||||
sudo python3 -m pip install .
|
||||
```
|
||||
|
||||
## 4) Install udev rules
|
||||
|
||||
Install the recommended `uinput` rule:
|
||||
|
||||
```bash
|
||||
sudo make install_udev_uinput
|
||||
```
|
||||
|
||||
Verify rule installation:
|
||||
|
||||
```bash
|
||||
ls -l /etc/udev/rules.d/42-logitech-unify-permissions.rules
|
||||
```
|
||||
|
||||
Rollback udev rule installation:
|
||||
|
||||
```bash
|
||||
sudo make uninstall_udev
|
||||
```
|
||||
|
||||
## 5) Run Solaar
|
||||
|
||||
```bash
|
||||
solaar
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```bash
|
||||
python3 -m solaar
|
||||
```
|
||||
|
||||
## 6) Automated install options
|
||||
|
||||
Use the guided installer in this repository:
|
||||
|
||||
```bash
|
||||
./tools/install-rhel.sh
|
||||
```
|
||||
|
||||
Minimal non-interactive example script:
|
||||
|
||||
```bash
|
||||
cat > install-rhel-solaar.sh <<'SCRIPT'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${EUID}" -eq 0 ]]; then
|
||||
echo "Run as a regular user with sudo access, not as root."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo dnf makecache --refresh
|
||||
sudo dnf install -y \
|
||||
git \
|
||||
gtk3 \
|
||||
python3 \
|
||||
python3-devel \
|
||||
python3-dbus \
|
||||
python3-gobject \
|
||||
python3-pip \
|
||||
python3-psutil \
|
||||
python3-pyudev \
|
||||
python3-setuptools \
|
||||
python3-xlib \
|
||||
python3-yaml
|
||||
|
||||
if [[ ! -d Solaar/.git ]]; then
|
||||
git clone https://github.com/pwr-Solaar/Solaar.git
|
||||
fi
|
||||
|
||||
cd Solaar
|
||||
python3 -m pip install --user .
|
||||
sudo make install_udev_uinput
|
||||
~/.local/bin/solaar --version
|
||||
SCRIPT
|
||||
|
||||
chmod +x install-rhel-solaar.sh
|
||||
./install-rhel-solaar.sh
|
||||
```
|
||||
|
||||
## 7) Verification
|
||||
|
||||
```bash
|
||||
command -v solaar
|
||||
solaar --version
|
||||
python3 -m pip show solaar
|
||||
```
|
||||
|
||||
If installed with `--user`, ensure `~/.local/bin` is on your `PATH`:
|
||||
|
||||
```bash
|
||||
echo "$PATH" | tr ':' '\n' | grep -Fx "$HOME/.local/bin" >/dev/null || \
|
||||
echo 'Add ~/.local/bin to PATH'
|
||||
```
|
||||
|
||||
## 8) Troubleshooting
|
||||
|
||||
Receiver not detected:
|
||||
|
||||
```bash
|
||||
lsusb | grep -Ei 'logitech|046d'
|
||||
sudo udevadm trigger
|
||||
```
|
||||
|
||||
Check access to hidraw devices:
|
||||
|
||||
```bash
|
||||
ls -l /dev/hidraw*
|
||||
getfacl /dev/hidraw* 2>/dev/null | sed -n '1,80p'
|
||||
```
|
||||
|
|
@ -1,5 +1,61 @@
|
|||
# Notes on Major Changes in Releases
|
||||
|
||||
## Version 1.1.18
|
||||
|
||||
* Solaar is only guaranteed to work in Python 3.13 or later.
|
||||
|
||||
## Version 1.1.17
|
||||
|
||||
* Several new features have been added related to the MX Master 4
|
||||
* The scroll ratchet force can be adjusted
|
||||
* The force required to click the button under your thumb can be adjusted
|
||||
* The haptic force can be adjusted
|
||||
* Haptic feeback can be triggered by commands like `solaar config 'mx master 4' haptic-play 'HAPPY ALER'`
|
||||
|
||||
## Version 1.1.16
|
||||
|
||||
* Two bugs that were affecting users in 1.1.15 are fixed.
|
||||
|
||||
## Version 1.1.15
|
||||
|
||||
* Some key names have been changed to match Logitech names. Rules that use removed names will no longer work and will end up with a key of 0.
|
||||
* 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.
|
||||
|
|
|
|||
31
bin/solaar
|
|
@ -21,35 +21,22 @@
|
|||
|
||||
def init_paths():
|
||||
"""Make the app work in the source tree."""
|
||||
import os.path as _path
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
# 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')
|
||||
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")
|
||||
for location in src_lib, share_lib:
|
||||
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])
|
||||
init_py = os.path.join(location, "solaar", "__init__.py")
|
||||
if os.path.exists(init_py):
|
||||
sys.path[0] = location
|
||||
break
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
init_paths()
|
||||
import solaar.gtk
|
||||
|
||||
solaar.gtk.main()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,132 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
|
||||
Version 2, June 1991
|
||||
|
||||
Copyright (C) 1989, 1991 Free Software Foundation, Inc.
|
||||
<https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it.
|
||||
|
||||
For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights.
|
||||
|
||||
We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software.
|
||||
|
||||
Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations.
|
||||
|
||||
Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all.
|
||||
|
||||
The precise terms and conditions for copying, distribution and modification follow.
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you".
|
||||
|
||||
Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does.
|
||||
|
||||
1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program.
|
||||
|
||||
You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee.
|
||||
|
||||
2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions:
|
||||
|
||||
a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change.
|
||||
b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License.
|
||||
c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.)
|
||||
|
||||
These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it.
|
||||
|
||||
Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program.
|
||||
|
||||
In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License.
|
||||
|
||||
3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following:
|
||||
|
||||
a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or,
|
||||
c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.)
|
||||
|
||||
The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable.
|
||||
|
||||
If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code.
|
||||
|
||||
4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance.
|
||||
|
||||
5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it.
|
||||
|
||||
6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License.
|
||||
|
||||
7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program.
|
||||
|
||||
If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances.
|
||||
|
||||
It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice.
|
||||
|
||||
This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License.
|
||||
|
||||
8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License.
|
||||
|
||||
9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation.
|
||||
|
||||
10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally.
|
||||
|
||||
NO WARRANTY
|
||||
|
||||
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
END OF TERMS AND CONDITIONS
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
one line to give the program's name and an idea of what it does.
|
||||
Copyright (C) yyyy name of author
|
||||
|
||||
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, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program is interactive, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
Gnomovision version 69, Copyright (C) year name of author
|
||||
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details
|
||||
type `show w'. This is free software, and you are welcome
|
||||
to redistribute it under certain conditions; type `show c'
|
||||
for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names:
|
||||
|
||||
Yoyodyne, Inc., hereby disclaims all copyright
|
||||
interest in the program `Gnomovision'
|
||||
(which makes passes at compilers) written
|
||||
by James Hacker.
|
||||
|
||||
signature of Moe Ghoul, 1 April 1989
|
||||
Moe Ghoul, President of Vice
|
||||
|
||||
This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License.
|
||||
|
|
@ -0,0 +1,374 @@
|
|||
# Logitech PRO X 2 Superstrike - Solaar CLI Reference
|
||||
|
||||
This document describes all available settings for the Logitech PRO X 2 Superstrike mouse via the Solaar CLI.
|
||||
|
||||
## Device Identification
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Name | PRO X 2 Superstrike |
|
||||
| WPID | 40BD |
|
||||
| Protocol | HID++ 4.2 |
|
||||
| Kind | mouse |
|
||||
|
||||
## General CLI Syntax
|
||||
|
||||
```bash
|
||||
# List all settings for device
|
||||
solaar config <device>
|
||||
|
||||
# Read a specific setting
|
||||
solaar config <device> <setting-name>
|
||||
|
||||
# Write a specific setting
|
||||
solaar config <device> <setting-name> <value>
|
||||
```
|
||||
|
||||
The `<device>` can be:
|
||||
- Device number (e.g., `1`)
|
||||
- Device name (e.g., `"PRO X 2 Superstrike"`)
|
||||
- Serial number (e.g., `A1C55DB2`)
|
||||
|
||||
---
|
||||
|
||||
## Available Settings
|
||||
|
||||
### 1. Onboard Profiles
|
||||
|
||||
Controls whether the device uses its onboard profile or host-controlled settings.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name | `onboard_profiles` |
|
||||
| Type | Choice |
|
||||
| Possible Values | `Disabled`, `Profile 1` |
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read current value
|
||||
solaar config 1 onboard_profiles
|
||||
|
||||
# Set to disabled (allows host control of DPI, report rate, etc.)
|
||||
solaar config 1 onboard_profiles Disabled
|
||||
|
||||
# Set to Profile 1 (use onboard profile)
|
||||
solaar config 1 onboard_profiles "Profile 1"
|
||||
```
|
||||
|
||||
**Note:** Many settings require `onboard_profiles` to be set to `Disabled` to be effective.
|
||||
|
||||
---
|
||||
|
||||
### 2. Report Rate
|
||||
|
||||
Controls the frequency of device movement reports.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name | `report_rate_extended` |
|
||||
| Type | Choice |
|
||||
| Possible Values | `8ms`, `4ms`, `2ms`, `1ms`, `500us`, `250us`, `125us` |
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read current value
|
||||
solaar config 1 report_rate_extended
|
||||
|
||||
# Set to 1ms (1000Hz)
|
||||
solaar config 1 report_rate_extended 1ms
|
||||
|
||||
# Set to 500us (2000Hz)
|
||||
solaar config 1 report_rate_extended 500us
|
||||
|
||||
# Set to 125us (8000Hz)
|
||||
solaar config 1 report_rate_extended 125us
|
||||
```
|
||||
|
||||
**Polling Rate Reference:**
|
||||
|
||||
| Value | Polling Rate |
|
||||
|-------|--------------|
|
||||
| `8ms` | 125 Hz |
|
||||
| `4ms` | 250 Hz |
|
||||
| `2ms` | 500 Hz |
|
||||
| `1ms` | 1000 Hz |
|
||||
| `500us` | 2000 Hz |
|
||||
| `250us` | 4000 Hz |
|
||||
| `125us` | 8000 Hz |
|
||||
|
||||
---
|
||||
|
||||
### 3. Sensitivity (DPI)
|
||||
|
||||
Controls mouse movement sensitivity.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name | `dpi_extended` |
|
||||
| Type | Complex (X, Y, LOD) |
|
||||
| DPI Range | 100 - 32000 |
|
||||
| LOD Values | `LOW`, `HIGH` |
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read current value
|
||||
solaar config 1 dpi_extended
|
||||
|
||||
# Set DPI (format: {X:<value>, Y:<value>, LOD:<value>})
|
||||
solaar config 1 dpi_extended "{X:800, Y:800, LOD:HIGH}"
|
||||
|
||||
# Set to 1600 DPI
|
||||
solaar config 1 dpi_extended "{X:1600, Y:1600, LOD:HIGH}"
|
||||
|
||||
# Set different X and Y sensitivity
|
||||
solaar config 1 dpi_extended "{X:800, Y:1600, LOD:LOW}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HITS Tuning Settings (Hall-Effect Inductive Trigger Switch)
|
||||
|
||||
These settings control the advanced click behavior of the PRO X 2 Superstrike's hall-effect switches.
|
||||
|
||||
### 4. Actuation Point
|
||||
|
||||
Controls how deep the button must be pressed to register a click.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name (Left) | `superstrike-tuning_actuation-0` |
|
||||
| Setting Name (Right) | `superstrike-tuning_actuation-1` |
|
||||
| Type | Range |
|
||||
| Range | 1 - 10 |
|
||||
| Default | 5 |
|
||||
|
||||
**Value Interpretation:**
|
||||
- `1` = Shallowest (hair trigger, minimal press)
|
||||
- `10` = Deepest (full press required)
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read left button actuation
|
||||
solaar config 1 superstrike-tuning_actuation-0
|
||||
|
||||
# Read right button actuation
|
||||
solaar config 1 superstrike-tuning_actuation-1
|
||||
|
||||
# Set left button to shallow actuation (hair trigger)
|
||||
solaar config 1 superstrike-tuning_actuation-0 1
|
||||
|
||||
# Set left button to deep actuation
|
||||
solaar config 1 superstrike-tuning_actuation-0 10
|
||||
|
||||
# Set right button to medium actuation
|
||||
solaar config 1 superstrike-tuning_actuation-1 5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Rapid Trigger Level
|
||||
|
||||
Controls the rapid trigger sensitivity, which allows the button to re-actuate quickly after partial release.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name (Left) | `superstrike-tuning_rapid-trigger-level-0` |
|
||||
| Setting Name (Right) | `superstrike-tuning_rapid-trigger-level-1` |
|
||||
| Type | Range |
|
||||
| Range | 1 - 5 |
|
||||
| Default | 3 |
|
||||
|
||||
**Value Interpretation:**
|
||||
- `1` = Fastest (most sensitive, smallest movement to re-trigger)
|
||||
- `5` = Slowest (least sensitive, larger movement needed)
|
||||
|
||||
**Note:** Rapid trigger cannot be disabled on this device. The minimum level is 1.
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read left button rapid trigger level
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-0
|
||||
|
||||
# Read right button rapid trigger level
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-1
|
||||
|
||||
# Set left button to fastest rapid trigger
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-0 1
|
||||
|
||||
# Set left button to slowest rapid trigger
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-0 5
|
||||
|
||||
# Set right button to medium rapid trigger
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-1 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 6. Click Haptics
|
||||
|
||||
Controls the intensity of the haptic feedback when clicking.
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Setting Name (Left) | `superstrike-tuning_haptics-0` |
|
||||
| Setting Name (Right) | `superstrike-tuning_haptics-1` |
|
||||
| Type | Range |
|
||||
| Range | 0 - 5 |
|
||||
| Default | 3 |
|
||||
|
||||
**Value Interpretation:**
|
||||
- `0` = Off (no haptic feedback)
|
||||
- `1` = Minimal
|
||||
- `2` = Light
|
||||
- `3` = Medium
|
||||
- `4` = Strong
|
||||
- `5` = Strongest (maximum haptic feedback)
|
||||
|
||||
**Commands:**
|
||||
|
||||
```bash
|
||||
# Read left button haptics level
|
||||
solaar config 1 superstrike-tuning_haptics-0
|
||||
|
||||
# Read right button haptics level
|
||||
solaar config 1 superstrike-tuning_haptics-1
|
||||
|
||||
# Disable haptics on left button
|
||||
solaar config 1 superstrike-tuning_haptics-0 0
|
||||
|
||||
# Set left button to maximum haptics
|
||||
solaar config 1 superstrike-tuning_haptics-0 5
|
||||
|
||||
# Set right button to medium haptics
|
||||
solaar config 1 superstrike-tuning_haptics-1 3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Settings Summary
|
||||
|
||||
| Setting | CLI Name | Type | Range/Values | Button-Specific |
|
||||
|---------|----------|------|--------------|-----------------|
|
||||
| Onboard Profiles | `onboard_profiles` | Choice | `Disabled`, `Profile 1` | No |
|
||||
| Report Rate | `report_rate_extended` | Choice | `8ms` to `125us` | No |
|
||||
| Sensitivity | `dpi_extended` | Complex | 100-32000 DPI | No |
|
||||
| Actuation Point | `superstrike-tuning_actuation-{0,1}` | Range | 1-10 | Yes |
|
||||
| Rapid Trigger | `superstrike-tuning_rapid-trigger-level-{0,1}` | Range | 1-5 | Yes |
|
||||
| Click Haptics | `superstrike-tuning_haptics-{0,1}` | Range | 0-5 | Yes |
|
||||
|
||||
---
|
||||
|
||||
## Batch Configuration Examples
|
||||
|
||||
### Gaming Profile (Fast Response)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Gaming profile: fast actuation, sensitive rapid trigger, medium haptics
|
||||
|
||||
solaar config 1 onboard_profiles Disabled
|
||||
solaar config 1 report_rate_extended 125us
|
||||
solaar config 1 dpi_extended "{X:800, Y:800, LOD:HIGH}"
|
||||
|
||||
# Left button - hair trigger
|
||||
solaar config 1 superstrike-tuning_actuation-0 1
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-0 1
|
||||
solaar config 1 superstrike-tuning_haptics-0 3
|
||||
|
||||
# Right button - hair trigger
|
||||
solaar config 1 superstrike-tuning_actuation-1 1
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-1 1
|
||||
solaar config 1 superstrike-tuning_haptics-1 3
|
||||
```
|
||||
|
||||
### Productivity Profile (Comfortable)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Productivity profile: deeper actuation, slower rapid trigger, strong haptics
|
||||
|
||||
solaar config 1 onboard_profiles Disabled
|
||||
solaar config 1 report_rate_extended 1ms
|
||||
solaar config 1 dpi_extended "{X:1600, Y:1600, LOD:HIGH}"
|
||||
|
||||
# Left button - comfortable click
|
||||
solaar config 1 superstrike-tuning_actuation-0 7
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-0 4
|
||||
solaar config 1 superstrike-tuning_haptics-0 5
|
||||
|
||||
# Right button - comfortable click
|
||||
solaar config 1 superstrike-tuning_actuation-1 7
|
||||
solaar config 1 superstrike-tuning_rapid-trigger-level-1 4
|
||||
solaar config 1 superstrike-tuning_haptics-1 5
|
||||
```
|
||||
|
||||
### Silent Profile (No Haptics)
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Silent profile: no haptic feedback
|
||||
|
||||
solaar config 1 superstrike-tuning_haptics-0 0
|
||||
solaar config 1 superstrike-tuning_haptics-1 0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Programmatic Usage
|
||||
|
||||
### Reading All Settings (JSON-like parsing)
|
||||
|
||||
```bash
|
||||
# Get all settings as output
|
||||
solaar config 1 2>/dev/null | grep "^[a-z]" | while read line; do
|
||||
setting=$(echo "$line" | cut -d'=' -f1 | tr -d ' ')
|
||||
value=$(echo "$line" | cut -d'=' -f2 | tr -d ' ')
|
||||
echo "{\"setting\": \"$setting\", \"value\": \"$value\"}"
|
||||
done
|
||||
```
|
||||
|
||||
### Reading a Single Setting Value
|
||||
|
||||
```bash
|
||||
# Extract just the value
|
||||
solaar config 1 superstrike-tuning_actuation-0 2>/dev/null | grep "^superstrike" | cut -d'=' -f2 | tr -d ' '
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```bash
|
||||
# Check if command succeeded
|
||||
if solaar config 1 superstrike-tuning_actuation-0 5 2>/dev/null; then
|
||||
echo "Setting applied successfully"
|
||||
else
|
||||
echo "Failed to apply setting"
|
||||
fi
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Error (device not found, invalid setting, invalid value) |
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Device Discovery**: Use `solaar show` to list all connected devices and their indices.
|
||||
|
||||
2. **Persistence**: Settings are saved to `~/.config/solaar/config.yaml` and automatically reapplied when the device reconnects.
|
||||
|
||||
3. **Onboard Profiles**: When `onboard_profiles` is set to `Profile 1`, some settings (DPI, report rate) are controlled by the device's onboard memory and cannot be changed via Solaar.
|
||||
|
||||
4. **HITS Settings**: The actuation, rapid trigger, and haptics settings are stored in the device and persist across reconnections, regardless of the onboard profile setting.
|
||||
|
||||
5. **Button Index**: `0` = Left button, `1` = Right button.
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
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
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
<!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>
|
||||
|
|
@ -21,9 +21,9 @@ Not all such devices supported in Solaar as information needs to be added to Sol
|
|||
for each device type that directly connects.
|
||||
|
||||
|
||||
## HID++
|
||||
## HID++ and Centurion
|
||||
|
||||
The devices that Solaar handles use Logitech's HID++ protocol.
|
||||
The devices that Solaar handles use Logitech's HID++ and Centurion protocols.
|
||||
|
||||
HID++ is a Logitech-proprietary protocol that extends the standard HID
|
||||
protocol for interfacing with receivers, keyboards, mice, and so on. It allows
|
||||
|
|
@ -43,6 +43,8 @@ Contrariwise, two different devices may appear different physically but
|
|||
actually look the same to software. (For example, some M185 mice look the
|
||||
same to software as some M310 mice.)
|
||||
|
||||
Centurion is closely related to HID++ and is used by some Logitech headsets.
|
||||
|
||||
The software identity of a receiver can be determined by its USB product ID
|
||||
(reported by Solaar and also viewable in Linux using `lsusb`). The software
|
||||
identity of a device that connects to a receiver can be determined by
|
||||
|
|
@ -51,17 +53,17 @@ 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.
|
||||
|
||||
For Unifying receivers, pairing adds a new paired device, but
|
||||
For Unifying and Bolt receivers, pairing adds a new paired device, but
|
||||
only if there is an open slot on the receiver. So these receivers need to
|
||||
be able to unpair devices that they have been paired with or else they will
|
||||
not have any open slots for pairing. Some other receivers, like the
|
||||
Nano receiver with USB ID `046d:c534`, can only pair with particular kinds of
|
||||
devices and pairing a new device replaces whatever device of that kind was
|
||||
not have any open slots for pairing. Some Nano and Lightspeed receivers, like the
|
||||
Nano receiver with USB ID `046d:c534`, can only pair with one keyboard and one mouse
|
||||
and pairing a new device replaces whatever device of that kind was
|
||||
previously paired to the receiver. These receivers cannot unpair. Further,
|
||||
some receivers can pair an unlimited number of times but others can only
|
||||
pair a limited number of times.
|
||||
|
|
@ -69,20 +71,22 @@ pair a limited number of times.
|
|||
Bolt receivers add an authentication phase to pairing,
|
||||
where the user has type a passcode or click some buttons to authenticate the device.
|
||||
|
||||
Only some connections between receivers and devices are possible. In should
|
||||
Only some connections between receivers and devices are possible. It should
|
||||
be possible to connect any device with a Unifying logo on it to any receiver
|
||||
with a Unifying logo on it. Receivers without the Unifying logo probably
|
||||
can connect only to the kind of devices they were bought with and devices
|
||||
without the Unifying logo can probably only connect to the kind of receiver
|
||||
that they were bought with.
|
||||
with a Unifying logo on it and any device with a Bolt logo on it to any receiver
|
||||
with a Bolt logo on it.
|
||||
|
||||
Many receivers without the Unifying or Bolt logo
|
||||
can connect only to the model of devices they were bought with and many devices
|
||||
without the Unifying or Bolt logo can only connect to a receiver
|
||||
that matches the one 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).
|
||||
For a list of features and their support see [the features page](features.md).
|
||||
|
||||
Solaar does not do much beyond using the HID++ protocol to change the
|
||||
Solaar does not do much beyond using the protocols to change the
|
||||
behavior of receivers and devices via changing their settings.
|
||||
In particular, Solaar cannot change how
|
||||
the operating system turns the keycodes that a keyboard produces into
|
||||
|
|
@ -112,7 +116,7 @@ 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.
|
||||
|
||||
Querying a device for its current state can require quite a few HID++
|
||||
Querying a device for its current state can require quite a few
|
||||
interactions. These interactions can temporarily slow down the device, so
|
||||
Solaar tries to internally cache information about devices while it is
|
||||
running. If the device
|
||||
|
|
@ -174,6 +178,154 @@ 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 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.
|
||||
|
||||
|
||||
### HITS Tuning (Hall-Effect Inductive Trigger Switch)
|
||||
|
||||
Some gaming mice (such as the PRO X 2 Superstrike) feature hall-effect magnetic switches on their primary buttons instead of traditional mechanical switches. These switches expose tunable parameters via the `SUPERSTRIKE_TUNING` HID++ feature (`0x1B0C`).
|
||||
|
||||
Solaar supports three per-button settings for each primary button (left = 0, right = 1):
|
||||
|
||||
- **Actuation Point** (`superstrike-tuning_actuation-{0,1}`): How deep the button must be pressed to register a click. Range 1–10, where 1 is the shallowest (hair trigger) and 10 is the deepest (full press). Default is 5.
|
||||
- **Rapid Trigger Level** (`superstrike-tuning_rapid-trigger-level-{0,1}`): Sensitivity of rapid re-actuation after partial release. Range 1–5, where 1 is the most sensitive and 5 is the least. This cannot be fully disabled.
|
||||
- **Click Haptics** (`superstrike-tuning_haptics-{0,1}`): Intensity of haptic feedback on click. Range 0–5, where 0 disables haptics and 5 is maximum intensity.
|
||||
|
||||
These settings are written directly to the device and persist across reconnections regardless of the onboard profile state.
|
||||
|
||||
### Extended DPI
|
||||
|
||||
Some gaming mice (such as the PRO X 2 Superstrike) support the `EXTENDED_ADJUSTABLE_DPI` feature (`0x2202`) which allows independent X and Y axis DPI configuration as well as lift-off distance (LOD) control. This is exposed via the `dpi_extended` setting:
|
||||
|
||||
```bash
|
||||
solaar config <device> dpi_extended "{X:1600, Y:1600, LOD:HIGH}"
|
||||
```
|
||||
|
||||
LOD values are `LOW` and `HIGH`. DPI range depends on the device sensor (up to 32000 DPI on the PRO X 2 Superstrike).
|
||||
|
||||
### Haptic Feedback
|
||||
|
||||
Some devices, such as the MX Master 4 have haptic feeback.
|
||||
The Solaar CLI can be used to 'play' wave forms, for example
|
||||
```
|
||||
solaar config 'mx master 4' haptic-play 'HAPPY ALERT'
|
||||
```
|
||||
Solaar rules can also do this using their `Set` action.
|
||||
|
||||
|
||||
### Extended Report Rate
|
||||
|
||||
Some gaming mice (such as the PRO X 2 Superstrike) support the `EXTENDED_ADJUSTABLE_REPORT_RATE` feature (`0x8061`) which enables sub-millisecond polling rates beyond the standard 1 ms (1000 Hz). This is exposed via the `report_rate_extended` setting:
|
||||
|
||||
| Value | Polling Rate |
|
||||
|---------|-------------|
|
||||
| `8ms` | 125 Hz |
|
||||
| `4ms` | 250 Hz |
|
||||
| `2ms` | 500 Hz |
|
||||
| `1ms` | 1000 Hz |
|
||||
| `500us` | 2000 Hz |
|
||||
| `250us` | 4000 Hz |
|
||||
| `125us` | 8000 Hz |
|
||||
|
||||
### 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.
|
||||
|
|
|
|||
|
|
@ -5,78 +5,32 @@ layout: page
|
|||
|
||||
# Supported receivers and devices
|
||||
|
||||
Solaar only supports Logitech receivers and devices that use the Logitech proprietary HID++ protocol.
|
||||
Solaar only supports Logitech receivers and devices that use the Logitech proprietary HID++ and Centurion protocols.
|
||||
|
||||
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 most recent and many older Logitech devices
|
||||
(keyboards, mice, trackballs, touchpads, and headsets)
|
||||
Solaar supports all Logitech devices (keyboards, mice, trackballs, touchpads, and headsets)
|
||||
that can connect to supported receivers.
|
||||
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.
|
||||
Solaar supports all Logitech devices that can connect via a USB cable or via Bluetooth,
|
||||
as long as the device uses the HID++ or Centurion protocol.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
## Adding new devices
|
||||
## Supporting old devices
|
||||
|
||||
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.
|
||||
Some old Logitech devices use an old version of HID++.
|
||||
For Solaar to support these devices well, Solaar needs some information about them.
|
||||
|
||||
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 the 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 the 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 the 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 the device or receiver and also provide the output of
|
||||
`lsusb -vv -d xxxx:yyyy` where xxxx:yyyy is the ID of the 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.
|
||||
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.
|
||||
|
||||
|
||||
## Adding new receivers
|
||||
|
|
@ -129,7 +83,7 @@ a subset of the HID++ 1.0 protocol. Only hardware pairing is supported.
|
|||
|
||||
|
||||
|
||||
## Supported Devices
|
||||
## Supported Devices (Historical Interest Only)
|
||||
|
||||
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.
|
||||
|
|
@ -255,7 +209,9 @@ 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 |
|
||||
| PRO X 2 Superstrike | 40BD | 4.2 |
|
||||
|
||||
### Trackballs (Unifying)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,35 @@
|
|||
./bin/solaar probe
|
||||
solaar version 1.1.11-80-gdea496f
|
||||
|
||||
EX100 Receiver 27 Mhz
|
||||
Device path : /dev/hidraw3
|
||||
Device path : /dev/hidraw2
|
||||
USB id : 046d:C517
|
||||
Serial : None
|
||||
Has 1 paired device(s) out of a maximum of 4.
|
||||
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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
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%.
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
Solaar version 1.1.7
|
||||
solaar version 1.1.12rc1
|
||||
|
||||
1: G502 Gaming Mouse
|
||||
Device path : /dev/hidraw4
|
||||
Device path : /dev/hidraw20
|
||||
WPID : 407F
|
||||
Codename : G502
|
||||
Kind : mouse
|
||||
Protocol : HID++ 4.2
|
||||
Polling rate : 1 ms (1000Hz)
|
||||
Serial number: 1F2DBC7E
|
||||
Report Rate : 1ms
|
||||
Serial number: DDDAADBC
|
||||
Model ID: 407FC08D0000
|
||||
Unit ID: 1F2DBC7E
|
||||
Bootloader: BOT 92.00.B0008
|
||||
Firmware: MPM 17.00.B0008
|
||||
Other:
|
||||
Unit ID: DDDAADBC
|
||||
1: BOT 92.00.B0008
|
||||
0: MPM 17.00.B0008
|
||||
3:
|
||||
The power switch is located on the base.
|
||||
Supports 30 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
|
|
@ -21,28 +21,34 @@ Solaar version 1.1.7
|
|||
Firmware: Bootloader BOT 92.00.B0008 AAEF21F1FA5F
|
||||
Firmware: Firmware MPM 17.00.B0008 407F21F1FA5F
|
||||
Firmware: Other
|
||||
Unit ID: 1F2DBC7E Model ID: 407FC08D0000 Transport IDs: {'wpid': '407F', 'usbid': 'C08D'}
|
||||
Unit ID: DDDAADBC 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: RESET {0020} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: BATTERY VOLTAGE {1001} V2
|
||||
Battery: 70% 3978mV , discharging.
|
||||
Battery: 90% 4166mV , 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): Enable
|
||||
Onboard Profiles : Enable
|
||||
Onboard Profiles (saved): Profile 1
|
||||
Onboard Profiles : Profile 1
|
||||
10: MOUSE BUTTON SPY {8110} V0
|
||||
11: REPORT RATE {8060} V0
|
||||
Polling Rate (ms): 1
|
||||
Polling Rate (ms) (saved): 1
|
||||
Polling Rate (ms) : 1
|
||||
Report Rate: 1ms
|
||||
Report Rate (saved): 1ms
|
||||
Report Rate : 1ms
|
||||
12: ADJUSTABLE DPI {2201} V1
|
||||
Sensitivity (DPI) (saved): 800
|
||||
Sensitivity (DPI) : 800
|
||||
Sensitivity (DPI) (saved): 900
|
||||
Sensitivity (DPI) : 900
|
||||
13: DEVICE RESET {1802} V0 internal, hidden
|
||||
14: unknown:1803 {1803} V0 internal, hidden
|
||||
15: OOBSTATE {1805} V0 internal, hidden
|
||||
|
|
@ -63,12 +69,12 @@ Solaar version 1.1.7
|
|||
Multiplier: 8
|
||||
Has invert: Normal wheel motion
|
||||
Has ratchet switch: Normal wheel mode
|
||||
Low resolution mode
|
||||
High resolution mode
|
||||
HID notification
|
||||
Scroll Wheel Direction (saved): False
|
||||
Scroll Wheel Direction : False
|
||||
Scroll Wheel Resolution (saved): False
|
||||
Scroll Wheel Resolution : False
|
||||
Scroll Wheel Resolution (saved): True
|
||||
Scroll Wheel Resolution : True
|
||||
Scroll Wheel Diversion (saved): False
|
||||
Scroll Wheel Diversion : False
|
||||
Battery: 70% 3978mV , discharging.
|
||||
Battery: 90% 4166mV , discharging.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,101 @@
|
|||
solaar version 1.1.19-25-g7520c9cc
|
||||
|
||||
1: G502 X PLUS
|
||||
Device path : /dev/hidraw8
|
||||
WPID : 4099
|
||||
Codename : G502 X PLUS
|
||||
Kind : mouse
|
||||
Protocol : HID++ 4.2
|
||||
Report Rate : 1ms
|
||||
Serial number: C6884511
|
||||
Model ID: 4099C0950000
|
||||
Unit ID: C6884511
|
||||
1: BL1 42.00.B0016
|
||||
0: MPM 27.00.B0016
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
3:
|
||||
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: 1 BL1 42.00.B0016 AB0BFBB13A33
|
||||
Firmware: 0 MPM 27.00.B0016 4099FBB13A33
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Firmware: 3
|
||||
Unit ID: C6884511 Model ID: 4099C0950000 Transport IDs: {'wpid': '4099', 'usbid': 'C095'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: G502 X PLUS
|
||||
Kind: mouse
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: UNIFIED BATTERY {1004} V3
|
||||
Battery: 77%, BatteryStatus.DISCHARGING.
|
||||
7: ADJUSTABLE DPI {2201} V2
|
||||
Sensitivity (DPI) (saved): 800
|
||||
Sensitivity (DPI) : 800
|
||||
8: 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
|
||||
9: RGB EFFECTS {8071} V2
|
||||
LED Control (saved): Solaar
|
||||
LED Control : Solaar
|
||||
LEDs Primary (saved): !LEDEffectSetting {ID: 0, color: 10820909, intensity: 0, period: 100, ramp: 0, speed: 0}
|
||||
LEDs Primary : !LEDEffectSetting {ID: 0, color: 10820909, intensity: 0, period: 100, ramp: 0, speed: 0}
|
||||
10: PER KEY LIGHTING V2 {8081} V2
|
||||
Per-key Lighting (saved): {A:No change, B:No change, C:No change, D:No change, E:No change, F:No change, G:No change, H:No change}
|
||||
Per-key Lighting : {A:No change, B:No change, C:No change, D:No change, E:No change, F:No change, G:No change, H:No change}
|
||||
11: ONBOARD PROFILES {8100} V0
|
||||
Device Mode: On-Board
|
||||
Onboard Profiles (saved): Profile 1
|
||||
Onboard Profiles : Profile 1
|
||||
12: MOUSE BUTTON SPY {8110} V0
|
||||
13: REPORT RATE {8060} V0
|
||||
Report Rate: 1ms
|
||||
Report Rate (saved): 1ms
|
||||
Report Rate : 1ms
|
||||
14: FORCE PAIRING {1500} V0
|
||||
15: DFU {00D0} V3
|
||||
16: DEVICE RESET {1802} V0
|
||||
17: unknown:1803 {0318} V0 internal, hidden, unknown:000010
|
||||
18: CONFIG DEVICE PROPS {1806} V8
|
||||
19: unknown:1811 {1118} V0 internal, hidden, unknown:000010
|
||||
20: OOBSTATE {1805} V0
|
||||
21: unknown:1830 {3018} V0 internal, hidden, unknown:000010
|
||||
22: unknown:1875 {7518} V0 internal, hidden, unknown:000010
|
||||
23: unknown:1861 {6118} V0 internal, hidden, unknown:000010
|
||||
24: unknown:1890 {9018} V0 internal, hidden, unknown:000008
|
||||
25: unknown:18A1 {A118} V0 internal, hidden, unknown:000010
|
||||
26: unknown:1801 {0118} V0 internal, hidden, unknown:000010
|
||||
27: unknown:1E00 {001E} V0 hidden
|
||||
28: unknown:1E22 {221E} V0 internal, hidden, unknown:000010
|
||||
29: unknown:1EB0 {B01E} V0 internal, hidden, unknown:000010
|
||||
30: unknown:18B1 {B118} V0 internal, hidden, unknown:000010
|
||||
31: unknown:18C0 {C018} V0 internal, hidden, unknown:000010
|
||||
Battery: 77%, BatteryStatus.DISCHARGING.
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
solaar version 1.1.13+dfsg-1
|
||||
|
||||
1: G515 LS TKL
|
||||
Device path : None
|
||||
WPID : 40B4
|
||||
Codename : G515 LS TKL
|
||||
Kind : keyboard
|
||||
Protocol : HID++ 4.2
|
||||
Report Rate : 8ms
|
||||
Serial number: 54FEF928
|
||||
Model ID: B38940B4C355
|
||||
Unit ID: 54FEF928
|
||||
1: BL2 19.01.B0011
|
||||
3:
|
||||
0: MPK 25.01.B0011
|
||||
3:
|
||||
The power switch is located on the top right corner.
|
||||
Supports 34 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V6
|
||||
Firmware: Bootloader BL2 19.01.B0011 ABD580558692
|
||||
Firmware: Other
|
||||
Firmware: Firmware MPK 25.01.B0011 40B480558692
|
||||
Firmware: Other
|
||||
Unit ID: 54FEF928 Model ID: B38940B4C355 Transport IDs: {'btleid': 'B389', 'wpid': '40B4', 'usbid': 'C355'}
|
||||
3: DEVICE NAME {0005} V3
|
||||
Name: G515 LS TKL
|
||||
Kind: keyboard
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: DEVICE FRIENDLY NAME {0007} V0
|
||||
Friendly Name: G515 LS TKL
|
||||
7: unknown:0011 {0011} V0
|
||||
8: UNIFIED BATTERY {1004} V5
|
||||
Battery: 82%, discharging.
|
||||
9: RGB EFFECTS {8071} V4
|
||||
LED Control (saved): Solaar
|
||||
LED Control : Solaar
|
||||
LEDs Primary (saved): !LEDEffectSetting {ID: 1, color: 16776960, intensity: 26, period: 2167, ramp: 1, speed: 0}
|
||||
LEDs Primary : HID++ error {'number': 1, 'request': 2537, 'error': 7, 'params': b'\x00'}
|
||||
10: PER KEY LIGHTING V2 {8081} V0
|
||||
Per-key Lighting (saved): {A:indian red, B:indian red, C:indian red, D:indian red, E:indian red, F:indian red, G:indian red, H:indian red, I:indian red, J:indian red, K:indian red, L:indian red, M:indian red, N:indian red, O:indian red, P:indian red, Q:indian red, R:indian red, S:indian red, T:indian red, U:indian red, V:indian red, W:indian red, X:indian red, Y:indian red, Z:indian red, 1:orange, 2:orange, 3:orange, 4:orange, 5:orange, 6:orange, 7:orange, 8:orange, 9:orange, 0:yellow, ENTER:green, ESC:green, BACKSPACE:red, TAB:yellow, SPACE:yellow, -:indian red, =:indian red, [:indian red, \:indian red, KEY 46:white, ~:indian red, ;:indian red, ':indian red, `:indian red, ,:indian red, .:indian red, /:indian red, CAPS LOCK:red, F1:indian red, F2:indian red, F3:indian red, F4:indian red, F5:indian red, F6:indian red, F7:indian red, F8:indian red, F9:indian red, F10:indian red, F11:indian red, F12:indian red, PRINT:red, SCROLL LOCK:orange, PASTE:indian red, INSERT:green, HOME:indian red, PAGE UP:yellow, DELETE:red, END:indian red, PAGE DOWN:yellow, RIGHT:indian red, LEFT:indian red, DOWN:indian red, UP:indian red, KEY 97:indian red, COMPOSE:white, POWER:white, KEY 100:indian red, KEY 101:red, KEY 102:red, KEY 103:red, LEFT CTRL:indian red, LEFT SHIFT:yellow, LEFT ALT:indian red, LEFT WINDOWS:blue, RIGHT CTRL:indian red, RIGHT SHIFT:yellow, RIGHT ALTGR:blue, RIGHT WINDOWS:indian red, KEY 254:white}
|
||||
Per-key Lighting : {A:No change, B:No change, C:No change, D:No change, E:No change, F:No change, G:No change, H:No change, I:No change, J:No change, K:No change, L:No change, M:No change, N:No change, O:No change, P:No change, Q:No change, R:No change, S:No change, T:No change, U:No change, V:No change, W:No change, X:No change, Y:No change, Z:No change, 1:No change, 2:No change, 3:No change, 4:No change, 5:No change, 6:No change, 7:No change, 8:No change, 9:No change, 0:No change, ENTER:No change, ESC:No change, BACKSPACE:No change, TAB:No change, SPACE:No change, -:No change, =:No change, [:No change, \:No change, KEY 46:No change, ~:No change, ;:No change, ':No change, `:No change, ,:No change, .:No change, /:No change, CAPS LOCK:No change, F1:No change, F2:No change, F3:No change, F4:No change, F5:No change, F6:No change, F7:No change, F8:No change, F9:No change, F10:No change, F11:No change, F12:No change, PRINT:No change, SCROLL LOCK:No change, PASTE:No change, INSERT:No change, HOME:No change, PAGE UP:No change, DELETE:No change, END:No change, PAGE DOWN:No change, RIGHT:No change, LEFT:No change, DOWN:No change, UP:No change, KEY 97:No change, COMPOSE:No change, POWER:No change, KEY 100:No change, KEY 101:No change, KEY 102:No change, KEY 103:No change, LEFT CTRL:No change, LEFT SHIFT:No change, LEFT ALT:No change, LEFT WINDOWS:No change, RIGHT CTRL:No change, RIGHT SHIFT:No change, RIGHT ALTGR:No change, RIGHT WINDOWS:No change, KEY 254:No change}
|
||||
11: unknown:1B10 {1B10} V0
|
||||
12: unknown:4523 {4523} V1
|
||||
13: KEYBOARD LAYOUT 2 {4540} V1
|
||||
14: BRIGHTNESS CONTROL {8040} V0
|
||||
Brightness Control (saved): 40
|
||||
Brightness Control : 40
|
||||
15: unknown:8101 {8101} V0
|
||||
16: unknown:1B05 {1B05} V0
|
||||
17: unknown:8051 {8051} V0
|
||||
18: DFU {00D0} V3
|
||||
19: DEVICE RESET {1802} V0 internal, hidden, unknown:000010
|
||||
20: unknown:1803 {1803} V1 internal, hidden, unknown:000010
|
||||
21: unknown:1807 {1807} V3 internal, hidden, unknown:000010
|
||||
22: unknown:1817 {1817} V0 internal, hidden, unknown:000010
|
||||
23: OOBSTATE {1805} V0 internal, hidden
|
||||
24: unknown:1830 {1830} V0 internal, hidden, unknown:000010
|
||||
25: unknown:1890 {1890} V9 internal, hidden, unknown:000008
|
||||
26: unknown:1891 {1891} V9 internal, hidden, unknown:000008
|
||||
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:18B0 {18B0} V1 internal, hidden, unknown:000010
|
||||
33: unknown:1801 {1801} V0 internal, hidden, unknown:000010
|
||||
Battery: 82%, discharging.
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
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%.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
Solaar version 1.1.3
|
||||
solaar version 1.1.19-25-g7520c9cc
|
||||
|
||||
1: G613 Wireless Mechanical Gaming Keyboard
|
||||
Device path : None
|
||||
|
|
@ -6,69 +6,71 @@ Solaar version 1.1.3
|
|||
Codename : G613
|
||||
Kind : keyboard
|
||||
Protocol : HID++ 4.2
|
||||
Polling rate : 1 ms (1000Hz)
|
||||
Serial number: 0DBC5FF6
|
||||
Report Rate : 1ms
|
||||
Serial number: 710EC3A3
|
||||
Model ID: B34F40650000
|
||||
Unit ID: 9AAB3225
|
||||
Bootloader: BOT 46.00.B0006
|
||||
Firmware: MPK 05.02.B0021
|
||||
Other:
|
||||
Unit ID: 2A923B25
|
||||
1: BOT 46.00.B0006
|
||||
0: MPK 05.02.B0021
|
||||
3:
|
||||
The power switch is located on the unknown.
|
||||
Supports 32 HID++ 2.0 features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: DEVICE FW VERSION {0003}
|
||||
Firmware: Bootloader BOT 46.00.B0006 00006E86A7BD
|
||||
Firmware: Firmware MPK 05.02.B0021 40656E86A7BD
|
||||
Firmware: Other
|
||||
Unit ID: 9AAB3225 Model ID: B34F40650000 Transport IDs: {'btleid': 'B34F', 'wpid': '4065'}
|
||||
3: DEVICE NAME {0005}
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V2
|
||||
Firmware: 1 BOT 46.00.B0006 00006E86A7BD
|
||||
Firmware: 0 MPK 05.02.B0021 40656E86A7BD
|
||||
Firmware: 3
|
||||
Unit ID: 2A923B25 Model ID: B34F40650000 Transport IDs: {'btleid': 'B34F', 'wpid': '4065'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: G613 Wireless Mechanical Gaming Keyboard
|
||||
Kind: keyboard
|
||||
4: WIRELESS DEVICE STATUS {1D4B}
|
||||
5: RESET {0020}
|
||||
6: DEVICE FRIENDLY NAME {0007}
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: DEVICE FRIENDLY NAME {0007} V0
|
||||
Friendly Name: G613
|
||||
7: BATTERY STATUS {1000}
|
||||
Battery: 50%, discharging, next level 20%.
|
||||
8: CHANGE HOST {1814}
|
||||
Change Host : 1:video2
|
||||
9: HOSTS INFO {1815}
|
||||
Host 0 (paired): video2
|
||||
Host 1 (paired): CB73CG2
|
||||
10: REPROG CONTROLS V4 {1B04}
|
||||
7: BATTERY STATUS {1000} V0
|
||||
Battery: 80%, BatteryStatus.DISCHARGING, next level 50%.
|
||||
8: CHANGE HOST {1814} V1
|
||||
Change Host : 1:cosmo
|
||||
9: HOSTS INFO {1815} V1
|
||||
Host 0 (paired): cosmo
|
||||
Host 1 (paired): Mi 11
|
||||
10: REPROG CONTROLS V4 {1B04} V3
|
||||
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}
|
||||
11: REPORT HID USAGE {1BC0}
|
||||
12: ENCRYPTION {4100}
|
||||
13: KEYBOARD DISABLE BY USAGE {4522}
|
||||
14: KEYBOARD LAYOUT 2 {4540}
|
||||
15: GKEY {8010}
|
||||
Divert G Keys (saved): True
|
||||
Divert G Keys : False
|
||||
16: REPORT RATE {8060}
|
||||
Polling Rate (ms): 1
|
||||
Polling Rate (ms) (saved): 1
|
||||
Polling Rate (ms) : 1
|
||||
17: DFUCONTROL SIGNED {00C2}
|
||||
18: DEVICE RESET {1802} internal, hidden
|
||||
19: unknown:1803 {1803} internal, hidden
|
||||
20: CONFIG DEVICE PROPS {1806} internal, hidden
|
||||
21: unknown:1813 {1813} internal, hidden
|
||||
22: OOBSTATE {1805} internal, hidden
|
||||
23: unknown:1830 {1830} internal, hidden
|
||||
24: unknown:1890 {1890} internal, hidden
|
||||
25: unknown:1891 {1891} internal, hidden
|
||||
26: unknown:18A1 {18A1} internal, hidden
|
||||
27: unknown:1DF3 {1DF3} internal, hidden
|
||||
28: unknown:1E00 {1E00} hidden
|
||||
29: unknown:1EB0 {1EB0} internal, hidden
|
||||
30: unknown:1861 {1861} internal, hidden
|
||||
31: unknown:18B1 {18B1} internal, hidden
|
||||
11: REPORT HID USAGE {1BC0} V1
|
||||
12: ENCRYPTION {4100} V0
|
||||
13: KEYBOARD DISABLE BY USAGE {4522} V0
|
||||
14: KEYBOARD LAYOUT 2 {4540} V0
|
||||
15: GKEY {8010} V0
|
||||
Divert G and M Keys (saved): False
|
||||
Divert G and M Keys : False
|
||||
16: REPORT RATE {8060} V0
|
||||
Report Rate: 1ms
|
||||
Report Rate (saved): 1ms
|
||||
Report Rate : 1ms
|
||||
17: DFUCONTROL SIGNED {00C2} V0
|
||||
18: DEVICE RESET {1802} V0
|
||||
19: unknown:1803 {0318} V0 internal, hidden
|
||||
20: CONFIG DEVICE PROPS {1806} V3
|
||||
21: unknown:1813 {1318} V0 internal, hidden
|
||||
22: OOBSTATE {1805} V0
|
||||
23: unknown:1830 {3018} V0 internal, hidden
|
||||
24: unknown:1890 {9018} V0 internal, hidden
|
||||
25: unknown:1891 {9118} V0 internal, hidden
|
||||
26: unknown:18A1 {A118} V0 internal, hidden
|
||||
27: unknown:1DF3 {F31D} V0 internal, hidden
|
||||
28: unknown:1E00 {001E} V0 hidden
|
||||
29: unknown:1EB0 {B01E} V0 internal, hidden
|
||||
30: unknown:1861 {6118} V0 internal, hidden
|
||||
31: unknown:18B1 {B118} 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
|
||||
0: Host Switch Channel 1 , default: Hostswitch Channel 1 => Hostswitch Channel 1
|
||||
persistently_divertable, 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
|
||||
1: Host Switch Channel 2 , default: Hostswitch Channel 2 => Hostswitch Channel 2
|
||||
persistently_divertable, divertable, pos:2, group:0, group mask:empty
|
||||
reporting: default
|
||||
Battery: 50%, discharging, next level 20%.
|
||||
Battery: 80%, BatteryStatus.DISCHARGING, next level 50%.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
Solaar version 1.1.5
|
||||
Solaar version 1.1.19
|
||||
|
||||
2: G733 Gaming Headset
|
||||
Device path : /dev/hidraw2
|
||||
G733 Gaming Headset
|
||||
Device path : /dev/hidraw0
|
||||
USB id : 046d:0AB5
|
||||
Codename : G733 Headset
|
||||
Kind : headset
|
||||
|
|
@ -9,24 +9,34 @@ Solaar version 1.1.5
|
|||
Serial number:
|
||||
Model ID: 0AB500000000
|
||||
Unit ID: FFFFFFFF
|
||||
Firmware: U1 37.00.B0131
|
||||
0: U1 37.00.B0131
|
||||
Supports 9 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V2
|
||||
Firmware: Firmware U1 37.00.B0131 0AB5
|
||||
Firmware: 0 U1 37.00.B0131 0AB5
|
||||
Unit ID: FFFFFFFF Model ID: 0AB500000000 Transport IDs: {'usbid': '0AB5'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: G733 Gaming Headset
|
||||
Kind: None
|
||||
4: COLOR LED EFFECTS {8070} V0
|
||||
4: COLOR LED EFFECTS {8070} V3
|
||||
Sterowanie diodami LED (saved): Device
|
||||
Sterowanie diodami LED : Device
|
||||
Diody LED None (saved): !LEDEffectSetting {ID: 0}
|
||||
Diody LED None : !LEDEffectSetting {ID: 0}
|
||||
Diody LED None (saved): !LEDEffectSetting {ID: 1, color: 131072, ramp: 0}
|
||||
Diody LED None : !LEDEffectSetting {ID: 1, color: 66048, ramp: 0}
|
||||
5: GKEY {8010} V0
|
||||
Divert G Keys (saved): True
|
||||
Divert G Keys : False
|
||||
6: EQUALIZER {8310} V0
|
||||
Przekieruj klawisze G i M (saved): False
|
||||
Przekieruj klawisze G i M : False
|
||||
6: EQUALIZER {8310} V1
|
||||
Korektor (saved): {0: 5, 1: 4, 2: 3, 3: 5, 4: 5, 5: 5, 6: 4, 7: 3, 8: 4, 9: 5}
|
||||
Korektor : {0: 5, 1: 4, 2: 3, 3: 5, 4: 5, 5: 5, 6: 4, 7: 3, 8: 4, 9: 5}
|
||||
7: SIDETONE {8300} V0
|
||||
Sidetone (saved): 65
|
||||
Sidetone : 65
|
||||
8: ADC MEASUREMENT {1F20} V0
|
||||
Battery status unavailable.
|
||||
Battery status unavailable.
|
||||
Efekt lokalny (saved): 0
|
||||
Efekt lokalny : 0
|
||||
8: ADC MEASUREMENT {1F20} V4
|
||||
Battery: 89% 4058mV , BatteryStatus.DISCHARGING.
|
||||
Zarządzanie energią (saved): 30
|
||||
Zarządzanie energią : 30
|
||||
Battery: 89% 4058mV , BatteryStatus.DISCHARGING.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
solaar version 1.1.19-25-g7520c9cc
|
||||
|
||||
Logitech G933 Gaming Wireless Headset
|
||||
Device path : /dev/hidraw4
|
||||
USB id : 046d:0A5B
|
||||
Codename : Logitech
|
||||
Kind : ?
|
||||
Protocol : HID++ 4.2
|
||||
Serial number:
|
||||
Model ID: 000000000A5B
|
||||
Unit ID: FFFFFFFF
|
||||
0: U 98.03.B0027
|
||||
Supports 9 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V2
|
||||
Firmware: 0 U 98.03.B0027 0A5B
|
||||
Unit ID: FFFFFFFF Model ID: 000000000A5B Transport IDs: {'btid': '0000', 'btleid': '0000'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: Logitech G933 Gaming Wireless Headset
|
||||
Kind: None
|
||||
4: COLOR LED EFFECTS {8070} V3
|
||||
LED Control : HID++ error {'number': 255, 'request': 1147, 'error': 7, 'params': b''}
|
||||
LEDs None (saved): !LEDEffectSetting {ID: 0}
|
||||
LEDs None : !LEDEffectSetting {ID: 0}
|
||||
LEDs None (saved): !LEDEffectSetting {ID: 0, color: 9519532, intensity: 0, period: 100, ramp: 0, speed: 0}
|
||||
LEDs None : !LEDEffectSetting {ID: 1, color: 0, ramp: 0}
|
||||
5: GKEY {8010} V0
|
||||
Divert G and M Keys (saved): False
|
||||
Divert G and M Keys : False
|
||||
6: EQUALIZER {8310} V1
|
||||
Equalizer (saved): {0: 8, 1: 8, 2: 4, 3: 2, 4: 1, 5: 4, 6: 7, 7: 10, 8: 5, 9: 11}
|
||||
Equalizer : {0: 8, 1: 8, 2: 4, 3: 2, 4: 1, 5: 4, 6: 7, 7: 10, 8: 5, 9: 11}
|
||||
7: SIDETONE {8300} V0
|
||||
Sidetone (saved): 30
|
||||
Sidetone : 30
|
||||
8: ADC MEASUREMENT {1F20} V3
|
||||
Battery: 100% 4183mV , BatteryStatus.RECHARGING.
|
||||
Power Management (saved): 30
|
||||
Power Management : 30
|
||||
Battery: 100% 4183mV , BatteryStatus.RECHARGING.
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
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.
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
solaar version 1.1.19-24-g693159ee
|
||||
|
||||
Centurion Receiver
|
||||
Device path : /dev/hidraw4
|
||||
USB id : 046d:0AF7
|
||||
Protocol : Centurion
|
||||
1 : 0.02
|
||||
Has 1 device(s) out of a maximum of 1.
|
||||
Supports 5 dongle features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: CENTURION DEVICE INFO {0100}
|
||||
Firmware: 1 0.02
|
||||
Hardware: model 26 rev 255 product 0508
|
||||
3: CENTPP BRIDGE {0003}
|
||||
4: CENTURION GENERIC DFU {010A}
|
||||
|
||||
1: PRO X 2 LIGHTSPEED
|
||||
Device path : /dev/hidraw4
|
||||
USB id : 046d:0AF7
|
||||
Codename : PRO X 2 LIGHTSPEED
|
||||
Kind : headset
|
||||
Protocol : Centurion 2.6
|
||||
Serial number: <redacted>
|
||||
Model ID: 0508
|
||||
Unit ID: <redacted>
|
||||
1: 0.02
|
||||
Supports 10 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: CENTURION DEVICE NAME {0101} V0
|
||||
3: CENTURION DEVICE INFO {0100} V0
|
||||
Firmware: 1 0.02
|
||||
Serial: <redacted>
|
||||
Hardware: model 26 rev 255 product 0508
|
||||
4: CENTURION BATTERY SOC {0104} V0
|
||||
Battery: 97%, BatteryStatus.DISCHARGING.
|
||||
5: CENTURION GENERIC DFU {010A} V0
|
||||
6: CENTURION AUTO SLEEP {0108} V0
|
||||
Auto Sleep Timeout: 10
|
||||
7: HEADSET AUDIO SIDETONE {0604} V0
|
||||
Headset Sidetone: 55
|
||||
8: HEADSET MIC SNR {0602} V0
|
||||
Mic SNR: True
|
||||
9: HEADSET ONBOARD EQ {0636} V0
|
||||
EQ: 80Hz:+0dB, 240Hz:+0dB, 750Hz:+0dB, 2200Hz:+0dB, 6600Hz:+0dB
|
||||
Battery: 97%, BatteryStatus.DISCHARGING.
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
Solaar version 1.1.19
|
||||
|
||||
1: PRO X 2 Superstrike
|
||||
Device path : None
|
||||
WPID : 40BD
|
||||
Codename : PRO X 2 Superstrike
|
||||
Kind : mouse
|
||||
Protocol : HID++ 4.2
|
||||
Report Rate : 1ms
|
||||
Serial number:
|
||||
Model ID: 40BDC0A80000
|
||||
Unit ID:
|
||||
Bootloader: BL2 73.00.B0011
|
||||
Firmware: MPM 42.00.B0011
|
||||
The power switch is located on the base.
|
||||
Supports 36 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V7
|
||||
Firmware: Bootloader BL2 73.00.B0011
|
||||
Firmware: Firmware MPM 42.00.B0011
|
||||
Unit ID: Model ID: 40BDC0A80000 Transport IDs: {'wpid': '40BD', 'usbid': 'C0A8'}
|
||||
3: DEVICE NAME {0005} V5
|
||||
Name: PRO X2 SUPERSTRIKE
|
||||
Kind: mouse
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
6: UNIFIED BATTERY {1004} V5
|
||||
7: XY STATS {2250} V1
|
||||
8: WHEEL STATS {2251} V0
|
||||
9: EXTENDED ADJUSTABLE DPI {2202} V0
|
||||
Sensitivity (DPI): {X:800, Y:800, LOD:HIGH}
|
||||
10: MODE STATUS {8090} V3
|
||||
11: unknown:80E0 {80E0} V0
|
||||
12: SUPERSTRIKE TUNING {1B0C} V0
|
||||
Left Button Actuation Point: 5
|
||||
Left Button Rapid Trigger Level: 3
|
||||
Left Button Click Haptics: 3
|
||||
Right Button Actuation Point: 5
|
||||
Right Button Rapid Trigger Level: 3
|
||||
Right Button Click Haptics: 3
|
||||
13: EXTENDED ADJUSTABLE REPORT RATE {8061} V0
|
||||
Report Rate: 1ms
|
||||
14: ONBOARD PROFILES {8100} V0
|
||||
Device Mode: On-Board
|
||||
Onboard Profiles: Profile 1
|
||||
15: MOUSE BUTTON SPY {8110} V0
|
||||
16: FORCE PAIRING {1500} V0
|
||||
17: unknown:1801 {1801} V0 internal, hidden
|
||||
18: DEVICE RESET {1802} V0
|
||||
19: unknown:1803 {1803} V0 internal, hidden
|
||||
20: CONFIG DEVICE PROPS {1806} V8
|
||||
21: unknown:1817 {1817} V0 internal, hidden
|
||||
22: OOBSTATE {1805} V0
|
||||
23: unknown:1830 {1830} V0 internal, hidden
|
||||
24: unknown:1877 {1877} V0 internal, hidden
|
||||
25: unknown:9403 {9403} V0 internal, hidden
|
||||
26: unknown:1861 {1861} V0 internal, hidden
|
||||
27: unknown:1890 {1890} V0 internal, hidden
|
||||
28: unknown:18A1 {18A1} V0 internal, hidden
|
||||
29: unknown:1E00 {1E00} V0 hidden
|
||||
30: unknown:1E02 {1E02} V0 internal, hidden
|
||||
31: unknown:1E22 {1E22} V0 internal, hidden
|
||||
32: unknown:1E30 {1E30} V0 internal, hidden
|
||||
33: unknown:1602 {1602} V0
|
||||
34: unknown:1EB0 {1EB0} V0 internal, hidden
|
||||
35: unknown:18B1 {18B1} V0 internal, hidden
|
||||
Battery: discharging.
|
||||
|
||||
SUPERSTRIKE TUNING Feature (0x1B0C)
|
||||
-----------------------------------
|
||||
This feature controls the HITS (Hall-Effect Inductive Trigger Switch) settings
|
||||
for the left and right mouse buttons.
|
||||
|
||||
Capabilities (function 0x00):
|
||||
Byte 0: Flags
|
||||
Byte 1: Button count (3, but only 0 and 1 are accessible)
|
||||
Byte 2: Max actuation value (40)
|
||||
Byte 3: Max rapid trigger value (20)
|
||||
Byte 4: Max haptics value (20)
|
||||
|
||||
Read button settings (function 0x20, param: button_index):
|
||||
Response: [button_index, actuation, rapid_trigger, haptics, ...]
|
||||
- actuation: 4-40 (quantized to multiples of 4)
|
||||
- rapid_trigger: 1-20 (cannot be set to 0)
|
||||
- haptics: 0-20 (quantized to multiples of 4: 0, 4, 8, 12, 16, 20)
|
||||
|
||||
Write button settings (function 0x10):
|
||||
Params: [button_index, actuation, rapid_trigger, haptics]
|
||||
|
||||
Solaar Settings:
|
||||
- superstrike-tuning_actuation-{0,1}: Range 1-10 (maps to device 4-40)
|
||||
- superstrike-tuning_rapid-trigger-level-{0,1}: Range 1-5 (maps to device 1-20)
|
||||
- superstrike-tuning_haptics-{0,1}: Range 0-5 (maps to device 0-20)
|
||||
|
||||
Note: Feature 0x80E0 (unknown:80E0) appears to be a non-functional stub for haptics.
|
||||
Haptics are actually controlled via byte 3 of the SUPERSTRIKE TUNING feature.
|
||||
|
||||
Note: Feature 0x9403 (unknown:9403) appears to be a hidden BHOP (bunny hop) feature
|
||||
that is not accessible via HID++.
|
||||
|
|
@ -1,59 +1,62 @@
|
|||
Solaar version 1.1.3
|
||||
Solaar version 1.1.14
|
||||
|
||||
1: PRO X Wireless
|
||||
2: PRO X Wireless
|
||||
Device path : None
|
||||
WPID : 4093
|
||||
Codename : PRO X
|
||||
Kind : mouse
|
||||
Protocol : HID++ 4.2
|
||||
Polling rate : 8 ms (125Hz)
|
||||
Serial number: 42F42E12
|
||||
Report Rate : 1ms
|
||||
Serial number: 8B24D1D1
|
||||
Model ID: 4093C0940000
|
||||
Unit ID: 42F42E12
|
||||
Bootloader: BL1 25.00.B0013
|
||||
Other:
|
||||
Firmware: MPM 25.01.B0018
|
||||
Unit ID: 8B24D1D1
|
||||
1: BL1 25.01.B0018
|
||||
3:
|
||||
0: MPM 25.01.B0018
|
||||
Supports 28 HID++ 2.0 features:
|
||||
0: ROOT {0000}
|
||||
1: FEATURE SET {0001}
|
||||
2: DEVICE FW VERSION {0003}
|
||||
Firmware: Bootloader BL1 25.00.B0013 AB00BE657A82
|
||||
Firmware: Other
|
||||
Firmware: Firmware MPM 25.01.B0018 4093FE92436C
|
||||
Unit ID: 42F42E12 Model ID: 4093C0940000 Transport IDs: {'wpid': '4093', 'usbid': 'C094'}
|
||||
3: DEVICE NAME {0005}
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V3
|
||||
Firmware: 1 BL1 25.01.B0018 AB00FE92436C
|
||||
Firmware: 3
|
||||
Firmware: 0 MPM 25.01.B0018 4093FE92436C
|
||||
Unit ID: 8B24D1D1 Model ID: 4093C0940000 Transport IDs: {'wpid': '4093', 'usbid': 'C094'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: PRO X Wireless
|
||||
Kind: mouse
|
||||
4: WIRELESS DEVICE STATUS {1D4B}
|
||||
5: RESET {0020}
|
||||
6: UNIFIED BATTERY {1004}
|
||||
7: COLOR LED EFFECTS {8070} internal, hidden
|
||||
8: ONBOARD PROFILES {8100}
|
||||
Device Mode: Host
|
||||
Onboard Profiles (saved): Disable
|
||||
Onboard Profiles : Disable
|
||||
9: MOUSE BUTTON SPY {8110}
|
||||
10: REPORT RATE {8060}
|
||||
Polling Rate (ms): 1
|
||||
Polling Rate (ms) (saved): 1
|
||||
Polling Rate (ms) : 1
|
||||
11: ADJUSTABLE DPI {2201}
|
||||
Sensitivity (DPI) (saved): 1000
|
||||
Sensitivity (DPI) : 1000
|
||||
12: unknown:1500 {1500}
|
||||
13: DEVICE RESET {1802} internal, hidden
|
||||
14: unknown:1803 {1803} internal, hidden
|
||||
15: CONFIG DEVICE PROPS {1806} internal, hidden
|
||||
16: unknown:1811 {1811} internal, hidden
|
||||
17: OOBSTATE {1805} internal, hidden
|
||||
18: unknown:1830 {1830} internal, hidden
|
||||
19: unknown:1890 {1890} internal, hidden
|
||||
20: unknown:1891 {1891} internal, hidden
|
||||
21: unknown:18A1 {18A1} internal, hidden
|
||||
22: unknown:1801 {1801} internal, hidden
|
||||
23: unknown:18B1 {18B1} internal, hidden
|
||||
24: unknown:1E00 {1E00} hidden
|
||||
25: unknown:1EB0 {1EB0} internal, hidden
|
||||
26: unknown:1863 {1863} internal, hidden
|
||||
27: unknown:1E22 {1E22} internal, hidden
|
||||
Battery: 76%, discharging.
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: UNIFIED BATTERY {1004} V1
|
||||
Battery: 71%, 0.
|
||||
7: COLOR LED EFFECTS {8070} V4 internal, hidden
|
||||
LED Control : HID++ error {'number': 2, 'request': 1908, 'error': 5, 'params': b''}
|
||||
8: ONBOARD PROFILES {8100} V0
|
||||
Device Mode: On-Board
|
||||
Onboard Profiles (saved): Profile 1
|
||||
Onboard Profiles : Profile 1
|
||||
9: MOUSE BUTTON SPY {8110} V0
|
||||
10: REPORT RATE {8060} V0
|
||||
Report Rate: 1ms
|
||||
Report Rate (saved): 1ms
|
||||
Report Rate : 1ms
|
||||
11: ADJUSTABLE DPI {2201} V2
|
||||
Sensitivity (DPI) (saved): 800
|
||||
Sensitivity (DPI) : 800
|
||||
12: FORCE PAIRING {1500} V0
|
||||
13: DEVICE RESET {1802} V0 internal, hidden
|
||||
14: unknown:1803 {1803} V0 internal, hidden
|
||||
15: CONFIG DEVICE PROPS {1806} V4 internal, hidden
|
||||
16: unknown:1811 {1811} V0 internal, hidden
|
||||
17: OOBSTATE {1805} V0 internal, hidden
|
||||
18: unknown:1830 {1830} V0 internal, hidden
|
||||
19: unknown:1890 {1890} V5 internal, hidden
|
||||
20: unknown:1891 {1891} V5 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:1E00 {1E00} V0 hidden
|
||||
25: unknown:1EB0 {1EB0} V0 internal, hidden
|
||||
26: unknown:1863 {1863} V0 internal, hidden
|
||||
27: unknown:1E22 {1E22} V0 internal, hidden
|
||||
Battery: 71%, 0.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,79 @@
|
|||
solaar version 1.1.19
|
||||
|
||||
1: Signature M550
|
||||
Device path : None
|
||||
WPID : B02B
|
||||
Codename : Logi M550
|
||||
Kind : mouse
|
||||
Protocol : HID++ 4.5
|
||||
Serial number: D074434E
|
||||
Model ID: B02B00000000
|
||||
Unit ID: D074434E
|
||||
1: BL1 39.01.B0013
|
||||
0: RBM 17.01.B0013
|
||||
3:
|
||||
The power switch is located on the (unknown).
|
||||
Supports 30 HID++ 2.0 features:
|
||||
0: ROOT {0000} V0
|
||||
1: FEATURE SET {0001} V0
|
||||
2: DEVICE FW VERSION {0003} V4
|
||||
Firmware: 1 BL1 39.01.B0013 B02BB0706FCD
|
||||
Firmware: 0 RBM 17.01.B0013 B02BB0706FCD
|
||||
Firmware: 3
|
||||
Unit ID: D074434E Model ID: B02B00000000 Transport IDs: {'btleid': 'B02B'}
|
||||
3: DEVICE NAME {0005} V0
|
||||
Name: Signature M550
|
||||
Kind: mouse
|
||||
4: WIRELESS DEVICE STATUS {1D4B} V0
|
||||
5: CONFIG CHANGE {0020} V0
|
||||
Configuration: 11000000000000000000000000000000
|
||||
6: DEVICE FRIENDLY NAME {0007} V0
|
||||
Friendly Name: Logi M550
|
||||
7: UNIFIED BATTERY {1004} V3
|
||||
Battery: 35%, BatteryStatus.DISCHARGING.
|
||||
8: REPROG CONTROLS V4 {1B04} V5
|
||||
Key/Button Actions (saved): {Middle Button:Mouse Middle Button}
|
||||
Key/Button Actions : {Middle Button:Mouse Middle Button}
|
||||
Key/Button Diversion (saved): {Middle Button:Regular}
|
||||
Key/Button Diversion : {Middle Button:Regular}
|
||||
9: HOSTS INFO {1815} V2
|
||||
Host 0 (paired): mst
|
||||
10: XY STATS {2250} V1
|
||||
11: LOWRES WHEEL {2130} V0
|
||||
Wheel Reports: HID
|
||||
Scroll Wheel Diversion (saved): False
|
||||
Scroll Wheel Diversion : False
|
||||
12: ADJUSTABLE DPI {2201} V2
|
||||
Sensitivity (DPI) (saved): 1000
|
||||
Sensitivity (DPI) : 1000
|
||||
13: DFUCONTROL {00C3} V0
|
||||
14: DEVICE RESET {1802} V0
|
||||
15: unknown:1803 {0318} V0 internal, hidden, unknown:000010
|
||||
16: CONFIG DEVICE PROPS {1806} V8
|
||||
17: unknown:1816 {1618} V0 internal, hidden, unknown:000010
|
||||
18: OOBSTATE {1805} V0
|
||||
19: unknown:1830 {3018} V0 internal, hidden, unknown:000010
|
||||
20: unknown:1891 {9118} V0 internal, hidden, unknown:000008
|
||||
21: unknown:18A1 {A118} V0 internal, hidden, unknown:000010
|
||||
22: unknown:1E00 {001E} V0 hidden
|
||||
23: unknown:1E02 {021E} V0 internal, hidden
|
||||
24: unknown:1E22 {221E} V0 internal, hidden, unknown:000010
|
||||
25: unknown:1602 {0216} V0
|
||||
26: unknown:1EB0 {B01E} V0 internal, hidden, unknown:000010
|
||||
27: unknown:1861 {6118} V0 internal, hidden, unknown:000010
|
||||
28: unknown:18B1 {B118} V0 internal, hidden, unknown:000010
|
||||
29: unknown:920A {0A92} V0 internal, hidden, unknown:000010
|
||||
Has 4 reprogrammable keys:
|
||||
0: Left Button , default: Left Click => Left Click
|
||||
analytics_key_events, mse, pos:0, group:1, group mask:empty
|
||||
reporting: default
|
||||
1: Right Button , default: Right Click => Right Click
|
||||
analytics_key_events, mse, pos:0, group:1, group mask:empty
|
||||
reporting: default
|
||||
2: Middle Button , default: Mouse Middle Button => Mouse Middle Button
|
||||
analytics_key_events, raw_xy, divertable, reprogrammable, mse, pos:0, group:2, group mask:g1,g2
|
||||
reporting: default
|
||||
3: Virtual Gesture Button , default: Virtual Gesture Button => Virtual Gesture Button
|
||||
force_raw_xy, raw_xy, virtual, divertable, pos:0, group:3, group mask:empty
|
||||
reporting: default
|
||||
Battery: 35%, BatteryStatus.DISCHARGING.
|
||||
|
|
@ -40,8 +40,7 @@ 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 {2250}
|
||||
11: LOWRES WHEEL {2130}
|
||||
10: XY STATS 11: LOWRES WHEEL {2130}
|
||||
Wheel Reports: HID
|
||||
Scroll Wheel Diversion (saved): False
|
||||
Scroll Wheel Diversion : False
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
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%.
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
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%.
|
||||
|
|
@ -1,3 +1,55 @@
|
|||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -33,14 +33,15 @@ Feature | ID | Status | Notes
|
|||
`UNIFIED_BATTERY` | `0x1004` | Supported | `get_battery`, read only
|
||||
`CHARGING_CONTROL` | `0x1010` | Unsupported |
|
||||
`LED_CONTROL` | `0x1300` | Unsupported |
|
||||
`FORCE_PAIRING` | `0x1500` | Unsupported |
|
||||
`GENERIC_TEST` | `0x1800` | Unsupported |
|
||||
`DEVICE_RESET` | `0x1802` | Unsupported |
|
||||
`OOBSTATE` | `0x1805` | Unsupported |
|
||||
`CONFIG_DEVICE_PROPS` | `0x1806` | Unsupported |
|
||||
`CHANGE_HOST` | `0x1814` | Supported | `ChangeHost`
|
||||
`HOSTS_INFO` | `0x1815` | Partial Support | `get_host_names`, partial listing only
|
||||
`BACKLIGHT` | `0x1981` | Unsupported |
|
||||
`BACKLIGHT2` | `0x1982` | Supported | `Backlight2`
|
||||
`BACKLIGHT` | `0x1981` | Supported | `Backlight`
|
||||
`BACKLIGHT2` | `0x1982` | Supported | `Backlight2`, ...
|
||||
`BACKLIGHT3` | `0x1983` | Unsupported |
|
||||
`PRESENTER_CONTROL` | `0x1A00` | Unsupported |
|
||||
`SENSOR_3D` | `0x1A01` | Unsupported |
|
||||
|
|
@ -49,12 +50,13 @@ Feature | ID | Status | Notes
|
|||
`REPROG_CONTROLS_V2_2` | `0x1B02` | Unsupported |
|
||||
`REPROG_CONTROLS_V3` | `0x1B03` | Unsupported |
|
||||
`REPROG_CONTROLS_V4` | `0x1B04` | Partial Support | `ReprogrammableKeys`, `DivertKeys`, `MouseGesture`, `get_keys`
|
||||
`SUPERSTRIKE_TUNING` | `0x1B0C` | Supported | `SuperstrikeTuning` (actuation point, rapid trigger, click haptics)
|
||||
`REPORT_HID_USAGE` | `0x1BC0` | Unsupported |
|
||||
`PERSISTENT_REMAPPABLE_ACTION` | `0x1C00` | Supported | `PersistentRemappableAction`
|
||||
`WIRELESS_DEVICE_STATUS` | `0x1D4B` | Read only | status reporting from device
|
||||
`REMAINING_PAIRING` | `0x1DF0` | Unsupported |
|
||||
`FIRMWARE_PROPERTIES` | `0x1F1F` | Unsupported |
|
||||
`ADC_MEASUREMENT` | `0x1F20` | Unsupported |
|
||||
`ADC_MEASUREMENT` | `0x1F20` | Supported | `ADCPower`
|
||||
`LEFT_RIGHT_SWAP` | `0x2001` | Unsupported |
|
||||
`SWAP_BUTTON_CANCEL` | `0x2005` | Unsupported |
|
||||
`POINTER_AXIS_ORIENTATION` | `0x2006` | Unsupported |
|
||||
|
|
@ -67,9 +69,12 @@ Feature | ID | Status | Notes
|
|||
`THUMB_WHEEL` | `0x2150` | Supported | `ThumbMode`, `ThumbInvert`
|
||||
`MOUSE_POINTER` | `0x2200` | Supported | `get_mouse_pointer_info`, read only
|
||||
`ADJUSTABLE_DPI` | `0x2201` | Supported | `AdjustableDpi`, `DpiSliding`
|
||||
`EXTENDED_ADJUSTABLE_DPI` | `0x2202` | Supported | `ExtendedAdjustableDpi` (X/Y DPI + lift-off distance)
|
||||
`POINTER_SPEED` | `0x2205` | Supported | `PointerSpeed`, `SpeedChange`, `get_pointer_speed_info`
|
||||
`ANGLE_SNAPPING` | `0x2230` | Unsupported |
|
||||
`SURFACE_TUNING` | `0x2240` | Unsupported |
|
||||
`XY_STATS` | `0x2250` | Unsupported |
|
||||
`WHEEL_STATS` | `0x2251` | Unsupported |
|
||||
`HYBRID_TRACKING` | `0x2400` | Unsupported |
|
||||
`FN_INVERSION` | `0x40A0` | Supported | `FnSwap`
|
||||
`NEW_FN_INVERSION` | `0x40A2` | Supported | `NewFnSwap`, `get_new_fn_inversion
|
||||
|
|
@ -97,22 +102,23 @@ Feature | ID | Status | Notes
|
|||
`GESTURE` | `0x6500` | Unsupported |
|
||||
`GESTURE_2` | `0x6501` | Partial Support | `Gesture2Gestures`, `Gesture2Params`
|
||||
`GKEY` | `0x8010` | Partial Support | `DivertGkeys`
|
||||
`MKEYS` | `0x8020` | Unsupported |
|
||||
`MR` | `0x8030` | Unsupported |
|
||||
`BRIGHTNESS_CONTROL` | `0x8040` | Unsupported |
|
||||
`REPORT_RATE` | `0x8060` | Supported | `ReportRate`
|
||||
`COLOR_LED_EFFECTS` | `0x8070` | Unsupported |
|
||||
`RGB_EFFECTS` | `0X8071` | Unsupported |
|
||||
`MKEYS` | `0x8020` | Supported | `MkeyLEDs`
|
||||
`MR` | `0x8030` | Supported | `MRKeyLED`
|
||||
`BRIGHTNESS_CONTROL` | `0x8040` | Supported | `BrightnessControl`
|
||||
`REPORT_RATE` | `0x8060` | Supported | `ReportRate`
|
||||
`EXTENDED_ADJUSTABLE_REPORT_RATE` | `0x8061` | Supported | `report_rate_extended` (sub-millisecond polling up to 8000 Hz)
|
||||
`COLOR_LED_EFFECTS` | `0x8070` | Supported | `LEDControl`, `LEDZoneSetting`
|
||||
`RGB_EFFECTS` | `0X8071` | Supported | `RGBControl`, `RGBEffectSetting`
|
||||
`PER_KEY_LIGHTING` | `0x8080` | Unsupported |
|
||||
`PER_KEY_LIGHTING_V2` | `0x8081` | Unsupported |
|
||||
`PER_KEY_LIGHTING_V2` | `0x8081` | Supported | `PerKeyLighting`
|
||||
`MODE_STATUS` | `0x8090` | Unsupported |
|
||||
`ONBOARD_PROFILES` | `0x8100` | Unsupported |
|
||||
`ONBOARD_PROFILES` | `0x8100` | Supported |
|
||||
`MOUSE_BUTTON_SPY` | `0x8110` | Unsupported |
|
||||
`LATENCY_MONITORING` | `0x8111` | Unsupported |
|
||||
`GAMING_ATTACHMENTS` | `0x8120` | Unsupported |
|
||||
`FORCE_FEEDBACK` | `0x8123` | Unsupported |
|
||||
`SIDETONE` | `0x8300` | Unsupported |
|
||||
`EQUALIZER` | `0x8310` | Unsupported |
|
||||
`SIDETONE` | `0x8300` | Supported | `Sidetone`
|
||||
`EQUALIZER` | `0x8310` | Supported | `Equalizer`
|
||||
`HEADSET_OUT` | `0x8320` | Unsupported |
|
||||
|
||||
A “read only” note means the feature is a read-only feature.
|
||||
|
|
|
|||
37
docs/i18n.md
|
|
@ -5,7 +5,7 @@ layout: page
|
|||
|
||||
# Translating Solaar
|
||||
|
||||
First, make sure you have installed the `gettext` package.
|
||||
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.
|
||||
|
||||
Here are the steps to add/update a translation (you should run all scripts from
|
||||
the source root):
|
||||
|
|
@ -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`.
|
||||
Alternatively, you can use the excellent [Poedit](https://poedit.net/) or [Lokalize](https://apps.kde.org/lokalize/).
|
||||
|
||||
4. Run `./tools/po-compile.sh`. It will bring up-to-date all the compiled
|
||||
language files, necessary at runtime.
|
||||
|
|
@ -43,16 +43,32 @@ 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]
|
||||
- Français: [Papoteur][papoteur], [David Geiger][david-geiger],
|
||||
[Damien Lallement][damsweb]
|
||||
- Italiano: [Michele Olivo][micheleolivo]
|
||||
- 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, [Niko Savola][nikosavola]
|
||||
- German: Daniel Frost
|
||||
- Greek: Vangelis Skarmoutsos
|
||||
- Indonesia: [Ferdina Kusumah][feku]
|
||||
- Italiano: [Michele Olivo][micheleolivo], Lorenzo
|
||||
- Japanese: Ryunosuke Toda
|
||||
- Norsk (Bokmål): [John Erling Blad][jeblad]
|
||||
- Polski: [Adrian Piotrowicz][nexces]
|
||||
- Portuguese-BR: [Drovetto][drovetto], [Josenivaldo Benito Jr.][jrbenito]
|
||||
- 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
|
||||
- Română: Daniel Pavel
|
||||
- Russian: [Dimitriy Ryazantcev][DJm00n]
|
||||
- Russian: [Dimitriy Ryazantcev][DJm00n], Anton Soroko
|
||||
- Serbian: [Renato Kaurić][renatoka]
|
||||
- Slovak: [Jose Riha][jose1711]
|
||||
- Svensk: [Daniel Zippert][zipperten], Emelie Snecker
|
||||
- Spanish, Castilian: Jose Luis Tirado
|
||||
- Swedish: John Erling Blad, [Daniel Zippert][zipperten], Emelie Snecker, Jonatan Nyberg
|
||||
- Turkish: Osman Karagöz
|
||||
- Ukrainian: Олександр Афанасьєв
|
||||
- Bulgarian Николай Йорданов
|
||||
|
||||
[Rongronggg9]: https://github.com/Rongronggg9
|
||||
[papoteur]: https://github.com/papoteur
|
||||
|
|
@ -66,3 +82,6 @@ Some of the languages Solaar has been translated to are listed below. A full lis
|
|||
[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
|
||||
[nikosavola]: https://github.com/nikosavola
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1,204 @@
|
|||
---
|
||||
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
|
||||
108
docs/index.md
|
|
@ -15,9 +15,11 @@ using one of the methods described below.
|
|||
|
||||
Solaar runs as a regular user process, albeit with direct access to the Linux interface
|
||||
that lets it directly communicate with the Logitech devices it manages using special
|
||||
Logitech-proprietary (HID++) commands.
|
||||
Logitech-proprietary (HID++ and Centurion) commands.
|
||||
Each Logitech device implements a different subset of these commands.
|
||||
Solaar is thus only able to make the changes to devices that devices implement.
|
||||
Solaar is thus only able to make the changes that a particular device supports.
|
||||
|
||||
Note: Support for Centurion devices is new and should be considered experimental.
|
||||
|
||||
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
|
||||
|
|
@ -46,8 +48,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 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 how 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 +77,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 some Logitech devices that connect via a USB cable or Bluetooth.
|
||||
Solaar will detect many 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])
|
||||
|
|
@ -95,7 +97,7 @@ 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/assets/solaar.svg
|
||||
[logo]: https://pwr-solaar.github.io/Solaar/img/solaar.svg
|
||||
|
||||
## Prebuilt packages
|
||||
|
||||
|
|
@ -106,12 +108,11 @@ 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 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]
|
||||
- NixOS Flake: see [Svenum/Solaar-Flake][nix flake]
|
||||
- Ubuntu/Kubuntu package in [Solaar stable ppa][ppa2]
|
||||
- NixOS Flake package in [Svenum/Solaar-Flake][nix flake]
|
||||
|
||||
Solaar is available from some other repositories
|
||||
but they are several versions behind the current version.
|
||||
but they may be 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
|
||||
|
|
@ -121,8 +122,6 @@ Solaar uses a standard system tray implementation; solaar-gnome3 is no longer re
|
|||
|
||||
[ppa4]: https://launchpad.net/~trebelnik-stefina
|
||||
[ppa2]: https://launchpad.net/~solaar-unifying/+archive/ubuntu/stable
|
||||
[ppa1]: https://launchpad.net/~solaar-unifying/+archive/ubuntu/ppa
|
||||
[ppa]: http://launchpad.net/~daniel.pavel/+archive/solaar
|
||||
[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
|
||||
|
|
@ -134,60 +133,10 @@ Solaar uses a standard system tray implementation; solaar-gnome3 is no longer re
|
|||
See [the installation page](https://pwr-solaar.github.io/Solaar/installation)
|
||||
for the step-by-step procedure for manual installation.
|
||||
|
||||
## Known Issues
|
||||
## License
|
||||
|
||||
- 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 Scroll Wheel Resolution setting 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", 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.
|
||||
|
||||
- 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
|
||||
Solaar to report incorrect battery levels.
|
||||
|
||||
- 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 implementations
|
||||
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.
|
||||
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
|
||||
themes or as non-symbolic when the theme uses symbolic icons. This is due to problems
|
||||
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
|
||||
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
|
||||
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.
|
||||
When the device changes back the first time any of these diverted keys is depressed Solaar will not
|
||||
realize that the key was newly depressed. For this reason Solaar rules that can change hosts should
|
||||
trigger on key releasing.
|
||||
This software is distributed under the terms of the
|
||||
[GNU Public License, v2](LICENSE.txt), or later.
|
||||
|
||||
## Contributing to Solaar
|
||||
|
||||
|
|
@ -201,28 +150,21 @@ 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.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
This software is distributed under the terms of the
|
||||
[GNU Public License, v2](COPYING).
|
||||
|
||||
## Thanks
|
||||
## Contributors
|
||||
|
||||
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 diggings in Logitech's HID++ protocol done by many other
|
||||
people:
|
||||
further thanks to the contributions of many other people, including:
|
||||
|
||||
- [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)
|
||||
- [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
|
||||
- [Lars-Dominik Braun](http://6xq.net/git/lars/lshidpp.git)
|
||||
- [Alexander Hofbauer](http://derhofbauer.at/blog/blog/2012/08/28/logitech-performance-mx)
|
||||
- [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
|
||||
- [Clach04](https://github.com/clach04)
|
||||
- [Peter F. Patel-Schneider](https://github.com/pfps)
|
||||
|
||||
Also, thanks to Douglas Wagner, Julien Gascard, and Peter Wu for helping with
|
||||
application testing and supporting new devices.
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -5,60 +5,82 @@ 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`.
|
||||
If you are using pipx add the `--system-site-packages` flag.
|
||||
`pip install --user solaar` or `pipx install --system-site-packages solaar`.
|
||||
|
||||
This will not install the Solaar udev rule, which you will need to install manually by copying
|
||||
`~/.local/share/solaar/udev-rules.d/42-logitech-unify-permissions.rules`
|
||||
`~/.local/lib/udev/rules.d/42-logitech-unify-permissions.rules`
|
||||
to `/etc/udev/rules.d` as root.
|
||||
|
||||
## macOS support
|
||||
## 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 hidapi library:
|
||||
After installing Solaar via pip use homebrew to install the needed libraries:
|
||||
```
|
||||
brew install hidapi
|
||||
```
|
||||
If you only want to use the CLI that's all that is needed. To use the GUI you need to also
|
||||
install GTK and its python bindings:
|
||||
```
|
||||
brew install gtk+3 pygobject3
|
||||
brew update
|
||||
brew install hidapi gtk+3 pygobject3
|
||||
```
|
||||
|
||||
# Manual installation from GitHub
|
||||
### Optional: Set up macOS launcher
|
||||
* Option A (recommended): Configure a LaunchAgent to automatically start Solaar and keep it running in the background.
|
||||
It will also automatically restart Solaar if it crashed or closed.
|
||||
```
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/pwr-Solaar/Solaar/refs/heads/master/tools/create-macos-launchagent.sh)
|
||||
```
|
||||
* Option B: Create Solaar.app launcher in /Applications.
|
||||
It can be added to Login Items to start on login, but it will not automatically recover on crashes.
|
||||
```
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/pwr-Solaar/Solaar/refs/heads/master/tools/create-macos-app.sh)
|
||||
```
|
||||
|
||||
# Installating 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`.
|
||||
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
|
||||
|
||||
If you have previously successfully installed a recent version of Solaar from a repository
|
||||
you should be able to skip this section.
|
||||
This is only relevant if you have problems with the easier methods above.
|
||||
|
||||
Solaar needs a reasonably new kernel with kernel modules `hid-logitech-dj`
|
||||
and `hid-logitech-hidpp` loaded.
|
||||
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.
|
||||
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`,
|
||||
and `python3-yaml` or `python3-pyyaml` packages installed.
|
||||
`python3-pyudev`, `python3-psutil`, `python3-xlib`, `python3-evdev`, `python3-typing-extensions`, `dbus-python`
|
||||
or `python3-dbus`, 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
|
||||
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`.
|
||||
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`.
|
||||
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
|
||||
|
|
@ -77,10 +99,11 @@ 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.
|
||||
|
||||
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`.
|
||||
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 `gitinfo` Python package is available, Solaar shows better information
|
||||
about which version of Solaar is running.
|
||||
|
|
@ -97,76 +120,23 @@ 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
|
||||
## Installing Solaar's udev rule manually
|
||||
|
||||
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 receivers and devices.
|
||||
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`.
|
||||
Let udev reload its rules by running `sudo udevadm control --reload-rules`.
|
||||
|
||||
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, as 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 Solaar 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
|
||||
# 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
|
||||
and set the LANGUAGE environment variable appropriately when running Solaar.
|
||||
|
||||
# Setting up Solaar's icons
|
||||
|
||||
Solaar uses a number of custom icons, which have to be installed in a place where GTK can access them.
|
||||
|
||||
If Solaar has never been installed, and only run from the download directory then Solaar will not be able to find the icons.
|
||||
If Solaar has only been installed for a user (e.g., via pip) then Solaar will be able to find the icons,
|
||||
but they may not show up in the system tray.
|
||||
|
||||
One solution is to install a version of Solaar on a system-wide basis.
|
||||
A more-recent version of Solaar can then be installed for a user or Solaar can be run out of the download directory.
|
||||
Another solution is to copy the Solaar custom icons from share/solaar/icons to a place they can be found by GTK,
|
||||
likely /usr/share/icons/hicolor/scalable/apps.
|
||||
|
||||
# Running Solaar at Startup
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
---
|
||||
title: Known Issues
|
||||
layout: page
|
||||
---
|
||||
|
||||
# Known Issues
|
||||
|
||||
- Some internal structures in Solaar have been updated to use more standard Python language features.
|
||||
This has caused some problems and introduced bugs are still being found.
|
||||
|
||||
- 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.
|
||||
|
||||
- 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 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
|
||||
Solaar to report incorrect battery levels.
|
||||
|
||||
- Solaar normally uses icon names for its icons, which in some system tray implementations
|
||||
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.
|
||||
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
|
||||
themes or as non-symbolic when the theme uses symbolic icons. This is due to problems
|
||||
in some system tray implementations. Changing to a different theme may help.
|
||||
The `--battery-icons=symbolic` option can be used to force symbolic icons.
|
||||
|
||||
- Solaar uses uinput to simulate input
|
||||
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
|
||||
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
|
||||
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.
|
||||
When the device changes back the first time any of these diverted keys is depressed Solaar will not
|
||||
realize that the key was newly depressed. For this reason Solaar rules that can change hosts should
|
||||
trigger on key releasing.
|
||||
115
docs/rules.md
|
|
@ -3,11 +3,10 @@ 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.
|
||||
|
||||
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.
|
||||
|
|
@ -16,16 +15,17 @@ although on GNOME desktop under Wayland, you can use those with the Solaar Gnome
|
|||
You can install it from `https://extensions.gnome.org/extension/6162/solaar-extension`.
|
||||
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.
|
||||
The easiest way to maintain write access to /dev/uinput is to use Solaar's alternative udev rule by downloading
|
||||
`https://raw.githubusercontent.com/pwr-Solaar/Solaar/master/rules.d-uinput/42-logitech-unify-permissions.rules`
|
||||
Simulated input uses uinput to simulate input so the user has to have write access to /dev/uinput.
|
||||
The easiest way to maintain write access to /dev/uinput is to use Solaar's udev rule by downloading
|
||||
`https://raw.githubusercontent.com/pwr-Solaar/Solaar/master/rules.d/42-logitech-unify-permissions.rules`
|
||||
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.
|
||||
|
||||
Logitech devices that use HID++ version 2.0 or greater produce feature-based
|
||||
notifications that Solaar can process using a simple rule language. For
|
||||
## 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
|
||||
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 +34,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,50 +42,60 @@ 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 sub-component of a component is an action. A
|
||||
rule is false if its last evaluated component evaluates to a false value.
|
||||
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.
|
||||
|
||||
## 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 sub-component of a component is an action.
|
||||
last evaluated subcomponent 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 which are evaluated the same
|
||||
as rules.
|
||||
|
||||
### Feature
|
||||
`Feature` conditions are 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
|
||||
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`
|
||||
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`
|
||||
notification for the period that the key or button is depressed.
|
||||
|
|
@ -95,6 +105,7 @@ 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.
|
||||
|
|
@ -104,24 +115,32 @@ The condition `Smart Shift` -> `Mouse Down` -> `Back Button` would match pressin
|
|||
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
|
||||
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.
|
||||
Some older devices do not have a useful serial number or unit ID and so cannot be tested for by these conditions.
|
||||
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.
|
||||
|
||||
`Host' conditions are true if the computers hostname starts with the condition's argument.
|
||||
### 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,
|
||||
|
|
@ -147,16 +166,17 @@ For settings that use gestures as an argument the internal name of the gesture i
|
|||
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 anded
|
||||
Three-element `TestBytes` conditions are true if the selected bytes bit-wise AND
|
||||
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
|
||||
|
|
@ -184,12 +204,15 @@ 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
|
||||
|
||||
### 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 "Control+a",
|
||||
a list of X11 key symbols, such as "a" or "CTRL + 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.
|
||||
|
|
@ -215,38 +238,43 @@ 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.
|
||||
|
||||
### Execute
|
||||
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.
|
||||
Processing of the rest of the rule continues immediately.
|
||||
|
||||
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
|
||||
## 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
|
||||
`Brightness Down` key press notifications into `XF86_MonBrightnessDown` key taps
|
||||
and `Brightness Up` key press notifications into `XF86_MonBrightnessUp` key taps.
|
||||
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.
|
||||
|
||||
## Example Solaar Rule File
|
||||
|
||||
Solaar reads rules from a YAML configuration file (normally `~/.config/solaar/rules.yaml`).
|
||||
This file contains zero or more documents, each a rule.
|
||||
|
|
@ -294,10 +322,11 @@ 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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 141 KiB |
|
|
@ -0,0 +1,40 @@
|
|||
---
|
||||
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.
|
||||
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
Clicking on “Quit” in the Solaar menu terminates the program.
|
||||
Clicking on “About Solaar” pops up a window with further information about Solaar.
|
||||
|
|
@ -32,10 +32,13 @@ 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
|
||||
|
||||
|
|
@ -61,7 +64,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.
|
||||
|
||||

|
||||

|
||||
|
||||
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
|
||||
|
|
@ -90,26 +93,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.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
Device settings now have a clickable icon that determines whether the
|
||||
setting can be changed and whether the setting is ignored.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||

|
||||
|
||||
#### Remapping key and button actions
|
||||
|
||||
|
|
@ -124,7 +127,7 @@ action is always the one shown first in the list. As with all settings,
|
|||
Solaar will remember past action settings and restore them on the device
|
||||
from then on.
|
||||
|
||||

|
||||

|
||||
|
||||
The names of the keys, buttons, and actions are mostly taken from Logitech
|
||||
documentation and may not be completely obvious.
|
||||
|
|
@ -133,9 +136,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.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 96 B |
|
Before Width: | Height: | Size: 432 B |
|
Before Width: | Height: | Size: 230 B |
|
Before Width: | Height: | Size: 14 KiB |
|
|
@ -1,49 +0,0 @@
|
|||
# -*- 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."""
|
||||
|
||||
import platform as _platform
|
||||
|
||||
if _platform.system() in ('Darwin', 'Windows'):
|
||||
from hidapi.hidapi import close # noqa: F401
|
||||
from hidapi.hidapi import enumerate # noqa: F401
|
||||
from hidapi.hidapi import find_paired_node # noqa: F401
|
||||
from hidapi.hidapi import find_paired_node_wpid # noqa: F401
|
||||
from hidapi.hidapi import get_manufacturer # noqa: F401
|
||||
from hidapi.hidapi import get_product # noqa: F401
|
||||
from hidapi.hidapi import get_serial # noqa: F401
|
||||
from hidapi.hidapi import monitor_glib # noqa: F401
|
||||
from hidapi.hidapi import open # noqa: F401
|
||||
from hidapi.hidapi import open_path # noqa: F401
|
||||
from hidapi.hidapi import read # noqa: F401
|
||||
from hidapi.hidapi import write # noqa: F401
|
||||
else:
|
||||
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'
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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
|
||||
centurion: bool = False
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -24,47 +22,49 @@ 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 platform as _platform
|
||||
import logging
|
||||
import platform
|
||||
import typing
|
||||
|
||||
from collections import namedtuple
|
||||
from logging import INFO as _INFO
|
||||
from logging import getLogger
|
||||
from threading import Thread
|
||||
from time import sleep
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
native_implementation = 'hidapi'
|
||||
from hidapi.common import DeviceInfo
|
||||
|
||||
# Device info as expected by Solaar
|
||||
DeviceInfo = namedtuple(
|
||||
'DeviceInfo', [
|
||||
'path',
|
||||
'bus_id',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'interface',
|
||||
'driver',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'serial',
|
||||
'release',
|
||||
'isDevice',
|
||||
'hidpp_short',
|
||||
'hidpp_long',
|
||||
]
|
||||
)
|
||||
del namedtuple
|
||||
LOGITECH_VENDOR_ID = 0x046D
|
||||
|
||||
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'
|
||||
"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:
|
||||
|
|
@ -74,15 +74,15 @@ for lib in _library_paths:
|
|||
except OSError:
|
||||
pass
|
||||
else:
|
||||
raise ImportError(f"Unable to load hdiapi library, tried: {' '.join(_library_paths)}")
|
||||
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),
|
||||
("major", ctypes.c_int),
|
||||
("minor", ctypes.c_int),
|
||||
("patch", ctypes.c_int),
|
||||
]
|
||||
|
||||
|
||||
|
|
@ -93,28 +93,27 @@ _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'}
|
||||
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)),
|
||||
("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.append(("bus_type", ctypes.c_int))
|
||||
_cDeviceInfo._fields_ = _cDeviceInfo_fields
|
||||
|
||||
# Set up hidapi functions
|
||||
|
|
@ -164,7 +163,7 @@ 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':
|
||||
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)
|
||||
|
|
@ -175,7 +174,7 @@ class HIDError(Exception):
|
|||
|
||||
|
||||
def _enumerate_devices():
|
||||
""" Returns all HID devices which are potentially useful to us """
|
||||
"""Returns all HID devices which are potentially useful to us"""
|
||||
devices = []
|
||||
c_devices = _hidapi.hid_enumerate(0, 0)
|
||||
p = c_devices
|
||||
|
|
@ -184,19 +183,12 @@ def _enumerate_devices():
|
|||
p = p.contents.next
|
||||
_hidapi.hid_free_enumeration(c_devices)
|
||||
|
||||
keyboard_or_mouse = {d['path'] for d in devices if d['usage_page'] == 1 and d['usage'] in (6, 2)}
|
||||
unique_devices = {}
|
||||
for device in devices:
|
||||
# On macOS we cannot access keyboard or mouse devices without special permissions. Since
|
||||
# we don't need them anyway we remove them so opening them doesn't cause errors later.
|
||||
if device['path'] in keyboard_or_mouse:
|
||||
# print(f"Ignoring keyboard or mouse device: {device}")
|
||||
continue
|
||||
|
||||
# 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
|
||||
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]))
|
||||
|
|
@ -205,10 +197,10 @@ def _enumerate_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)
|
||||
|
||||
|
|
@ -221,118 +213,173 @@ class _DeviceMonitor(Thread):
|
|||
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('remove', device)
|
||||
self.device_callback(ACTION_REMOVE, device)
|
||||
for key, device in current_devices.items():
|
||||
if key not in self.prev_devices:
|
||||
self.device_callback('add', device)
|
||||
self.device_callback(ACTION_ADD, device)
|
||||
self.prev_devices = current_devices
|
||||
sleep(self.polling_delay)
|
||||
|
||||
|
||||
# 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):
|
||||
vid = device['vendor_id']
|
||||
pid = device['product_id']
|
||||
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:
|
||||
if device.get("bus_type") == 0x01:
|
||||
bus_id = 0x03 # USB
|
||||
elif device.get('bus_type') == 0x02:
|
||||
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
|
||||
|
||||
# Skip Logitech webcams to prevent them from locking up during hidpp checks
|
||||
# (product IDs range for webcams from docs/usb.ids.txt)
|
||||
if vid == LOGITECH_VENDOR_ID and 0x0800 <= pid <= 0x09FF:
|
||||
logger.info(f"Skipping Logitech webcam {device['path']} ({bus_id}/{vid:04X}/{pid:04X})")
|
||||
return None
|
||||
|
||||
# Check for hidpp support
|
||||
device['hidpp_short'] = False
|
||||
device['hidpp_long'] = False
|
||||
device["hidpp_short"] = False
|
||||
device["hidpp_long"] = False
|
||||
device_handle = None
|
||||
try:
|
||||
device_handle = open_path(device['path'])
|
||||
report = get_input_report(device_handle, 0x10, 32)
|
||||
|
||||
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
|
||||
report = get_input_report(device_handle, 0x11, 32)
|
||||
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
|
||||
except HIDError as e: # noqa: F841
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info(f"Error opening device {device['path']} ({bus_id}/{vid:04X}/{pid:04X}) for hidpp check: {e}")
|
||||
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)
|
||||
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info(
|
||||
'Found device BID %s VID %04X PID %04X HID++ %s %s', bus_id, vid, pid, device['hidpp_short'], device['hidpp_long']
|
||||
)
|
||||
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']:
|
||||
if not device["hidpp_short"] and not device["hidpp_long"]:
|
||||
return None
|
||||
|
||||
filter = filterfn(bus_id, vid, pid, device['hidpp_short'], device['hidpp_long'])
|
||||
if not filter:
|
||||
filtered_result = filter_func(bus_id, vid, pid, device["hidpp_short"], device["hidpp_long"])
|
||||
if not filtered_result:
|
||||
return
|
||||
isDevice = filter.get('isDevice')
|
||||
is_device = filtered_result.get("isDevice")
|
||||
|
||||
if action == 'add':
|
||||
if action == ACTION_ADD:
|
||||
d_info = DeviceInfo(
|
||||
path=device['path'].decode(),
|
||||
path=device["path"].decode(),
|
||||
bus_id=bus_id,
|
||||
vendor_id=f'{vid:04X}',
|
||||
product_id=f'{pid:04X}',
|
||||
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=isDevice,
|
||||
hidpp_short=device['hidpp_short'],
|
||||
hidpp_long=device['hidpp_long'],
|
||||
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 == 'remove':
|
||||
elif action == ACTION_REMOVE:
|
||||
d_info = DeviceInfo(
|
||||
path=device['path'].decode(),
|
||||
path=device["path"].decode(),
|
||||
bus_id=None,
|
||||
vendor_id=f'{vid:04X}',
|
||||
product_id=f'{pid:04X}',
|
||||
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=isDevice,
|
||||
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, index, timeout):
|
||||
|
||||
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, index):
|
||||
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(callback, filterfn):
|
||||
from gi.repository import GLib
|
||||
def monitor_glib(
|
||||
glib: GLib,
|
||||
callback: Callable,
|
||||
filter_func: Callable[[int, int, int, bool, bool], dict[str, Any]],
|
||||
) -> None:
|
||||
"""Monitor GLib.
|
||||
|
||||
def device_callback(action, device):
|
||||
# print(f"device_callback({action}): {device}")
|
||||
if action == 'add':
|
||||
d_info = _match(action, device, filterfn)
|
||||
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 == 'remove':
|
||||
glib.idle_add(callback, action, d_info)
|
||||
elif action == ACTION_REMOVE:
|
||||
# Removed devices will be detected by Solaar directly
|
||||
pass
|
||||
|
||||
|
|
@ -340,7 +387,7 @@ def monitor_glib(callback, filterfn):
|
|||
monitor.start()
|
||||
|
||||
|
||||
def enumerate(filterfn):
|
||||
def enumerate(filter_func) -> DeviceInfo:
|
||||
"""Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
|
|
@ -349,7 +396,7 @@ def enumerate(filterfn):
|
|||
:returns: a list of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
for device in _enumerate_devices():
|
||||
d_info = _match('add', device, filterfn)
|
||||
d_info = _match(ACTION_ADD, device, filter_func)
|
||||
if d_info:
|
||||
yield d_info
|
||||
|
||||
|
|
@ -370,7 +417,7 @@ def open(vendor_id, product_id, serial=None):
|
|||
return device_handle
|
||||
|
||||
|
||||
def open_path(device_path):
|
||||
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().
|
||||
|
|
@ -386,7 +433,7 @@ def open_path(device_path):
|
|||
return device_handle
|
||||
|
||||
|
||||
def close(device_handle):
|
||||
def close(device_handle) -> None:
|
||||
"""Close a HID device.
|
||||
|
||||
:param device_handle: a device handle returned by open() or open_path().
|
||||
|
|
@ -395,7 +442,7 @@ def close(device_handle):
|
|||
_hidapi.hid_close(device_handle)
|
||||
|
||||
|
||||
def write(device_handle, data):
|
||||
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().
|
||||
|
|
@ -452,15 +499,14 @@ def read(device_handle, bytes_count, timeout_ms=None):
|
|||
|
||||
if bytes_read < 0:
|
||||
raise HIDError(_hidapi.hid_error(device_handle))
|
||||
return None
|
||||
|
||||
return data.raw[:bytes_read]
|
||||
|
||||
|
||||
def get_input_report(device_handle, report_id, size):
|
||||
def _get_input_report(device_handle, report_id, size):
|
||||
assert device_handle
|
||||
data = ctypes.create_string_buffer(size)
|
||||
data[0] = bytearray((report_id, ))
|
||||
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))
|
||||
|
|
@ -472,7 +518,7 @@ def _readstring(device_handle, func, max_length=255):
|
|||
buf = ctypes.create_unicode_buffer(max_length)
|
||||
ret = func(device_handle, buf, max_length)
|
||||
if ret < 0:
|
||||
raise HIDError('Error reading device property')
|
||||
raise HIDError("Error reading device property")
|
||||
return buf.value
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -16,47 +14,46 @@
|
|||
## 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, unhexlify
|
||||
from select import select as _select
|
||||
from binascii import hexlify
|
||||
from binascii import unhexlify
|
||||
from select import select
|
||||
from threading import Lock
|
||||
from threading import Thread
|
||||
|
||||
import hidapi as _hid
|
||||
if platform.system() == "Linux":
|
||||
import hidapi.udev_impl as hidapi
|
||||
else:
|
||||
import hidapi.hidapi_impl as hidapi
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
try:
|
||||
read_packet = raw_input
|
||||
except NameError:
|
||||
# Python 3 equivalent of raw_input
|
||||
read_packet = input
|
||||
LOGITECH_VENDOR_ID = 0x046D
|
||||
|
||||
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 = marker + ' ' + data
|
||||
s = f"{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
|
||||
|
|
@ -65,18 +62,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
|
||||
|
|
@ -85,107 +82,103 @@ 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 = _hid.read(handle, 128, timeout)
|
||||
reply = hidapi.read(handle, 128, timeout)
|
||||
except OSError as e:
|
||||
_error('Read failed, aborting: ' + str(e), True)
|
||||
_error(f"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('Invalid input: ' + str(e))
|
||||
_error(f"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\x00\x01\x02\x03\x04\x05\x06\x07":
|
||||
_error("Invalid HID++ request: second byte must be 0xFF or one of 0x00..0x07")
|
||||
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 == 0x046d:
|
||||
return {'vid': 0x046d}
|
||||
if vid == LOGITECH_VENDOR_ID:
|
||||
return {"vid": vid}
|
||||
|
||||
device = args.device
|
||||
if args.hidpp and not device:
|
||||
for d in _hid.enumerate(matchfn):
|
||||
if d.driver == 'logitech-djreceiver':
|
||||
device = args.path
|
||||
d = None
|
||||
if not device:
|
||||
for d in hidapi.enumerate(matchfn):
|
||||
if (d.hidpp_short or d.hidpp_long) and (args.id is None or args.id.lower() == d.product_id.lower()):
|
||||
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 = _hid.open_path(device)
|
||||
handle = hidapi.open_path(device)
|
||||
if not handle:
|
||||
sys.exit('!! Failed to open %s, aborting.' % device)
|
||||
sys.exit(f"!! Failed to open {device}, aborting.")
|
||||
print(
|
||||
'.. Opened handle %r, vendor %r product %r serial %r.' %
|
||||
(handle, _hid.get_manufacturer(handle), _hid.get_product(handle), _hid.get_serial(handle))
|
||||
".. Opened device %r, vendor %r product %r serial %r."
|
||||
% (
|
||||
device,
|
||||
hidapi.get_manufacturer(handle) or d.vendor_id if d else None,
|
||||
hidapi.get_product(handle) or d.product_id if d else None,
|
||||
hidapi.get_serial(handle),
|
||||
)
|
||||
)
|
||||
if args.hidpp:
|
||||
if _hid.get_manufacturer(handle) is not None and _hid.get_manufacturer(handle) != b'Logitech':
|
||||
sys.exit('!! Only Logitech devices support the HID++ protocol.')
|
||||
print('.. HID++ validation enabled.')
|
||||
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.")
|
||||
else:
|
||||
if (_hid.get_manufacturer(handle) == b'Logitech' and b'Receiver' in _hid.get_product(handle)):
|
||||
if hidapi.get_manufacturer(handle) == b"Logitech" and b"Receiver" in hidapi.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(
|
||||
'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'
|
||||
)
|
||||
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("command", nargs="?", help="command to send (otherwise get commands from input)")
|
||||
group = arg_parser.add_mutually_exclusive_group()
|
||||
group.add_argument("-p", "--path", help="HID raw device to connect to (/dev/hidrawX); ")
|
||||
group.add_argument("-i", "--id", help="product ID of device to connect to (XXXX)")
|
||||
return arg_parser.parse_args()
|
||||
|
||||
|
||||
|
|
@ -193,13 +186,22 @@ def main():
|
|||
args = _parse_arguments()
|
||||
handle = _open(args)
|
||||
|
||||
if interactive:
|
||||
print('.. Press ^C/^D to exit, or type hex bytes to write to the device.')
|
||||
if args.command: # send a command
|
||||
data = _validate_input(args.command, args.hidpp)
|
||||
if data:
|
||||
hidapi.write(handle, data)
|
||||
reply = hidapi.read(handle, 128, 1)
|
||||
if reply:
|
||||
hexs = strhex(reply)
|
||||
s = "[%s %s %s %s] %s" % (hexs[0:2], hexs[2:4], hexs[4:8], hexs[8:], repr(data))
|
||||
print(s)
|
||||
exit()
|
||||
|
||||
if interactive:
|
||||
print(".. Press ^C/^D to exit, or type hex bytes to write to the device.")
|
||||
|
||||
import readline
|
||||
if args.history is None:
|
||||
import os.path
|
||||
args.history = os.path.join(os.path.expanduser('~'), '.hidconsole-history')
|
||||
args.history = os.path.join(os.path.expanduser("~"), ".hidconsole-history")
|
||||
try:
|
||||
readline.read_history_file(args.history)
|
||||
except Exception:
|
||||
|
|
@ -207,18 +209,17 @@ def main():
|
|||
pass
|
||||
|
||||
try:
|
||||
from threading import Thread
|
||||
t = Thread(target=_continuous_read, args=(handle, ))
|
||||
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 = read_packet(prompt)
|
||||
line = line.strip().replace(' ', '')
|
||||
line = input(prompt)
|
||||
line = line.strip().replace(" ", "")
|
||||
# print ("line", line)
|
||||
if not line:
|
||||
continue
|
||||
|
|
@ -227,12 +228,12 @@ def main():
|
|||
if data is None:
|
||||
continue
|
||||
|
||||
_print('<<', data)
|
||||
_hid.write(handle, data)
|
||||
_print("<<", data)
|
||||
hidapi.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:
|
||||
|
|
@ -240,16 +241,15 @@ def main():
|
|||
time.sleep(0.700)
|
||||
except EOFError:
|
||||
if interactive:
|
||||
print('')
|
||||
print("")
|
||||
else:
|
||||
time.sleep(1)
|
||||
|
||||
finally:
|
||||
print('.. Closing handle %r' % handle)
|
||||
_hid.close(handle)
|
||||
hidapi.close(handle)
|
||||
if interactive:
|
||||
readline.write_history_file(args.history)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -15,6 +13,7 @@
|
|||
## 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
|
||||
|
|
@ -24,47 +23,37 @@ The docstrings are mostly copied from the hidapi API header, with changes where
|
|||
necessary.
|
||||
"""
|
||||
|
||||
import errno as _errno
|
||||
import os as _os
|
||||
import warnings as _warnings
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
|
||||
# the tuple object we'll expose when enumerating devices
|
||||
from collections import namedtuple
|
||||
from logging import INFO as _INFO
|
||||
from logging import getLogger
|
||||
from select import select as _select
|
||||
from select import select
|
||||
from time import sleep
|
||||
from time import time as _timestamp
|
||||
from time import time
|
||||
from typing import Callable
|
||||
|
||||
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
|
||||
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__)
|
||||
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
native_implementation = 'udev'
|
||||
fileopen = open
|
||||
|
||||
DeviceInfo = namedtuple(
|
||||
'DeviceInfo', [
|
||||
'path',
|
||||
'bus_id',
|
||||
'vendor_id',
|
||||
'product_id',
|
||||
'interface',
|
||||
'driver',
|
||||
'manufacturer',
|
||||
'product',
|
||||
'serial',
|
||||
'release',
|
||||
'isDevice',
|
||||
'hidpp_short',
|
||||
'hidpp_long',
|
||||
]
|
||||
)
|
||||
del namedtuple
|
||||
ACTION_ADD = "add"
|
||||
ACTION_REMOVE = "remove"
|
||||
|
||||
#
|
||||
# exposed API
|
||||
|
|
@ -90,69 +79,82 @@ def exit():
|
|||
return True
|
||||
|
||||
|
||||
# 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
|
||||
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
|
||||
return
|
||||
hid_id = hid_device.get('HID_ID')
|
||||
hid_id = hid_device.properties.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:
|
||||
bid, vid, pid = hid_id.split(":")
|
||||
hid_hid_device = hid_device.find_parent("hid")
|
||||
if hid_hid_device is not None:
|
||||
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 as _ReportDescriptor
|
||||
from hid_parser import ReportDescriptor
|
||||
|
||||
# from hid_parser import Usage as _Usage
|
||||
hidpp_short = hidpp_long = False
|
||||
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 = hidpp_long = centurion = 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())
|
||||
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))
|
||||
# and _Usage(0xFF00, 0x0002) in rd.get_input_items(0x11)[0].usages # be more permissive
|
||||
if not hidpp_short and not hidpp_long:
|
||||
# Centurion transport: report ID 0x51, 63-byte reports (usage page 0xFFA0)
|
||||
centurion = (
|
||||
0x51 in rd.input_report_ids and 63 * 8 == int(rd.get_input_report_size(0x51)) and 0x51 in rd.output_report_ids
|
||||
)
|
||||
if not hidpp_short and not hidpp_long and not centurion:
|
||||
return
|
||||
except Exception as e: # if can't process report descriptor fall back to old scheme
|
||||
hidpp_short = hidpp_long = None
|
||||
_log.warn('Report Descriptor not processed for BID %s VID %s PID %s: %s', bid, vid, pid, e)
|
||||
hidpp_short = None
|
||||
hidpp_long = None
|
||||
centurion = False
|
||||
logger.info(
|
||||
"Report Descriptor not processed for DEVICE %s BID %s VID %s PID %s: %s",
|
||||
device.device_node,
|
||||
bid,
|
||||
vid,
|
||||
pid,
|
||||
e,
|
||||
)
|
||||
|
||||
filter = filterfn(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short, hidpp_long)
|
||||
if not filter:
|
||||
filtered_result = filter_func(int(bid, 16), int(vid, 16), int(pid, 16), hidpp_short or centurion, hidpp_long or centurion)
|
||||
if not filtered_result:
|
||||
return
|
||||
hid_driver = filter.get('hid_driver')
|
||||
interface_number = filter.get('usb_interface')
|
||||
isDevice = filter.get('isDevice')
|
||||
interface_number = filtered_result.get("usb_interface")
|
||||
isDevice = filtered_result.get("isDevice")
|
||||
|
||||
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')
|
||||
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")
|
||||
# print('*** usb interface', action, device, 'usb_interface:', intf_device, 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
|
||||
)
|
||||
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 not (hidpp_short or hidpp_long or interface_number is None or interface_number == usb_interface):
|
||||
return
|
||||
attrs = intf_device.attributes if intf_device else None
|
||||
attrs = intf_device.attributes if intf_device is not None else None
|
||||
|
||||
d_info = DeviceInfo(
|
||||
path=device.device_node,
|
||||
|
|
@ -161,19 +163,18 @@ def _match(action, device, filterfn):
|
|||
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.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.properties.get("HID_UNIQ"),
|
||||
release=attrs.get("bcdDevice") if attrs else None,
|
||||
isDevice=isDevice,
|
||||
hidpp_short=hidpp_short,
|
||||
hidpp_long=hidpp_long,
|
||||
centurion=centurion if centurion else False,
|
||||
)
|
||||
return d_info
|
||||
|
||||
elif action == 'remove':
|
||||
# print (dict(device), dict(usb_device))
|
||||
|
||||
elif action == ACTION_REMOVE:
|
||||
d_info = DeviceInfo(
|
||||
path=device.device_node,
|
||||
bus_id=None,
|
||||
|
|
@ -192,41 +193,41 @@ def _match(action, device, filterfn):
|
|||
return d_info
|
||||
|
||||
|
||||
def find_paired_node(receiver_path, index, timeout):
|
||||
def find_paired_node(receiver_path: str, index: int, timeout: int):
|
||||
"""Find the node of a device paired with a receiver"""
|
||||
context = _Context()
|
||||
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS')
|
||||
context = pyudev.Context()
|
||||
receiver_phys = pyudev.Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
|
||||
|
||||
if not receiver_phys:
|
||||
return None
|
||||
|
||||
phys = f'{receiver_phys}:{index}'
|
||||
timeout += _timestamp()
|
||||
delta = _timestamp()
|
||||
phys = f"{receiver_phys}:{index}" # noqa: E231
|
||||
timeout += time()
|
||||
delta = time()
|
||||
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 = _timestamp()
|
||||
delta = time()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_paired_node_wpid(receiver_path, index):
|
||||
def find_paired_node_wpid(receiver_path: str, index: int):
|
||||
"""Find the node of a device paired with a receiver, get wpid from udev"""
|
||||
context = _Context()
|
||||
receiver_phys = _Devices.from_device_file(context, receiver_path).find_parent('hid').get('HID_PHYS')
|
||||
context = pyudev.Context()
|
||||
receiver_phys = pyudev.Devices.from_device_file(context, receiver_path).find_parent("hid").get("HID_PHYS")
|
||||
|
||||
if not receiver_phys:
|
||||
return None
|
||||
|
||||
phys = f'{receiver_phys}:{index}'
|
||||
for dev in context.list_devices(subsystem='hidraw'):
|
||||
dev_phys = dev.find_parent('hid').get('HID_PHYS')
|
||||
phys = f"{receiver_phys}:{index}" # noqa: E231
|
||||
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
|
||||
|
|
@ -234,55 +235,48 @@ def find_paired_node_wpid(receiver_path, index):
|
|||
return None
|
||||
|
||||
|
||||
def monitor_glib(callback, filterfn):
|
||||
from gi.repository import GLib
|
||||
def monitor_glib(glib: GLib, callback: Callable, filter_func: Callable):
|
||||
"""Monitor GLib.
|
||||
|
||||
c = _Context()
|
||||
Parameters
|
||||
----------
|
||||
glib
|
||||
GLib instance.
|
||||
"""
|
||||
c = pyudev.Context()
|
||||
m = pyudev.Monitor.from_netlink(c)
|
||||
m.filter_by(subsystem="hidraw")
|
||||
|
||||
# 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:
|
||||
def _process_udev_event(monitor, condition, cb, filter_func):
|
||||
if condition == glib.IO_IN:
|
||||
event = monitor.receive_device()
|
||||
if event:
|
||||
action, device = event
|
||||
# print ("***", action, device)
|
||||
if action == 'add':
|
||||
d_info = _match(action, device, filterfn)
|
||||
if action == ACTION_ADD:
|
||||
d_info = _match(action, device, filter_func)
|
||||
if d_info:
|
||||
GLib.idle_add(cb, action, d_info)
|
||||
elif action == 'remove':
|
||||
glib.idle_add(cb, action, d_info)
|
||||
elif action == 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, filterfn)
|
||||
# print ("did io_add_watch_full")
|
||||
glib.io_add_watch_full(m, glib.PRIORITY_LOW, glib.IO_IN, _process_udev_event, callback, filter_func)
|
||||
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, filterfn)
|
||||
# print ("did io_add_watch with priority")
|
||||
glib.io_add_watch(m, glib.PRIORITY_LOW, glib.IO_IN, _process_udev_event, callback, filter_func)
|
||||
except Exception:
|
||||
GLib.io_add_watch(m, GLib.IO_IN, _process_udev_event, callback, filterfn)
|
||||
# print ("did io_add_watch")
|
||||
glib.io_add_watch(m, glib.IO_IN, _process_udev_event, callback, filter_func)
|
||||
|
||||
logger.debug("Starting dbus monitoring")
|
||||
m.start()
|
||||
|
||||
|
||||
def enumerate(filterfn):
|
||||
def enumerate(filter_func: typing.Callable[[int, int, int, bool, bool], dict[str, typing.Any]]):
|
||||
"""Enumerate the HID Devices.
|
||||
|
||||
List all the HID devices attached to the system, optionally filtering by
|
||||
|
|
@ -291,8 +285,9 @@ def enumerate(filterfn):
|
|||
:returns: a list of matching ``DeviceInfo`` tuples.
|
||||
"""
|
||||
|
||||
for dev in _Context().list_devices(subsystem='hidraw'):
|
||||
dev_info = _match('add', dev, filterfn)
|
||||
logger.debug("Starting dbus enumeration")
|
||||
for dev in pyudev.Context().list_devices(subsystem="hidraw"):
|
||||
dev_info = _match(ACTION_ADD, dev, filter_func)
|
||||
if dev_info:
|
||||
yield dev_info
|
||||
|
||||
|
|
@ -321,29 +316,29 @@ def open_path(device_path):
|
|||
:returns: an opaque device handle, or ``None``.
|
||||
"""
|
||||
assert device_path
|
||||
assert device_path.startswith('/dev/hidraw')
|
||||
assert device_path.startswith("/dev/hidraw")
|
||||
|
||||
_log.info('OPEN PATH %s', device_path)
|
||||
logger.info("OPEN PATH %s", device_path)
|
||||
retrycount = 0
|
||||
while (retrycount < 3):
|
||||
while retrycount < 3:
|
||||
retrycount += 1
|
||||
try:
|
||||
return _os.open(device_path, _os.O_RDWR | _os.O_SYNC)
|
||||
return os.open(device_path, os.O_RDWR | os.O_SYNC)
|
||||
except OSError as e:
|
||||
_log.info('OPEN PATH FAILED %s ERROR %s %s', device_path, e.errno, e)
|
||||
if e.errno == _errno.EACCES:
|
||||
logger.info("OPEN PATH FAILED %s ERROR %s %s", device_path, e.errno, e)
|
||||
if e.errno == errno.EACCES:
|
||||
sleep(0.1)
|
||||
else:
|
||||
raise
|
||||
raise e
|
||||
|
||||
|
||||
def close(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
|
||||
_os.close(device_handle)
|
||||
os.close(device_handle)
|
||||
|
||||
|
||||
def write(device_handle, data):
|
||||
|
|
@ -372,17 +367,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, 'written %d bytes out of expected %d' % (bytes_written, len(data)))
|
||||
raise OSError(errno.EIO, f"written {int(bytes_written)} bytes out of expected {len(data)}")
|
||||
|
||||
|
||||
def read(device_handle, bytes_count, timeout_ms=-1):
|
||||
|
|
@ -403,26 +398,28 @@ 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, 'exception on file descriptor %d' % device_handle)
|
||||
raise OSError(errno.EIO, f"exception on file descriptor {int(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))
|
||||
if not data: # empty read when select() said readable means EOF (device removed)
|
||||
raise OSError(errno.EIO, f"device disconnected on file descriptor {int(device_handle)}")
|
||||
return data
|
||||
else:
|
||||
return b''
|
||||
return b""
|
||||
|
||||
|
||||
_DEVICE_STRINGS = {
|
||||
0: 'manufacturer',
|
||||
1: 'product',
|
||||
2: 'serial',
|
||||
0: "manufacturer",
|
||||
1: "product",
|
||||
2: "serial",
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -467,22 +464,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 = _Device.from_device_number(_Context(), 'char', stat.st_rdev)
|
||||
except (DeviceNotFoundError, ValueError):
|
||||
dev = pyudev.Devices.from_device_number(pyudev.Context(), "char", stat.st_rdev)
|
||||
except (pyudev.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
|
||||
|
|
@ -1,43 +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://github.com/freedesktop/xorg-proto-x11proto.git'
|
||||
xx = 'https://gitlab.freedesktop.org/xorg/proto/xorgproto/-/tree/master/include/X11/'
|
||||
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://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+))?"
|
||||
|
||||
|
||||
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)
|
||||
# text = Path(temp, 'keysymdef.h').read_text()
|
||||
text = Path(temp, 'include/X11/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, 'keysymdef.h').read_text()
|
||||
text = Path(temp, 'include/X11/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
|
||||
run(["git", "clone", repo, "."], cwd=temp)
|
||||
|
||||
with open('keysymdef.py', 'w') as f:
|
||||
f.write('# flake8: noqa\nkeysymdef = \\\n')
|
||||
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")
|
||||
pprint(keysymdef, f)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -15,35 +13,12 @@
|
|||
## 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 connected through a Logitech Universal
|
||||
Receiver (UR).
|
||||
"""Low-level interface for devices using Logitech HID++ protocol.
|
||||
|
||||
Uses the HID api exposed through hidapi.py, a Python thin layer over a native
|
||||
Uses the HID api exposed through hidapi_impl.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
|
||||
|
||||
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'
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -16,38 +14,80 @@
|
|||
## 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 used by the API proper.
|
||||
# Unlikely to be used directly unless you're expanding the API.
|
||||
"""Base low-level functions as API for upper layers."""
|
||||
|
||||
import threading as _threading
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import logging
|
||||
import platform
|
||||
import struct
|
||||
import threading
|
||||
import typing
|
||||
|
||||
from collections import namedtuple
|
||||
from contextlib import contextmanager
|
||||
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 random import getrandbits
|
||||
from time import time
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
|
||||
import hidapi as _hid
|
||||
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
|
||||
|
||||
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
|
||||
if typing.TYPE_CHECKING:
|
||||
import gi
|
||||
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
from hidapi.common import DeviceInfo
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
gi.require_version("Gdk", "3.0")
|
||||
from gi.repository import GLib # NOQA: E402
|
||||
|
||||
_SHORT_MESSAGE_SIZE = 7
|
||||
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
|
||||
_LONG_MESSAGE_SIZE = 20
|
||||
_MEDIUM_MESSAGE_SIZE = 15
|
||||
_MAX_READ_SIZE = 32
|
||||
|
|
@ -56,13 +96,21 @@ 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
|
||||
}
|
||||
# Centurion transport (used by PRO X 2 LIGHTSPEED headset and similar)
|
||||
# Uses report ID 0x51 on usage page 0xFFA0, 64-byte frames.
|
||||
# Wire format (CPL): [0x51, cpl_length, flags=0x00, feat_idx, func_sw, params..., pad]
|
||||
# cpl_length = number of bytes from flags to end of meaningful data (includes flags byte).
|
||||
# The device_index byte from standard HID++ is NOT present in Centurion framing.
|
||||
CENTURION_REPORT_ID = 0x51
|
||||
CENTURION_FRAME_SIZE = 64 # 1 byte report ID + 63 bytes payload
|
||||
_CENTURION_MSG_SIZE = 63 # max reconstructed message size after unwrapping (2 + 61 payload bytes)
|
||||
|
||||
# Set of handles that use Centurion framing
|
||||
_centurion_handles: set[int] = set()
|
||||
# Raw Centurion protocol version (major, minor) by handle, from ping response
|
||||
_centurion_protocol_versions: dict[int, tuple[int, int]] = {}
|
||||
|
||||
|
||||
"""Default timeout on read (in seconds)."""
|
||||
DEFAULT_TIMEOUT = 4
|
||||
# the receiver itself should reply very fast, within 500ms
|
||||
|
|
@ -72,84 +120,156 @@ _DEVICE_REQUEST_TIMEOUT = DEFAULT_TIMEOUT
|
|||
# when pinging, be extra patient (no longer)
|
||||
_PING_TIMEOUT = DEFAULT_TIMEOUT
|
||||
|
||||
#
|
||||
# Exceptions that may be raised by this API.
|
||||
#
|
||||
hidapi = typing.cast(HIDProtocol, hidapi)
|
||||
|
||||
request_lock = threading.Lock() # serialize all requests
|
||||
handles_lock = {}
|
||||
|
||||
|
||||
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
|
||||
@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 NoSuchDevice(_KwException):
|
||||
"""Raised when trying to reach a device number not paired to the receiver."""
|
||||
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 DeviceUnreachable(_KwException):
|
||||
"""Raised when a request is made to an unreachable (turned off) device."""
|
||||
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}
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
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 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 product_information(usb_id: int) -> dict[str, Any]:
|
||||
"""Returns hardcoded information from USB receiver."""
|
||||
return base_usb.get_receiver_info(usb_id)
|
||||
|
||||
|
||||
def receivers():
|
||||
"""Enumerate all the receivers attached to the machine."""
|
||||
yield from _hid.enumerate(filter_receivers)
|
||||
yield from hidapi.enumerate(get_known_receiver_info)
|
||||
|
||||
|
||||
def filter(bus_id, vendor_id, product_id, hidpp_short=False, hidpp_long=False):
|
||||
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:
|
||||
"""Check that this product is of interest and if so return the device record for further checking"""
|
||||
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):
|
||||
|
||||
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):
|
||||
return record
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
def receivers_and_devices():
|
||||
"""Enumerate all the receivers and devices directly attached to the machine."""
|
||||
yield from _hid.enumerate(filter)
|
||||
yield from hidapi.enumerate(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 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 open_path(path):
|
||||
def open_path(path) -> int:
|
||||
"""Checks if the given Linux device path points to the right UR device.
|
||||
|
||||
:param path: the Linux device path.
|
||||
|
|
@ -162,7 +282,7 @@ def open_path(path):
|
|||
:returns: an open receiver handle if this is the right Linux device, or
|
||||
``None``.
|
||||
"""
|
||||
return _hid.open_path(path)
|
||||
return hidapi.open_path(path)
|
||||
|
||||
|
||||
def open():
|
||||
|
|
@ -181,13 +301,13 @@ def close(handle):
|
|||
if handle:
|
||||
try:
|
||||
if isinstance(handle, int):
|
||||
_hid.close(handle)
|
||||
_centurion_handles.discard(handle)
|
||||
_centurion_protocol_versions.pop(handle, None)
|
||||
hidapi.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
|
||||
|
|
@ -210,19 +330,63 @@ 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 = _pack('!BB18s', HIDPP_LONG_MESSAGE_ID, devnumber, 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)
|
||||
else:
|
||||
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:]))
|
||||
wdata = struct.pack("!BB5s", HIDPP_SHORT_MESSAGE_ID, devnumber, data)
|
||||
|
||||
ihandle = int(handle)
|
||||
if ihandle in _centurion_handles:
|
||||
# Centurion CPL framing: [0x51, cpl_length, flags=0x00, feat_idx, func_sw, params...]
|
||||
# cpl_length = len(meaningful_payload) + 1 (the +1 counts the flags byte)
|
||||
# The device_index is stripped — only the HID++ payload (feat_idx + func_sw + params) remains.
|
||||
payload = wdata[2:] # skip report_id and devnumber from standard frame
|
||||
cpl_length = len(data) + 1 # data is the unpadded payload; +1 for flags byte
|
||||
wdata = struct.pack("!BBB", CENTURION_REPORT_ID, cpl_length, 0x00) + payload
|
||||
wdata = wdata + b"\x00" * (CENTURION_FRAME_SIZE - len(wdata))
|
||||
|
||||
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:]),
|
||||
)
|
||||
|
||||
try:
|
||||
_hid.write(int(handle), wdata)
|
||||
hidapi.write(ihandle, wdata)
|
||||
except Exception as reason:
|
||||
_log.error('write failed, assuming handle %r no longer available', handle)
|
||||
logger.error("write failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
raise exceptions.NoReceiver(reason=reason) from reason
|
||||
|
||||
|
||||
def write_centurion_cpl(handle, layer3_payload, flags=0x00):
|
||||
"""Send a Centurion CPL frame with the given Layer 3+ payload.
|
||||
|
||||
Builds: [0x51, cpl_length, flags, layer3_payload..., zero-pad to 64 bytes]
|
||||
where cpl_length = len(layer3_payload) + 1 (the +1 counts the flags byte).
|
||||
|
||||
For multi-fragment sends, flags encodes fragment index and continuation:
|
||||
flags = (fragment_index << 1) | (1 if more_fragments else 0)
|
||||
Single-frame messages use flags=0x00 (default).
|
||||
"""
|
||||
ihandle = int(handle)
|
||||
if ihandle not in _centurion_handles:
|
||||
raise ValueError("write_centurion_cpl called on non-Centurion handle")
|
||||
cpl_length = len(layer3_payload) + 1 # +1 for flags byte
|
||||
wdata = struct.pack("!BBB", CENTURION_REPORT_ID, cpl_length, flags) + layer3_payload
|
||||
wdata = wdata + b"\x00" * (CENTURION_FRAME_SIZE - len(wdata))
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("(%s) <= centurion_cpl[%s]", handle, common.strhex(wdata[: cpl_length + 2]))
|
||||
try:
|
||||
hidapi.write(ihandle, wdata)
|
||||
except Exception as reason:
|
||||
logger.error("write failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise exceptions.NoReceiver(reason=reason) from reason
|
||||
|
||||
|
||||
def read(handle, timeout=DEFAULT_TIMEOUT):
|
||||
|
|
@ -243,19 +407,31 @@ def read(handle, timeout=DEFAULT_TIMEOUT):
|
|||
return reply
|
||||
|
||||
|
||||
# sanity checks on message report id and size
|
||||
def check_message(data):
|
||||
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.
|
||||
"""
|
||||
assert isinstance(data, bytes), (repr(data), type(data))
|
||||
|
||||
# mapping from report_id to accepted message lengths
|
||||
report_lengths = {
|
||||
HIDPP_SHORT_MESSAGE_ID: (SHORT_MESSAGE_SIZE,),
|
||||
HIDPP_LONG_MESSAGE_ID: (_LONG_MESSAGE_SIZE, _CENTURION_MSG_SIZE),
|
||||
DJ_MESSAGE_ID: (_MEDIUM_MESSAGE_SIZE,),
|
||||
0x21: (_MAX_READ_SIZE,),
|
||||
}
|
||||
|
||||
report_id = ord(data[:1])
|
||||
if report_id in report_lengths: # is this an HID++ or DJ message?
|
||||
if report_lengths.get(report_id) == len(data):
|
||||
if report_id in report_lengths:
|
||||
if len(data) in report_lengths[report_id]:
|
||||
return True
|
||||
else:
|
||||
_log.warn('unexpected message size: report_id %02X message %s' % (report_id, _strhex(data)))
|
||||
logger.warning(f"unexpected message size: report_id {report_id:02X} message {common.strhex(data)}")
|
||||
return False
|
||||
|
||||
|
||||
def _read(handle, timeout):
|
||||
def _read(handle, timeout) -> tuple[int, int, bytes]:
|
||||
"""Read an incoming packet from the receiver.
|
||||
|
||||
:returns: a tuple of (report_id, devnumber, data), or `None`.
|
||||
|
|
@ -264,108 +440,95 @@ def _read(handle, timeout):
|
|||
been physically removed from the machine, or the kernel driver has been
|
||||
unloaded. The handle will be closed automatically.
|
||||
"""
|
||||
ihandle = int(handle)
|
||||
is_centurion = ihandle in _centurion_handles
|
||||
read_size = CENTURION_FRAME_SIZE if is_centurion else _MAX_READ_SIZE
|
||||
try:
|
||||
# convert timeout to milliseconds, the hidapi expects it
|
||||
timeout = int(timeout * 1000)
|
||||
data = _hid.read(int(handle), _MAX_READ_SIZE, timeout)
|
||||
data = hidapi.read(ihandle, read_size, timeout)
|
||||
except Exception as reason:
|
||||
_log.warn('read failed, assuming handle %r no longer available', handle)
|
||||
logger.warning("read failed, assuming handle %r no longer available", handle)
|
||||
close(handle)
|
||||
raise NoReceiver(reason=reason)
|
||||
raise exceptions.NoReceiver(reason=reason) from reason
|
||||
|
||||
if data and check_message(data): # ignore messages that fail check
|
||||
if data and is_centurion and ord(data[:1]) == CENTURION_REPORT_ID:
|
||||
# Unwrap Centurion CPL framing:
|
||||
# RX: [0x51, cpl_length, flags=0x00, feat_idx, func_sw, data...]
|
||||
# cpl_length includes the flags byte, so meaningful payload starts at byte 3
|
||||
# and has (cpl_length - 1) bytes.
|
||||
# Reconstruct as HID++ long message: [0x11, devnumber=0xFF, feat_idx, func_sw, data...]
|
||||
cpl_length = ord(data[1:2])
|
||||
inner_payload = data[3 : 2 + cpl_length] # bytes 3..2+cpl_length-1 = cpl_length-1 bytes
|
||||
data = bytes([HIDPP_LONG_MESSAGE_ID, 0xFF]) + inner_payload
|
||||
# Pad to a valid message size: standard long (20) or Centurion extended (63)
|
||||
if len(data) <= _LONG_MESSAGE_SIZE:
|
||||
data = data + b"\x00" * (_LONG_MESSAGE_SIZE - len(data))
|
||||
elif len(data) <= _CENTURION_MSG_SIZE:
|
||||
data = data + b"\x00" * (_CENTURION_MSG_SIZE - len(data))
|
||||
else:
|
||||
data = data[:_CENTURION_MSG_SIZE]
|
||||
|
||||
if data and _is_relevant_message(data): # ignore messages that fail check
|
||||
report_id = ord(data[:1])
|
||||
devnumber = ord(data[1:2])
|
||||
|
||||
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:]))
|
||||
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:]),
|
||||
)
|
||||
|
||||
return report_id, devnumber, data[2:]
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
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):
|
||||
def make_notification(report_id: int, devnumber: int, data: bytes) -> HIDPPNotification | None:
|
||||
"""Guess if this is a notification (and not just a request reply), and
|
||||
return a Notification tuple if it is."""
|
||||
return a Notification 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
|
||||
return None
|
||||
|
||||
# DJ input records are not notifications
|
||||
if report_id == DJ_MESSAGE_ID and (sub_id < 0x10):
|
||||
return
|
||||
return None
|
||||
|
||||
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
|
||||
return None
|
||||
|
||||
if (
|
||||
# standard HID++ 1.0 notification, SubId may be 0x40 - 0x7F
|
||||
(sub_id >= 0x40) or # noqa: E131
|
||||
(sub_id >= 0x40) # noqa: E131
|
||||
or
|
||||
# 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 _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 = {}
|
||||
return HIDPPNotification(report_id, devnumber, sub_id, address, data[2:])
|
||||
return None
|
||||
|
||||
|
||||
def handle_lock(handle):
|
||||
with request_lock:
|
||||
if handles_lock.get(handle) is None:
|
||||
if _log.isEnabledFor(_INFO):
|
||||
_log.info('New lock %s', repr(handle))
|
||||
handles_lock[handle] = _threading.Lock() # Serialize requests on the handle
|
||||
if logger.isEnabledFor(logging.INFO):
|
||||
logger.info("New lock %s", repr(handle))
|
||||
handles_lock[handle] = threading.Lock() # Serialize requests on the handle
|
||||
return handles_lock[handle]
|
||||
|
||||
|
||||
|
|
@ -375,15 +538,37 @@ def acquire_timeout(lock, handle, timeout):
|
|||
result = lock.acquire(timeout=timeout)
|
||||
try:
|
||||
if not result:
|
||||
_log.error('lock on handle %d not acquired, probably due to timeout', int(handle))
|
||||
logger.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, *params, no_reply=False, return_error=False, long_message=False, protocol=1.0):
|
||||
def request(
|
||||
handle,
|
||||
devnumber,
|
||||
request_id: int,
|
||||
*params,
|
||||
no_reply: bool = False,
|
||||
return_error: bool = False,
|
||||
long_message: bool = False,
|
||||
protocol: float = 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.
|
||||
|
|
@ -391,19 +576,14 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
|
|||
: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
|
||||
"""
|
||||
|
||||
# import inspect as _inspect
|
||||
# print ('\n '.join(str(s) for s in _inspect.stack()))
|
||||
|
||||
with acquire_timeout(handle_lock(handle), handle, 10.):
|
||||
with acquire_timeout(handle_lock(handle), handle, 10.0):
|
||||
assert isinstance(request_id, int)
|
||||
if (devnumber != 0xFF or protocol >= 2.0) and request_id < 0x8000:
|
||||
# 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.
|
||||
# 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.
|
||||
request_id = (request_id & 0xFFF0) | 0x08 | _random_bits(3)
|
||||
sw_id = _get_next_sw_id()
|
||||
request_id = (request_id & 0xFFF0) | sw_id # was 0x08 | getrandbits(3)
|
||||
|
||||
timeout = _RECEIVER_REQUEST_TIMEOUT if devnumber == 0xFF else _DEVICE_REQUEST_TIMEOUT
|
||||
# be extra patient on long register read
|
||||
|
|
@ -411,19 +591,17 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
|
|||
timeout *= 2
|
||||
|
||||
if params:
|
||||
params = b''.join(_pack('B', p) if isinstance(p, int) else p for p in params)
|
||||
params = b"".join(struct.pack("B", p) if isinstance(p, int) else p for p in params)
|
||||
else:
|
||||
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
|
||||
params = b""
|
||||
request_data = struct.pack("!H", request_id) + params
|
||||
|
||||
ihandle = int(handle)
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
notifications_hook = getattr(handle, "notifications_hook", None)
|
||||
try:
|
||||
_skip_incoming(handle, ihandle, notifications_hook)
|
||||
except NoReceiver:
|
||||
_log.warn('device or receiver disconnected')
|
||||
_read_input_buffer(handle, ihandle, notifications_hook)
|
||||
except exceptions.NoReceiver:
|
||||
logger.warning("device or receiver disconnected")
|
||||
return None
|
||||
write(ihandle, devnumber, request_data, long_message)
|
||||
|
||||
|
|
@ -431,33 +609,52 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
|
|||
return None
|
||||
|
||||
# we consider timeout from this point
|
||||
request_started = _timestamp()
|
||||
request_started = time()
|
||||
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 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]
|
||||
):
|
||||
error = ord(reply_data[3:4])
|
||||
|
||||
if _log.isEnabledFor(_DEBUG):
|
||||
_log.debug(
|
||||
'(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error,
|
||||
_hidpp10.ERROR[error]
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"(%s) device 0x%02X error on request {%04X}: %d = %s",
|
||||
handle,
|
||||
devnumber,
|
||||
request_id,
|
||||
error,
|
||||
Hidpp10ErrorCode(error),
|
||||
)
|
||||
return _hidpp10.ERROR[error] if return_error else None
|
||||
if reply_data[:1] == b'\xFF' and reply_data[1:3] == request_data[:2]:
|
||||
return Hidpp10ErrorCode(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])
|
||||
_log.error(
|
||||
'(%s) device %d error on feature request {%04X}: %d = %s', handle, devnumber, request_id, error,
|
||||
_hidpp20.ERROR[error]
|
||||
try:
|
||||
error_name = Hidpp20ErrorCode(error)
|
||||
except ValueError:
|
||||
error_name = f"unknown:{error:02X}"
|
||||
logger.error(
|
||||
"(%s) device %d error on feature request {%04X}: %d = %s",
|
||||
handle,
|
||||
devnumber,
|
||||
request_id,
|
||||
error,
|
||||
error_name,
|
||||
)
|
||||
raise exceptions.FeatureCallError(
|
||||
number=devnumber,
|
||||
request=request_id,
|
||||
error=error,
|
||||
params=params,
|
||||
)
|
||||
raise _hidpp20.FeatureCallError(number=devnumber, request=request_id, error=error, params=params)
|
||||
|
||||
if reply_data[:2] == request_data[:2]:
|
||||
if devnumber == 0xFF:
|
||||
|
|
@ -475,78 +672,138 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
|
|||
else:
|
||||
# a reply was received, but did not match our request in any way
|
||||
# reset the timeout starting point
|
||||
request_started = _timestamp()
|
||||
request_started = time()
|
||||
|
||||
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))
|
||||
# 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
|
||||
# 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)
|
||||
logger.warning(
|
||||
"timeout (%0.2f/%0.2f) on device %d request {%04X} params [%s]",
|
||||
delta,
|
||||
timeout,
|
||||
devnumber,
|
||||
request_id,
|
||||
common.strhex(params),
|
||||
)
|
||||
# raise DeviceUnreachable(number=devnumber, request=request_id)
|
||||
|
||||
|
||||
def ping(handle, devnumber, long_message=False):
|
||||
def ping(handle, devnumber, long_message: bool = 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 _log.isEnabledFor(_DEBUG):
|
||||
_log.debug('(%s) pinging device %d', handle, devnumber)
|
||||
with acquire_timeout(handle_lock(handle), handle, 10.):
|
||||
notifications_hook = getattr(handle, 'notifications_hook', None)
|
||||
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)
|
||||
try:
|
||||
_skip_incoming(handle, int(handle), notifications_hook)
|
||||
except NoReceiver:
|
||||
_log.warn('device or receiver disconnected')
|
||||
_read_input_buffer(handle, int(handle), notifications_hook)
|
||||
except exceptions.NoReceiver:
|
||||
logger.warning("device or receiver disconnected")
|
||||
return
|
||||
|
||||
# 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))
|
||||
# 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)
|
||||
|
||||
request_started = _timestamp() # we consider timeout from this point
|
||||
request_started = time() # we consider timeout from this point
|
||||
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_data[:2] == request_data[:2] and reply_data[4:5] == request_data[-1:]:
|
||||
if reply_devnumber == devnumber or reply_devnumber == devnumber ^ 0xFF: # BT device returning 0x00
|
||||
is_centurion = int(handle) in _centurion_handles
|
||||
mark_ok = is_centurion or reply_data[4:5] == request_data[-1:]
|
||||
if reply_data[:2] == request_data[:2] and mark_ok:
|
||||
# HID++ 2.0+ device, currently connected
|
||||
return ord(reply_data[2:3]) + ord(reply_data[3:4]) / 10.0
|
||||
major = ord(reply_data[2:3])
|
||||
minor = ord(reply_data[3:4])
|
||||
if is_centurion:
|
||||
_centurion_protocol_versions[int(handle)] = (major, minor)
|
||||
return major + minor / 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]
|
||||
): # error response
|
||||
error = ord(reply_data[3:4])
|
||||
if error == _hidpp10.ERROR.invalid_SubID__command: # a valid reply from a HID++ 1.0 device
|
||||
if error == Hidpp10ErrorCode.INVALID_SUB_ID_COMMAND:
|
||||
# a valid reply from a HID++ 1.0 device
|
||||
return 1.0
|
||||
if error == _hidpp10.ERROR.resource_error or error == _hidpp10.ERROR.connection_request_failed:
|
||||
if error in [Hidpp10ErrorCode.RESOURCE_ERROR, Hidpp10ErrorCode.CONNECTION_REQUEST_FAILED]:
|
||||
return # device unreachable
|
||||
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 error == Hidpp10ErrorCode.UNKNOWN_DEVICE: # no device with that number currently accessible
|
||||
logger.info("(%s) device %d error on ping request: unknown device", handle, devnumber)
|
||||
raise exceptions.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 = _timestamp() - request_started
|
||||
delta = time() - request_started
|
||||
|
||||
_log.warn('(%s) timeout (%0.2f/%0.2f) on device %d ping', handle, delta, _PING_TIMEOUT, devnumber)
|
||||
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.
|
||||
"""
|
||||
is_centurion = ihandle in _centurion_handles
|
||||
read_size = CENTURION_FRAME_SIZE if is_centurion else _MAX_READ_SIZE
|
||||
|
||||
while True:
|
||||
try:
|
||||
# read whatever is already in the buffer, if any
|
||||
data = hidapi.read(ihandle, 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_centurion and ord(data[:1]) == CENTURION_REPORT_ID:
|
||||
# Unwrap Centurion CPL framing same as in _read()
|
||||
cpl_length = ord(data[1:2])
|
||||
inner_payload = data[3 : 2 + cpl_length]
|
||||
data = bytes([HIDPP_LONG_MESSAGE_ID, 0xFF]) + inner_payload
|
||||
if len(data) <= _LONG_MESSAGE_SIZE:
|
||||
data = data + b"\x00" * (_LONG_MESSAGE_SIZE - len(data))
|
||||
elif len(data) <= _CENTURION_MSG_SIZE:
|
||||
data = data + b"\x00" * (_CENTURION_MSG_SIZE - len(data))
|
||||
else:
|
||||
data = data[:_CENTURION_MSG_SIZE]
|
||||
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
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -16,235 +14,225 @@
|
|||
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||
|
||||
## 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
|
||||
"""Collection of known Logitech product IDs.
|
||||
|
||||
# USB ids of Logitech wireless receivers.
|
||||
# Only receivers supporting the HID++ protocol can go in here.
|
||||
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
|
||||
|
||||
from .descriptors import DEVICES as _DEVICES
|
||||
from .i18n import _
|
||||
USB ids of Logitech wireless receivers.
|
||||
Only receivers supporting the HID++ protocol can go in here.
|
||||
"""
|
||||
|
||||
# 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 __future__ import annotations
|
||||
|
||||
_DRIVER = ('hid-generic', 'generic-usb', 'logitech-djreceiver')
|
||||
from typing import Any
|
||||
|
||||
_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
|
||||
}
|
||||
from solaar.i18n import _
|
||||
|
||||
_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
|
||||
}
|
||||
# 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
|
||||
|
||||
_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
|
||||
}
|
||||
LOGITECH_VENDOR_ID = 0x046D
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
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_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
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
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,
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
|
||||
_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
|
||||
}
|
||||
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": True,
|
||||
"re_pairs": 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,
|
||||
}
|
||||
|
||||
|
||||
# Receivers added here should also be listed in
|
||||
# share/solaar/io.github.pwr_solaar.solaar.metainfo.xml
|
||||
# share/solaar/io.github.pwr_solaar.solaar.meta-info.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_C542 = _nano_receiver(0xc542) # does not use HID++
|
||||
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_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)
|
||||
LIGHTSPEED_RECEIVER_C54D = _lightspeed_receiver(0xC54D)
|
||||
|
||||
# EX100 old style receiver pre-unifying protocol
|
||||
# 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
|
||||
EX100_27MHZ_RECEIVER_C517 = _ex100_receiver(0xC517)
|
||||
|
||||
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
|
||||
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,
|
||||
0xC54D: LIGHTSPEED_RECEIVER_C54D,
|
||||
0xC517: EX100_27MHZ_RECEIVER_C517,
|
||||
}
|
||||
|
||||
_bt_device = lambda product_id: {'vendor_id': 0x046d, 'product_id': product_id, 'bus_id': 0x5, 'isDevice': True}
|
||||
|
||||
DEVICES = []
|
||||
def get_receiver_info(product_id: int) -> dict[str, Any]:
|
||||
"""Returns hardcoded information about a Logitech 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))
|
||||
Parameters
|
||||
----------
|
||||
product_id
|
||||
Product ID (pid) of the receiver, e.g. 0xC548 for a Logitech
|
||||
Bolt receiver.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Any]
|
||||
Receiver info with mandatory fields:
|
||||
- vendor_id
|
||||
- product_id
|
||||
|
||||
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)
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If the product ID is unknown.
|
||||
"""
|
||||
try:
|
||||
return KNOWN_RECEIVERS[product_id]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
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
|
||||
raise ValueError(f"Unknown product ID '0x{product_id:02X}'")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,511 @@
|
|||
## 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.
|
||||
|
||||
"""Centurion device protocol — receiver class, factory, device info/firmware/battery queries.
|
||||
|
||||
CenturionReceiver is a lightweight receiver-like container for Centurion
|
||||
(PRO X 2 LIGHTSPEED and similar) dongles. Protocol functions query device
|
||||
info, firmware, serial, name, and battery via Centurion-specific HID++ features.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from typing import Callable
|
||||
|
||||
from solaar import configuration
|
||||
|
||||
from . import base
|
||||
from . import exceptions
|
||||
from . import hidpp10
|
||||
from .centurion_constants import CenturionCoreFeature
|
||||
from .common import Alert
|
||||
from .common import Battery
|
||||
from .common import BatteryStatus
|
||||
from .common import FirmwareKind
|
||||
from .common import _read_usb_product_string
|
||||
from .hidpp20_constants import SupportedFeature
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# --- Centurion protocol functions (standalone, operate on any device-like object) ---
|
||||
|
||||
|
||||
def get_firmware_centurion(device):
|
||||
"""Reads firmware info from a Centurion device via DeviceInfo (0x0100) function 1."""
|
||||
from . import common
|
||||
|
||||
fw = []
|
||||
seen = set() # track response signatures to detect duplicates
|
||||
for index in range(0, 8): # try up to 8 entities
|
||||
try:
|
||||
report = device.feature_request(SupportedFeature.CENTURION_DEVICE_INFO, 0x10, index)
|
||||
except exceptions.FeatureCallError:
|
||||
break
|
||||
if not report or len(report) < 5:
|
||||
break
|
||||
# Dedup: parent device returns the same response for every entity index
|
||||
sig = bytes(report[: 5 + report[4]])
|
||||
if sig in seen:
|
||||
break
|
||||
seen.add(sig)
|
||||
fw_type = report[0]
|
||||
version = struct.unpack("!H", report[2:4])[0]
|
||||
name_len = report[4]
|
||||
name = report[5 : 5 + name_len].decode("ascii", errors="replace").rstrip("\x00") if name_len else ""
|
||||
version_str = f"{version >> 8}.{version & 0xFF:02d}"
|
||||
kind = FirmwareKind(fw_type) if fw_type <= 3 else FirmwareKind.Other
|
||||
fw.append(common.FirmwareInfo(kind, name, version_str, None))
|
||||
return tuple(fw) if fw else None
|
||||
|
||||
|
||||
def get_serial_centurion(device):
|
||||
"""Reads the serial number from a Centurion device via DeviceInfo (0x0100) function 2."""
|
||||
try:
|
||||
report = device.feature_request(SupportedFeature.CENTURION_DEVICE_INFO, 0x20)
|
||||
except exceptions.FeatureCallError:
|
||||
return None
|
||||
if not report or len(report) < 2:
|
||||
return None
|
||||
str_len = report[0]
|
||||
return report[1 : 1 + str_len].decode("ascii", errors="replace").rstrip("\x00")
|
||||
|
||||
|
||||
def get_hardware_info_centurion(device):
|
||||
"""Reads hardware info from a Centurion device via DeviceInfo (0x0100) function 0.
|
||||
|
||||
Returns (modelId, hardwareRevision, productId) or None.
|
||||
"""
|
||||
try:
|
||||
report = device.feature_request(SupportedFeature.CENTURION_DEVICE_INFO)
|
||||
except exceptions.FeatureCallError:
|
||||
return None
|
||||
if not report or len(report) < 4:
|
||||
return None
|
||||
model_id = report[0]
|
||||
hw_revision = report[1]
|
||||
product_id = struct.unpack("!H", report[2:4])[0]
|
||||
return model_id, hw_revision, product_id
|
||||
|
||||
|
||||
def _centurion_sub_device_info_request(device, function=0x00, *params):
|
||||
"""Send a DeviceInfo (0x0100) request to the sub-device via bridge."""
|
||||
sub_indices = getattr(device, "_centurion_sub_indices", {})
|
||||
sub_idx = sub_indices.get(SupportedFeature.CENTURION_DEVICE_INFO)
|
||||
if sub_idx is None:
|
||||
return None
|
||||
return device.centurion_bridge_request(sub_idx, function, *params)
|
||||
|
||||
|
||||
def get_firmware_centurion_sub(device):
|
||||
"""Reads firmware info from the Centurion sub-device (headset) via bridge."""
|
||||
from . import common
|
||||
|
||||
fw = []
|
||||
seen = set()
|
||||
for index in range(0, 8):
|
||||
report = _centurion_sub_device_info_request(device, 0x10, index)
|
||||
if not report or len(report) < 5:
|
||||
break
|
||||
sig = bytes(report[: 5 + report[4]])
|
||||
if sig in seen:
|
||||
break
|
||||
seen.add(sig)
|
||||
fw_type = report[0]
|
||||
version = struct.unpack("!H", report[2:4])[0]
|
||||
name_len = report[4]
|
||||
name = report[5 : 5 + name_len].decode("ascii", errors="replace").rstrip("\x00") if name_len else ""
|
||||
version_str = f"{version >> 8}.{version & 0xFF:02d}"
|
||||
kind = FirmwareKind(fw_type) if fw_type <= 3 else FirmwareKind.Other
|
||||
fw.append(common.FirmwareInfo(kind, name, version_str, None))
|
||||
return tuple(fw) if fw else None
|
||||
|
||||
|
||||
def get_serial_centurion_sub(device):
|
||||
"""Reads the serial number from the Centurion sub-device (headset) via bridge."""
|
||||
report = _centurion_sub_device_info_request(device, 0x20)
|
||||
if not report or len(report) < 2:
|
||||
return None
|
||||
str_len = report[0]
|
||||
return report[1 : 1 + str_len].decode("ascii", errors="replace").rstrip("\x00")
|
||||
|
||||
|
||||
def get_hardware_info_centurion_sub(device):
|
||||
"""Reads hardware info from the Centurion sub-device (headset) via bridge.
|
||||
|
||||
Returns (modelId, hardwareRevision, productId) or None.
|
||||
"""
|
||||
report = _centurion_sub_device_info_request(device)
|
||||
if not report or len(report) < 4:
|
||||
return None
|
||||
model_id = report[0]
|
||||
hw_revision = report[1]
|
||||
product_id = struct.unpack("!H", report[2:4])[0]
|
||||
return model_id, hw_revision, product_id
|
||||
|
||||
|
||||
def get_name_centurion(device):
|
||||
"""Reads a Centurion device's name via DeviceName (0x0101).
|
||||
|
||||
Tries two response formats:
|
||||
1. Inline: function 0 returns [name_len, name_bytes...] (like serial)
|
||||
2. Chunked: function 0 returns [name_len], function 1 returns [name_bytes...] (like standard DeviceName)
|
||||
"""
|
||||
try:
|
||||
reply = device.feature_request(SupportedFeature.CENTURION_DEVICE_NAME)
|
||||
except exceptions.FeatureCallError:
|
||||
return None
|
||||
if not reply:
|
||||
return None
|
||||
name_length = reply[0]
|
||||
if name_length == 0:
|
||||
return None
|
||||
# If the full name is inline (length + name bytes in one response)
|
||||
if len(reply) >= 1 + name_length:
|
||||
return reply[1 : 1 + name_length].decode("utf-8", errors="replace").rstrip("\x00")
|
||||
# Otherwise, fetch name in chunks via function 1 (like standard DEVICE_NAME)
|
||||
name = b""
|
||||
while len(name) < name_length:
|
||||
try:
|
||||
fragment = device.feature_request(SupportedFeature.CENTURION_DEVICE_NAME, 0x10, len(name))
|
||||
except exceptions.FeatureCallError:
|
||||
break
|
||||
if fragment:
|
||||
name += fragment[: name_length - len(name)]
|
||||
else:
|
||||
break
|
||||
return name.decode("utf-8", errors="replace").rstrip("\x00") if name else None
|
||||
|
||||
|
||||
def get_battery_centurion(device):
|
||||
"""Query battery via CENTURION_BATTERY_SOC."""
|
||||
try:
|
||||
report = device.feature_request(SupportedFeature.CENTURION_BATTERY_SOC)
|
||||
if report is not None:
|
||||
return decipher_battery_centurion(report)
|
||||
except exceptions.FeatureCallError:
|
||||
if SupportedFeature.CENTURION_BATTERY_SOC in device.features:
|
||||
return SupportedFeature.CENTURION_BATTERY_SOC
|
||||
return None
|
||||
|
||||
|
||||
def decipher_battery_centurion(report) -> tuple[SupportedFeature, Battery]:
|
||||
"""Decipher CENTURION_BATTERY_SOC (0x0104) response.
|
||||
|
||||
Response format (3 bytes):
|
||||
Byte 0: Battery Percentage (0-100)
|
||||
Byte 1: Battery Percentage (duplicate)
|
||||
Byte 2: Charging Status (0=discharging, 1=charging, 2=charging via USB, 3=charge complete)
|
||||
"""
|
||||
if len(report) < 1:
|
||||
return SupportedFeature.CENTURION_BATTERY_SOC, Battery(None, None, BatteryStatus.DISCHARGING, None)
|
||||
soc = report[0]
|
||||
logger.debug("centurion battery SOC raw: %s", report[:8].hex())
|
||||
charging_status = report[2] if len(report) >= 3 else 0
|
||||
if charging_status in (1, 2):
|
||||
status = BatteryStatus.RECHARGING
|
||||
elif charging_status == 3:
|
||||
status = BatteryStatus.FULL
|
||||
else:
|
||||
status = BatteryStatus.DISCHARGING
|
||||
return SupportedFeature.CENTURION_BATTERY_SOC, Battery(soc, None, status, None)
|
||||
|
||||
|
||||
# --- CenturionReceiver class ---
|
||||
|
||||
|
||||
class CenturionReceiver:
|
||||
"""A lightweight receiver-like container for Centurion (PRO X 2 LIGHTSPEED) dongles.
|
||||
|
||||
Provides the Receiver interface to the UI so the dongle appears as a parent
|
||||
with the headset as an indented child device. NOT a subclass of Receiver —
|
||||
Receiver's __init__ does HID++ 1.0 register reads and pairing setup that
|
||||
don't apply to Centurion.
|
||||
|
||||
All centurion communication (bridge, features, settings, battery) lives in
|
||||
the child Device; this class is just a UI container + handle owner.
|
||||
"""
|
||||
|
||||
read_register: Callable = hidpp10.read_register
|
||||
write_register: Callable = hidpp10.write_register
|
||||
number = 0xFF
|
||||
kind = None
|
||||
isDevice = False
|
||||
may_unpair = False
|
||||
re_pairs = False
|
||||
max_devices = 1
|
||||
|
||||
def __init__(self, low_level, handle, device_info, setting_callback=None):
|
||||
assert handle
|
||||
self.low_level = low_level
|
||||
self.handle = handle
|
||||
self.path = device_info.path
|
||||
self.product_id = device_info.product_id
|
||||
self.setting_callback = setting_callback
|
||||
self.status_callback = None
|
||||
self.notification_flags = None
|
||||
self._devices = {}
|
||||
self._firmware = None
|
||||
self._dongle_features = None # independently probed dongle features
|
||||
self.cleanups = []
|
||||
|
||||
# Receiver identity
|
||||
self.serial = None
|
||||
self._usb_name = getattr(device_info, "product", None)
|
||||
if not self._usb_name and self.path:
|
||||
self._usb_name = _read_usb_product_string(self.path)
|
||||
self.name = "Centurion Receiver"
|
||||
|
||||
# Dummy pairing object — lock_open stays False
|
||||
from .receiver import Pairing
|
||||
|
||||
self.pairing = Pairing()
|
||||
|
||||
# Discover dongle features independently
|
||||
self._discover_dongle_features()
|
||||
|
||||
# Read serial from dongle's CENTURION_DEVICE_INFO if available
|
||||
if self.serial is None:
|
||||
try:
|
||||
s = get_serial_centurion(self)
|
||||
if s and s.strip() and s.strip().isprintable():
|
||||
self.serial = s.strip()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def enable_connection_notifications(self, enable=True):
|
||||
return False
|
||||
|
||||
def remaining_pairings(self, cache=True):
|
||||
return None
|
||||
|
||||
def device_codename(self, n):
|
||||
return self._usb_name
|
||||
|
||||
def request(self, request_id, *params, no_reply=False):
|
||||
"""Send an HID++ request directly to the dongle (not through bridge)."""
|
||||
if self.handle:
|
||||
return self.low_level.request(
|
||||
self.handle, 0xFF, request_id, *params, no_reply=no_reply, long_message=True, protocol=2.0
|
||||
)
|
||||
|
||||
def feature_request(self, feature, function=0x00, *params, no_reply=False):
|
||||
"""Send a feature request to the dongle using discovered feature indices."""
|
||||
if self._dongle_features is None:
|
||||
self._discover_dongle_features()
|
||||
feature_int = int(feature)
|
||||
for _feat, feat_id, index in self._dongle_features or []:
|
||||
if feat_id == feature_int:
|
||||
request_id = (index << 8) | (function & 0xFF)
|
||||
return self.request(request_id, *params, no_reply=no_reply)
|
||||
raise exceptions.FeatureNotSupported(feature)
|
||||
|
||||
def _discover_dongle_features(self):
|
||||
"""Independently discover features on the dongle hardware."""
|
||||
self._dongle_features = []
|
||||
try:
|
||||
# Query ROOT for FEATURE_SET index
|
||||
response = self.request(0x0000, 0x00, 0x01)
|
||||
if response is None or response[0] == 0:
|
||||
return
|
||||
fs_index = response[0]
|
||||
# Get feature count
|
||||
count_resp = self.request(fs_index << 8)
|
||||
if count_resp is None:
|
||||
return
|
||||
feature_count = count_resp[0]
|
||||
# Enumerate features via CenturionFeatureSet (func 1 = 0x10, per-index query)
|
||||
for idx in range(feature_count):
|
||||
resp = self.request((fs_index << 8) | 0x10, idx)
|
||||
if resp is None or len(resp) < 3:
|
||||
continue
|
||||
feat_id = struct.unpack("!H", resp[1:3])[0]
|
||||
try:
|
||||
feature = SupportedFeature(feat_id)
|
||||
except ValueError:
|
||||
feature = f"unknown:{feat_id:04X}"
|
||||
self._dongle_features.append((feature, feat_id, idx))
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("Centurion dongle features: %s", self._dongle_features)
|
||||
except Exception:
|
||||
logger.debug("Centurion dongle feature discovery failed", exc_info=True)
|
||||
|
||||
@property
|
||||
def dongle_features(self):
|
||||
"""Return list of (feature, feat_id, index) tuples for dongle features."""
|
||||
if self._dongle_features is None:
|
||||
self._discover_dongle_features()
|
||||
return self._dongle_features
|
||||
|
||||
def count(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
|
||||
@property
|
||||
def firmware(self):
|
||||
if self._firmware is None and self.handle:
|
||||
self._firmware = get_firmware_centurion(self)
|
||||
return self._firmware or ()
|
||||
|
||||
def notify_devices(self):
|
||||
"""Create child Device for the headset and trigger its initialization."""
|
||||
# Import Device locally to avoid circular import (centurion.py ↔ device.py)
|
||||
from .device import Device
|
||||
|
||||
# Signal receiver to UI first — tray/window need the receiver entry
|
||||
# before a child device can be added under it.
|
||||
self.changed(alert=Alert.NONE)
|
||||
|
||||
# Create child Device with receiver=self, number=1
|
||||
pairing_info = {
|
||||
"wpid": self.product_id,
|
||||
"kind": None,
|
||||
"serial": None,
|
||||
"polling": None,
|
||||
"power_switch": None,
|
||||
}
|
||||
dev = Device(
|
||||
self.low_level,
|
||||
self,
|
||||
1,
|
||||
None,
|
||||
pairing_info=pairing_info,
|
||||
setting_callback=self.setting_callback,
|
||||
)
|
||||
# Set centurion attributes on the child
|
||||
dev.centurion = True
|
||||
dev.product_id = self.product_id
|
||||
dev.hidpp_long = True
|
||||
dev._centurion_usb_name = self._usb_name
|
||||
# Pre-set bridge index from dongle features so ping can probe the headset
|
||||
for _feat, feat_id, idx in self._dongle_features or []:
|
||||
if feat_id == CenturionCoreFeature.CENT_PP_BRIDGE:
|
||||
dev._centurion_bridge_index = idx
|
||||
break
|
||||
|
||||
self._devices[1] = dev
|
||||
configuration.attach_to(dev)
|
||||
dev.status_callback = self.status_callback
|
||||
|
||||
# Ping to determine online status.
|
||||
# Notify UI either way — offline devices show as greyed out (matching receiver behavior).
|
||||
online = dev.ping()
|
||||
dev.changed(active=online)
|
||||
if self.status_callback is not None:
|
||||
self.status_callback(dev)
|
||||
|
||||
def changed(self, alert=Alert.NOTIFICATION, reason=None):
|
||||
if self.status_callback is not None:
|
||||
self.status_callback(self, alert=alert, reason=reason)
|
||||
|
||||
def status_string(self):
|
||||
count = self.count()
|
||||
if count == 0:
|
||||
return "No devices."
|
||||
return f"{count} device connected."
|
||||
|
||||
def close(self):
|
||||
handle, self.handle = self.handle, None
|
||||
for _n, d in self._devices.items():
|
||||
if d:
|
||||
d.close()
|
||||
self._devices.clear()
|
||||
for cleanup in self.cleanups:
|
||||
cleanup(self)
|
||||
return handle and self.low_level.close(handle)
|
||||
|
||||
def __del__(self):
|
||||
self.close()
|
||||
|
||||
def __iter__(self):
|
||||
for dev in self._devices.values():
|
||||
if dev is not None:
|
||||
yield dev
|
||||
|
||||
def __getitem__(self, key):
|
||||
dev = self._devices.get(key)
|
||||
if dev is not None:
|
||||
return dev
|
||||
raise IndexError(key)
|
||||
|
||||
def __len__(self):
|
||||
return len([d for d in self._devices.values() if d is not None])
|
||||
|
||||
def __contains__(self, dev):
|
||||
if isinstance(dev, int):
|
||||
return self._devices.get(dev) is not None
|
||||
return self.__contains__(dev.number)
|
||||
|
||||
def __bool__(self):
|
||||
return self.handle is not None
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __eq__(self, other):
|
||||
return other is not None and self.kind == other.kind and self.path == other.path
|
||||
|
||||
def __ne__(self, other):
|
||||
return other is None or self.kind != other.kind or self.path != other.path
|
||||
|
||||
def __hash__(self):
|
||||
return self.path.__hash__()
|
||||
|
||||
def __str__(self):
|
||||
return "<%s(%s,%s%s)>" % (
|
||||
self.name.replace(" ", "") if self.name else "CenturionReceiver",
|
||||
self.path,
|
||||
"" if isinstance(self.handle, int) else "T",
|
||||
self.handle,
|
||||
)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
|
||||
def create_centurion_receiver(low_level, device_info, setting_callback=None):
|
||||
"""Opens a Centurion dongle and wraps it as a receiver-like container.
|
||||
|
||||
Creates a CenturionReceiver, discovers its features, then checks if
|
||||
CentPPBridge (0x0003) is among them. If not, this is a direct-connected
|
||||
device (wired headset) — close and return None so the caller can fall
|
||||
back to create_device().
|
||||
|
||||
:returns: A CenturionReceiver, or None.
|
||||
"""
|
||||
try:
|
||||
handle = low_level.open_path(device_info.path)
|
||||
if handle:
|
||||
base._centurion_handles.add(int(handle))
|
||||
cr = CenturionReceiver(low_level, handle, device_info, setting_callback)
|
||||
# Check if any discovered feature is CentPPBridge (0x0003)
|
||||
has_bridge = any(feat_id == CenturionCoreFeature.CENT_PP_BRIDGE for _, feat_id, _ in (cr.dongle_features or []))
|
||||
if not has_bridge:
|
||||
logger.info("Centurion device %s has no bridge, treating as direct device", device_info.path)
|
||||
base._centurion_handles.discard(int(handle))
|
||||
cr.handle = None # prevent __del__ from double-closing
|
||||
low_level.close(handle)
|
||||
return None
|
||||
return cr
|
||||
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
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
"""Centurion transport-specific constants.
|
||||
|
||||
Feature IDs that collide with HID++ 2.0 core features live here
|
||||
so they can coexist with SupportedFeature (which requires unique values).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
|
||||
from .hidpp20_constants import SupportedFeature
|
||||
|
||||
|
||||
class CenturionCoreFeature(IntEnum):
|
||||
"""Centurion transport-specific features that collide with HID++ 2.0 core IDs."""
|
||||
|
||||
CENTURION_ROOT = 0x0000
|
||||
CENTURION_FEATURE_SET = 0x0001
|
||||
CENT_PP_BRIDGE = 0x0003
|
||||
MULTI_HOST_CONTROL = 0x0005
|
||||
KEEP_ALIVE = 0x0007
|
||||
|
||||
def __str__(self):
|
||||
return self.name.replace("_", " ")
|
||||
|
||||
|
||||
def resolve_feature(feat_id: int, centurion: bool = False):
|
||||
"""Resolve a feature ID to the appropriate enum, checking centurion-specific
|
||||
features first when on the centurion transport."""
|
||||
if centurion:
|
||||
try:
|
||||
return CenturionCoreFeature(feat_id)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return SupportedFeature(feat_id)
|
||||
except ValueError:
|
||||
return None
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
# -*- 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,17 +14,297 @@
|
|||
## 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
|
||||
|
||||
# Some common functions and types.
|
||||
import binascii
|
||||
import dataclasses
|
||||
import typing
|
||||
|
||||
from binascii import hexlify as _hexlify
|
||||
from collections import namedtuple
|
||||
from enum import Flag
|
||||
from enum import IntEnum
|
||||
from typing import Generator
|
||||
from typing import Iterable
|
||||
from typing import Optional
|
||||
from typing import Union
|
||||
|
||||
is_string = lambda d: isinstance(d, str)
|
||||
import yaml
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
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):
|
||||
|
|
@ -35,7 +314,7 @@ class NamedInt(int):
|
|||
(case-insensitive)."""
|
||||
|
||||
def __new__(cls, value, name):
|
||||
assert is_string(name)
|
||||
assert isinstance(name, str)
|
||||
obj = int.__new__(cls, value)
|
||||
obj.name = str(name)
|
||||
return obj
|
||||
|
|
@ -50,11 +329,11 @@ class NamedInt(int):
|
|||
return int(self) == int(other) and self.name == other.name
|
||||
if isinstance(other, int):
|
||||
return int(self) == int(other)
|
||||
if is_string(other):
|
||||
if isinstance(other, str):
|
||||
return self.name.lower() == other.lower()
|
||||
# this should catch comparisons with bytes in Py3
|
||||
if other is not None:
|
||||
raise TypeError('Unsupported type ' + str(type(other)))
|
||||
raise TypeError(f"Unsupported type {str(type(other))}")
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
|
@ -66,7 +345,20 @@ class NamedInt(int):
|
|||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInt(%d, %r)' % (int(self), self.name)
|
||||
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)
|
||||
|
||||
|
||||
class NamedInts:
|
||||
|
|
@ -82,17 +374,15 @@ class NamedInts:
|
|||
if the value already exists in the set (int or string), ValueError will be
|
||||
raised.
|
||||
"""
|
||||
__slots__ = ('__dict__', '_values', '_indexed', '_fallback', '_is_sorted')
|
||||
|
||||
def __init__(self, dict=None, **kwargs):
|
||||
__slots__ = ("__dict__", "_values", "_indexed", "_fallback", "_is_sorted")
|
||||
|
||||
def __init__(self, dict_=None, **kwargs):
|
||||
def _readable_name(n):
|
||||
if not is_string(n):
|
||||
raise TypeError('expected string, got ' + str(type(n)))
|
||||
return n.replace('__', '/').replace('_', ' ')
|
||||
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
|
||||
|
|
@ -116,13 +406,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 'unknown:%06X' % unknown_bits
|
||||
yield f"unknown:{unknown_bits:06X}"
|
||||
|
||||
def _sort_values(self):
|
||||
self._values = sorted(self._values)
|
||||
|
|
@ -140,10 +430,10 @@ class NamedInts:
|
|||
self._sort_values()
|
||||
return value
|
||||
|
||||
elif is_string(index):
|
||||
elif isinstance(index, str):
|
||||
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)
|
||||
|
|
@ -177,17 +467,17 @@ class NamedInts:
|
|||
def __setitem__(self, index, name):
|
||||
assert isinstance(index, int), type(index)
|
||||
if isinstance(name, NamedInt):
|
||||
assert int(index) == int(name), repr(index) + ' ' + repr(name)
|
||||
assert int(index) == int(name), f"{repr(index)} {repr(name)}"
|
||||
value = name
|
||||
elif is_string(name):
|
||||
elif isinstance(name, str):
|
||||
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('%s (%d) already known' % (value, int(value)))
|
||||
raise ValueError(f"{value} ({int(value)}) already known")
|
||||
if int(value) in self._indexed:
|
||||
raise ValueError('%d (%s) already known' % (int(value), value))
|
||||
raise ValueError(f"{int(value)} ({value}) already known")
|
||||
|
||||
self._values.append(value)
|
||||
self._is_sorted = False
|
||||
|
|
@ -200,7 +490,7 @@ class NamedInts:
|
|||
return self[value] == value
|
||||
elif isinstance(value, int):
|
||||
return value in self._indexed
|
||||
elif is_string(value):
|
||||
elif isinstance(value, str):
|
||||
return value in self.__dict__ or value in self._values
|
||||
|
||||
def __iter__(self):
|
||||
|
|
@ -210,14 +500,41 @@ class NamedInts:
|
|||
return len(self._values)
|
||||
|
||||
def __repr__(self):
|
||||
return 'NamedInts(%s)' % ', '.join(repr(v) for v in self._values)
|
||||
return f"NamedInts({', '.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
|
||||
|
||||
|
|
@ -229,18 +546,18 @@ class UnsortedNamedInts(NamedInts):
|
|||
def strhex(x):
|
||||
assert x is not None
|
||||
"""Produce a hex-string representation of a sequence of bytes."""
|
||||
return _hexlify(x).decode('ascii').upper()
|
||||
return binascii.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):
|
||||
|
|
@ -258,9 +575,116 @@ class KwException(Exception):
|
|||
return self.args[0].get(k) # was self.args[0][k]
|
||||
|
||||
|
||||
"""Firmware information."""
|
||||
FirmwareInfo = namedtuple('FirmwareInfo', ['kind', 'name', 'version', 'extras'])
|
||||
class FirmwareKind(IntEnum):
|
||||
Firmware = 0x00
|
||||
Bootloader = 0x01
|
||||
Hardware = 0x02
|
||||
Other = 0x03
|
||||
|
||||
BATTERY_APPROX = NamedInts(empty=0, critical=5, low=20, good=50, full=90)
|
||||
|
||||
del namedtuple
|
||||
@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
|
||||
|
||||
|
||||
def _read_usb_product_string(hidraw_path):
|
||||
"""Read the USB product string from sysfs for a hidraw device path."""
|
||||
import pathlib
|
||||
|
||||
try:
|
||||
# /sys/class/hidraw/hidrawN/device/../../product → USB device product string
|
||||
hidraw_name = pathlib.Path(hidraw_path).name
|
||||
product_path = pathlib.Path("/sys/class/hidraw") / hidraw_name / "device" / ".." / ".." / "product"
|
||||
product = product_path.read_text().strip()
|
||||
return product if product else None
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -16,30 +14,44 @@
|
|||
## 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
|
||||
|
||||
from collections import namedtuple
|
||||
"""Devices (not receivers) known to Solaar.
|
||||
|
||||
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
|
||||
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 .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 = {}
|
||||
|
|
@ -57,24 +69,35 @@ def _D(
|
|||
interface=None,
|
||||
btid=None,
|
||||
):
|
||||
|
||||
if kind is None:
|
||||
kind = (
|
||||
_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
|
||||
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
|
||||
)
|
||||
assert kind is not None, 'descriptor for %s does not have kind set' % name
|
||||
assert kind is not None, f"descriptor for {name} does not have kind set"
|
||||
|
||||
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', '%s has protocol %0.1f, wpid %s' % (name, protocol, w)
|
||||
assert w[0:1] == "4", f"{name} has protocol {protocol:0.1f}, wpid {w}"
|
||||
else:
|
||||
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)
|
||||
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}"
|
||||
|
||||
device_descriptor = _DeviceDescriptor(
|
||||
name=name,
|
||||
|
|
@ -83,26 +106,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, 'duplicate usbid in device descriptors: %s' % (found, )
|
||||
assert found is None, f"duplicate usbid in device descriptors: {found}"
|
||||
if btid:
|
||||
found = get_btid(btid)
|
||||
assert found is None, 'duplicate btid in device descriptors: %s' % (found, )
|
||||
assert found is None, f"duplicate btid in device descriptors: {found}"
|
||||
|
||||
assert codename not in DEVICES, 'duplicate codename in device descriptors: %s' % (DEVICES[codename], )
|
||||
assert codename not in DEVICES, f"duplicate codename in device descriptors: {DEVICES[codename]}"
|
||||
if codename:
|
||||
DEVICES[codename] = device_descriptor
|
||||
|
||||
if wpid:
|
||||
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], )
|
||||
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]}"
|
||||
DEVICES_WPID[w] = device_descriptor
|
||||
|
||||
|
||||
|
|
@ -157,176 +179,290 @@ 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. 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.
|
||||
# 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
|
||||
# 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=(_R.battery_status, ))
|
||||
_D('Wireless Keyboard S510', codename='S510', protocol=1.0, wpid='0056', registers=(_R.battery_status, ))
|
||||
_D('Wireless Wave Keyboard K550', codename='K550', protocol=1.0, wpid='0060', registers=(_R.battery_status, ),
|
||||
settings=[_ST.RegisterFnSwap])
|
||||
_D('Wireless Keyboard EX100', codename='EX100', protocol=1.0, wpid='0065', registers=(_R.battery_status, ))
|
||||
_D('Wireless Keyboard MK300', codename='MK300', protocol=1.0, wpid='0068', registers=(_R.battery_status, ))
|
||||
_D('Number Pad N545', codename='N545', protocol=1.0, wpid='2006', registers=(_R.battery_status, ))
|
||||
_D('Wireless Compact Keyboard K340', codename='K340', protocol=1.0, wpid='2007', registers=(_R.battery_status, ))
|
||||
_D('Wireless Keyboard MK700', codename='MK700', protocol=1.0, wpid='2008',
|
||||
registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap])
|
||||
_D('Wireless Wave Keyboard K350', codename='K350', protocol=1.0, wpid='200A', registers=(_R.battery_status, ))
|
||||
_D('Wireless Keyboard MK320', codename='MK320', protocol=1.0, wpid='200F', registers=(_R.battery_status, ))
|
||||
_D('Wireless Illuminated Keyboard K800', codename='K800', protocol=1.0, wpid='2010',
|
||||
registers=(_R.battery_status, _R.three_leds), settings=[_ST.RegisterFnSwap, _ST.RegisterHandDetection])
|
||||
_D('Wireless Keyboard K520', codename='K520', protocol=1.0, wpid='2011',
|
||||
registers=(_R.battery_status, ), settings=[_ST.RegisterFnSwap])
|
||||
_D('Wireless Solar Keyboard K750', codename='K750', protocol=2.0, wpid='4002', settings=[_ST.FnSwap])
|
||||
_D('Wireless Keyboard K270 (unifying)', codename='K270', protocol=2.0, wpid='4003')
|
||||
_D('Wireless Keyboard K360', codename='K360', protocol=2.0, wpid='4004', settings=[_ST.FnSwap])
|
||||
_D('Wireless Keyboard K230', codename='K230', protocol=2.0, wpid='400D')
|
||||
_D('Wireless Touch Keyboard K400', codename='K400', protocol=2.0, wpid=('400E', '4024'), settings=[_ST.FnSwap])
|
||||
_D('Wireless Keyboard MK270', codename='MK270', protocol=2.0, wpid='4023', settings=[_ST.FnSwap])
|
||||
_D('Illuminated Living-Room Keyboard K830', codename='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', codename='K780', protocol=4.5, wpid='405B', settings=[_ST.NewFnSwap])
|
||||
_D('Wireless Keyboard K375s', codename='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('Wireless Keyboard K470', codename='K470', protocol=4.5, wpid='4075', 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('Illuminated Keyboard', codename='Illuminated', protocol=1.0, usbid=0xc318, interface=1, settings=[_ST.RegisterFnSwap])
|
||||
_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', settings=[_ST.RegisterFnSwap])
|
||||
_D('K845 Mechanical Keyboard', codename='K845', usbid=0xc341, interface=3)
|
||||
_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")
|
||||
# incorrect _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=1)
|
||||
|
||||
# Mice
|
||||
|
||||
_D('LX5 Cordless Mouse', codename='LX5', protocol=1.0, wpid='0036', registers=(_R.battery_status, ))
|
||||
_D('LX7 Cordless Laser Mouse', codename='LX7', protocol=1.0, wpid='0039', registers=(_R.battery_status, ))
|
||||
_D('Wireless Wave Mouse M550', codename='M550', protocol=1.0, wpid='003C', 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("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)
|
||||
|
||||
|
||||
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', codename='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', codename='M305', protocol=1.0, wpid='101F',
|
||||
registers=(_R.battery_status, ), settings=[_ST.RegisterSideScroll])
|
||||
_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=(_R.battery_status, _R.three_leds,), settings=[_ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
|
||||
_D('Wireless Mouse M310', codename='M310', protocol=1.0, wpid='1024', registers=(_R.battery_status, ))
|
||||
_D('Wireless Mouse M510', codename='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', 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', settings=[_ST.HiResScroll])
|
||||
_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', settings=[_ST.HiresSmoothInvert])
|
||||
_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,
|
||||
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', 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('G9 Laser Mouse', codename='G9', usbid=0xc048, interface=1, protocol=1.0,
|
||||
settings=[_PerformanceMXDpi, _ST.RegisterSmoothScroll, _ST.RegisterSideScroll])
|
||||
_D('G9x Laser Mouse', codename='G9x', usbid=0xc066, 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(None, kind=_DK.mouse, usbid=0xc092, interface=1) # two mice share this ID
|
||||
_D('M500S Mouse', codename='M500S', usbid=0xc093, interface=1)
|
||||
_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)
|
||||
# _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("G500 Gaming Mouse", codename="G500 Gaming", usbid=0xC068, interface=1, protocol=1.0)
|
||||
_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("Logitech MX Revolution Mouse M-RCL 124", codename="M-RCL 124", btid=0xB007, interface=1)
|
||||
|
||||
# Trackballs
|
||||
|
||||
_D('Wireless Trackball M570', codename='M570')
|
||||
_D("Wireless Trackball M570", codename="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("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
|
||||
|
||||
# Headset
|
||||
|
||||
_D('G533 Gaming Headset', codename='G533 Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0a66)
|
||||
_D('G535 Gaming Headset', codename='G535 Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0ac4)
|
||||
_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('G733 Gaming Headset', codename='G733 Headset New', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0afe)
|
||||
_D('PRO X Wireless Gaming Headset', codename='PRO Headset', protocol=2.0, interface=3, kind=_DK.headset, usbid=0x0aba)
|
||||
_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,
|
||||
)
|
||||
# PRO X 2 LIGHTSPEED Gaming Headset (0x0AF7) — fully probed via Centurion transport, no static descriptor needed
|
||||
|
|
|
|||
|
|
@ -0,0 +1,129 @@
|
|||
## 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
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
## 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
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -15,382 +13,275 @@
|
|||
## 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 logging import getLogger # , DEBUG as _DEBUG
|
||||
import logging
|
||||
|
||||
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 import Any
|
||||
|
||||
_log = getLogger(__name__)
|
||||
del getLogger
|
||||
from typing_extensions import Protocol
|
||||
|
||||
#
|
||||
# Constants - most of them as defined by the official Logitech HID++ 1.0
|
||||
# documentation, some of them guessed.
|
||||
#
|
||||
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
|
||||
|
||||
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
|
||||
#
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def read_register(device, register_number, *params):
|
||||
assert device is not None, 'tried to read register %02X from invalid device %s' % (register_number, device)
|
||||
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}"
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8100 | (int(register_number) & 0x2FF)
|
||||
request_id = 0x8100 | (int(register) & 0x2FF)
|
||||
return device.request(request_id, *params)
|
||||
|
||||
|
||||
def write_register(device, register_number, *value):
|
||||
assert device is not None, 'tried to write register %02X to invalid device %s' % (register_number, device)
|
||||
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}"
|
||||
# support long registers by adding a 2 in front of the register number
|
||||
request_id = 0x8000 | (int(register_number) & 0x2FF)
|
||||
request_id = 0x8000 | (int(register) & 0x2FF)
|
||||
return device.request(request_id, *value)
|
||||
|
||||
|
||||
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 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])
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
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)
|
||||
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
|
||||
|
||||
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)
|
||||
# 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):
|
||||
flags = self._get_register(device, Registers.NOTIFICATIONS)
|
||||
if flags is not None:
|
||||
return NotificationFlag(flags)
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def parse_battery_status(register, reply):
|
||||
if register == REGISTERS.battery_charge:
|
||||
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:
|
||||
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
|
||||
|
||||
if register == REGISTERS.battery_status:
|
||||
battery_status = status_byte_to_battery_status(status_byte)
|
||||
return Battery(charge, None, battery_status, None)
|
||||
|
||||
if register == Registers.BATTERY_STATUS:
|
||||
status_byte = ord(reply[:1])
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
status_text = charging_byte_to_status_text(charging_byte)
|
||||
charge = status_byte_to_charge(status_byte)
|
||||
|
||||
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 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)
|
||||
return Battery(charge, None, status_text, None)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,256 @@
|
|||
## 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 IntEnum
|
||||
from enum import IntFlag
|
||||
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(IntFlag):
|
||||
"""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, flags) -> List[str]:
|
||||
"""Extract the names of the flags from the integer."""
|
||||
if flags is None:
|
||||
return []
|
||||
if flags.name is not None:
|
||||
return flags.name.replace("_", " ").lower().split("|")
|
||||
# Python < 3.11: .name is None for composite flags, decompose manually
|
||||
return [m.name.replace("_", " ").lower() for m in cls if m.value and m in flags]
|
||||
|
||||
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(flags, fallback: str) -> str:
|
||||
flag_names = []
|
||||
if flags is not None and flags is not False:
|
||||
if flags.value == 0:
|
||||
flag_names = (fallback,)
|
||||
else:
|
||||
flag_names = NotificationFlag.flag_names(flags)
|
||||
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
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.name.lower().replace("_", " ")
|
||||
|
||||
|
||||
class BoltPairingError(IntEnum):
|
||||
DEVICE_TIMEOUT = 0x01
|
||||
FAILED = 0x02
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.name.lower().replace("_", " ")
|
||||
|
||||
|
||||
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(IntFlag):
|
||||
"""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
|
||||
|
|
@ -0,0 +1,385 @@
|
|||
## 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
|
||||
PROPERTY_ACCESS = 0x0011
|
||||
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
|
||||
BLE_PRO_PRE_PAIRING = 0x1816
|
||||
BACKLIGHT = 0x1981
|
||||
BACKLIGHT2 = 0x1982
|
||||
BACKLIGHT3 = 0x1983
|
||||
ILLUMINATION = 0x1990
|
||||
FORCE_SENSING_BUTTON = 0x19C0
|
||||
HAPTIC = 0x19B0
|
||||
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
|
||||
ANALOG_BUTTONS = 0x1B0C # Analog button tuning (actuation point, rapid trigger, haptics)
|
||||
FULL_KEY_CUSTOMIZATION = 0x1B05
|
||||
CONTROL_LIST = 0x1B10
|
||||
SWITCH_SWAPABILITY = 0x1B20
|
||||
DEVICE_MODE = 0x1B30
|
||||
REPORT_HID_USAGE = 0x1BC0
|
||||
PERSISTENT_REMAPPABLE_ACTION = 0x1C00
|
||||
WIRELESS_DEVICE_STATUS = 0x1D4B
|
||||
REMAINING_PAIRING = 0x1DF0
|
||||
ENABLE_HIDDEN_FEATURES = 0x1E00
|
||||
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
|
||||
KEYBOARD_DISABLE_CONTROLS = 0x4523
|
||||
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
|
||||
LOGI_MODIFIERS = 0x8051
|
||||
REPORT_RATE = 0x8060
|
||||
EXTENDED_ADJUSTABLE_REPORT_RATE = 0x8061
|
||||
COLOR_LED_EFFECTS = 0x8070
|
||||
RGB_EFFECTS = 0x8071
|
||||
RPM_INDICATOR = 0x807A
|
||||
RPM_LED_PATTERN = 0x807B
|
||||
PER_KEY_LIGHTING = 0x8080
|
||||
PER_KEY_LIGHTING_V2 = 0x8081
|
||||
MODE_STATUS = 0x8090
|
||||
LEGACY_AXIS_RESPONSE_CURVE = 0x80A3
|
||||
AXIS_RESPONSE_CURVE = 0x80A4
|
||||
BANDED_AXIS = 0x80B1
|
||||
COMBINED_PEDALS = 0x80D0
|
||||
BUNNY_HOPPING = 0x80E0
|
||||
ONBOARD_PROFILES = 0x8100
|
||||
PROFILE_MANAGEMENT = 0x8101
|
||||
MOUSE_BUTTON_SPY = 0x8110
|
||||
LATENCY_MONITORING = 0x8111
|
||||
GAMING_ATTACHMENTS = 0x8120
|
||||
FORCE_FEEDBACK = 0x8123
|
||||
DUAL_CLUTCH = 0x8127
|
||||
WHEEL_CENTER_POSITION = 0x812C
|
||||
DISPLAY_GAME_DATA = 0x8130
|
||||
CENTER_SPRING = 0x8131
|
||||
AXIS_MAPPING = 0x8132
|
||||
GLOBAL_DAMPING = 0x8133
|
||||
BRAKE_FORCE = 0x8134
|
||||
PEDAL_STATUS = 0x8135
|
||||
TORQUE_LIMIT = 0x8136
|
||||
CONFIGURATION_PROFILES = 0x8137
|
||||
OPERATING_RANGE = 0x8138
|
||||
TRUE_FORCE = 0x8139
|
||||
FFB_FILTER = 0x8140
|
||||
# Headsets
|
||||
SIDETONE = 0x8300
|
||||
EQUALIZER = 0x8310
|
||||
HEADSET_OUT = 0x8320
|
||||
# Centurion core
|
||||
CENTURION_DEVICE_INFO = 0x0100
|
||||
CENTURION_DEVICE_NAME = 0x0101
|
||||
CENTURION_ROOT = 0x0102
|
||||
CENTURION_MEMFAULT = 0x0103
|
||||
CENTURION_BATTERY_SOC = 0x0104
|
||||
CENTURION_AUTO_SLEEP = 0x0108
|
||||
CENTURION_GENERIC_DFU = 0x010A
|
||||
CENTURION_LED_BRIGHTNESS = 0x0110
|
||||
CENTURION_EU_POWER_MODE = 0x0115
|
||||
CENTURION_DEVICE_BOOL_STATE = 0x0116
|
||||
# Headsets (Centurion-era)
|
||||
HEADSET_VOLUME = 0x0200
|
||||
HEADSET_EQ = 0x0201
|
||||
HEADSET_ADVANCED_PARA_EQ = 0x020D
|
||||
HEADSET_MIC_TEST = 0x020E
|
||||
HEADSET_EQ_STYLES = 0x0213
|
||||
BT_HOST_INFO = 0x0305
|
||||
LIGHTSPEED_PAIRING = 0x0309
|
||||
BT_GAMING_MODE = 0x030A
|
||||
HEADSET_RGB_EFFECTS = 0x0600
|
||||
HEADSET_MIC_MUTE = 0x0601
|
||||
HEADSET_MIC_SNR = 0x0602
|
||||
HEADSET_AUDIO_SIDETONE = 0x0604
|
||||
HEADSET_HOST_SWITCH = 0x0607
|
||||
HEADSET_MIX = 0x0609
|
||||
HEADSET_TONES = 0x060B
|
||||
HEADSET_NOISE_EXPOSURE = 0x060D
|
||||
HEADSET_AI_NOISE_REDUCTION = 0x060E
|
||||
HEADSET_MIC_GAIN = 0x0611
|
||||
HEADSET_USAGE_TRACKING = 0x0617
|
||||
HEADSET_BATTERY_SAVER = 0x0618
|
||||
HEADSET_RGB_HOSTMODE = 0x0620
|
||||
HEADSET_RGB_ONBOARD_EFFECTS = 0x0621
|
||||
HEADSET_RGB_SIGNATURE_EFFECTS = 0x0622
|
||||
HEADSET_DO_NOT_DISTURB = 0x0631
|
||||
CENTURION_ONBOARD_PROFILES = 0x0634
|
||||
HEADSET_RGB_STREAMING = 0x0635
|
||||
HEADSET_ONBOARD_EQ = 0x0636
|
||||
# Audio mixing / LogiVoice
|
||||
MIXER_AUDIO = 0x0800
|
||||
MIXER_MIC = 0x0801
|
||||
LOGIVOICE = 0x0900
|
||||
LOGIVOICE_NOISE_REDUCTION = 0x0901
|
||||
LOGIVOICE_NOISE_GATE = 0x0902
|
||||
LOGIVOICE_COMPRESSOR = 0x0903
|
||||
LOGIVOICE_DE_ESSER = 0x0904
|
||||
LOGIVOICE_DE_POPPER = 0x0905
|
||||
LOGIVOICE_LIMITER = 0x0906
|
||||
LOGIVOICE_HIGH_PASS_FILTER = 0x0907
|
||||
LOGIVOICE_EQUALIZER = 0x0908
|
||||
LOGIVOICE_AINR = 0x0909
|
||||
METERING = 0x0B01
|
||||
MIC_GAIN_AUTO_MODE = 0x0B02
|
||||
# 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
|
||||
|
||||
|
||||
HapticWaveForms = NamedInts(
|
||||
SHARP_STATE_CHANGE=0x00,
|
||||
DAMP_STATE_CHANGE=0x01,
|
||||
SHARP_COLLISION=0x02,
|
||||
DAMP_COLLISION=0x03,
|
||||
SUBTLE_COLLISION=0x04,
|
||||
HAPPY_ALERT=0x05,
|
||||
ANGRY_ALERT=0x06,
|
||||
COMPLETED=0x07,
|
||||
SQUARE=0x08,
|
||||
WAVE=0x09,
|
||||
FIREWORK=0x0A,
|
||||
MAD=0x0B,
|
||||
KNOCK=0x0C,
|
||||
JINGLE=0x0D,
|
||||
RINGING=0xE,
|
||||
WHISPER_COLLISION=0x1B,
|
||||
)
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
# -*- python-mode -*-
|
||||
|
||||
## Copyright (C) 2012-2013 Daniel Pavel
|
||||
##
|
||||
## This program is free software; you can redistribute it and/or modify
|
||||
|
|
@ -18,70 +16,65 @@
|
|||
|
||||
# Translation support for the Logitech receivers library
|
||||
|
||||
import gettext as _gettext
|
||||
import 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"),
|
||||
)
|
||||
|
|
|
|||