258 lines
8.1 KiB
Python
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
|