diff --git a/lib/logitech_receiver/hidpp20.py b/lib/logitech_receiver/hidpp20.py index b5689c6c..4224d270 100644 --- a/lib/logitech_receiver/hidpp20.py +++ b/lib/logitech_receiver/hidpp20.py @@ -215,9 +215,11 @@ class FeaturesArray(dict): def _discover_sub_device_features(self, bridge_index): """Phase B: Discover sub-device features via CentPPBridge. - Uses CenturionFeatureSet bulk query (function 1) routed through the bridge. - Each response fits only ~13-14 features in a single Centurion frame, so - we loop with increasing start_index until we see an empty batch. + Uses per-index queries: GetCount (func 0) returns total count, then + GetFeatureId (func 1) returns one feature per call. Avoids the + single-frame truncation of bulk queries — a Centurion frame is 64 + bytes so a bulk reply can only fit ~13 features regardless of how + many the sub-device actually has. """ # First, find the sub-device's FeatureSet index via CenturionRoot (sub_feat_idx=0) # Query: CenturionRoot.GetFeature(0x0001) to find FeatureSet index on sub-device @@ -232,45 +234,34 @@ class FeaturesArray(dict): logger.warning("Sub-device FeatureSet not found (index=0)") return - # Bulk enumerate: CenturionFeatureSet.GetFeatureId(func=1=0x10, start_index=N) - # Response per batch: [count, (feat_hi, feat_lo, type, flags) × count] - sub_feat_idx = 0 # sub-device feature indices start at 0 - max_batches = 16 # safety bound: 16 × 14 = 224 features max - for _batch in range(max_batches): - response = self.device.centurion_bridge_request(sub_fs_index, 0x10, sub_feat_idx) - if response is None or len(response) < 1: - break - entry_count = response[0] - if entry_count == 0: - break - entries = response[1:] - parsed_this_batch = 0 - for i in range(entry_count): - offset = i * 4 - if offset + 2 > len(entries): - break - feat_id = struct.unpack("!H", entries[offset : offset + 2])[0] - try: - feature = SupportedFeature(feat_id) - except ValueError: - feature = f"unknown:{feat_id:04X}" - # Store sub-device index for ALL features (including parent overlaps) - # This enables querying the sub-device's copy of shared features via bridge - self.device._centurion_sub_indices[feature] = sub_feat_idx - # Only store unique sub-device features in dict (skip parent overlaps like ROOT, FEATURE_SET) - # This avoids clobbering parent inverse entries via __setitem__ - if dict.get(self, feature) is None: - dict.__setitem__(self, feature, sub_feat_idx) - self.device._centurion_sub_features.add(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 - parsed_this_batch += 1 - # If this batch was short (device returned fewer than requested), we're done. - if parsed_this_batch < entry_count: - break + # Query feature count (function 0 = GetCount). Response: [count, ...]. + count_resp = self.device.centurion_bridge_request(sub_fs_index, 0x00) + if count_resp is None or len(count_resp) < 1: + logger.warning("Failed to read Centurion sub-device feature count") + return + total_count = count_resp[0] + logger.info("Centurion sub-device: FeatureSet reports %d features", total_count) + + # Per-index query: GetFeatureId (function 1 = 0x10). Response: [remaining, feat_hi, feat_lo, type, flags]. + sub_feat_idx = 0 + for idx in range(total_count): + response = self.device.centurion_bridge_request(sub_fs_index, 0x10, idx) + if response is None or len(response) < 3: + logger.debug("Centurion sub-device: no response at index %d", idx) + continue + feat_id = struct.unpack("!H", response[1:3])[0] + try: + feature = SupportedFeature(feat_id) + except ValueError: + feature = f"unknown:{feat_id:04X}" + self.device._centurion_sub_indices[feature] = sub_feat_idx + if dict.get(self, feature) is None: + dict.__setitem__(self, feature, sub_feat_idx) + self.device._centurion_sub_features.add(feature) + 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 self._sub_feature_count = sub_feat_idx logger.info("Centurion sub-device: discovered %d features total", sub_feat_idx) diff --git a/tests/logitech_receiver/test_hidpp20_complex.py b/tests/logitech_receiver/test_hidpp20_complex.py index 1e3ef9dc..b033de0f 100644 --- a/tests/logitech_receiver/test_hidpp20_complex.py +++ b/tests/logitech_receiver/test_hidpp20_complex.py @@ -956,29 +956,18 @@ def test_centurion_sub_device_feature_discovery(): dev = fake_hidpp.Device("CENT_SUB", True, 2.6, fake_hidpp.r_centurion_headset, centurion=True) # Set up bridge responses for sub-device discovery: # 1. CenturionRoot.GetFeature(0x0001) -> FeatureSet at sub-index 1 - # 2. CenturionFeatureSet.GetFeatureId(index=0) -> bulk feature list + # 2. CenturionFeatureSet.GetCount (func 0) -> total feature count + # 3. CenturionFeatureSet.GetFeatureId (func 0x10) per-index -> one feature each dev._bridge_responses = { # CenturionRoot(idx=0).GetFeature(func=0) with feature_id=0x0001 -> sub_fs_index=1 (0x00, 0x00, "0001"): bytes([0x01, 0x00, 0x00]), - # CenturionFeatureSet(idx=1).GetFeatureId(func=0x10, start=0) -> 3 features - # Response: [count, (feat_hi, feat_lo, type, flags) × count] - (0x01, 0x10, "00"): bytes( - [ - 0x03, # 3 features - 0x06, - 0x04, - 0x00, - 0x00, # HEADSET_AUDIO_SIDETONE (0x0604) at sub-idx 0 - 0x06, - 0x01, - 0x00, - 0x00, # HEADSET_MIC_MUTE (0x0601) at sub-idx 1 - 0x06, - 0x11, - 0x00, - 0x00, # HEADSET_MIC_GAIN (0x0611) at sub-idx 2 - ] - ), + # CenturionFeatureSet(idx=1).GetCount (func=0) -> 3 features + (0x01, 0x00, ""): bytes([0x03, 0x00, 0x00]), + # CenturionFeatureSet(idx=1).GetFeatureId (func=0x10, index=N) -> one feature per response. + # Response format: [remaining, feat_hi, feat_lo, type, flags] + (0x01, 0x10, "00"): bytes([0x02, 0x06, 0x04, 0x00, 0x00]), # HEADSET_AUDIO_SIDETONE at sub-idx 0 + (0x01, 0x10, "01"): bytes([0x01, 0x06, 0x01, 0x00, 0x00]), # HEADSET_MIC_MUTE at sub-idx 1 + (0x01, 0x10, "02"): bytes([0x00, 0x06, 0x11, 0x00, 0x00]), # HEADSET_MIC_GAIN at sub-idx 2 } featuresarray = hidpp20.FeaturesArray(dev) dev.features = featuresarray