G522 mic-mute fix + cluster-info decode + 0x0623 probe
Three findings from G HUB pcap analysis (~/ghub/g522/*.pcapng): 1. Mic-mute on G522 actually works — we had the wrong fnids. The firmware uses fn 0x10 (function 1) for state-change events / reads and fn 0x20 (function 2) for SetState; the standard fn 0x10 SetState path returns 0x0A NOT_SUPPORTED. Replace the previous "suppress entirely on G522" quirk with a per-PID rw_options override that uses the right fnids. Other headsets keep the standard pattern. 2. setRGBClusterEffect (0x0621 fn 0x30) takes effect_id 0x0000 paired with RGB bytes to mean "Static color" — *not* "Off / Disabled" as our cluster-info parse had guessed. Add a structured decoder for getRGBClusterInfo (fn 0x10) that emits a human-readable summary alongside the raw hex, with effect_id 0x0000 labelled "Static". Records turn out to be 4 bytes each (effect_id LE u16, slot_idx LE u16) — most other HID++ fields are BE but this one is LE per the captured factory-default bytes. 3. Sub-feature 0x0623 is present on G522 but unmapped (it appeared as `unknown:0623` in the feature-table dump). Add a placeholder SupportedFeature entry and probe the first 8 functions read-side so the next bring-up captures whatever responds.
This commit is contained in:
parent
2e07b112ff
commit
56bce3fc1b
|
|
@ -214,6 +214,10 @@ class SupportedFeature(IntEnum):
|
|||
HEADSET_RGB_HOSTMODE = 0x0620
|
||||
HEADSET_RGB_ONBOARD_EFFECTS = 0x0621
|
||||
HEADSET_RGB_SIGNATURE_EFFECTS = 0x0622
|
||||
# 0x0623 is present on G522 sub-device but its function set is unmapped;
|
||||
# add probe coverage in rgb_effects_probe so the next bring-up captures
|
||||
# whatever read functions respond.
|
||||
HEADSET_RGB_0623 = 0x0623
|
||||
HEADSET_DO_NOT_DISTURB = 0x0631
|
||||
CENTURION_ONBOARD_PROFILES = 0x0634
|
||||
HEADSET_RGB_STREAMING = 0x0635
|
||||
|
|
|
|||
|
|
@ -1,10 +1,20 @@
|
|||
"""Read-only corpus probe for HEADSET_RGB_ONBOARD_EFFECTS (0x0621) and
|
||||
HEADSET_RGB_SIGNATURE_EFFECTS (0x0622).
|
||||
"""Read-only corpus probe for the headset RGB feature triplet:
|
||||
|
||||
- HEADSET_RGB_ONBOARD_EFFECTS (0x0621)
|
||||
- HEADSET_RGB_SIGNATURE_EFFECTS (0x0622)
|
||||
- HEADSET_RGB_0623 (0x0623, function set unmapped)
|
||||
|
||||
Logs raw response bytes and lengths at INFO so field testers without
|
||||
``-dd`` can still capture the data. All calls are strictly read-side —
|
||||
no setters are invoked. If either feature isn't present the probe
|
||||
no setters are invoked. If a feature isn't present the probe
|
||||
short-circuits cleanly.
|
||||
|
||||
Pcap analysis of G HUB's color-set traffic confirmed that on 0x0621,
|
||||
``setRGBClusterEffect`` (fn 0x30) takes a 10-byte payload
|
||||
``[cluster, effect_id_BE_u16, R, G, B, ...]`` where ``effect_id=0x0000``
|
||||
means "Static (with RGB)" — this is also the slot-0 entry in the
|
||||
fn 0x10 ``getRGBClusterInfo`` reply, which we decode structurally so
|
||||
the test corpus shows effect-id semantics in plaintext.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -94,6 +104,53 @@ def _call(device, feature: SupportedFeature, fn: int, *params):
|
|||
return resp
|
||||
|
||||
|
||||
# Names for known effect_ids on the headset RGB cluster. Confirmed via
|
||||
# pcap analysis of G HUB color-set traffic: setRGBClusterEffect with
|
||||
# effect_id=0x0000 + RGB writes a static color, so 0x0000 is "Static"
|
||||
# rather than the "Off / Disabled" we'd guessed from cluster ordering.
|
||||
# Other ids haven't been observed on the wire yet — names are placeholder
|
||||
# until further pcap traffic confirms them.
|
||||
_EFFECT_ID_NAMES = {
|
||||
0x0000: "Static",
|
||||
0x0001: "Effect 0x0001",
|
||||
0x0006: "Effect 0x0006",
|
||||
0x0007: "Effect 0x0007",
|
||||
0x000F: "Effect 0x000F",
|
||||
0x007F: "Effect 0x007F",
|
||||
}
|
||||
|
||||
|
||||
def _decode_cluster_info(resp) -> str | None:
|
||||
"""Decode a 0x0621 fn 0x10 getRGBClusterInfo reply into a readable
|
||||
summary. Best-effort — returns None on unexpected length/shape.
|
||||
|
||||
Observed shape on G522: 4-byte records (effect_id LE u16, slot_idx
|
||||
LE u16). The effect_id at slot 0 is 0x0000 = "Static" (with RGB),
|
||||
confirmed by pcap of G HUB color-set traffic. Records continue
|
||||
until trailing-zero padding.
|
||||
|
||||
Note: most HID++ multi-byte fields are BE, but this particular
|
||||
response uses LE — confirmed against captured factory-default bytes
|
||||
on G522 where the values 0x0001 / 0x000F / 0x007F appear at byte 0
|
||||
of each record with byte 1 = 0x00 (consistent with LE u16).
|
||||
"""
|
||||
if not resp or len(resp) < 4:
|
||||
return None
|
||||
effects = []
|
||||
seen_static = False
|
||||
for i in range(0, len(resp) - 3, 4):
|
||||
eid = resp[i] | (resp[i + 1] << 8)
|
||||
slot = resp[i + 2] | (resp[i + 3] << 8)
|
||||
# Skip purely-zero padding once we've seen the (effect=0, slot=0) entry.
|
||||
if eid == 0 and slot == 0:
|
||||
if seen_static:
|
||||
continue
|
||||
seen_static = True
|
||||
name = _EFFECT_ID_NAMES.get(eid, f"0x{eid:04X}")
|
||||
effects.append(f"slot={slot}:{name}")
|
||||
return ", ".join(effects) if effects else None
|
||||
|
||||
|
||||
def probe_onboard_effects(device) -> None:
|
||||
"""Probe 0x0621 RGBOnboardEffects read-side functions."""
|
||||
feature = SupportedFeature.HEADSET_RGB_ONBOARD_EFFECTS
|
||||
|
|
@ -109,6 +166,9 @@ def probe_onboard_effects(device) -> None:
|
|||
resp = _call(device, feature, 0x10, cluster_idx)
|
||||
if resp is None:
|
||||
break
|
||||
decoded = _decode_cluster_info(resp)
|
||||
if decoded:
|
||||
logger.info("RGB probe: 0x0621.fn10(%02X) decoded: %s", cluster_idx, decoded)
|
||||
|
||||
# fn 0x20 getRGBClusterEffect — current state per cluster.
|
||||
for cluster_idx in range(8):
|
||||
|
|
@ -120,6 +180,24 @@ def probe_onboard_effects(device) -> None:
|
|||
_call(device, feature, 0x40)
|
||||
|
||||
|
||||
def probe_unknown_0623(device) -> None:
|
||||
"""Probe 0x0623 (purpose unmapped) — present on G522 sub-device.
|
||||
|
||||
Function set unknown. Try a small window of low function indexes to
|
||||
capture whatever responds. Strictly read-side; we don't know what
|
||||
arguments the functions take so we just call each with no payload
|
||||
and let the device 0x0A any function that needs args.
|
||||
"""
|
||||
feature = SupportedFeature.HEADSET_RGB_0623
|
||||
if not device.features or feature not in device.features:
|
||||
return
|
||||
logger.info("RGB probe: 0x0623 HEADSET_RGB_0623 present on %s", device)
|
||||
# Functions 0..7 covers the typical "info / get* / get*" range; if
|
||||
# anything responds we'll have first bytes to triangulate against.
|
||||
for fn_idx in range(8):
|
||||
_call(device, feature, fn_idx << 4)
|
||||
|
||||
|
||||
def probe_signature_effects(device) -> None:
|
||||
"""Probe 0x0622 RGBSignatureEffects read-side functions."""
|
||||
feature = SupportedFeature.HEADSET_RGB_SIGNATURE_EFFECTS
|
||||
|
|
@ -158,3 +236,7 @@ def probe(device) -> None:
|
|||
probe_signature_effects(device)
|
||||
except Exception as e:
|
||||
logger.info("RGB probe: signature-effects probe raised %r", e)
|
||||
try:
|
||||
probe_unknown_0623(device)
|
||||
except Exception as e:
|
||||
logger.info("RGB probe: 0x0623 probe raised %r", e)
|
||||
|
|
|
|||
|
|
@ -1623,15 +1623,19 @@ class HeadsetMicMute(settings.Setting):
|
|||
|
||||
@classmethod
|
||||
def build(cls, device):
|
||||
# G522 advertises 0x0601 in its FeatureSet but the firmware returns
|
||||
# 0x0A UNSUPPORTED for both GetState and SetState. The physical mute
|
||||
# switch doesn't drive this feature anyway — G HUB attempts and
|
||||
# silently ignores failures. Hide the toggle on G522 PIDs (0x0B18
|
||||
# wireless, 0x0B19 wired) so users don't see a permanently-broken UI.
|
||||
# product_id is the uppercase hex string form set by hidapi_impl
|
||||
# (`f"{pid:04X}"`), so compare against strings rather than ints.
|
||||
# G522 firmware uses non-standard fnids for HEADSET_MIC_MUTE:
|
||||
# fn 0x10 (function 1) emits state-change events from the device
|
||||
# (physical mute switch + host writes); also serves as GetState.
|
||||
# fn 0x20 (function 2) is SetState — the standard fn 0x10 SetState
|
||||
# returns 0x0A UNSUPPORTED on this firmware.
|
||||
# Other headsets are presumably standard so we only override the
|
||||
# rw_options on the known-quirky G522 PIDs (0x0B18 wireless, 0x0B19
|
||||
# wired-mode firmware). product_id is the uppercase hex string set
|
||||
# by hidapi_impl (`f"{pid:04X}"`), so compare against strings.
|
||||
if getattr(device, "product_id", None) in ("0B18", "0B19"):
|
||||
return None
|
||||
rw = settings.FeatureRW(cls.feature, read_fnid=0x10, write_fnid=0x20)
|
||||
validator = settings_validator.BooleanValidator()
|
||||
return cls(device, rw, validator)
|
||||
return super().build(device)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue