Record Centurion sub-device feature versions + version-gate AutoSleep

The Centurion sub-device discovery ignored the type/version bytes in
the per-index getFeatureId response and defaulted every feature's
version to 0. That made version-gated settings (sidetone, auto-sleep)
send V0 payload formats on features that may actually be V2/V3/V4 —
which the G522 rejects with OUT_OF_RANGE (error 0x03).

Evidence from tester's log (version acfd02ab):
  bridge sub-device error: orig_feat_idx=13 orig_func=0x1B error=0x03
  bridge sub-device error: orig_feat_idx=20 orig_func=0x1F error=0x03

feat_idx 13 is HeadsetAudioSidetone (0x0604); 20 is CenturionAutoSleep
(0x0108). Both have version-gated payload formats per the protocol doc.

Fixes:
1. In _discover_sub_device_features, extract response[3] (type) and
   response[4] (version) and store them in self.version / self.flags,
   so get_feature_version() returns the real version for downstream
   callers.

2. Add HeadsetAutoSleep.build() that picks byte_count and max_value
   based on the reported version (V<3=1 byte, V=3=2 bytes, V>=4=3).
   HeadsetSidetone.build() already had version gating — it just wasn't
   getting the real version before.

Does NOT address the mic-mute (error 0x0A) and mic-gain (error 0x0B)
rejections — those are non-standard error codes likely meaning the G522
either gates those behind the physical mic button (mic-mute) or rejects
writes for other reasons (mic-gain). Needs separate investigation once
the simpler version fix lands.
This commit is contained in:
Ken Sanislo 2026-04-17 23:51:34 -07:00
parent acfd02abe8
commit 4cc8ac0d46
2 changed files with 38 additions and 4 deletions

View File

@ -242,7 +242,11 @@ class FeaturesArray(dict):
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].
# Per-index query: GetFeatureId (function 1 = 0x10).
# Response: [remaining, feat_hi, feat_lo, type, version].
# We now also record `type` (flags) and `version` for each feature so
# version-gated settings (sidetone, auto-sleep, etc.) can use the
# correct payload format instead of defaulting to V0.
sub_feat_idx = 0
for idx in range(total_count):
response = self.device.centurion_bridge_request(sub_fs_index, 0x10, idx)
@ -250,6 +254,8 @@ class FeaturesArray(dict):
logger.debug("Centurion sub-device: no response at index %d", idx)
continue
feat_id = struct.unpack("!H", response[1:3])[0]
feat_type = response[3] if len(response) > 3 else 0
feat_version = response[4] if len(response) > 4 else 0
try:
feature = SupportedFeature(feat_id)
except ValueError:
@ -259,8 +265,18 @@ class FeaturesArray(dict):
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)
# Record version/flags so downstream settings can version-gate their
# payload format. get_feature_version(feature) reads self.version[feature].
self.version[feature] = feat_version
self.flags[feature] = feat_type
if feat_version > 0 and logger.isEnabledFor(logging.DEBUG):
logger.debug(
"Centurion sub-device feature: %s at sub-index %d, version=%d, flags=0x%02X",
feature,
sub_feat_idx,
feat_version,
feat_type,
)
sub_feat_idx += 1
self._sub_feature_count = sub_feat_idx
logger.info("Centurion sub-device: discovered %d features total", sub_feat_idx)

View File

@ -1690,9 +1690,27 @@ class HeadsetAutoSleep(settings.Setting):
rw_options = {"read_fnid": 0x00, "write_fnid": 0x10}
validator_class = settings_validator.RangeValidator
min_value = 0
max_value = 255
# Timer byte count depends on feature version:
# V<3 : 1 byte (0-255)
# V=3 : 2 bytes (0-65535)
# V>=4: 3 bytes (0-16777215)
# build() picks the correct width based on the device's reported version.
max_value = 0xFFFFFF
validator_options = {"byte_count": 1}
@classmethod
def build(cls, device):
version = device.features.get_feature_version(cls.feature) or 0
if version >= 4:
byte_count, max_value = 3, 0xFFFFFF
elif version >= 3:
byte_count, max_value = 2, 0xFFFF
else:
byte_count, max_value = 1, 0xFF
rw = settings.FeatureRW(cls.feature, **cls.rw_options)
validator = settings_validator.RangeValidator(min_value=0, max_value=max_value, byte_count=byte_count)
return cls(device, rw, validator)
class HeadsetOnboardEQ(settings.RangeFieldSetting):
name = "headset-onboard-eq"