HeadsetSidetone: scale UI percent to the device's gain-step grid
The 0x0604 wire "level" is a gain-step index 0..N-1, not a 0-100 percent. G HUB reads the step count N from getSidetoneLevelSettings (function 2) and scales: level = (N-1)*pct/100. Solaar wrote the raw percent, which only matches when N == 101 — so V2 headsets reporting a smaller N (G522: N=10) got out-of-range step writes the firmware silently ACK'd without applying. Read func 2 at build time, take reply byte 2 as N (default 101 when the call is absent — V1 — or the reply is unusable). The validator now maps the 0-100 UI percent to/from the wire step index and clamps writes to N-1. V1 devices (e.g. PRO X 2) keep N=101, which makes the scaling an exact identity — their wire traffic is unchanged. The raw func-2 reply is still logged at debug level for a future G HUB setpoint correlation.
This commit is contained in:
parent
c9db25a302
commit
5b704c5bd7
|
|
@ -1708,10 +1708,35 @@ class HeadsetSidetone(settings.Setting):
|
||||||
description = _("Sidetone level (0 = off, 100 = max).")
|
description = _("Sidetone level (0 = off, 100 = max).")
|
||||||
feature = _F.HEADSET_AUDIO_SIDETONE
|
feature = _F.HEADSET_AUDIO_SIDETONE
|
||||||
rw_options = {"read_fnid": 0x00, "write_fnid": 0x10}
|
rw_options = {"read_fnid": 0x00, "write_fnid": 0x10}
|
||||||
validator_class = settings_validator.RangeValidator
|
|
||||||
min_value = 0
|
min_value = 0
|
||||||
max_value = 100
|
max_value = 100
|
||||||
|
|
||||||
|
class validator_class(settings_validator.RangeValidator):
|
||||||
|
"""UI value is 0-100 percent; the wire level is a gain-step index
|
||||||
|
0..N-1. N (gain_steps) comes from getSidetoneLevelSettings on V2
|
||||||
|
devices, or defaults to 101 on V1 — which makes step == percent, so
|
||||||
|
V1 round-trips unchanged. The firmware silently ignores out-of-range
|
||||||
|
step writes, so writes are clamped to N-1."""
|
||||||
|
|
||||||
|
gain_steps = 101
|
||||||
|
|
||||||
|
def _level_bytes(self, raw):
|
||||||
|
return common.bytes2int(raw[self.read_skip_byte_count : self.read_skip_byte_count + self._byte_count])
|
||||||
|
|
||||||
|
def validate_read(self, reply_bytes):
|
||||||
|
level = self._level_bytes(reply_bytes)
|
||||||
|
steps = self.gain_steps
|
||||||
|
return int(round(level * 100 / (steps - 1))) if steps > 1 else level
|
||||||
|
|
||||||
|
def prepare_write(self, new_value, current_value=None):
|
||||||
|
steps = self.gain_steps
|
||||||
|
level = int(round((steps - 1) * new_value / 100)) if steps > 1 else new_value
|
||||||
|
level = max(0, min((steps - 1) if steps > 1 else self.max_value, level))
|
||||||
|
to_write = self.write_prefix_bytes + common.int2bytes(level, self._byte_count)
|
||||||
|
if current_value is not None and self._level_bytes(current_value) == level:
|
||||||
|
return None
|
||||||
|
return to_write
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build(cls, device):
|
def build(cls, device):
|
||||||
# Version <= 1: GetSidetone returns [mic_count, mic_id, level]; SetSidetone takes [mic_id, level]
|
# Version <= 1: GetSidetone returns [mic_count, mic_id, level]; SetSidetone takes [mic_id, level]
|
||||||
|
|
@ -1721,18 +1746,23 @@ class HeadsetSidetone(settings.Setting):
|
||||||
skip, prefix = 3, b"\x01\xff"
|
skip, prefix = 3, b"\x01\xff"
|
||||||
else:
|
else:
|
||||||
skip, prefix = 2, b"\x01"
|
skip, prefix = 2, b"\x01"
|
||||||
# getSidetoneLevelSettings (fn 2) carries the gain-step count that scales
|
# V2 getSidetoneLevelSettings (fn 2) reply byte 2 is the gain-step count N.
|
||||||
# wire level <-> UI percent; its byte layout is unmapped. Log the raw
|
# V1 has no such call — N stays 101 (step == percent). Raw reply still
|
||||||
# reply under -dd so a user log pins it down.
|
# logged at debug so a G HUB setpoint correlation can refine the layout.
|
||||||
if logger.isEnabledFor(logging.DEBUG):
|
gain_steps = 101
|
||||||
|
if version > 1:
|
||||||
try:
|
try:
|
||||||
reply = device.feature_request(cls.feature, 0x20)
|
reply = device.feature_request(cls.feature, 0x20)
|
||||||
logger.debug("%s: getSidetoneLevelSettings raw reply: %s", cls.name, reply.hex() if reply else reply)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
reply = None
|
||||||
logger.debug("%s: getSidetoneLevelSettings probe raised %s", cls.name, e)
|
logger.debug("%s: getSidetoneLevelSettings probe raised %s", cls.name, e)
|
||||||
|
logger.debug("%s: getSidetoneLevelSettings raw reply: %s", cls.name, reply.hex() if reply else reply)
|
||||||
|
if reply is not None and len(reply) >= 3 and reply[2] > 1:
|
||||||
|
gain_steps = reply[2]
|
||||||
rw = settings.FeatureRW(cls.feature, **cls.rw_options)
|
rw = settings.FeatureRW(cls.feature, **cls.rw_options)
|
||||||
validator = cls.validator_class.build(cls, device, read_skip_byte_count=skip, write_prefix_bytes=prefix)
|
validator = cls.validator_class.build(cls, device, read_skip_byte_count=skip, write_prefix_bytes=prefix)
|
||||||
if validator:
|
if validator:
|
||||||
|
validator.gain_steps = gain_steps
|
||||||
return cls(device, rw, validator)
|
return cls(device, rw, validator)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue