"""Persistent configuration for PolterHID. `settings.json` is deliberately kept outside the source tree and is created on first boot. The web password is stored in clear text because TLS is explicitly not used; use this only on a trusted training Wi-Fi network. """ try: import ujson as json except ImportError: import json DEFAULTS = { "wifi_ssid": "CHANGE_ME", "wifi_password": "CHANGE_ME", "web_password": "change-this-before-deploying", # Set false when validating on a board without the T-Dongle-S3 display. "display_enabled": False, "enabled": True, "return_enabled": True, "mouse_enabled": True, "mouse_distance": 1, "return_min_seconds": 45, "return_max_seconds": 120, "mouse_min_seconds": 20, "mouse_max_seconds": 60, } SETTINGS_FILE = "settings.json" def load(): """Return validated settings, creating the file on first boot.""" values = DEFAULTS.copy() try: with open(SETTINGS_FILE, "r") as file: stored = json.load(file) if isinstance(stored, dict): for key in DEFAULTS: if key in stored: values[key] = stored[key] except (OSError, ValueError): save(values) _validate(values) return values def save(values): """Atomically replace the settings file where the port supports rename.""" _validate(values) temporary = SETTINGS_FILE + ".tmp" with open(temporary, "w") as file: json.dump(values, file) try: import os os.remove(SETTINGS_FILE) except OSError: pass import os os.rename(temporary, SETTINGS_FILE) def apply_form(values, form): """Update configurable runtime values from a decoded HTTP form.""" for key in ("enabled", "return_enabled", "mouse_enabled"): values[key] = form.get(key) == "on" for key in ( "return_min_seconds", "return_max_seconds", "mouse_min_seconds", "mouse_max_seconds", "mouse_distance", ): if key in form: try: values[key] = int(form[key]) except (TypeError, ValueError): raise ValueError("%s must be a whole number" % key) _validate(values) def _validate(values): for key in ("wifi_ssid", "wifi_password", "web_password"): if not isinstance(values.get(key), str) or not values[key]: raise ValueError("%s must be a non-empty string" % key) distance = values["mouse_distance"] if not isinstance(distance, int) or distance < 1 or distance > 127: raise ValueError("mouse distance must be between 1 and 127 pixels") for prefix in ("return", "mouse"): minimum = values[prefix + "_min_seconds"] maximum = values[prefix + "_max_seconds"] if not isinstance(minimum, int) or not isinstance(maximum, int): raise ValueError("%s intervals must be integers" % prefix) if minimum < 1 or maximum < minimum or maximum > 86400: raise ValueError("invalid %s interval" % prefix)