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 0d95ec840c
commit 256f2e556b
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):