Add G560 audio features: bass level, working equalizer, jack detection

Add the BassTone feature (0x8305) as a 0-100 slider — subwoofer level
on the G560, hardware-verified.

Fix the Equalizer setting (0x8310), which never built on devices
reporting signed dB limits: getEQInfo dbMin/dbMax are signed (the G560
reports -20..+6) but were unpacked unsigned, tripping the validator's
min/max assertion.  The reported bounds are also exclusive — the G560
NACKs writes at exactly dbMin/dbMax — so present dbMin+1..dbMax-1.
Render with the vertical-slider graphic EQ panel.  Note: on the G560
the stored curve round-trips but has no audible effect on USB
playback; whether it shapes the aux input path is still unknown.

Rename HEADSET_OUT (0x8320) to JACK_DETECTION: the feature reports
3.5mm jack presence (getJackStatus statuses/available bitmasks), it
is not an output-mode control.

Fix solaar show sending little-endian feature IDs to ROOT.getFeature,
which silently dropped the flags/version column for every
non-palindromic feature ID and displayed unknown features byte-swapped
(the G560's 0x8305 rendered as "unknown:8305 {0583}").
This commit is contained in:
Ken Sanislo 2026-07-16 20:20:35 -07:00
parent 38b5665e51
commit 0cc048cdf6
5 changed files with 44 additions and 15 deletions

View File

@ -4,12 +4,12 @@ G560 Gaming Speaker
Codename : G560 Gaming Speaker
Kind : speaker
Protocol : HID++ 4.2
Serial number:
Serial number:
Model ID: 000000000A78
Unit ID: FFFFFFFF
0: U1 22.04.B0370
Supports 10 HID++ 2.0 features:
0: ROOT {0000} V0
0: ROOT {0000} V0
1: FEATURE SET {0001}
2: DEVICE FW VERSION {0003}
Firmware: 0 U1 22.04.B0370 0A78
@ -60,5 +60,5 @@ G560 Gaming Speaker
8: BRIGHTNESS CONTROL {8040}
Brightness Control (saved): 100
Brightness Control : 100
9: unknown:8305 {0583} V0
9: unknown:8305 {0583} V0
Battery status unavailable.

View File

@ -175,10 +175,11 @@ class SupportedFeature(IntEnum):
OPERATING_RANGE = 0x8138
TRUE_FORCE = 0x8139
FFB_FILTER = 0x8140
# Headsets
# Headsets / speakers
SIDETONE = 0x8300
BASS_TONE = 0x8305
EQUALIZER = 0x8310
HEADSET_OUT = 0x8320
JACK_DETECTION = 0x8320 # jack presence (was HEADSET_OUT)
# Centurion core
CENTURION_DEVICE_INFO = 0x0100
CENTURION_DEVICE_NAME = 0x0101

View File

@ -1587,6 +1587,16 @@ class Sidetone(settings.Setting):
max_value = 100
class BassTone(settings.Setting):
name = "bass_tone"
label = _("Bass Level")
description = _("Set subwoofer bass level.")
feature = _F.BASS_TONE
validator_class = settings_validator.RangeValidator
min_value = 0
max_value = 100
class Equalizer(settings.RangeFieldSetting):
name = "equalizer"
label = _("Equalizer")
@ -1596,16 +1606,21 @@ class Equalizer(settings.RangeFieldSetting):
keys_universe = []
class validator_class(settings_validator.PackedRangeValidator):
kind = settings.Kind.GRAPHIC_EQ
@classmethod
def build(cls, setting_class, device):
data = device.feature_request(_F.EQUALIZER, 0x00)
if not data:
return None
count, dbRange, _x, dbMin, dbMax = struct.unpack("!BBBBB", data[:5])
if dbMin == 0:
dbMin = -dbRange
if dbMax == 0:
dbMax = dbRange
# dbMin/dbMax are signed; the G560 reports an asymmetric -20..+6
count, dbRange, _x, dbMin, dbMax = struct.unpack("!BBBbb", data[:5])
if dbMin == 0 and dbMax == 0:
dbMin, dbMax = -dbRange, dbRange
else:
# reported bounds are exclusive: the G560 NACKs writes at
# exactly dbMin/dbMax but accepts dbMin+1..dbMax-1
dbMin, dbMax = dbMin + 1, dbMax - 1
map = common.NamedInts()
for g in range((count + 6) // 7):
freqs = device.feature_request(_F.EQUALIZER, 0x10, g * 7)
@ -4520,6 +4535,7 @@ SETTINGS: list[settings.Setting] = [
HapticLevel,
PlayHapticWaveForm,
Sidetone,
BassTone,
Equalizer,
ADCPower,
HeadsetEcoMode,

View File

@ -239,11 +239,9 @@ def _print_device(dev, num=None):
# For centurion child, skip dongle features (already shown on the receiver)
if is_centurion_child and not in_sub_device:
continue
if isinstance(feature, str):
feature_bytes = bytes.fromhex(feature[-4:])
else:
feature_bytes = feature.to_bytes(2, byteorder="little")
feature_int = int.from_bytes(feature_bytes, byteorder="little")
# ROOT.getFeature takes the feature ID big-endian.
feature_int = int(feature[-4:], 16) if isinstance(feature, str) else int(feature)
feature_bytes = feature_int.to_bytes(2, byteorder="big")
display_name = feature
if is_centurion_child and in_sub_device:
# Use cached version — skip slow bridge ROOT queries

View File

@ -271,6 +271,11 @@ simple_tests = [
fake_hidpp.Response("05", 0x0400),
fake_hidpp.Response("0A", 0x0410, "0A"),
),
Setup(
FeatureTest(settings_templates.BassTone, 0x46, 0x50),
fake_hidpp.Response("46", 0x0400),
fake_hidpp.Response("50", 0x0410, "50"),
),
Setup(
FeatureTest(settings_templates.ADCPower, 5, 0xA, version=0x03),
fake_hidpp.Response("05", 0x0410),
@ -662,6 +667,15 @@ key_tests = [
fake_hidpp.Response("E010", 0x0430, "02E010"),
fake_hidpp.Response("E018", 0x0430, "02E018"),
),
Setup( # signed asymmetric dB range as reported by the G560 (-20..+6, bounds exclusive)
FeatureTest(settings_templates.Equalizer, {0: -4, 1: 2}, {1: 5}, 2),
[-19, 5],
fake_hidpp.Response("021A00EC06", 0x0400),
fake_hidpp.Response("0000200040", 0x0410, "00"),
fake_hidpp.Response("FC02", 0x0420, "00"),
fake_hidpp.Response("FC02", 0x0430, "02FC02"),
fake_hidpp.Response("FC05", 0x0430, "02FC05"),
),
Setup( # HeadsetOnboardEQ: 2 bands, 128Hz/-2dB/Q10 and 256Hz/+3dB/Q10
FeatureTest(settings_templates.HeadsetOnboardEQ, {0: -2, 1: 3}, {1: 5}, 2),
[-12, 12],