Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
logitech_receiver: Move hidpp10 constants into new module
Browse files Browse the repository at this point in the history
Related #1097
MattHag committed Feb 14, 2024
1 parent 1aa1096 commit 2685d25
Showing 8 changed files with 183 additions and 181 deletions.
12 changes: 6 additions & 6 deletions lib/logitech_receiver/base.py
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@

import hidapi as _hid

from . import hidpp10 as _hidpp10
from . import hidpp10_constants as _hidpp10_constants
from . import hidpp20 as _hidpp20
from .base_usb import ALL as _RECEIVER_USB_IDS
from .base_usb import DEVICES as _DEVICE_IDS
@@ -422,9 +422,9 @@ def request(handle, devnumber, request_id, *params, no_reply=False, return_error
if logger.isEnabledFor(logging.DEBUG):
logger.debug(
'(%s) device 0x%02X error on request {%04X}: %d = %s', handle, devnumber, request_id, error,
_hidpp10.ERROR[error]
_hidpp10_constants.ERROR[error]
)
return _hidpp10.ERROR[error] if return_error else None
return _hidpp10_constants.ERROR[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])
@@ -507,11 +507,11 @@ def ping(handle, devnumber, long_message=False):
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 == _hidpp10_constants.ERROR.invalid_SubID__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 == _hidpp10_constants.ERROR.resource_error or error == _hidpp10_constants.ERROR.connection_request_failed:
return # device unreachable
if error == _hidpp10.ERROR.unknown_device: # no paired device with that number
if error == _hidpp10_constants.ERROR.unknown_device: # no paired device with that number
logger.error('(%s) device %d error on ping request: unknown device', handle, devnumber)
raise exceptions.NoSuchDevice(number=devnumber, request=request_id)

4 changes: 2 additions & 2 deletions lib/logitech_receiver/descriptors.py
Original file line number Diff line number Diff line change
@@ -28,8 +28,8 @@

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
from .hidpp10_constants import DEVICE_KIND as _DK
from .hidpp10_constants import REGISTERS as _R

#
#
8 changes: 4 additions & 4 deletions lib/logitech_receiver/device.py
Original file line number Diff line number Diff line change
@@ -27,17 +27,17 @@

from . import base as _base
from . import descriptors as _descriptors
from . import hidpp10 as _hidpp10
from . import hidpp20 as _hidpp20
from . import hidpp10_constants as _hidpp10_constants
from .common import strhex as _strhex
from .settings_templates import check_feature_settings as _check_feature_settings

logger = logging.getLogger(__name__)

_R = _hidpp10.REGISTERS
_IR = _hidpp10.INFO_SUBREGISTERS
_R = _hidpp10_constants.REGISTERS
_IR = _hidpp10_constants.INFO_SUBREGISTERS

KIND_MAP = {kind: _hidpp10.DEVICE_KIND[str(kind)] for kind in _hidpp20.DEVICE_KIND}
KIND_MAP = {kind: _hidpp10_constants.DEVICE_KIND[str(kind)] for kind in _hidpp20.DEVICE_KIND}

#
#
152 changes: 1 addition & 151 deletions lib/logitech_receiver/hidpp10.py
Original file line number Diff line number Diff line change
@@ -20,164 +20,14 @@

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 .hidpp10_constants import REGISTERS

logger = logging.getLogger(__name__)

#
# Constants - 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,
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
#
151 changes: 151 additions & 0 deletions lib/logitech_receiver/hidpp10_constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from .common import NamedInts

#
# Constants - 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,
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,
)
23 changes: 12 additions & 11 deletions lib/logitech_receiver/receiver.py
Original file line number Diff line number Diff line change
@@ -23,14 +23,15 @@

from . import base as _base
from . import hidpp10 as _hidpp10
from . import hidpp10_constants as _hidpp10_constants
from .base_usb import product_information as _product_information
from .common import strhex as _strhex
from .device import Device

logger = logging.getLogger(__name__)

_R = _hidpp10.REGISTERS
_IR = _hidpp10.INFO_SUBREGISTERS
_R = _hidpp10_constants.REGISTERS
_IR = _hidpp10_constants.INFO_SUBREGISTERS

#
#
@@ -120,9 +121,9 @@ def enable_connection_notifications(self, enable=True):

if enable:
set_flag_bits = (
_hidpp10.NOTIFICATION_FLAG.battery_status
| _hidpp10.NOTIFICATION_FLAG.wireless
| _hidpp10.NOTIFICATION_FLAG.software_present
_hidpp10_constants.NOTIFICATION_FLAG.battery_status
| _hidpp10_constants.NOTIFICATION_FLAG.wireless
| _hidpp10_constants.NOTIFICATION_FLAG.software_present
)
else:
set_flag_bits = 0
@@ -132,7 +133,7 @@ def enable_connection_notifications(self, enable=True):
return None

