HeadsetRGBColor: reorder host mode before zone query, parse tight response

Field testing on G522 showed two problems:

1. GetRGBZoneInfo returned all zeros (count=0, all-zero body) when
   queried BEFORE SetHostModeState(1). Protocol doc's recommended
   order is: claim host mode first, then enumerate zones. Our code
   was querying zones first.

2. The response format the G522 returns does NOT match the doc's
   layout. Doc says [count, 3 reserved, 1 reserved, zone_ids...] but
   the G522 seems to pack them tight as [count, zone_ids...] — the
   SetRgbZonesSingleValue response "0801020304050607080000" decodes
   cleanly as count=8, zones=[1..8] under the tight format.

Fixes:
- Call SetHostModeState(1) before GetRGBZoneInfo.
- Try tight format first, fall back to doc format. Only cache a
  parsed result if it makes sense (non-zero zone IDs, count matches).
- Don't cache ambiguous results so subsequent writes retry.

This also incidentally suggests the G522 has 8 RGB zones (not the 2
left/right earcups we were guessing) — the red set we sent in the
previous test probably did set zones 0x01 and 0x02 correctly, but
those zones are only a fraction of the total lighting so the color
change wasn't visible.
This commit is contained in:
Ken Sanislo 2026-04-17 19:44:07 -07:00
parent bde3c3bc86
commit 6e84325d67
1 changed files with 44 additions and 24 deletions

View File

@ -1906,6 +1906,11 @@ class HeadsetRGBColor(settings.Setting):
logger.info("HeadsetRGBColor: device offline, skipping write of %s", name)
return None
try:
# Order per protocol doc: claim host mode FIRST, then enumerate zones,
# then set colors, then commit. GetRGBZoneInfo returns zero counts if
# we query before claiming control.
resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x80, b"\x01")
logger.info("HeadsetRGBColor: SetHostModeState(1) resp=%s", resp.hex() if resp else resp)
zone_ids = self._zone_ids(device)
if not zone_ids:
logger.warning("HeadsetRGBColor: no zones available; cannot set color %s", name)
@ -1919,9 +1924,6 @@ class HeadsetRGBColor(settings.Setting):
len(zone_ids),
[f"0x{z:02X}" for z in zone_ids],
)
# Enable host mode first.
resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x80, b"\x01")
logger.info("HeadsetRGBColor: SetHostModeState(1) resp=%s", resp.hex() if resp else resp)
# SetRgbZonesSingleValue: [R, G, B, count, zone_ids...]
payload = bytes([r, g, b, len(zone_ids)]) + bytes(zone_ids)
resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x50, payload)
@ -1941,9 +1943,13 @@ class HeadsetRGBColor(settings.Setting):
@staticmethod
def _zone_ids(device):
"""Query GetRGBZoneInfo (function 1) and return list of zone IDs."""
"""Query GetRGBZoneInfo (function 1) and return list of zone IDs.
Caller must have enabled host mode first querying before SetHostModeState
has been observed to return all zeros on the G522.
"""
cached = getattr(device, "_headset_rgb_zone_ids", None)
if cached is not None:
if cached:
return cached
try:
resp = device.feature_request(_F.HEADSET_RGB_HOSTMODE, 0x10)
@ -1952,31 +1958,45 @@ class HeadsetRGBColor(settings.Setting):
resp = None
if not resp or len(resp) < 1:
logger.warning(
"HeadsetRGBColor: GetRGBZoneInfo returned %s, falling back to zones [0x01, 0x02]",
"HeadsetRGBColor: GetRGBZoneInfo returned %s; NOT caching so we retry next write",
resp,
)
device._headset_rgb_zone_ids = [0x01, 0x02]
return device._headset_rgb_zone_ids
return []
zone_count = resp[0]
# Response: [count, 3 reserved, reserved, zone_ids...]
zone_ids = list(resp[5 : 5 + zone_count]) if len(resp) >= 5 + zone_count else []
# Fallback to typical left/right earcup zone IDs if response format differs.
if not zone_ids:
logger.warning(
"HeadsetRGBColor: GetRGBZoneInfo unexpected format count=%d resp=%s, falling back to [0x01, 0x02]",
zone_count,
resp.hex(),
)
zone_ids = [0x01, 0x02]
else:
# Tight format observed on G522: [count, zone_ids...] with no reserved gap.
# The protocol doc shows 3-byte + 1-byte reserved gaps before zone IDs, but
# the G522 sub-device packs them immediately after the count.
tight = list(resp[1 : 1 + zone_count]) if 1 <= zone_count <= len(resp) - 1 else []
# Filter out zero bytes that are just padding (zone IDs should be non-zero).
tight = [z for z in tight if z != 0]
if tight and len(tight) == zone_count:
logger.info(
"HeadsetRGBColor: discovered %d zone(s) %s (raw resp=%s)",
len(zone_ids),
[f"0x{z:02X}" for z in zone_ids],
"HeadsetRGBColor: discovered %d zone(s) %s (tight format, raw resp=%s)",
len(tight),
[f"0x{z:02X}" for z in tight],
resp.hex(),
)
device._headset_rgb_zone_ids = zone_ids
return zone_ids
device._headset_rgb_zone_ids = tight
return tight
# Try the doc's format (with reserved gap) as fallback
gap = list(resp[5 : 5 + zone_count]) if len(resp) >= 5 + zone_count else []
gap = [z for z in gap if z != 0]
if gap and len(gap) == zone_count:
logger.info(
"HeadsetRGBColor: discovered %d zone(s) %s (doc format, raw resp=%s)",
len(gap),
[f"0x{z:02X}" for z in gap],
resp.hex(),
)
device._headset_rgb_zone_ids = gap
return gap
# Neither format parsed cleanly; don't cache so next write retries.
logger.warning(
"HeadsetRGBColor: GetRGBZoneInfo ambiguous count=%d resp=%s; not caching",
zone_count,
resp.hex(),
)
return []
class BrightnessControl(settings.Setting):