headset RGB: honor explicit black (0) onboard-effect colors

_HeadsetOnboardEffect.__init__ seeded per-effect defaults for any
field that was falsy, so a Static color1 of 0x000000 (black) was
treated as unset and overwritten with the white default — setting the
onboard color to black turned the LEDs white instead of off.

Switch the constructor to None-sentinel defaults: a field is seeded
from _DEFAULTS only when genuinely absent (None), so an explicit 0 is
honored. The UI's get_value() always passes explicit values, and a
fresh effect-pick seeds its RANGE widgets UI-side via _apply_id_defaults,
so animated effects still get sane defaults.

Reported by @rouderz on PR #3181.
This commit is contained in:
Ken Sanislo 2026-05-18 15:33:04 -07:00 committed by Peter F. Patel-Schneider
parent 5f6def437e
commit 7c7666466a
1 changed files with 14 additions and 18 deletions

View File

@ -2723,13 +2723,12 @@ 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 # Per-effect default parameter values, applied only to fields left
# uses that would otherwise be 0. Picking an effect in the UI rebuilds # unset (passed as None). An explicit value is always honored — passing
# this object from the (initially zeroed) field widgets, so without # 0 means the caller chose 0, e.g. a black Static color1 turns the LEDs
# seeding the picker emits an all-zero frame: the firmware rejects # off. Only a genuinely absent field falls back to the default (a fresh
# ColorCycle/ColorWave outright and renders Breathing/DualColor/Static # effect-pick seeds its RANGE widgets UI-side via _apply_id_defaults).
# as black or zero-intensity. intensity 0 in particular reads as "LEDs # Defaults confirmed against the LGHUB binary decode of 0x0621.
# off". Defaults confirmed against the LGHUB binary decode of 0x0621.
_DEFAULTS = { _DEFAULTS = {
0: {"color1": 0xFFFFFF}, # Static / Fixed 0: {"color1": 0xFFFFFF}, # Static / Fixed
1: {"intensity": 100, "saturation": 255, "period": 5000}, # Color Cycle 1: {"intensity": 100, "saturation": 255, "period": 5000}, # Color Cycle
@ -2740,24 +2739,21 @@ class _HeadsetOnboardEffect:
# speed is accepted only to load configs persisted before the 0x0621 # speed is accepted only to load configs persisted before the 0x0621
# decode (DualColor byte 6 was mislabelled "speed"; it is intensity). # 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=None, color2=None, intensity=None, saturation=None, period=None, speed=0, direction=None):
self.ID = int(ID) self.ID = int(ID)
defaults = self._DEFAULTS.get(self.ID, {})
color1 = defaults.get("color1", 0) if color1 is None else color1
color2 = defaults.get("color2", 0) if color2 is None else color2
intensity = defaults.get("intensity", 0) if intensity is None else intensity
saturation = defaults.get("saturation", 0) if saturation is None else saturation
period = defaults.get("period", 0) if period is None else period
direction = defaults.get("direction", 0) if direction is None else direction
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.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):