G915 TKL: device-gated RGB-claim quirk — keep onboard mode on claim
A usbmon capture of G HUB's bring-up (receiver passed to a Win11 VM) showed the root cause of the dead F-row/media keys AND the F4 blackout: the RGB SW-control claim switched the keyboard to host mode (ONBOARD_PROFILES 0x10 0x02). On the G915 TKL host mode disables the F-row/media keys (#1100), and the host-mode transition made the firmware re-assert the M/MR indicator (overlaid on F1-F4 on this keyless model) over the per-key colours. G HUB drives the LEDs from onboard mode via the RGB SetSWControl claim alone, never switching. Gated by modelId in device_quirks.py so no other device changes: - rgb_claim_keeps_onboard_mode(): _claim_sw_control skips the onboard-> host switch for listed models -> F-keys/media keys work and the power-on F4 blackout is gone, with no timer and no G-key divert. - rgb_repaint_on_profile_change(): re-assert + repaint on a reported profile change so an accidental switch doesn't strand the scheme. Documented residual (cannot be fixed without breaking the keys): F1-F4 are the M1/M2/M3/MR indicator zones on the TKL. The firmware re-asserts the active-profile indicator on them on profile switch and after idle, so those keys can show the indicator instead of the user colour. Claiming that subsystem (G-key divert) would stop it but disables the F-row/media keys, so onboard mode + working keys is the chosen trade-off. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
19865f6b54
commit
73adad2a52
|
|
@ -69,6 +69,38 @@ HEADSET_SIGNATURE_EFFECTS_ALLOWED: dict[str, dict[int, set[str]]] = {
|
|||
}
|
||||
|
||||
|
||||
# Keyboards whose firmware breaks the F-row / media keys when the RGB
|
||||
# SW-control claim switches them to host mode (ONBOARD_PROFILES fn 0x10 mode
|
||||
# 0x02) — Solaar #1100. A USB capture of G HUB's bring-up shows it drives the
|
||||
# LEDs from onboard mode via the RGB SetSWControl claim alone, never making
|
||||
# that switch. For these models the claim keeps onboard mode, which also avoids
|
||||
# the host-mode transition that made the firmware re-assert the M/MR indicator
|
||||
# over a per-key cell (the G915 TKL F4 blackout).
|
||||
RGB_CLAIM_KEEPS_ONBOARD_MODE: set[str] = {
|
||||
"B35F408EC343", # G915 TKL LIGHTSPEED — verified: host mode disables F-keys/media keys
|
||||
}
|
||||
|
||||
# Keyboards where switching onboard profiles drops the software per-key/zone
|
||||
# paint (the firmware loads the switched-to profile's own onboard lighting).
|
||||
# For these, re-assert the claim + repaint on a profile-change notification so
|
||||
# an accidental profile switch doesn't strand the user's software scheme.
|
||||
RGB_REPAINT_ON_PROFILE_CHANGE: set[str] = {
|
||||
"B35F408EC343", # G915 TKL LIGHTSPEED
|
||||
}
|
||||
|
||||
|
||||
def rgb_claim_keeps_onboard_mode(device) -> bool:
|
||||
"""True when the RGB SW-control claim must NOT switch this model to host
|
||||
mode (it would disable the F-row / media keys)."""
|
||||
return (getattr(device, "modelId", None) or "") in RGB_CLAIM_KEEPS_ONBOARD_MODE
|
||||
|
||||
|
||||
def rgb_repaint_on_profile_change(device) -> bool:
|
||||
"""True when a profile-change notification should re-apply the claimed
|
||||
software RGB state on this model (the switch drops the per-key paint)."""
|
||||
return (getattr(device, "modelId", None) or "") in RGB_REPAINT_ON_PROFILE_CHANGE
|
||||
|
||||
|
||||
def rgb_effects_nvconfig_allowed_fields(device, cap_id: int) -> set[str] | None:
|
||||
"""Color fields to expose for an 0x8071 NvConfig boot effect.
|
||||
|
||||
|
|
|
|||
|
|
@ -542,6 +542,18 @@ def profile_change(device, profile_sector):
|
|||
device.setting_callback(device, AdjustableDpi, [profile.resolutions[resolution_index]])
|
||||
device.setting_callback(device, ReportRate, [profile.report_rate])
|
||||
break
|
||||
# A profile switch reloads that profile's onboard lighting, dropping the
|
||||
# software per-key/zone paint. On models prone to this, re-assert the claim
|
||||
# and repaint (only when Solaar already holds the LED claim) so an
|
||||
# accidental profile switch doesn't strand the user's scheme.
|
||||
if device_quirks.rgb_repaint_on_profile_change(device):
|
||||
for s in device.settings or []:
|
||||
if s.name == "rgb_control" and getattr(s, "_value", None):
|
||||
try:
|
||||
s._claim_sw_control(device)
|
||||
except Exception as e:
|
||||
logger.warning("%s: repaint after profile change failed: %s", device, e)
|
||||
break
|
||||
|
||||
|
||||
class OnboardProfiles(settings.Setting):
|
||||
|
|
@ -3330,11 +3342,16 @@ class RGBControl(settings.Setting):
|
|||
return value
|
||||
|
||||
def _claim_sw_control(self, device):
|
||||
# Disable firmware power management via profile management or onboard profiles
|
||||
if device.features and _F.PROFILE_MANAGEMENT in device.features:
|
||||
device.feature_request(_F.PROFILE_MANAGEMENT, 0x60, b"\x05")
|
||||
elif device.features and _F.ONBOARD_PROFILES in device.features:
|
||||
device.feature_request(_F.ONBOARD_PROFILES, 0x10, b"\x02")
|
||||
# Disable firmware power management via profile management or onboard
|
||||
# profiles. Skipped on models where the onboard->host mode switch
|
||||
# disables the F-row / media keys (device_quirks, Solaar #1100): they
|
||||
# drive the LEDs from onboard mode via the SetSWControl claim alone,
|
||||
# exactly as G HUB does.
|
||||
if not device_quirks.rgb_claim_keeps_onboard_mode(device):
|
||||
if device.features and _F.PROFILE_MANAGEMENT in device.features:
|
||||
device.feature_request(_F.PROFILE_MANAGEMENT, 0x60, b"\x05")
|
||||
elif device.features and _F.ONBOARD_PROFILES in device.features:
|
||||
device.feature_request(_F.ONBOARD_PROFILES, 0x10, b"\x02")
|
||||
# Claim LED pipeline: SetSWControl(mode=3, flags=5)
|
||||
device.feature_request(_F.RGB_EFFECTS, 0x50, rgb_power.SW_ACTIVE)
|
||||
# Reset per-key one-shot flags so the first write after this claim
|
||||
|
|
@ -3349,15 +3366,15 @@ class RGBControl(settings.Setting):
|
|||
# Register cleanup for graceful release on device close
|
||||
if rgb_power.cleanup not in device.cleanups:
|
||||
device.cleanups.append(rgb_power.cleanup)
|
||||
# Repaint LEDs with Solaar's saved state. Without this the firmware's
|
||||
# last-active onboard profile keeps showing until the user changes
|
||||
# something — the takeover would look like it did nothing.
|
||||
# Repaint saved zone effects and the per-key buffer after the claim.
|
||||
# (No settle-repaint timer needed: keeping onboard mode on the affected
|
||||
# models avoids the host-mode transition that caused the F4 blackout.)
|
||||
self._repaint_after_claim(device)
|
||||
|
||||
def _repaint_after_claim(self, device):
|
||||
"""Push saved zone effects and (if opted in) per-key buffer to the
|
||||
device after a fresh SW claim. Best-effort: individual failures get
|
||||
logged but don't abort the rest of the repaint."""
|
||||
"""Push saved zone effects and the per-key buffer to the device after
|
||||
a fresh SW claim. Best-effort: individual failures get logged but
|
||||
don't abort the rest of the repaint."""
|
||||
for s in device.settings:
|
||||
if s.name.startswith("rgb_zone_") and s._value is not None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -763,3 +763,30 @@ def test_perkey_read_heals_poisoned_persisted_map(monkeypatch):
|
|||
|
||||
assert -1 not in result
|
||||
assert result[7] == 0x00FF00
|
||||
|
||||
|
||||
# --- post-claim repaint ------------------------------------------------------
|
||||
|
||||
|
||||
def test_repaint_after_claim_pushes_zones_and_perkey(monkeypatch):
|
||||
"""The post-claim repaint pushes saved zone effects and the per-key
|
||||
buffer. (The F4/MR blackout is handled structurally by keeping affected
|
||||
models in onboard mode — device_quirks.rgb_claim_keeps_onboard_mode — not
|
||||
by any timed repaint.)"""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from logitech_receiver import settings_templates
|
||||
|
||||
ctl = settings_templates.RGBControl.__new__(settings_templates.RGBControl)
|
||||
zone = MagicMock()
|
||||
zone.name = "rgb_zone_1"
|
||||
zone._value = object()
|
||||
device = MagicMock()
|
||||
device.settings = [zone]
|
||||
perkey = MagicMock()
|
||||
perkey._value = {1: 0xFF0000}
|
||||
monkeypatch.setattr(rgb_power, "perkey_has_paint", lambda d: (perkey, True))
|
||||
|
||||
ctl._repaint_after_claim(device)
|
||||
zone.write.assert_called_once()
|
||||
perkey.write.assert_called_once()
|
||||
|
|
|
|||
Loading…
Reference in New Issue