From 0fd262424e016a0a2a6679eb5f762fd6176578af Mon Sep 17 00:00:00 2001 From: "Peter F. Patel-Schneider" Date: Mon, 3 Nov 2025 15:58:56 +0900 Subject: [PATCH] settings: add setting for HAPTIC feature --- lib/logitech_receiver/hidpp20_constants.py | 21 +++++++ lib/logitech_receiver/settings.py | 1 + lib/logitech_receiver/settings_new.py | 1 + lib/logitech_receiver/settings_templates.py | 68 +++++++++++++++++++++ lib/solaar/ui/config_panel.py | 4 +- 5 files changed, 93 insertions(+), 2 deletions(-) diff --git a/lib/logitech_receiver/hidpp20_constants.py b/lib/logitech_receiver/hidpp20_constants.py index 45d0e7e8..e5c7f85d 100644 --- a/lib/logitech_receiver/hidpp20_constants.py +++ b/lib/logitech_receiver/hidpp20_constants.py @@ -66,6 +66,7 @@ class SupportedFeature(IntEnum): BACKLIGHT3 = 0x1983 ILLUMINATION = 0x1990 FORCE_SENSING_BUTTON = 0x19C0 + HAPTIC = 0x19B0 PRESENTER_CONTROL = 0x1A00 SENSOR_3D = 0x1A01 REPROG_CONTROLS = 0x1B00 @@ -277,3 +278,23 @@ class ParamId(IntEnum): PIXEL_ZONE = 2 # 4 2-byte integers, left, bottom, width, height; pixels RATIO_ZONE = 3 # 4 bytes, left, bottom, width, height; unit 1/240 pad size SCALE_FACTOR = 4 # 2-byte integer, with 256 as normal scale + + +HapticWaveForms = NamedInts( + SHARP_STATE_CHANGE=0x00, + DAMP_STATE_CHANGE=0x01, + SHARP_COLLISION=0x02, + DAMP_COLLISION=0x03, + SUBTLE_COLLISION=0x04, + HAPPY_ALERT=0x05, + ANGRY_ALERT=0x06, + COMPLETED=0x07, + SQUARE=0x08, + WAVE=0x09, + FIREWORK=0x0A, + MAD=0x0B, + KNOCK=0x0C, + JINGLE=0x0D, + RINGING=0xE, + WHISPER_COLLISION=0x1B, +) diff --git a/lib/logitech_receiver/settings.py b/lib/logitech_receiver/settings.py index 25b3fb01..f0f5442b 100644 --- a/lib/logitech_receiver/settings.py +++ b/lib/logitech_receiver/settings.py @@ -57,6 +57,7 @@ class Setting: rw_options = {} validator_class = None validator_options = {} + display = True # display setting in UI def __init__(self, device, rw, validator): self._device = device diff --git a/lib/logitech_receiver/settings_new.py b/lib/logitech_receiver/settings_new.py index bc0e807b..9e1161ab 100644 --- a/lib/logitech_receiver/settings_new.py +++ b/lib/logitech_receiver/settings_new.py @@ -40,6 +40,7 @@ class Setting: choices_universe = None # All possible acceptable keys, for settings with keys kind = Kind.NONE # What GUI interface to use persist = True # Whether to remember the setting + display = True # display setting in UI _device = None # The device that this setting is for _device_object = None # The object that interacts with the feature for the device _value = None # Stored value as maintained by Solaar, used for persistence diff --git a/lib/logitech_receiver/settings_templates.py b/lib/logitech_receiver/settings_templates.py index 2cd5b327..44ad57a4 100644 --- a/lib/logitech_receiver/settings_templates.py +++ b/lib/logitech_receiver/settings_templates.py @@ -1809,6 +1809,72 @@ class ForceSensing(settings_new.Settings): return setting +class HapticLevel(settings.Setting): + name = "haptic-level" + label = _("Haptic Feeback Level") + description = _("Change power of haptic feedback. (Zero to turn off.)") + feature = _F.HAPTIC + choices_universe = common.NamedInts(Off=0, Low=25, Medium=50, High=75, Maximum=100) + min_value = 0 + max_value = 100 + + class rw_class(settings.FeatureRW): + def __init__(self, feature): + super().__init__(feature, read_fnid=0x10, write_fnid=0x20) + + def read(self, device, data_bytes=b""): + result = device.feature_request(self.feature, 0x10) + if result[0] & 0x01 == 0: # disabled, return 0 + return b"\x00" + else: # enabled, return second byte + return result[1:2] + + def write(self, device, data_bytes): + if data_bytes == b"\x00": + write_bytes = b"\x00\x32" # disable, at 50 percent + else: + write_bytes = b"\x01" + data_bytes + reply = device.feature_request(self.feature, 0x20, write_bytes) + return reply + + @classmethod + def build(cls, device): + response = device.feature_request(cls.feature, 0x10) + if response: + rw = cls.rw_class(cls.feature) + levels = response[2] & 0x01 + if levels: # device only has four levels + validator = settings_validator.ChoicesValidator(choices=cls.choices_universe) + else: # device has all levels + validator = settings_validator.RangeValidator(min_value=cls.min_value, max_value=cls.max_value) + return cls(device, rw, validator) + + +# This setting is not displayed in the UI +# Use `solaar config haptic-play
` to play a haptic form +class PlayHapticWaveForm(settings.Setting): + name = "haptic-play" + label = _("Play Haptic Waveform") + description = _("Tell device to play a haptic waveform.") + feature = _F.HAPTIC + choices_universe = hidpp20_constants.HapticWaveForms + rw_options = {"read_fnid": None, "write_fnid": 0x40} # nothing to read + persist = False # persisting this setting is useless + display = False # don't display in UI, interact using `solaar config ...` + + class validator_class(settings_validator.ChoicesValidator): + @classmethod + def build(cls, setting_class, device): + response = device.feature_request(_F.HAPTIC, 0x00) + if response: + waves = common.NamedInts() + waveforms = int.from_bytes(response[4:8]) + for waveform in hidpp20_constants.HapticWaveForms: + if (1 << int(waveform)) & waveforms: + waves[int(waveform)] = str(waveform) + return cls(choices=waves, byte_count=1) + + SETTINGS: list[settings.Setting] = [ RegisterHandDetection, # simple RegisterSmoothScroll, # simple @@ -1866,6 +1932,8 @@ SETTINGS: list[settings.Setting] = [ Gesture2Gestures, # working Gesture2Divert, Gesture2Params, # working + HapticLevel, + PlayHapticWaveForm, Sidetone, Equalizer, ADCPower, diff --git a/lib/solaar/ui/config_panel.py b/lib/solaar/ui/config_panel.py index 71443bdc..bdc5946b 100644 --- a/lib/solaar/ui/config_panel.py +++ b/lib/solaar/ui/config_panel.py @@ -16,7 +16,6 @@ ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. import logging -import traceback from enum import Enum from threading import Timer @@ -70,7 +69,6 @@ def _write_async(setting, value, sbox, sensitive=True, key=None): v = setting.write_key_value(key, v) v = {key: v} except Exception: - traceback.print_exc() v = None if sb: GLib.idle_add(_update_setting_item, sb, v, True, sensitive, priority=99) @@ -660,6 +658,8 @@ def _change_icon(allowed, icon): def _create_sbox(s, _device): + if not s.display: + return sbox = Gtk.HBox(homogeneous=False, spacing=6) sbox.setting = s sbox.kind = s.kind