Solaar/lib/logitech_receiver/hidpp10_constants.py

258 lines
8.1 KiB
Python

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