Use per-index queries for Centurion sub-device feature enumeration
The previous "bulk" query (CenturionFeatureSet.GetFeatureId with
start_index, per-batch response) was limited by the 64-byte Centurion
frame size to ~13 features per response, and the device apparently
does not fragment MessageEvent responses — so start_index=14 returned
empty and we missed the remaining features.
Evidence: wired G522 exposes 30 features directly but wireless via
bridge only ever reports 13. Same sub-device, different access path,
same underlying feature set.
Switch to per-index queries matching how _discover_dongle_features
already works on the parent:
1. CenturionFeatureSet.GetCount (func 0) -> total feature count
2. CenturionFeatureSet.GetFeatureId (func 1) per index -> one feature
per response
Slower (N round-trips instead of 1) but handles any feature count
without fragmentation. Expected to reveal RGB, advanced EQ, sidetone,
etc. on the wireless G522.
Updated test fixture with per-index response format.
This commit is contained in:
parent
f469e86b9c
commit
cfc69431d1
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue