Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor settings and related code #2660

Merged
merged 28 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
a2021ba
Split up huge settings module
MattHag Nov 3, 2024
b0599e7
Refactor: Convert Kind to IntEnum
MattHag Nov 3, 2024
cc743b8
type hints: Introduce settings protocol
MattHag Nov 3, 2024
9dbd368
Refactor: Remove diversion alias
MattHag Nov 3, 2024
6869f19
Simplify settings UI class
MattHag Nov 3, 2024
aff2872
Enforce rules on RuleComponentUI subclasses
MattHag Nov 3, 2024
4213a1a
settings: Add docstrings and type hint
MattHag Nov 3, 2024
6207ff1
Remove NamedInts: Convert Column to enum
MattHag Nov 4, 2024
9a60d65
Remove NamedInts: Convert Task to enum
MattHag Nov 4, 2024
e412279
Add type hints
MattHag Nov 4, 2024
8d8d540
key flags: Move to module of use
MattHag Nov 4, 2024
6c4612f
Add type hints
MattHag Nov 4, 2024
f1ee1c4
Remove NamedInts: Convert KeyFlag to Flag
MattHag Nov 5, 2024
3f0d3ed
Remove NamedInts: Convert Spec to enum
MattHag Nov 5, 2024
41ac048
Remove NamedInts: Convert ActionId to enum
MattHag Nov 5, 2024
729c713
mapping flag: Move to module of use
MattHag Nov 5, 2024
3bee2b1
Remove NamedInts: Convert MappingFlag to flag
MattHag Nov 5, 2024
6d51b22
Remove NamedInts: Convert PowerSwitchLocation to flag
MattHag Nov 5, 2024
b9154bf
Remove NamedInts: Convert HorizontalScroll to enum
MattHag Nov 5, 2024
55b5c08
Remove NamedInts: Convert LedRampChoice to flag
MattHag Nov 5, 2024
1aadc6c
charge status: Refactor to enum and move to module of use
MattHag Nov 5, 2024
8d51658
Remove NamedInts: Convert LedFormChoices to enum
MattHag Nov 5, 2024
726eedc
Fix KeyFlag conversion
MattHag Nov 5, 2024
de6d8b4
Fixes on top of refactoring
MattHag Nov 5, 2024
ad9e65b
Prepare refactoring of NotificationFlag
MattHag Nov 16, 2024
dcf5460
Remove NamedInts: Convert NotificationFlag to flag
MattHag Nov 16, 2024
b42da96
Remove NamedInts: Convert DeviceFeature to flag
MattHag Nov 16, 2024
5333e0d
Refactor InfoSubRegisters: Use IntEnum in favour of NamedInts
Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lib/hidapi/hidapi_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def run(self):

def _match(
action: str,
device,
device: dict[str, Any],
filter_func: Callable[[int, int, int, bool, bool], dict[str, Any]],
):
"""
Expand Down Expand Up @@ -393,7 +393,7 @@ def open(vendor_id, product_id, serial=None):
return device_handle


def open_path(device_path) -> Any:
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().
Expand Down
6 changes: 3 additions & 3 deletions lib/logitech_receiver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ 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):
def open_path(self, path) -> int:
...

def enumerate(self, filter_func: Callable[[int, int, int, bool, bool], dict[str, typing.Any]]) -> DeviceInfo:
Expand Down Expand Up @@ -233,7 +233,7 @@ def notify_on_receivers_glib(glib: GLib, callback: Callable):
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.
Expand Down Expand Up @@ -356,7 +356,7 @@ def _is_relevant_message(data: bytes) -> bool:
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`.
Expand Down
6 changes: 3 additions & 3 deletions lib/logitech_receiver/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import dataclasses
import typing

from enum import Flag
from enum import IntEnum
from typing import Generator
from typing import Iterable
Expand Down Expand Up @@ -589,7 +590,7 @@ class FirmwareInfo:
extras: str | None


class BatteryStatus(IntEnum):
class BatteryStatus(Flag):
DISCHARGING = 0x00
RECHARGING = 0x01
ALMOST_FULL = 0x02
Expand Down Expand Up @@ -649,8 +650,7 @@ def to_str(self) -> str:
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)}
else:
return ""
return ""


class Alert(IntEnum):
Expand Down
18 changes: 10 additions & 8 deletions lib/logitech_receiver/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import time
import typing

from typing import Any
from typing import Callable
from typing import Optional
from typing import Protocol
Expand All @@ -39,6 +38,7 @@
from . import settings_templates
from .common import Alert
from .common import Battery
from .hidpp10_constants import NotificationFlag
from .hidpp20_constants import SupportedFeature

if typing.TYPE_CHECKING:
Expand All @@ -51,7 +51,7 @@


class LowLevelInterface(Protocol):
def open_path(self, path) -> Any:
def open_path(self, path) -> int:
...

def find_paired_node(self, receiver_path: str, index: int, timeout: int):
Expand Down Expand Up @@ -468,10 +468,8 @@ def enable_connection_notifications(self, enable=True):

if enable:
set_flag_bits = (
hidpp10_constants.NOTIFICATION_FLAG.battery_status
| hidpp10_constants.NOTIFICATION_FLAG.ui
| hidpp10_constants.NOTIFICATION_FLAG.configuration_complete
)
NotificationFlag.BATTERY_STATUS | NotificationFlag.UI | NotificationFlag.CONFIGURATION_COMPLETE
).value
else:
set_flag_bits = 0
ok = _hidpp10.set_notification_flags(self, set_flag_bits)
Expand All @@ -480,8 +478,12 @@ def enable_connection_notifications(self, enable=True):

flag_bits = _hidpp10.get_notification_flags(self)
if logger.isEnabledFor(logging.INFO):
flag_names = None if flag_bits is None else tuple(hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits))
logger.info("%s: device notifications %s %s", self, "enabled" if enable else "disabled", flag_names)
if flag_bits is None:
flag_names = None
else:
flag_names = hidpp10_constants.NotificationFlag.flag_names(flag_bits)
is_enabled = "enabled" if enable else "disabled"
logger.info(f"{self}: device notifications {is_enabled} {flag_names}")
return flag_bits if ok else None

def add_notification_handler(self, id: str, fn):
Expand Down
5 changes: 3 additions & 2 deletions lib/logitech_receiver/hidpp10.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .common import BatteryLevelApproximation
from .common import BatteryStatus
from .common import FirmwareKind
from .hidpp10_constants import NotificationFlag
from .hidpp10_constants import Registers

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -190,7 +191,7 @@ def set_3leds(self, device: Device, battery_level=None, charging=None, warning=N
def get_notification_flags(self, device: Device):
return self._get_register(device, Registers.NOTIFICATIONS)

def set_notification_flags(self, device: Device, *flag_bits):
def set_notification_flags(self, device: Device, *flag_bits: NotificationFlag):
assert device is not None

# Avoid a call if the device is not online,
Expand All @@ -200,7 +201,7 @@ def set_notification_flags(self, device: Device, *flag_bits):
if device.protocol and device.protocol >= 2.0:
return

flag_bits = sum(int(b) for b in flag_bits)
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
Expand Down
189 changes: 116 additions & 73 deletions lib/logitech_receiver/hidpp10_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
## You should have received a copy of the GNU General Public License along
## with this program; if not, write to the Free Software Foundation, Inc.,
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
from __future__ import annotations

from enum import Flag
from enum import IntEnum
from typing import List

from .common import NamedInts

Expand All @@ -41,51 +45,86 @@
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,
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,
)
class PowerSwitchLocation(IntEnum):
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


class NotificationFlag(Flag):
"""Some flags are used both by devices and receivers.

