settings: add new settings type for structure-backed setting
This commit is contained in:
parent
ec5b406909
commit
f739331dc2
|
|
@ -140,7 +140,7 @@ class Device:
|
||||||
self._modelId = None # model id (contains identifiers for the transports of the device)
|
self._modelId = None # model id (contains identifiers for the transports of the device)
|
||||||
self._tid_map = None # map from transports to product identifiers
|
self._tid_map = None # map from transports to product identifiers
|
||||||
self._persister = None # persister holds settings
|
self._persister = None # persister holds settings
|
||||||
self._led_effects = self._firmware = self._keys = self._remap_keys = self._gestures = None
|
self._led_effects = self._firmware = self._keys = self._remap_keys = self._gestures = self._force_buttons = None
|
||||||
self._profiles = self._backlight = self._settings = None
|
self._profiles = self._backlight = self._settings = None
|
||||||
self.registers = []
|
self.registers = []
|
||||||
self.notification_flags = None
|
self.notification_flags = None
|
||||||
|
|
@ -346,6 +346,12 @@ class Device:
|
||||||
self._profiles = _hidpp20.get_profiles(self)
|
self._profiles = _hidpp20.get_profiles(self)
|
||||||
return self._profiles
|
return self._profiles
|
||||||
|
|
||||||
|
def force_buttons(self):
|
||||||
|
if self._force_buttons is None:
|
||||||
|
if self.online and self.protocol >= 2.0:
|
||||||
|
self._force_buttons = _hidpp20.get_force_buttons(self) or ()
|
||||||
|
return self._force_buttons
|
||||||
|
|
||||||
def set_configuration(self, configuration_, no_reply=False):
|
def set_configuration(self, configuration_, no_reply=False):
|
||||||
if self.online and self.protocol >= 2.0:
|
if self.online and self.protocol >= 2.0:
|
||||||
_hidpp20.config_change(self, configuration_, no_reply=no_reply)
|
_hidpp20.config_change(self, configuration_, no_reply=no_reply)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import socket
|
||||||
import struct
|
import struct
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
from collections import UserDict
|
||||||
from enum import Flag
|
from enum import Flag
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
@ -1713,6 +1714,12 @@ class Hidpp20:
|
||||||
if SupportedFeature.BACKLIGHT2 in device.features:
|
if SupportedFeature.BACKLIGHT2 in device.features:
|
||||||
return Backlight(device)
|
return Backlight(device)
|
||||||
|
|
||||||
|
def get_force_buttons(self, device: Device):
|
||||||
|
if getattr(device, "_force_buttons", None) is not None:
|
||||||
|
return device._force_buttons
|
||||||
|
if SupportedFeature.FORCE_SENSING_BUTTON in device.features:
|
||||||
|
return ForceSensingButtonArray(device)
|
||||||
|
|
||||||
def get_profiles(self, device: Device):
|
def get_profiles(self, device: Device):
|
||||||
if getattr(device, "_profiles", None) is not None:
|
if getattr(device, "_profiles", None) is not None:
|
||||||
return device._profiles
|
return device._profiles
|
||||||
|
|
@ -2021,3 +2028,77 @@ def estimate_battery_level_percentage(value_millivolt: int) -> int | None:
|
||||||
percent = p_low + (p_high - p_low) * (value_millivolt - v_low) / (v_high - v_low)
|
percent = p_low + (p_high - p_low) * (value_millivolt - v_low) / (v_high - v_low)
|
||||||
return round(percent)
|
return round(percent)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class ForceSensingButton:
|
||||||
|
"""A button that has a force value at which to trigger the button"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, device, number: int):
|
||||||
|
buttondata = device.feature_request(SupportedFeature.FORCE_SENSING_BUTTON, 0x10, number)
|
||||||
|
buttoncurrent = device.feature_request(SupportedFeature.FORCE_SENSING_BUTTON, 0x20, number)
|
||||||
|
if buttondata is not None and buttoncurrent is not None:
|
||||||
|
changeable, default, max_value, min_value = struct.unpack("!HHHH", buttondata[:8])
|
||||||
|
changeable = changeable & 0x01
|
||||||
|
current = struct.unpack("!H", buttoncurrent[:2])[0]
|
||||||
|
return cls(device, number, changeable, default, max_value, min_value, current)
|
||||||
|
|
||||||
|
def __init__(self, device, number: int, changeable: bool, default: int, max_value: int, min_value: int, current: int):
|
||||||
|
self._device = device
|
||||||
|
self.number = number
|
||||||
|
self.changeable = changeable
|
||||||
|
self.default = default
|
||||||
|
self.min_value = min_value
|
||||||
|
self.max_value = max_value
|
||||||
|
self._current = current
|
||||||
|
|
||||||
|
def get_current(self) -> int:
|
||||||
|
return self._current
|
||||||
|
|
||||||
|
def set_current(self, current: int) -> None:
|
||||||
|
if not self.changeable:
|
||||||
|
logger.warning(f"FORCE_SENSING_BUTTON on device {self._device} does not allow changing force.")
|
||||||
|
if self.min_value <= current <= self.max_value:
|
||||||
|
ret = self._device.feature_request(
|
||||||
|
SupportedFeature.FORCE_SENSING_BUTTON, 0x30, struct.pack("!BH", self.number, current)
|
||||||
|
)
|
||||||
|
if ret is None and logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug(f"FORCE_SENSING_BUTTON setButtonConfig on device {self._device} didn't respond.")
|
||||||
|
|
||||||
|
def acceptable_current(self, value: int) -> bool:
|
||||||
|
return self.min_value <= value <= self.max_value
|
||||||
|
|
||||||
|
|
||||||
|
class ForceSensingButtonArray(UserDict):
|
||||||
|
"""A map of buttons supporting force sensing"""
|
||||||
|
|
||||||
|
def __new__(cls, device: Device):
|
||||||
|
assert device is not None
|
||||||
|
count = device.feature_request(SupportedFeature.FORCE_SENSING_BUTTON, 0x00)
|
||||||
|
if count:
|
||||||
|
instance = super().__new__(cls)
|
||||||
|
instance._count = ord(count[:1])
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def __init__(self, device: Device):
|
||||||
|
super().__init__(self)
|
||||||
|
self.device = device
|
||||||
|
for index in range(0, self._count):
|
||||||
|
self[index] = None
|
||||||
|
|
||||||
|
def __getitem__(self, index: int):
|
||||||
|
item = super().__getitem__(index)
|
||||||
|
if item is None:
|
||||||
|
self.query_key(index)
|
||||||
|
return super().__getitem__(index)
|
||||||
|
|
||||||
|
def query_key(self, index):
|
||||||
|
if index not in self:
|
||||||
|
raise IndexError(index)
|
||||||
|
button = ForceSensingButton.create(self.device, index)
|
||||||
|
if button:
|
||||||
|
self[index] = button
|
||||||
|
return button
|
||||||
|
|
||||||
|
def acceptable(self, index: int, value: int) -> bool:
|
||||||
|
return self[index].acceptable(value)
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ class SupportedFeature(IntEnum):
|
||||||
BACKLIGHT2 = 0x1982
|
BACKLIGHT2 = 0x1982
|
||||||
BACKLIGHT3 = 0x1983
|
BACKLIGHT3 = 0x1983
|
||||||
ILLUMINATION = 0x1990
|
ILLUMINATION = 0x1990
|
||||||
|
FORCE_SENSING_BUTTON = 0x19C0
|
||||||
PRESENTER_CONTROL = 0x1A00
|
PRESENTER_CONTROL = 0x1A00
|
||||||
SENSOR_3D = 0x1A01
|
SENSOR_3D = 0x1A01
|
||||||
REPROG_CONTROLS = 0x1B00
|
REPROG_CONTROLS = 0x1B00
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ SENSITIVITY_IGNORE = "ignore"
|
||||||
|
|
||||||
|
|
||||||
class Kind(IntEnum):
|
class Kind(IntEnum):
|
||||||
|
NONE = 0
|
||||||
TOGGLE = 0x01
|
TOGGLE = 0x01
|
||||||
CHOICE = 0x02
|
CHOICE = 0x02
|
||||||
RANGE = 0x04
|
RANGE = 0x04
|
||||||
|
|
@ -43,6 +44,7 @@ class Kind(IntEnum):
|
||||||
PACKED_RANGE = 0x20
|
PACKED_RANGE = 0x20
|
||||||
MULTIPLE_RANGE = 0x40
|
MULTIPLE_RANGE = 0x40
|
||||||
HETERO = 0x80
|
HETERO = 0x80
|
||||||
|
MAP_RANGE = 0x102
|
||||||
|
|
||||||
|
|
||||||
class Setting:
|
class Setting:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
## Copyright (C) 2025 Solaar contributors
|
||||||
|
##
|
||||||
|
## This program is free software; you can redistribute it and/or modify
|
||||||
|
## it under the terms of the GNU General Public License as published by
|
||||||
|
## the Free Software Foundation; either version 2 of the License, or
|
||||||
|
## (at your option) any later version.
|
||||||
|
##
|
||||||
|
## This program is distributed in the hope that it will be useful,
|
||||||
|
## but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
## GNU General Public License for more details.
|
||||||
|
##
|
||||||
|
## You should have received a copy of the GNU General Public License along
|
||||||
|
## with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
## A new way of supporting settings, using a feature-specifi device class to store, read, and write relevant information
|
||||||
|
## The setting uses the device class to interact with the device feature.
|
||||||
|
## The setting uses a persist class to keep track of the setting.
|
||||||
|
|
||||||
|
## Interface:
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from .settings import Kind
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Setting:
|
||||||
|
name = None # Solaar internal name for the setting
|
||||||
|
label = None # Solaar user name for the setting (translatable)
|
||||||
|
description = None # Solaar extra desciption for the setting (translatable)
|
||||||
|
feature = None # Logitech feature that the setting uses
|
||||||
|
min_version = 0 # Minimum version of the feature needed
|
||||||
|
setup = None # method name on Device class to get the device object
|
||||||
|
get = None # method name on the device object to get the setting value
|
||||||
|
set = None # method name on the device object to set the setting value
|
||||||
|
acceptable = None # method name on the device object to check for acceptable values
|
||||||
|
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
|
||||||
|
_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
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_properties(cl, cls):
|
||||||
|
assert cls.name and cls.label and cls.description, "New settings require a name, label, and description"
|
||||||
|
assert cls.feature, "New settings require a feature"
|
||||||
|
assert cls.setup, "New settings require a setup device method"
|
||||||
|
assert cls.get and cls.set and cls.acceptable, "New settings require get, set, and acceptable methods"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build(cls, device):
|
||||||
|
"""Create the setting."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _pre_read(self, cached):
|
||||||
|
"""Get information from and save information to the persister"""
|
||||||
|
# Get the persister map if available and not done already
|
||||||
|
if self.persist and self._value is None and getattr(self._device, "persister", None):
|
||||||
|
self._value = self._device.persister.get(self.name)
|
||||||
|
# If this is new save its current value for the next time
|
||||||
|
if cached and self._value is not None:
|
||||||
|
if getattr(self._device, "persister", None) and self.name not in self._device.persister:
|
||||||
|
self._device.persister[self.name] = self._value if self.persist else None
|
||||||
|
|
||||||
|
def read(self, cached=True):
|
||||||
|
"""Get all the data for the setting. If cached is True the data in the _value can be used."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def write(self, value, save=True):
|
||||||
|
"""Write the value to the device. If saved is True also save in the persister"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def apply(self):
|
||||||
|
"""Write saved data to the device, using persisted data if available"""
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s: apply (%s)", self.name, self._device)
|
||||||
|
value = None
|
||||||
|
try:
|
||||||
|
value = self.read(self.persist) # Don't use persisted value if setting doesn't persist
|
||||||
|
if self.persist and value is not None: # If setting doesn't persist no need to write value just read
|
||||||
|
self.write(value, save=False)
|
||||||
|
except Exception as e:
|
||||||
|
if logger.isEnabledFor(logging.WARNING):
|
||||||
|
logger.warning("%s: error applying %s so ignore it (%s): %s", self.name, value, self._device, repr(e))
|
||||||
|
|
||||||
|
def val_to_string(self, value):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
## key mapping from symbols to values????
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(Setting):
|
||||||
|
"""A setting descriptor for multiple keys.
|
||||||
|
Supported by a class that provides the interface to the device, see ForceSensingButtonArray in hidpp20.py
|
||||||
|
Picks out a field from the mapped device feature objects."""
|
||||||
|
|
||||||
|
# setup creates a dictionary with entries for all the keys
|
||||||
|
# get, set, and acceptable are methods of dict value objects, not of the device object itself
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build(cls, device):
|
||||||
|
cls.check_properties(cls)
|
||||||
|
_device_object = getattr(device, cls.setup)()
|
||||||
|
if _device_object:
|
||||||
|
setting = cls()
|
||||||
|
setting._device = device
|
||||||
|
setting._device_object = _device_object
|
||||||
|
setting._value = {}
|
||||||
|
return setting
|
||||||
|
|
||||||
|
def read(self, cached=True):
|
||||||
|
self._pre_read(cached)
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s: settings read %r from %s", self.name, self._value, self._device)
|
||||||
|
for key in self._device_object:
|
||||||
|
self.read_key(key, cached)
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def read_key(self, key, cached=True):
|
||||||
|
"""Get the data for the key. If cached is True the data in the _device_object can be used."""
|
||||||
|
self._pre_read(cached)
|
||||||
|
if key not in self._device_object:
|
||||||
|
logger.error("%s: settings illegal read key %r for %s", self.name, key, self._device)
|
||||||
|
return None
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s: settings key %r read %r from %s", self.name, key, self._value, self._device)
|
||||||
|
if cached and key in self._value and self._value[key] is not None:
|
||||||
|
return self._value[key]
|
||||||
|
if cached:
|
||||||
|
data = self._device_object[key]
|
||||||
|
self._value[key] = getattr(data, self.get)()
|
||||||
|
return self._value[key]
|
||||||
|
if self._device.online:
|
||||||
|
data = self._device_object.query_key(key)
|
||||||
|
self._value[key] = getattr(data, self.get)()
|
||||||
|
return self._value[key]
|
||||||
|
|
||||||
|
def write(self, value, save=True):
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s: settings read %r from %s", self.name, self._value, self._device)
|
||||||
|
for key, val in value.items():
|
||||||
|
self.write_key_value(key, val, save)
|
||||||
|
|
||||||
|
def write_key_value(self, key, value, save=True):
|
||||||
|
"""Write the data for the key. If saved is True also save in the persister"""
|
||||||
|
if key not in self._device_object:
|
||||||
|
logger.error("%s: settings illegal write key %r for %s", self.name, key, self._device)
|
||||||
|
return None
|
||||||
|
if logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug("%s: settings write key %r value %r to %s", self.name, key, value, self._device)
|
||||||
|
if self._device.online:
|
||||||
|
if self._device_object[key] is None:
|
||||||
|
self.read_key(key)
|
||||||
|
if self._device_object[key] is None:
|
||||||
|
logger.error("%s: settings illegal write key %r for %s", self.name, key, self._device)
|
||||||
|
return None
|
||||||
|
if not getattr(self._device_object[key], self.acceptable)(value):
|
||||||
|
logger.error("%s: settings illegal write key %r value %r for %s", self.name, key, value, self._device)
|
||||||
|
return None
|
||||||
|
self._value[key] = value
|
||||||
|
if self._device.persister and self.persist and save:
|
||||||
|
self._device.persister[self.name][key] = value
|
||||||
|
getattr(self._device_object[key], self.set)(value)
|
||||||
|
return value
|
||||||
|
|
@ -36,6 +36,7 @@ from . import exceptions
|
||||||
from . import hidpp20
|
from . import hidpp20
|
||||||
from . import hidpp20_constants
|
from . import hidpp20_constants
|
||||||
from . import settings
|
from . import settings
|
||||||
|
from . import settings_new
|
||||||
from . import settings_validator
|
from . import settings_validator
|
||||||
from . import special_keys
|
from . import special_keys
|
||||||
from .hidpp10_constants import Registers
|
from .hidpp10_constants import Registers
|
||||||
|
|
@ -1779,6 +1780,20 @@ class PerKeyLighting(settings.Settings):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# Allow changes to force sensing buttons
|
||||||
|
class ForceSensing(settings_new.Settings):
|
||||||
|
name = "force-sensing"
|
||||||
|
label = _("Force Sensing Buttons")
|
||||||
|
description = _("Change the force required to activate button.")
|
||||||
|
feature = _F.FORCE_SENSING_BUTTON
|
||||||
|
setup = "force_buttons"
|
||||||
|
get = "get_current"
|
||||||
|
set = "set_current"
|
||||||
|
acceptable = "acceptable_current"
|
||||||
|
choices_universe = list(range(0, 256))
|
||||||
|
kind = settings.Kind.MAP_RANGE
|
||||||
|
|
||||||
|
|
||||||
SETTINGS: list[settings.Setting] = [
|
SETTINGS: list[settings.Setting] = [
|
||||||
RegisterHandDetection, # simple
|
RegisterHandDetection, # simple
|
||||||
RegisterSmoothScroll, # simple
|
RegisterSmoothScroll, # simple
|
||||||
|
|
@ -1824,6 +1839,7 @@ SETTINGS: list[settings.Setting] = [
|
||||||
PersistentRemappableAction,
|
PersistentRemappableAction,
|
||||||
DivertKeys, # working
|
DivertKeys, # working
|
||||||
DisableKeyboardKeys, # working
|
DisableKeyboardKeys, # working
|
||||||
|
ForceSensing,
|
||||||
CrownSmooth, # working
|
CrownSmooth, # working
|
||||||
DivertCrown, # working
|
DivertCrown, # working
|
||||||
DivertGkeys, # working
|
DivertGkeys, # working
|
||||||
|
|
|
||||||
|
|
@ -278,6 +278,8 @@ def set(dev, setting: SettingsProtocol, args, save):
|
||||||
key = args.value_key
|
key = args.value_key
|
||||||
all_keys = getattr(setting, "choices_universe", None)
|
all_keys = getattr(setting, "choices_universe", None)
|
||||||
ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, NamedInts) else to_int(key)
|
ikey = all_keys[int(key) if key.isdigit() else key] if isinstance(all_keys, NamedInts) else to_int(key)
|
||||||
|
print("S", args.extra2, key, type(all_keys), ikey)
|
||||||
|
print("SS", args)
|
||||||
if args.extra2 is None or to_int(args.extra2) is None:
|
if args.extra2 is None or to_int(args.extra2) is None:
|
||||||
raise Exception(f"{setting.name}: setting needs an integer value, not {args.extra2}")
|
raise Exception(f"{setting.name}: setting needs an integer value, not {args.extra2}")
|
||||||
if not setting._value: # ensure that there are values to look through
|
if not setting._value: # ensure that there are values to look through
|
||||||
|
|
@ -295,7 +297,19 @@ def set(dev, setting: SettingsProtocol, args, save):
|
||||||
result = setting.write_key_value(int(k), item, save=save)
|
result = setting.write_key_value(int(k), item, save=save)
|
||||||
value = item
|
value = item
|
||||||
|
|
||||||
|
elif setting.kind == settings.Kind.MAP_RANGE:
|
||||||
|
if args.extra_subkey is None:
|
||||||
|
_print_setting_keyed(setting, args.value_key)
|
||||||
|
return None, None, None
|
||||||
|
key = int(args.value_key)
|
||||||
|
value = int(args.extra_subkey)
|
||||||
|
if key not in setting._device_object:
|
||||||
|
raise Exception(f"{setting.name}: key '{key}' not in setting")
|
||||||
|
message = f"Setting {setting.name} of {dev.name} key {key} to {value}"
|
||||||
|
result = setting.write_key_value(key, value, save=save)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
print("KIND", setting.kind)
|
||||||
raise Exception("NotImplemented")
|
raise Exception("NotImplemented")
|
||||||
|
|
||||||
return result, message, value
|
return result, message, value
|
||||||
|
|
|
||||||
|
|
@ -260,7 +260,8 @@ def _print_device(dev, num=None):
|
||||||
v = setting.val_to_string(setting._device.persister.get(setting.name))
|
v = setting.val_to_string(setting._device.persister.get(setting.name))
|
||||||
print(f" {setting.label} (saved): {v}")
|
print(f" {setting.label} (saved): {v}")
|
||||||
try:
|
try:
|
||||||
v = setting.val_to_string(setting.read(False))
|
v = setting.read(False)
|
||||||
|
v = setting.val_to_string(v)
|
||||||
except exceptions.FeatureCallError as e:
|
except exceptions.FeatureCallError as e:
|
||||||
v = "HID++ error " + str(e)
|
v = "HID++ error " + str(e)
|
||||||
except AssertionError as e:
|
except AssertionError as e:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue