Add diagnostic logging for EQ setting build failures
Centurion feature discovery now logs each parent + sub-device feature at INFO with name/index/version/flags. `check_feature` logs INFO when it skips a setting for min_version or the INTERNAL flag. HeadsetAdvancedEQ and HeadsetOnboardEQ `build()` paths log at every failure branch. The three AdvancedParaEQ helpers log raw response bytes. On the G522, `HEADSET_ADVANCED_PARA_EQ` (0x020D) is present in the feature set but no settings panel appears and current logging gives us no way to tell which step silently returns None.
This commit is contained in:
parent
92e70d277f
commit
63e01b2984
|
|
@ -24,10 +24,13 @@ per band and the device applies them. Band entries are 3 bytes each:
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import struct
|
||||
|
||||
from .hidpp20_constants import SupportedFeature
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Direction parameter for getCustomEQ / getActiveEQ etc.
|
||||
DIRECTION_PLAYBACK = 0
|
||||
DIRECTION_CAPTURE = 1
|
||||
|
|
@ -39,7 +42,11 @@ def get_advanced_eq_info(device):
|
|||
Returns (band_count, db_range, capabilities, db_min, db_max) or None.
|
||||
"""
|
||||
result = device.feature_request(SupportedFeature.HEADSET_ADVANCED_PARA_EQ, 0x00)
|
||||
if result is None or len(result) < 5:
|
||||
if result is None:
|
||||
logger.info("AdvancedParaEQ getEQInfos: feature_request returned None")
|
||||
return None
|
||||
if len(result) < 5:
|
||||
logger.info("AdvancedParaEQ getEQInfos: short response (len=%d) %s", len(result), result.hex())
|
||||
return None
|
||||
band_count = result[0]
|
||||
db_range = result[1]
|
||||
|
|
@ -47,14 +54,28 @@ def get_advanced_eq_info(device):
|
|||
# dbMin / dbMax are signed int8 in the doc.
|
||||
db_min = struct.unpack("b", bytes([result[3]]))[0]
|
||||
db_max = struct.unpack("b", bytes([result[4]]))[0]
|
||||
logger.info(
|
||||
"AdvancedParaEQ getEQInfos: bands=%d dbRange=%d caps=0x%02X dbMin=%d dbMax=%d raw=%s",
|
||||
band_count,
|
||||
db_range,
|
||||
capabilities,
|
||||
db_min,
|
||||
db_max,
|
||||
result.hex(),
|
||||
)
|
||||
return (band_count, db_range, capabilities, db_min, db_max)
|
||||
|
||||
|
||||
def get_advanced_eq_active_slot(device, direction=DIRECTION_PLAYBACK):
|
||||
"""Query getActiveEQ (function 3). Returns the active slot index, or None."""
|
||||
result = device.feature_request(SupportedFeature.HEADSET_ADVANCED_PARA_EQ, 0x30, direction)
|
||||
if result is None or len(result) < 1:
|
||||
if result is None:
|
||||
logger.info("AdvancedParaEQ getActiveEQ(dir=%d): feature_request returned None", direction)
|
||||
return None
|
||||
if len(result) < 1:
|
||||
logger.info("AdvancedParaEQ getActiveEQ(dir=%d): empty response", direction)
|
||||
return None
|
||||
logger.info("AdvancedParaEQ getActiveEQ(dir=%d): slot=%d raw=%s", direction, result[0], result.hex())
|
||||
return result[0]
|
||||
|
||||
|
||||
|
|
@ -65,6 +86,7 @@ def get_advanced_eq_params(device, direction=DIRECTION_PLAYBACK, slot=0):
|
|||
"""
|
||||
result = device.feature_request(SupportedFeature.HEADSET_ADVANCED_PARA_EQ, 0x10, direction, slot)
|
||||
if result is None:
|
||||
logger.info("AdvancedParaEQ getCustomEQ(dir=%d slot=%d): feature_request returned None", direction, slot)
|
||||
return None
|
||||
bands = []
|
||||
offset = 0
|
||||
|
|
@ -78,4 +100,12 @@ def get_advanced_eq_params(device, direction=DIRECTION_PLAYBACK, slot=0):
|
|||
gain_db = struct.unpack("b", bytes([result[offset + 2]]))[0]
|
||||
bands.append((freq, gain_db))
|
||||
offset += 3
|
||||
logger.info(
|
||||
"AdvancedParaEQ getCustomEQ(dir=%d slot=%d): parsed %d band(s) %s raw=%s",
|
||||
direction,
|
||||
slot,
|
||||
len(bands),
|
||||
bands,
|
||||
result.hex(),
|
||||
)
|
||||
return bands
|
||||
|
|
|
|||
|
|
@ -785,7 +785,17 @@ class Device:
|
|||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("bridge idx=%d fn=0x%02X -> OK", sub_feat_idx, sub_function)
|
||||
return self._parse_bridge_response(reply_data)
|
||||
# Unsolicited notification, skip it
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"bridge skipping reply (pre-ACK): got sub_cpl=0x%02X sub_idx=0x%02X func_sw=0x%02X"
|
||||
" (expected idx=0x%02X func_sw=0x%02X) data=%s",
|
||||
reply_data[4] if len(reply_data) > 4 else 0,
|
||||
reply_data[5] if len(reply_data) > 5 else 0,
|
||||
reply_data[6] if len(reply_data) > 6 else 0,
|
||||
sub_feat_idx,
|
||||
expected_sub_func_sw,
|
||||
reply_data.hex(),
|
||||
)
|
||||
if not ack_received:
|
||||
logger.warning("centurion_bridge_request: no ACK received")
|
||||
return None
|
||||
|
|
@ -803,7 +813,17 @@ class Device:
|
|||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug("bridge idx=%d fn=0x%02X -> OK", sub_feat_idx, sub_function)
|
||||
return self._parse_bridge_response(reply_data)
|
||||
# Unsolicited notification for a different feature, skip it
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
logger.debug(
|
||||
"bridge skipping reply (post-ACK): got sub_cpl=0x%02X sub_idx=0x%02X func_sw=0x%02X"
|
||||
" (expected idx=0x%02X func_sw=0x%02X) data=%s",
|
||||
reply_data[4] if len(reply_data) > 4 else 0,
|
||||
reply_data[5] if len(reply_data) > 5 else 0,
|
||||
reply_data[6] if len(reply_data) > 6 else 0,
|
||||
sub_feat_idx,
|
||||
expected_sub_func_sw,
|
||||
reply_data.hex(),
|
||||
)
|
||||
logger.warning("centurion_bridge_request: no MessageEvent received")
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -209,6 +209,13 @@ class FeaturesArray(dict):
|
|||
# use the correct payload format on direct USB Centurion devices too.
|
||||
self.version[feature] = feat_version
|
||||
self.flags[feature] = feat_type
|
||||
logger.info(
|
||||
"Centurion parent feature: %s at index %d, version=%d, flags=0x%02X",
|
||||
feature,
|
||||
index,
|
||||
feat_version,
|
||||
feat_type,
|
||||
)
|
||||
if feature is CenturionCoreFeature.CENT_PP_BRIDGE:
|
||||
bridge_index = index
|
||||
|
||||
|
|
@ -275,14 +282,16 @@ class FeaturesArray(dict):
|
|||
# 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,
|
||||
)
|
||||
# Log every sub-device feature at INFO so field diagnostics can see
|
||||
# both the version (V-gated payload formats) and flags (INTERNAL/HIDDEN
|
||||
# bits silently suppress settings panels in check_feature).
|
||||
logger.info(
|
||||
"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)
|
||||
|
|
|
|||
|
|
@ -1748,25 +1748,23 @@ class HeadsetAutoSleep(settings.Setting):
|
|||
rw_options = {"read_fnid": 0x00, "write_fnid": 0x10}
|
||||
validator_class = settings_validator.RangeValidator
|
||||
min_value = 0
|
||||
# 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
|
||||
# Wire format byte count depends on feature version (V<3: 1B, V=3: 2B, V>=4: 3B).
|
||||
# UI slider is capped at 240 min regardless — firmware accepts larger values, but
|
||||
# a 31-year slider (24-bit max) is not useful.
|
||||
max_value = 240
|
||||
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
|
||||
byte_count = 3
|
||||
elif version >= 3:
|
||||
byte_count, max_value = 2, 0xFFFF
|
||||
byte_count = 2
|
||||
else:
|
||||
byte_count, max_value = 1, 0xFF
|
||||
byte_count = 1
|
||||
rw = settings.FeatureRW(cls.feature, **cls.rw_options)
|
||||
validator = settings_validator.RangeValidator(min_value=0, max_value=max_value, byte_count=byte_count)
|
||||
validator = settings_validator.RangeValidator(min_value=0, max_value=cls.max_value, byte_count=byte_count)
|
||||
return cls(device, rw, validator)
|
||||
|
||||
|
||||
|
|
@ -1785,10 +1783,19 @@ class HeadsetOnboardEQ(settings.RangeFieldSetting):
|
|||
def build(cls, setting_class, device):
|
||||
info = hidpp20.get_onboard_eq_info(device)
|
||||
if not info:
|
||||
logger.info("HeadsetOnboardEQ.build: getEQInfo failed, no panel will be built")
|
||||
return None
|
||||
_has_hw_eq, num_bands = info
|
||||
bands = hidpp20.get_onboard_eq_params(device, slot=0x00)
|
||||
if not bands or len(bands) != num_bands:
|
||||
if not bands:
|
||||
logger.info("HeadsetOnboardEQ.build: getEQParameters returned no bands, no panel will be built")
|
||||
return None
|
||||
if len(bands) != num_bands:
|
||||
logger.info(
|
||||
"HeadsetOnboardEQ.build: band count mismatch — EQInfo=%d getEQParameters=%d; skipping",
|
||||
num_bands,
|
||||
len(bands),
|
||||
)
|
||||
return None
|
||||
keys = common.NamedInts()
|
||||
for i, (freq, _gain, _q) in enumerate(bands):
|
||||
|
|
@ -1796,6 +1803,7 @@ class HeadsetOnboardEQ(settings.RangeFieldSetting):
|
|||
v = cls(keys, min_value=-12, max_value=12, count=num_bands, byte_count=1)
|
||||
v._band_freqs = [freq for freq, _g, _q in bands]
|
||||
v._band_qs = [q for _f, _g, q in bands]
|
||||
logger.info("HeadsetOnboardEQ.build: panel built with %d band(s)", num_bands)
|
||||
return v
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
|
|
@ -1872,13 +1880,22 @@ class HeadsetAdvancedEQ(settings.RangeFieldSetting):
|
|||
def build(cls, setting_class, device):
|
||||
info = hidpp20.get_advanced_eq_info(device)
|
||||
if not info:
|
||||
logger.info("HeadsetAdvancedEQ.build: getEQInfos failed, no panel will be built")
|
||||
return None
|
||||
band_count, _db_range, _caps, db_min, db_max = info
|
||||
# Use the active slot on playback direction so the displayed EQ
|
||||
# matches what the user is actually hearing.
|
||||
active_slot = hidpp20.get_advanced_eq_active_slot(device, direction=0) or 0
|
||||
bands = hidpp20.get_advanced_eq_params(device, direction=0, slot=active_slot)
|
||||
if not bands or len(bands) != band_count:
|
||||
if not bands:
|
||||
logger.info("HeadsetAdvancedEQ.build: getCustomEQ returned no bands, no panel will be built")
|
||||
return None
|
||||
if len(bands) != band_count:
|
||||
logger.info(
|
||||
"HeadsetAdvancedEQ.build: band count mismatch — EQInfos=%d getCustomEQ=%d; skipping",
|
||||
band_count,
|
||||
len(bands),
|
||||
)
|
||||
return None
|
||||
keys = common.NamedInts()
|
||||
for i, (freq, _gain) in enumerate(bands):
|
||||
|
|
@ -1886,6 +1903,13 @@ class HeadsetAdvancedEQ(settings.RangeFieldSetting):
|
|||
v = cls(keys, min_value=db_min, max_value=db_max, count=band_count, byte_count=1)
|
||||
v._band_freqs = [freq for freq, _g in bands]
|
||||
v._active_slot = active_slot
|
||||
logger.info(
|
||||
"HeadsetAdvancedEQ.build: panel built with %d band(s), slot=%d, range=[%d,%d]",
|
||||
band_count,
|
||||
active_slot,
|
||||
db_min,
|
||||
db_max,
|
||||
)
|
||||
return v
|
||||
|
||||
def validate_read(self, reply_bytes):
|
||||
|
|
@ -2697,8 +2721,22 @@ def check_feature(device, settings_class: SettingsProtocol) -> None | bool | Set
|
|||
if settings_class.feature not in device.features:
|
||||
return
|
||||
if settings_class.min_version > device.features.get_feature_version(settings_class.feature):
|
||||
logger.info(
|
||||
"check_feature %s [%s]: min_version=%d > device feature version=%d; skipping",
|
||||
settings_class.name,
|
||||
settings_class.feature,
|
||||
settings_class.min_version,
|
||||
device.features.get_feature_version(settings_class.feature) or 0,
|
||||
)
|
||||
return
|
||||
if device.features.get_hidden(settings_class.feature):
|
||||
flags = device.features.flags.get(settings_class.feature, 0)
|
||||
logger.info(
|
||||
"check_feature %s [%s]: feature has INTERNAL flag set (flags=0x%02X); skipping",
|
||||
settings_class.name,
|
||||
settings_class.feature,
|
||||
flags,
|
||||
)
|
||||
return
|
||||
try:
|
||||
detected = settings_class.build(device)
|
||||
|
|
|
|||
Loading…
Reference in New Issue