flag_bits = _hidpp10.get_notification_flags(self)
flag_names = None if flag_bits is None else tuple(_hidpp10.NOTIFICATION_FLAG.flag_names(flag_bits))
flag_names = None if flag_bits is None else tuple(_hidpp10_constants.NOTIFICATION_FLAG.flag_names(flag_bits))
if logger.isEnabledFor(logging.INFO):
logger.info('%s: receiver notifications %s => %s', self, 'enabled' if enable else 'disabled', flag_names)
return flag_bits
@@ -154,7 +155,7 @@ def device_pairing_information(self, n):
pair_info = self.read_register(_R.receiver_info, _IR.bolt_pairing_information + n)
if pair_info:
wpid = _strhex(pair_info[3:4]) + _strhex(pair_info[2:3])
kind = _hidpp10.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F]
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[1:2]) & 0x0F]
return wpid, kind, 0
else:
raise _base.NoSuchDevice(number=n, receiver=self, error='read Bolt wpid')
@@ -164,19 +165,19 @@ def device_pairing_information(self, n):
pair_info = self.read_register(_R.receiver_info, _IR.pairing_information + n - 1)
if pair_info: # may be either a Unifying receiver, or an Unifying-ready receiver
wpid = _strhex(pair_info[3:5])
kind = _hidpp10.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F]
kind = _hidpp10_constants.DEVICE_KIND[ord(pair_info[7:8]) & 0x0F]
polling_rate = str(ord(pair_info[2:3])) + 'ms'
elif self.receiver_kind == '27Mz': # 27Mhz receiver, fill extracting WPID from udev path
wpid = _hid.find_paired_node_wpid(self.path, n)
if not wpid:
logger.error('Unable to get wpid from udev for device %d of %s', n, self)
raise _base.NoSuchDevice(number=n, receiver=self, error='Not present 27Mhz device')
kind = _hidpp10.DEVICE_KIND[self.get_kind_from_index(n)]
kind = _hidpp10_constants.DEVICE_KIND[self.get_kind_from_index(n)]
elif not self.receiver_kind == 'unifying': # unifying protocol not supported, may be an old Nano receiver
device_info = self.read_register(_R.receiver_info, 0x04)
if device_info:
wpid = _strhex(device_info[3:5])
kind = _hidpp10.DEVICE_KIND[0x00] # unknown kind
kind = _hidpp10_constants.DEVICE_KIND[0x00] # unknown kind
else:
raise _base.NoSuchDevice(number=n, receiver=self, error='read pairing information - non-unifying')
else:
@@ -195,7 +196,7 @@ def device_extended_pairing_information(self, n):
return '?', power_switch
pair_info = self.read_register(_R.receiver_info, _IR.extended_pairing_information + n - 1)
if pair_info:
power_switch = _hidpp10.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F]
power_switch = _hidpp10_constants.POWER_SWITCH_LOCATION[ord(pair_info[9:10]) & 0x0F]
else: # some Nano receivers?
pair_info = self.read_register(0x2D5)
if pair_info:
6 changes: 3 additions & 3 deletions lib/logitech_receiver/settings_templates.py
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@
from struct import unpack as _unpack
from time import time as _time

from . import hidpp10 as _hidpp10
from . import hidpp10_constants as _hidpp10_constants
from . import hidpp20 as _hidpp20
from . import special_keys as _special_keys
from .common import NamedInt as _NamedInt
@@ -53,8 +53,8 @@

logger = logging.getLogger(__name__)

_DK = _hidpp10.DEVICE_KIND
_R = _hidpp10.REGISTERS
_DK = _hidpp10_constants.DEVICE_KIND
_R = _hidpp10_constants.REGISTERS
_F = _hidpp20.FEATURE

_GG = _hidpp20.GESTURE
8 changes: 4 additions & 4 deletions lib/solaar/listener.py
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@

from logitech_receiver import Device, Receiver
from logitech_receiver import base as _base
from logitech_receiver import hidpp10 as _hidpp10
from logitech_receiver import hidpp10_constants as _hidpp10_constants
from logitech_receiver import listener as _listener
from logitech_receiver import notifications as _notifications
from logitech_receiver import status as _status
@@ -40,8 +40,8 @@

logger = logging.getLogger(__name__)

_R = _hidpp10.REGISTERS
_IR = _hidpp10.INFO_SUBREGISTERS
_R = _hidpp10_constants.REGISTERS
_IR = _hidpp10_constants.INFO_SUBREGISTERS

#
#
@@ -87,7 +87,7 @@ def has_started(self):
logger.info('%s: notifications listener has started (%s)', self.receiver, self.receiver.handle)
nfs = self.receiver.enable_connection_notifications()
if logger.isEnabledFor(logging.WARNING):
if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10.NOTIFICATION_FLAG.wireless):
if not self.receiver.isDevice and not ((nfs if nfs else 0) & _hidpp10_constants.NOTIFICATION_FLAG.wireless):
logger.warning(
'Receiver on %s might not support connection notifications, GUI might not show its devices',
self.receiver.path

0 comments on commit 2685d25

Please sign in to comment.