The Logitech documentation mentions that the first and last (third)
byte are used for devices while the second is used for the receiver.
In practise, the second byte is also used for some device-specific
notifications (keyboard illumination level). Do not simply set all
notification bits if the software does not support it. For example,
enabling keyboard_sleep_raw makes the Sleep key a no-operation
unless the software is updated to handle that event.

Observations:
- wireless and software present seen on receivers,
reserved_r1b4 as well
- the rest work only on devices as far as we can tell right now
In the future would be useful to have separate enums for receiver
and device notification flags, but right now we don't know enough.
Additional flags taken from https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
"""

@classmethod
def flag_names(cls, flag_bits: int) -> List[str]:
"""Extract the names of the flags from the integer."""
indexed = {item.value: item.name for item in cls}

flag_names = []
unknown_bits = flag_bits
for k in indexed:
# Ensure that the key (flag value) is a power of 2 (a single bit flag)
assert bin(k).count("1") == 1
if k & flag_bits == k:
unknown_bits &= ~k
flag_names.append(indexed[k].replace("_", " ").lower())

# Yield any remaining unknown bits
if unknown_bits != 0:
flag_names.append(f"unknown:{unknown_bits:06X}")
return flag_names

NUMPAD_NUMERICAL_KEYS = 0x800000
F_LOCK_STATUS = 0x400000
ROLLER_H = 0x200000
BATTERY_STATUS = 0x100000 # send battery charge notifications (0x07 or 0x0D)
MOUSE_EXTRA_BUTTONS = 0x080000
ROLLER_V = 0x040000
POWER_KEYS = 0x020000 # system control keys such as Sleep
KEYBOARD_MULTIMEDIA_RAW = 0x010000 # consumer controls such as Mute and Calculator
MULTI_TOUCH = 0x001000 # notify on multi-touch changes
SOFTWARE_PRESENT = 0x000800 # software is controlling part of device behaviour
LINK_QUALITY = 0x000400 # notify on link quality changes
UI = 0x000200 # notify on UI changes
WIRELESS = 0x000100 # notify when the device wireless goes on/off-line
CONFIGURATION_COMPLETE = 0x000004
VOIP_TELEPHONY = 0x000002
THREED_GESTURE = 0x000001


def flags_to_str(flag_bits: int | None, fallback: str) -> str:
flag_names = []
if flag_bits is not None:
if flag_bits == 0:
flag_names = (fallback,)
else:
flag_names = NotificationFlag.flag_names(flag_bits)
return f"\n{' ':15}".join(sorted(flag_names))


class ErrorCode(IntEnum):
Expand Down Expand Up @@ -155,33 +194,37 @@ class Registers(IntEnum):


# 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,
)
class InfoSubRegisters(IntEnum):
SERIAL_NUMBER = 0x01 # not found on many receivers
FW_VERSION = 0x02
RECEIVER_INFORMATION = 0x03
PAIRING_INFORMATION = 0x20 # 0x2N, by connected device
EXTENDED_PAIRING_INFORMATION = 0x30 # 0x3N, by connected device
DEVICE_NAME = 0x40 # 0x4N, by connected device
BOLT_PAIRING_INFORMATION = 0x50 # 0x5N, by connected device
BOLT_DEVICE_NAME = 0x60 # 0x6N01, by connected device


class DeviceFeature(Flag):
"""Features for devices.

Flags taken from
https://drive.google.com/file/d/0BxbRzx7vEV7eNDBheWY0UHM5dEU/view?usp=sharing
"""

# 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,
)
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
Loading
Loading