Merge branch 'master' into fix_2827
This commit is contained in:
commit
b2b032d8bf
|
|
@ -69,12 +69,15 @@ jobs:
|
|||
- 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: |
|
||||
export DYLD_LIBRARY_PATH=$(brew --prefix hidapi)/lib:$DYLD_LIBRARY_PATH && pytest --cov --cov-report=xml
|
||||
pytest --cov --cov-report=xml
|
||||
- name: Upload coverage to Codecov
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v4.5.0
|
||||
|
|
|
|||
2
Makefile
2
Makefile
|
|
@ -17,7 +17,7 @@ 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 libgirepository-1.0-dev
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -209,6 +209,7 @@ so what is important for support is the USB WPID or Bluetooth model ID.
|
|||
|
||||
| Device | WPID | HID++ |
|
||||
|------------------------------|------|-------|
|
||||
| G604 Wireless Gaming Mouse | 4085 | 4.2 |
|
||||
| PRO X Superlight Wireless | 4093 | 4.2 |
|
||||
|
||||
### Trackballs (Unifying)
|
||||
|
|
|
|||
|
|
@ -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%.
|
||||
|
|
@ -42,8 +42,8 @@ or `make install_dnf` or `make install_brew`.
|
|||
These might not install all needed packages in older versions of your distribution.
|
||||
Next, install the Solaar rule via `make install_udev`.
|
||||
If you are using Wayland instead of X11 you may want to instead `make install_udev_uinput`
|
||||
Finally, install Solaar via `make install_pip` or `make install_pipx`.
|
||||
so that Solaar rules can simulate input in Wayland.
|
||||
Finally, install Solaar via `make install_pip` or `make install_pipx`.
|
||||
|
||||
Parts of the installation process require sudo privileges so you may be asked for your password.
|
||||
|
||||
|
|
@ -92,10 +92,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.
|
||||
|
|
@ -131,6 +132,6 @@ and set the LANGUAGE environment variable appropriately when running Solaar.
|
|||
|
||||
Distributions can cause Solaar can be run automatically at user login by installing a desktop file at
|
||||
`/etc/xdg/autostart/solaar.desktop`. An example of this file content can be seen in the repository at
|
||||
[`share/autostart/solaar.desktop`](/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.
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@ class Device:
|
|||
self.battery_info = None
|
||||
self.link_encrypted = None
|
||||
self._active = None # lags self.online - is used to help determine when to setup devices
|
||||
self.present = True # used for devices that are integral with their receiver but that separately be disconnected
|
||||
|
||||
self._feature_settings_checked = False
|
||||
self._gestures_lock = threading.Lock()
|
||||
|
|
@ -396,8 +397,8 @@ class Device:
|
|||
self.persister["_battery"] = feature.value
|
||||
return battery
|
||||
except Exception:
|
||||
if self.persister and battery_feature is None:
|
||||
self.persister["_battery"] = result
|
||||
if self.persister and battery_feature is None and result is not None:
|
||||
self.persister["_battery"] = result.value
|
||||
|
||||
def set_battery_info(self, info):
|
||||
"""Update battery information for device, calling changed callback if necessary"""
|
||||
|
|
@ -432,6 +433,8 @@ class Device:
|
|||
def changed(self, active=None, alert=Alert.NONE, reason=None, push=False):
|
||||
"""The status of the device had changed, so invoke the status callback.
|
||||
Also push notifications and settings to the device when necessary."""
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("device %d changing: active=%s %s present=%s", self.number, active, self._active, self.present)
|
||||
if active is not None:
|
||||
self.online = active
|
||||
was_active, self._active = self._active, active
|
||||
|
|
@ -533,7 +536,8 @@ class Device:
|
|||
return hidpp20.feature_request(self, feature, function, *params, no_reply=no_reply)
|
||||
|
||||
def ping(self):
|
||||
"""Checks if the device is online, returns True of False"""
|
||||
"""Checks if the device is online and present, returns True of False.
|
||||
Some devices are integral with their receiver but may not be present even if the receiver responds to ping."""
|
||||
long = self.hidpp_long is True or (
|
||||
self.hidpp_long is None and (self.bluetooth or self._protocol is not None and self._protocol >= 2.0)
|
||||
)
|
||||
|
|
@ -542,9 +546,11 @@ class Device:
|
|||
protocol = self.low_level.ping(handle, self.number, long_message=long)
|
||||
except exceptions.NoReceiver: # if ping fails, device is offline
|
||||
protocol = None
|
||||
self.online = protocol is not None
|
||||
self.online = protocol is not None and self.present
|
||||
if protocol:
|
||||
self._protocol = protocol
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("pinged %s: online %s protocol %s present %s", self.number, self.online, protocol, self.present)
|
||||
return self.online
|
||||
|
||||
def notify_devices(self): # no need to notify, as there are none
|
||||
|
|
|
|||
|
|
@ -238,6 +238,7 @@ def _process_hidpp10_notification(device: Device, notification: HIDPPNotificatio
|
|||
|
||||
|
||||
def _process_feature_notification(device: Device, notification: HIDPPNotification):
|
||||
old_present, device.present = device.present, True # the device is generating a feature notification so it must be present
|
||||
try:
|
||||
feature = device.features.get_feature(notification.sub_id)
|
||||
except IndexError:
|
||||
|
|
@ -277,9 +278,11 @@ def _process_feature_notification(device: Device, notification: HIDPPNotificatio
|
|||
elif feature == SupportedFeature.ADC_MEASUREMENT:
|
||||
if notification.address == 0x00:
|
||||
result = hidpp20.decipher_adc_measurement(notification.data)
|
||||
if result:
|
||||
if result: # if good data and the device was not present then a push is needed
|
||||
device.set_battery_info(result[1])
|
||||
else: # this feature is used to signal device becoming inactive
|
||||
device.changed(active=True, alert=Alert.ALL, reason=_("ADC measurement notification"), push=not old_present)
|
||||
else: # this feature is also used to signal device becoming inactive
|
||||
device.present = False # exception to device presence
|
||||
device.changed(active=False)
|
||||
else:
|
||||
logger.warning("%s: unknown ADC MEASUREMENT %s", device, notification)
|
||||
|
|
@ -451,7 +454,7 @@ def handle_discovery_status(receiver: Receiver, notification: HIDPPNotification)
|
|||
receiver.pairing.device_passkey = None
|
||||
discover_error = ord(notification.data[:1])
|
||||
if discover_error:
|
||||
receiver.pairing.error = discover_string = hidpp10_constants.BoltPairingError(discover_error)
|
||||
receiver.pairing.error = discover_string = hidpp10_constants.BoltPairingError(discover_error).name
|
||||
logger.warning("bolt discovering error %d: %s", discover_error, discover_string)
|
||||
receiver.changed(reason=reason)
|
||||
return True
|
||||
|
|
@ -493,7 +496,7 @@ def handle_pairing_status(receiver: Receiver, notification: HIDPPNotification) -
|
|||
elif notification.address == 0x02 and not pair_error:
|
||||
receiver.pairing.new_device = receiver.register_new_device(notification.data[7])
|
||||
if pair_error:
|
||||
receiver.pairing.error = error_string = hidpp10_constants.BoltPairingError(pair_error)
|
||||
receiver.pairing.error = error_string = hidpp10_constants.BoltPairingError(pair_error).name
|
||||
receiver.pairing.new_device = None
|
||||
logger.warning("pairing error %d: %s", pair_error, error_string)
|
||||
receiver.changed(reason=reason)
|
||||
|
|
|
|||
|
|
@ -533,7 +533,7 @@ class Ex100Receiver(Receiver):
|
|||
online = True
|
||||
encrypted = bool(notification.data[0] & 0x80)
|
||||
kind = extract_device_kind(_get_kind_from_index(self, number))
|
||||
wpid = extract_wpid("00" + notification.data[2:3])
|
||||
wpid = "00" + extract_wpid(notification.data[2:3])
|
||||
return online, encrypted, wpid, kind
|
||||
|
||||
def device_pairing_information(self, number: int) -> dict:
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ from . import common
|
|||
from . import descriptors
|
||||
from . import desktop_notifications
|
||||
from . import diversion
|
||||
from . import exceptions
|
||||
from . import hidpp20
|
||||
from . import hidpp20_constants
|
||||
from . import settings
|
||||
|
|
@ -1534,6 +1535,7 @@ class ADCPower(settings.Setting):
|
|||
label = _("Power Management")
|
||||
description = _("Power off in minutes (0 for never).")
|
||||
feature = _F.ADC_MEASUREMENT
|
||||
min_version = 2 # documentation for version 1 does not mention this capability
|
||||
rw_options = {"read_fnid": 0x10, "write_fnid": 0x20}
|
||||
validator_class = settings_validator.RangeValidator
|
||||
min_value = 0x00
|
||||
|
|
@ -1905,7 +1907,7 @@ def check_feature(device, settings_class: SettingsProtocol) -> None | bool | Set
|
|||
logger.error(
|
||||
"check_feature %s [%s] error %s\n%s", settings_class.name, settings_class.feature, e, traceback.format_exc()
|
||||
)
|
||||
return False # differentiate from an error-free determination that the setting is not supported
|
||||
raise e # differentiate from an error-free determination that the setting is not supported
|
||||
|
||||
|
||||
def check_feature_settings(device, already_known) -> bool:
|
||||
|
|
@ -1914,7 +1916,7 @@ def check_feature_settings(device, already_known) -> bool:
|
|||
Returns
|
||||
-------
|
||||
bool
|
||||
True, if device was queried to find features, False otherwise.
|
||||
True, if device was fully queried to find features, False otherwise.
|
||||
"""
|
||||
if not device.features or not device.online:
|
||||
return False
|
||||
|
|
@ -1926,7 +1928,21 @@ def check_feature_settings(device, already_known) -> bool:
|
|||
if sclass.feature:
|
||||
known_present = device.persister and sclass.name in device.persister
|
||||
if not any(s.name == sclass.name for s in already_known) and (known_present or sclass.name not in absent):
|
||||
setting = check_feature(device, sclass)
|
||||
try:
|
||||
setting = check_feature(device, sclass)
|
||||
except Exception as err:
|
||||
# on an internal HID++ error, assume offline and stop further checking
|
||||
if (
|
||||
isinstance(err, exceptions.FeatureCallError)
|
||||
and err.error == hidpp20_constants.ErrorCode.LOGITECH_ERROR
|
||||
):
|
||||
logger.warning(f"HID++ internal error checking feature {sclass.name}: make device not present")
|
||||
device.online = False
|
||||
device.present = False
|
||||
return False
|
||||
else:
|
||||
logger.warning(f"ignore feature {sclass.name} because of error {err}")
|
||||
|
||||
if isinstance(setting, list):
|
||||
for s in setting:
|
||||
already_known.append(s)
|
||||
|
|
@ -1948,6 +1964,9 @@ def check_feature_settings(device, already_known) -> bool:
|
|||
def check_feature_setting(device, setting_name: str) -> settings.Setting | None:
|
||||
for sclass in SETTINGS:
|
||||
if sclass.feature and sclass.name == setting_name and device.features:
|
||||
setting = check_feature(device, sclass)
|
||||
try:
|
||||
setting = check_feature(device, sclass)
|
||||
except Exception:
|
||||
return None
|
||||
if setting:
|
||||
return setting
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
|
||||
import gettext
|
||||
import locale
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ from solaar import NAME
|
|||
|
||||
_LOCALE_DOMAIN = NAME.lower()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _find_locale_path(locale_domain: str) -> str:
|
||||
prefix_share = os.path.normpath(os.path.join(os.path.realpath(sys.path[0]), ".."))
|
||||
|
|
@ -37,18 +40,23 @@ def _find_locale_path(locale_domain: str) -> str:
|
|||
raise FileNotFoundError(f"Could not find locale path for {locale_domain}")
|
||||
|
||||
|
||||
def set_locale_to_system_default():
|
||||
def set_locale_to_system_default() -> None:
|
||||
"""Sets locale for translations to the system default.
|
||||
|
||||
If locale is unsupported, fallback to standard English without
|
||||
translation 'C'.
|
||||
|
||||
Set LC_ALL environment variable to enforce a locale setting e.g.
|
||||
'de_DE.UTF-8'. Run Solaar with your desired localization, for German
|
||||
use:
|
||||
'LC_ALL=de_DE.UTF-8 solaar'
|
||||
"""
|
||||
try:
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
except Exception:
|
||||
pass
|
||||
locale.setlocale(locale.LC_ALL, "") # system default
|
||||
except locale.Error:
|
||||
logger.error("User locale not supported by system, using no translation.")
|
||||
locale.setlocale(locale.LC_ALL, "C") # untranslated (English)
|
||||
return
|
||||
|
||||
try:
|
||||
path = _find_locale_path(_LOCALE_DOMAIN)
|
||||
|
|
|
|||
2
po/sv.po
2
po/sv.po
|
|
@ -274,7 +274,7 @@ msgstr "funktion"
|
|||
|
||||
#: lib/logitech_receiver/settings_templates.py:139
|
||||
msgid "Swap Fx function"
|
||||
msgstr "Skifta Fx-funktion"
|
||||
msgstr "Byt Fx-funktion"
|
||||
|
||||
#: lib/logitech_receiver/settings_templates.py:140
|
||||
msgid ""
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
# because they could perform firmware updates.
|
||||
KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"
|
||||
|
||||
ACTION != "add", GOTO="solaar_end"
|
||||
ACTION == "remove", GOTO="solaar_end"
|
||||
SUBSYSTEM != "hidraw", GOTO="solaar_end"
|
||||
|
||||
# USB-connected Logitech receivers and devices
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
# Allowing users to write to the device is potentially dangerous
|
||||
# because they could perform firmware updates.
|
||||
|
||||
ACTION != "add", GOTO="solaar_end"
|
||||
ACTION == "remove", GOTO="solaar_end"
|
||||
SUBSYSTEM != "hidraw", GOTO="solaar_end"
|
||||
|
||||
# USB-connected Logitech receivers and devices
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@
|
|||
<developer id="pwr-solaar.github.io">
|
||||
<name>pwr-Solaar</name>
|
||||
</developer>
|
||||
<url type="homepage">https://github.com/pwr-Solaar/Solaar</url>
|
||||
<url type="homepage">https://pwr-solaar.github.io/Solaar/</url>
|
||||
<url type="vcs-browser">https://github.com/pwr-Solaar/Solaar</url>
|
||||
<content_rating type="oars-1.0" />
|
||||
<update_contact>pfpschneider_AT_gmail.com</update_contact>
|
||||
|
||||
|
|
|
|||
|
|
@ -389,6 +389,7 @@ class Device:
|
|||
sliding = profiles = _backlight = _keys = _remap_keys = _led_effects = _gestures = None
|
||||
_gestures_lock = threading.Lock()
|
||||
number = "d1"
|
||||
present = True
|
||||
|
||||
read_register = device.Device.read_register
|
||||
write_register = device.Device.write_register
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ def test_process_receiver_notification(sub_id, notification_data, expected_error
|
|||
result = notifications.process_receiver_notification(receiver, notification)
|
||||
|
||||
assert result
|
||||
assert receiver.pairing.error == expected_error
|
||||
assert receiver.pairing.error == (None if expected_error is None else expected_error.name)
|
||||
assert receiver.pairing.new_device is expected_new_device
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -268,7 +268,7 @@ simple_tests = [
|
|||
fake_hidpp.Response("0A", 0x0410, "0A"),
|
||||
),
|
||||
Setup(
|
||||
FeatureTest(settings_templates.ADCPower, 5, 0xA),
|
||||
FeatureTest(settings_templates.ADCPower, 5, 0xA, version=0x03),
|
||||
fake_hidpp.Response("05", 0x0410),
|
||||
fake_hidpp.Response("0A", 0x0420, "0A"),
|
||||
),
|
||||
|
|
|
|||
Loading…
Reference in New Issue