headset 0x0621: fix onboard effect param encoding from binary decode
Breathing rendered all LEDs off because wire byte 3 (CE[6]) was hardcoded 0 — it is the intensity field (Breathing.Params field 3, value/100). Encode it, expose the intensity slider, and seed a non-zero default so picking the effect does not send an off frame. DualColor wire byte 6 is intensity, not "speed" — the firmware-lighting decode shows no speed/period field for DualColor. Rename throughout. Custom (effect 5) is a stored card-reference effect, not a parametric one; it cannot be set via setRGBClusterEffect. Drop it from the picker. Also seed sane per-effect defaults (the picker rebuilds the effect from zeroed widgets, so an unseeded pick sends an all-zero frame the firmware rejects) and clamp the period slider to 1000-20000 ms, matching the keyboard/mouse RGB effects.
This commit is contained in:
parent
4a7edd75ce
commit
5aae38f929
|
|
@ -2687,15 +2687,41 @@ class _HeadsetOnboardEffect:
|
||||||
leading clusterIndex). intensity is a 0-100 percent; saturation is a raw
|
leading clusterIndex). intensity is a 0-100 percent; saturation is a raw
|
||||||
0-255 byte (same as the keyboard RGB effects)."""
|
0-255 byte (same as the keyboard RGB effects)."""
|
||||||
|
|
||||||
|
# Per-effect default parameter values, applied to any field the effect
|
||||||
|
# uses that would otherwise be 0. Picking an effect in the UI rebuilds
|
||||||
|
# this object from the (initially zeroed) field widgets, so without
|
||||||
|
# seeding the picker emits an all-zero frame: the firmware rejects
|
||||||
|
# ColorCycle/ColorWave outright and renders Breathing/DualColor/Static
|
||||||
|
# as black or zero-intensity. intensity 0 in particular reads as "LEDs
|
||||||
|
# off". Defaults confirmed against the LGHUB binary decode of 0x0621.
|
||||||
|
_DEFAULTS = {
|
||||||
|
0: {"color1": 0xFFFFFF}, # Static / Fixed
|
||||||
|
1: {"intensity": 100, "saturation": 255, "period": 5000}, # Color Cycle
|
||||||
|
2: {"intensity": 100, "saturation": 255, "period": 5000}, # Color Wave
|
||||||
|
3: {"color1": 0xFFFFFF, "intensity": 100, "period": 5000}, # Breathing
|
||||||
|
4: {"color1": 0xFFFFFF, "color2": 0x0000FF, "intensity": 100}, # Dual Color
|
||||||
|
}
|
||||||
|
|
||||||
|
# speed is accepted only to load configs persisted before the 0x0621
|
||||||
|
# decode (DualColor byte 6 was mislabelled "speed"; it is intensity).
|
||||||
def __init__(self, ID=0, color1=0, color2=0, intensity=0, saturation=0, period=0, speed=0, direction=0):
|
def __init__(self, ID=0, color1=0, color2=0, intensity=0, saturation=0, period=0, speed=0, direction=0):
|
||||||
self.ID = int(ID)
|
self.ID = int(ID)
|
||||||
self.intensity = max(0, min(100, int(intensity)))
|
self.intensity = max(0, min(100, int(intensity)))
|
||||||
self.saturation = max(0, min(255, int(saturation)))
|
self.saturation = max(0, min(255, int(saturation)))
|
||||||
self.period = max(0, min(0xFFFF, int(period)))
|
self.period = max(0, min(0xFFFF, int(period)))
|
||||||
self.speed = max(0, min(0xFF, int(speed)))
|
|
||||||
self.direction = max(0, min(3, int(direction)))
|
self.direction = max(0, min(3, int(direction)))
|
||||||
for k, v in (("color1", color1), ("color2", color2)):
|
for k, v in (("color1", color1), ("color2", color2)):
|
||||||
setattr(self, k, common.ColorInt(int(v) & 0xFFFFFF))
|
setattr(self, k, common.ColorInt(int(v) & 0xFFFFFF))
|
||||||
|
# Seed any field the selected effect uses that arrived as 0 so the
|
||||||
|
# picker never sends an all-zero (rejected / black) frame. An effect
|
||||||
|
# actually active on the device reports non-zero values, so reads
|
||||||
|
# via from_bytes are unaffected.
|
||||||
|
for field, default in self._DEFAULTS.get(self.ID, {}).items():
|
||||||
|
if not getattr(self, field):
|
||||||
|
if field.startswith("color"):
|
||||||
|
setattr(self, field, common.ColorInt(default & 0xFFFFFF))
|
||||||
|
else:
|
||||||
|
setattr(self, field, default)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_bytes(cls, data, options=None):
|
def from_bytes(cls, data, options=None):
|
||||||
|
|
@ -2712,13 +2738,14 @@ class _HeadsetOnboardEffect:
|
||||||
kw["saturation"] = p[3]
|
kw["saturation"] = p[3]
|
||||||
if eid == 2:
|
if eid == 2:
|
||||||
kw["direction"] = p[4]
|
kw["direction"] = p[4]
|
||||||
elif eid == 3: # Breathing
|
elif eid == 3: # Breathing: R, G, B, intensity, period u16 BE
|
||||||
kw["color1"] = (p[0] << 16) | (p[1] << 8) | p[2]
|
kw["color1"] = (p[0] << 16) | (p[1] << 8) | p[2]
|
||||||
|
kw["intensity"] = p[3]
|
||||||
kw["period"] = (p[4] << 8) | p[5]
|
kw["period"] = (p[4] << 8) | p[5]
|
||||||
elif eid == 4: # DualColor
|
elif eid == 4: # DualColor: R1, G1, B1, R2, G2, B2, intensity
|
||||||
kw["color1"] = (p[0] << 16) | (p[1] << 8) | p[2]
|
kw["color1"] = (p[0] << 16) | (p[1] << 8) | p[2]
|
||||||
kw["color2"] = (p[3] << 16) | (p[4] << 8) | p[5]
|
kw["color2"] = (p[3] << 16) | (p[4] << 8) | p[5]
|
||||||
kw["speed"] = p[6]
|
kw["intensity"] = p[6]
|
||||||
return cls(**kw)
|
return cls(**kw)
|
||||||
|
|
||||||
def to_bytes(self, options=None):
|
def to_bytes(self, options=None):
|
||||||
|
|
@ -2733,14 +2760,14 @@ class _HeadsetOnboardEffect:
|
||||||
p[3] = self.saturation
|
p[3] = self.saturation
|
||||||
if eid == 2:
|
if eid == 2:
|
||||||
p[4] = self.direction
|
p[4] = self.direction
|
||||||
elif eid == 3: # Breathing: R, G, B, CE[6]=0, period u16 BE
|
elif eid == 3: # Breathing: R, G, B, intensity, period u16 BE, pad
|
||||||
p[0], p[1], p[2] = (c1 >> 16) & 0xFF, (c1 >> 8) & 0xFF, c1 & 0xFF
|
p[0], p[1], p[2] = (c1 >> 16) & 0xFF, (c1 >> 8) & 0xFF, c1 & 0xFF
|
||||||
|
p[3] = self.intensity
|
||||||
p[4], p[5] = (self.period >> 8) & 0xFF, self.period & 0xFF
|
p[4], p[5] = (self.period >> 8) & 0xFF, self.period & 0xFF
|
||||||
elif eid == 4: # DualColor: R1,G1,B1, R2,G2,B2, speed
|
elif eid == 4: # DualColor: R1,G1,B1, R2,G2,B2, intensity
|
||||||
p[0], p[1], p[2] = (c1 >> 16) & 0xFF, (c1 >> 8) & 0xFF, c1 & 0xFF
|
p[0], p[1], p[2] = (c1 >> 16) & 0xFF, (c1 >> 8) & 0xFF, c1 & 0xFF
|
||||||
p[3], p[4], p[5] = (c2 >> 16) & 0xFF, (c2 >> 8) & 0xFF, c2 & 0xFF
|
p[3], p[4], p[5] = (c2 >> 16) & 0xFF, (c2 >> 8) & 0xFF, c2 & 0xFF
|
||||||
p[6] = self.speed
|
p[6] = self.intensity
|
||||||
# eid 5 Custom: no inline parameters
|
|
||||||
return bytes([(eid >> 8) & 0xFF, eid & 0xFF]) + bytes(p)
|
return bytes([(eid >> 8) & 0xFF, eid & 0xFF]) + bytes(p)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
|
@ -2776,21 +2803,21 @@ class HeadsetOnboardEffect(settings.Setting):
|
||||||
feature = _F.HEADSET_RGB_ONBOARD_EFFECTS
|
feature = _F.HEADSET_RGB_ONBOARD_EFFECTS
|
||||||
|
|
||||||
_CLUSTER = 0
|
_CLUSTER = 0
|
||||||
|
# Custom (5) is intentionally absent — it is a stored card-reference
|
||||||
|
# effect, not a parametric one; it cannot be set via setRGBClusterEffect.
|
||||||
_ALL_EFFECTS = (
|
_ALL_EFFECTS = (
|
||||||
("Static", 0),
|
("Static", 0),
|
||||||
("Color Cycle", 1),
|
("Color Cycle", 1),
|
||||||
("Color Wave", 2),
|
("Color Wave", 2),
|
||||||
("Breathing", 3),
|
("Breathing", 3),
|
||||||
("Dual Color", 4),
|
("Dual Color", 4),
|
||||||
("Custom", 5),
|
|
||||||
)
|
)
|
||||||
_EFFECT_FIELDS = {
|
_EFFECT_FIELDS = {
|
||||||
0: ("color1",),
|
0: ("color1",),
|
||||||
1: ("intensity", "period", "saturation"),
|
1: ("intensity", "period", "saturation"),
|
||||||
2: ("intensity", "period", "saturation", "direction"),
|
2: ("intensity", "period", "saturation", "direction"),
|
||||||
3: ("color1", "period"),
|
3: ("color1", "intensity", "period"),
|
||||||
4: ("color1", "color2", "speed"),
|
4: ("color1", "color2", "intensity"),
|
||||||
5: (),
|
|
||||||
}
|
}
|
||||||
_DIRECTIONS = common.NamedInts(**{"Horizontal": 0, "Vertical": 1, "Reverse Horizontal": 2, "Reverse Vertical": 3})
|
_DIRECTIONS = common.NamedInts(**{"Horizontal": 0, "Vertical": 1, "Reverse Horizontal": 2, "Reverse Vertical": 3})
|
||||||
|
|
||||||
|
|
@ -2831,12 +2858,13 @@ class HeadsetOnboardEffect(settings.Setting):
|
||||||
if off + 2 > len(info):
|
if off + 2 > len(info):
|
||||||
break
|
break
|
||||||
eid = (info[off] << 8) | info[off + 1]
|
eid = (info[off] << 8) | info[off + 1]
|
||||||
if 0 <= eid <= 5 and eid not in supported:
|
if 0 <= eid <= 4 and eid not in supported:
|
||||||
supported.append(eid)
|
supported.append(eid)
|
||||||
if not supported:
|
if not supported:
|
||||||
# Unparseable reply — offer all six; the firmware rejects any it
|
# Unparseable reply — offer the five parametric effects; the
|
||||||
# doesn't support. See the 0x0621 fallback note in protocol RE.
|
# firmware rejects any it doesn't support. Custom (5) is never
|
||||||
supported = [0, 1, 2, 3, 4, 5]
|
# offered here. See the 0x0621 fallback note in protocol RE.
|
||||||
|
supported = [0, 1, 2, 3, 4]
|
||||||
rw = cls.rw_class(cls.feature, cls._CLUSTER)
|
rw = cls.rw_class(cls.feature, cls._CLUSTER)
|
||||||
validator = settings_validator.HeteroValidator(data_class=_HeadsetOnboardEffect, options=None)
|
validator = settings_validator.HeteroValidator(data_class=_HeadsetOnboardEffect, options=None)
|
||||||
setting = cls(device, rw, validator)
|
setting = cls(device, rw, validator)
|
||||||
|
|
@ -2848,8 +2876,14 @@ class HeadsetOnboardEffect(settings.Setting):
|
||||||
{"name": "color2", "kind": settings.Kind.COLOR, "label": _("Secondary")},
|
{"name": "color2", "kind": settings.Kind.COLOR, "label": _("Secondary")},
|
||||||
{"name": "intensity", "kind": settings.Kind.RANGE, "label": _("Intensity"), "min": 0, "max": 100},
|
{"name": "intensity", "kind": settings.Kind.RANGE, "label": _("Intensity"), "min": 0, "max": 100},
|
||||||
{"name": "saturation", "kind": settings.Kind.RANGE, "label": _("Saturation"), "min": 0, "max": 255},
|
{"name": "saturation", "kind": settings.Kind.RANGE, "label": _("Saturation"), "min": 0, "max": 255},
|
||||||
{"name": "period", "kind": settings.Kind.RANGE, "label": _("Period"), "min": 0, "max": 0xFFFF},
|
{
|
||||||
{"name": "speed", "kind": settings.Kind.RANGE, "label": _("Speed"), "min": 0, "max": 0xFF},
|
"name": "period",
|
||||||
|
"kind": settings.Kind.RANGE,
|
||||||
|
"label": _("Period"),
|
||||||
|
"min": 1000,
|
||||||
|
"max": 20000,
|
||||||
|
"display_seconds": True,
|
||||||
|
},
|
||||||
{"name": "direction", "kind": settings.Kind.CHOICE, "label": _("Direction"), "choices": cls._DIRECTIONS},
|
{"name": "direction", "kind": settings.Kind.CHOICE, "label": _("Direction"), "choices": cls._DIRECTIONS},
|
||||||
]
|
]
|
||||||
setting.fields_map = {eid: (id_choices[eid], {field: 1 for field in cls._EFFECT_FIELDS[eid]}) for eid in supported}
|
setting.fields_map = {eid: (id_choices[eid], {field: 1 for field in cls._EFFECT_FIELDS[eid]}) for eid in supported}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue