Skip to content
Snippets Groups Projects
Select Git revision
  • c6ee27341063eef8a377d408d231a9fdfe22f031
  • wip-bootstrap default
  • dualcore
  • ch3/leds
  • ch3/time
  • master
6 results

objenumerate.c

Blame
  • settings.py 8.06 KiB
    """
    Settings framework for flow3r badge.
    
    We call settings 'tunables', trying to emphasize that they are some kind of
    value that can be changed.
    
    Settings are persisted in /flash/settings.json, loaded on startup and saved on
    request.
    """
    
    import json
    
    from st3m import logging
    from st3m.goose import (
        ABCBase,
        abstractmethod,
        Any,
        Dict,
        List,
        Optional,
        Callable,
    )
    from st3m.utils import reduce
    
    log = logging.Log(__name__, level=logging.INFO)
    
    SETTINGS_JSON_FILE = "/flash/settings.json"
    
    
    class Tunable(ABCBase):
        """
        Base class for all settings. An instance of a Tunable is some kind of
        setting with some kind of value. Each setting has exactly one instance of
        Tunable in memory that represents its current configuration.
    
        Other than a common interface, this also implements a mechanism for
        downstream consumers to subscribe to tunable changes, and allows notifying
        them.
        """
    
        def __init__(self) -> None:
            self._subscribers: List[Callable[[], None]] = []
    
        def subscribe(self, s: Callable[[], None]) -> None:
            """
            Subscribe to be updated when this tunable value's changes.
            """
            self._subscribers.append(s)
    
        def notify(self) -> None:
            """
            Notify all subscribers.
            """
            for s in self._subscribers:
                s()
    
        @abstractmethod
        def name(self) -> str:
            """
            Human-readable name of this setting.
            """
            ...
    
        @abstractmethod
        def save(self) -> Dict[str, Any]:
            """
            Return dictionary that contains this setting's persistance data. Will be
            merged with all other tunable's results.
            """
            ...
    
        @abstractmethod
        def load(self, d: Dict[str, Any]) -> None:
            """
            Load in-memory state from persisted data.
            """
            ...
    
    
    class UnaryTunable(Tunable):
        """
        Basic implementation of a Tunable for single values. Most settings will be
        UnaryTunables, with notable exceptions being things like lists or optional
        settings.
    
        UnaryTunable implements persistence by always being saved/loaded to same
        json.style.path (only traversing nested dictionaries).
        """
    
        def __init__(self, name: str, key: str, default: Any):
            """
            Create an UnaryTunable with a given human-readable name, some
            persistence key and some default value.
            """
            super().__init__()
            self.key = key
            self._name = name
            self.value: Any = default
    
        def name(self) -> str:
            return self._name
    
        def set_value(self, v: Any) -> None:
            """
            Call to set value in-memory and notify all listeners.
            """
            self.value = v
            self.notify()
    
        def save(self) -> Dict[str, Any]:
            res: Dict[str, Any] = {}
            sub = res
    
            parts = self.key.split(".")
            for i, part in enumerate(parts):
                if i == len(parts) - 1:
                    sub[part] = self.value
                else:
                    sub[part] = {}
                    sub = sub[part]
            return res
    
        def load(self, d: Dict[str, Any]) -> None:
            def _get(v: Dict[str, Any], k: str) -> Any:
                if k in v:
                    return v[k]
                else:
                    return {}
    
            path = self.key.split(".")
            k = path[-1]
            d = reduce(_get, path[:-1], d)
            if k in d:
                self.value = d[k]
    
    
    class OnOffTunable(UnaryTunable):
        """
        OnOffTunable is a UnaryTunable that has two values: on or off, and is
        rendered accordingly as a slider switch.
        """
    
        def __init__(self, name: str, key: str, default: bool) -> None:
            super().__init__(name, key, default)
    
        def press(self, vm: Optional["ViewManager"]) -> None:
            if self.value is True:
                self.set_value(False)
            else:
                self.set_value(True)
    
    
    class StringTunable(UnaryTunable):
        """
        StringTunable is a UnaryTunable that has a string value
        """
    
        def __init__(self, name: str, key: str, default: Optional[str]) -> None:
            super().__init__(name, key, default)
    
        def press(self, vm: Optional["ViewManager"]) -> None:
            # Text input not supported at the moment
            pass
    
    
    class NumberTunable(UnaryTunable):
        """
        NumberTunable is a UnaryTunable that has a numeric value
        """
    
        def __init__(self, name: str, key: int | float, default: Optional[str]) -> None:
            super().__init__(name, key, default)
    
        def press(self, vm: Optional["ViewManager"]) -> None:
            # Number adjustment not supported at the moment
            pass
    
    
    # TODO: invert Tunable <-> Widget dependency to be able to define multiple different widget renderings for the same underlying tunable type
    class ObfuscatedStringTunable(UnaryTunable):
        """
        ObfuscatedStringTunable is a UnaryTunable that has a string value that should not be revealed openly.
        """
    
        def __init__(self, name: str, key: str, default: Optional[str]) -> None:
            super().__init__(name, key, default)
    
        def press(self, vm: Optional["ViewManager"]) -> None:
            # Text input not supported at the moment
            pass
    
    
    # Actual tunables / settings.
    onoff_button_swap = OnOffTunable("Swap Buttons", "system.swap_buttons", False)
    onoff_show_fps = OnOffTunable("Show FPS", "system.show_fps", False)
    onoff_debug = OnOffTunable("Debug Overlay", "system.debug", False)
    onoff_debug_touch = OnOffTunable("Touch Overlay", "system.debug_touch", False)
    onoff_debug_ftop = OnOffTunable("Debug: ftop", "system.ftop_enabled", False)
    onoff_show_tray = OnOffTunable("Show Icons", "system.show_icons", True)
    onoff_wifi = OnOffTunable("Enable WiFi on Boot", "system.wifi.enabled", False)
    onoff_wifi_preference = OnOffTunable(
        "Let apps change WiFi", "system.wifi.allow_apps_to_change_wifi", True
    )
    str_wifi_ssid = StringTunable("WiFi SSID", "system.wifi.ssid", "Camp2023-open")
    str_wifi_psk = ObfuscatedStringTunable("WiFi Password", "system.wifi.psk", None)
    str_hostname = StringTunable("Hostname", "system.hostname", "flow3r")
    num_volume_step_db = StringTunable(
        "Volume Change dB", "system.audio.volume_step_dB", 2.5
    )
    num_startup_volume_db = StringTunable(
        "Startup Volume dB", "system.audio.startup_volume_dB", -10
    )
    num_headphones_min_db = StringTunable(
        "Min Headphone Volume dB", "system.audio.headphones_min_dB", -30
    )
    num_speakers_min_db = StringTunable(
        "Min Speakers Volume dB", "system.audio.speakers_min_dB", -30
    )
    num_headphones_max_db = StringTunable(
        "Max Headphone Volume dB", "system.audio.headphones_max_dB", 3
    )
    num_speakers_max_db = StringTunable(
        "Max Speakers Volume dB", "system.audio.speakers_max_dB", 14
    )
    
    # List of all settings to be loaded/saved
    load_save_settings: List[UnaryTunable] = [
        onoff_show_tray,
        onoff_button_swap,
        onoff_debug,
        onoff_debug_ftop,
        onoff_debug_touch,
        onoff_wifi,
        onoff_wifi_preference,
        onoff_show_fps,
        str_wifi_ssid,
        str_wifi_psk,
        str_hostname,
        num_volume_step_db,
        num_startup_volume_db,
        num_headphones_min_db,
        num_speakers_min_db,
        num_headphones_max_db,
        num_speakers_max_db,
    ]
    
    
    def load_all() -> None:
        """
        Load all settings from flash.
        """
        data = {}
        try:
            with open(SETTINGS_JSON_FILE, "r") as f:
                data = json.load(f)
        except Exception as e:
            log.warning("Could not load settings: " + str(e))
            return
    
        log.info("Loaded settings from flash")
        for setting in load_save_settings:
            setting.load(data)
    
    
    def _update(d: Dict[str, Any], u: Dict[str, Any]) -> Dict[str, Any]:
        """
        Recursive update dictionary.
        """
        for k, v in u.items():
            if isinstance(v, dict):
                d[k] = _update(d.get(k, {}), v)
            else:
                d[k] = v
        return d
    
    
    def save_all() -> None:
        """
        Save all settings to flash.
        """
        res: Dict[str, Any] = {}
        for setting in load_save_settings:
            res = _update(res, setting.save())
        try:
            with open(SETTINGS_JSON_FILE, "w") as f:
                json.dump(res, f)
        except Exception as e:
            log.warning("Could not save settings: " + str(e))
            return
    
        log.info("Saved settings to flash")
    
    
    load_all()