Add CenturionCoreFeature enum for colliding feature IDs

Centurion transport reuses HID++ 2.0 feature IDs 0x0000, 0x0001,
0x0003, 0x0005, 0x0007 with different meanings. Since SupportedFeature
(IntEnum) requires unique values, create a separate CenturionCoreFeature
enum and resolve_feature() helper for transport-aware lookup.

Also replace the +0x100 offset hack in FeaturesArray.inverse with a
dedicated sub_inverse dict for sub-device feature indexing.
This commit is contained in:
Ken Sanislo 2026-03-10 21:10:29 -07:00
parent 8e074524ce
commit fc874710cd
5 changed files with 55 additions and 15 deletions

View File

@ -39,6 +39,7 @@ from .common import Alert
from .common import Battery
from .common import BatteryStatus
from .common import FirmwareKind
from .centurion_constants import CenturionCoreFeature
from .common import _read_usb_product_string
from .hidpp20_constants import SupportedFeature
@ -395,7 +396,7 @@ class CenturionReceiver:
dev._centurion_usb_name = self._usb_name
# Pre-set bridge index from dongle features so ping can probe the headset
for _feat, feat_id, idx in self._dongle_features or []:
if feat_id == 0x0003: # CentPPBridge
if feat_id == CenturionCoreFeature.CENT_PP_BRIDGE:
dev._centurion_bridge_index = idx
break
@ -493,7 +494,7 @@ def create_centurion_receiver(low_level, device_info, setting_callback=None):
base._centurion_handles.add(int(handle))
cr = CenturionReceiver(low_level, handle, device_info, setting_callback)
# Check if any discovered feature is CentPPBridge (0x0003)
has_bridge = any(feat_id == 0x0003 for _, feat_id, _ in (cr.dongle_features or []))
has_bridge = any(feat_id == CenturionCoreFeature.CENT_PP_BRIDGE for _, feat_id, _ in (cr.dongle_features or []))
if not has_bridge:
logger.info("Centurion device %s has no bridge, treating as direct device", device_info.path)
base._centurion_handles.discard(int(handle))

View File

@ -0,0 +1,38 @@
"""Centurion transport-specific constants.
Feature IDs that collide with HID++ 2.0 core features live here
so they can coexist with SupportedFeature (which requires unique values).
"""
from __future__ import annotations
from enum import IntEnum
from .hidpp20_constants import SupportedFeature
class CenturionCoreFeature(IntEnum):
"""Centurion transport-specific features that collide with HID++ 2.0 core IDs."""
CENTURION_ROOT = 0x0000
CENTURION_FEATURE_SET = 0x0001
CENT_PP_BRIDGE = 0x0003
MULTI_HOST_CONTROL = 0x0005
KEEP_ALIVE = 0x0007
def __str__(self):
return self.name.replace("_", " ")
def resolve_feature(feat_id: int, centurion: bool = False):
"""Resolve a feature ID to the appropriate enum, checking centurion-specific
features first when on the centurion transport."""
if centurion:
try:
return CenturionCoreFeature(feat_id)
except ValueError:
pass
try:
return SupportedFeature(feat_id)
except ValueError:
return None

View File

@ -52,6 +52,8 @@ from .hidpp20_constants import ErrorCode
from .hidpp20_constants import FeatureFlag
from .hidpp20_constants import GestureId
from .hidpp20_constants import ParamId
from .centurion_constants import CenturionCoreFeature
from .centurion_constants import resolve_feature
from .hidpp20_constants import SupportedFeature
logger = logging.getLogger(__name__)
@ -140,6 +142,7 @@ class FeaturesArray(dict):
self.supported = True # Actually don't know whether it is supported yet
self.device = device
self.inverse = {}
self.sub_inverse = {}
self.version = {}
self.flags = {}
self.count = 0
@ -195,14 +198,12 @@ class FeaturesArray(dict):
continue
# Centurion FeatureSet response: [remaining_count, feat_hi, feat_lo, type, flags]
feat_id = struct.unpack("!H", response[1:3])[0]
try:
feature = SupportedFeature(feat_id)
except ValueError:
feature = resolve_feature(feat_id, centurion=True)
if feature is None:
feature = f"unknown:{feat_id:04X}"
self[feature] = index
self.inverse[index] = feature
# Feature 0x0003 on Centurion = CentPPBridge (not FirmwareInfo)
if feat_id == 0x0003:
if feature is CenturionCoreFeature.CENT_PP_BRIDGE:
bridge_index = index
if bridge_index is not None:
@ -257,8 +258,8 @@ class FeaturesArray(dict):
if dict.get(self, feature) is None:
dict.__setitem__(self, feature, sub_feat_idx)
self.device._centurion_sub_features.add(feature)
# Always store in offset inverse for sub-device enumerate/display
self.inverse[sub_feat_idx + 0x100] = feature
# Always store in sub_inverse for sub-device enumerate/display
self.sub_inverse[sub_feat_idx] = feature
if logger.isEnabledFor(logging.DEBUG):
logger.debug("Centurion sub-device feature: %s at sub-index %d", feature, sub_feat_idx)
sub_feat_idx += 1
@ -300,7 +301,7 @@ class FeaturesArray(dict):
# Also yield sub-device features for Centurion devices
sub_count = getattr(self, "_sub_feature_count", 0)
for sub_idx in range(sub_count):
feature = self.inverse.get(sub_idx + 0x100)
feature = self.sub_inverse.get(sub_idx)
if feature is not None:
yield feature, sub_idx

View File

@ -27,6 +27,7 @@ from solaar.i18n import _
from . import common
from . import hidpp20_constants
from . import settings_validator
from .centurion_constants import CenturionCoreFeature
from .common import NamedInt
logger = logging.getLogger(__name__)
@ -628,7 +629,7 @@ class FeatureRW:
read_prefix=b"",
no_reply=False,
):
assert isinstance(feature, hidpp20_constants.SupportedFeature)
assert isinstance(feature, (hidpp20_constants.SupportedFeature, CenturionCoreFeature))
self.feature = feature
self.read_fnid = read_fnid
self.write_fnid = write_fnid
@ -665,7 +666,7 @@ class FeatureRWMap(FeatureRW):
key_byte_count=default_key_byte_count,
no_reply=False,
):
assert isinstance(feature, hidpp20_constants.SupportedFeature)
assert isinstance(feature, (hidpp20_constants.SupportedFeature, CenturionCoreFeature))
self.feature = feature
self.read_fnid = read_fnid
self.write_fnid = write_fnid

View File

@ -232,8 +232,7 @@ def _print_device(dev, num=None):
else:
feature_bytes = feature.to_bytes(2, byteorder="little")
feature_int = int.from_bytes(feature_bytes, byteorder="little")
# On Centurion, parent feature 0x0003 is CentPPBridge, not DEVICE_FW_VERSION
display_name = "CENTPP BRIDGE" if is_centurion and not in_sub_device and feature_int == 0x0003 else feature
display_name = feature
if is_centurion_child and in_sub_device:
# Use cached version — skip slow bridge ROOT queries
version = dev.features.get_feature_version(feature_int) or 0
@ -339,7 +338,7 @@ def _print_device(dev, num=None):
if hw_info:
model_id, hw_rev, product_id = hw_info
print(f" Hardware: model {model_id}" f" rev {hw_rev} product {product_id:04X}")
elif feature == SupportedFeature.DEVICE_FW_VERSION and not (is_centurion and not in_sub_device):
elif isinstance(feature, SupportedFeature) and feature == SupportedFeature.DEVICE_FW_VERSION:
for fw in _hidpp20.get_firmware(dev):
extras = strhex(fw.extras) if fw.extras else ""
print(f" Firmware: {fw.kind} {fw.name} {fw.version} {extras}")