Require serial_enabled true in settings to open serial_port; default false in set_defaults for Wi-Fi-only and dev machines. Co-authored-by: Cursor <cursoragent@cursor.com>
90 lines
3.4 KiB
Python
90 lines
3.4 KiB
Python
import json
|
||
import os
|
||
import binascii
|
||
|
||
|
||
def _settings_path():
|
||
"""Path to settings.json in project root (writable without root)."""
|
||
try:
|
||
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||
return os.path.join(base, "settings.json")
|
||
except Exception:
|
||
return "settings.json"
|
||
|
||
|
||
class Settings(dict):
|
||
SETTINGS_FILE = None # Set in __init__ from _settings_path()
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
if Settings.SETTINGS_FILE is None:
|
||
Settings.SETTINGS_FILE = _settings_path()
|
||
self.load() # Load settings from file during initialization
|
||
|
||
def generate_secret_key(self):
|
||
"""Generate a random secret key for session signing."""
|
||
try:
|
||
# Try to use os.urandom for secure random bytes
|
||
random_bytes = os.urandom(32)
|
||
return binascii.hexlify(random_bytes).decode('utf-8')
|
||
except (AttributeError, NotImplementedError):
|
||
# Fallback for MicroPython or systems without os.urandom
|
||
try:
|
||
import secrets
|
||
return secrets.token_hex(32)
|
||
except ImportError:
|
||
# Last resort: use a combination of time and random
|
||
import time
|
||
import random
|
||
random.seed(time.time())
|
||
return binascii.hexlify(bytes([random.randint(0, 255) for _ in range(32)])).decode('utf-8')
|
||
|
||
def set_defaults(self):
|
||
"""Set default settings if they don't exist."""
|
||
if 'session_secret_key' not in self:
|
||
self['session_secret_key'] = self.generate_secret_key()
|
||
# Save immediately when generating a new key
|
||
self.save()
|
||
# ESP-NOW STA channel (2.4 GHz) for LED drivers / bridge alignment; 1–11
|
||
if 'wifi_channel' not in self:
|
||
self['wifi_channel'] = 6
|
||
# Wi-Fi LED drivers: controller opens WebSocket to device (firmware serves /ws)
|
||
if 'wifi_driver_ws_port' not in self:
|
||
self['wifi_driver_ws_port'] = 80
|
||
if 'wifi_driver_ws_path' not in self:
|
||
self['wifi_driver_ws_path'] = '/ws'
|
||
# Seconds between UDP discovery nudges when a Wi-Fi driver WebSocket is
|
||
# down (0 disables). Helps drivers that reconnect after seeing traffic on 8766.
|
||
if 'wifi_driver_hello_interval_s' not in self:
|
||
self['wifi_driver_hello_interval_s'] = 10.0
|
||
# UART to ESP32 ESP-NOW bridge; default off (Wi-Fi drivers need no serial).
|
||
if 'serial_enabled' not in self:
|
||
self['serial_enabled'] = False
|
||
|
||
def save(self):
|
||
try:
|
||
j = json.dumps(self)
|
||
with open(self.SETTINGS_FILE, 'w') as file:
|
||
file.write(j)
|
||
print("Settings saved successfully.")
|
||
except Exception as e:
|
||
print(f"Error saving settings: {e}")
|
||
|
||
def load(self):
|
||
loaded_from_file = False
|
||
try:
|
||
with open(self.SETTINGS_FILE, 'r') as file:
|
||
loaded_settings = json.load(file)
|
||
self.update(loaded_settings)
|
||
loaded_from_file = True
|
||
print("Settings loaded successfully.")
|
||
except Exception as e:
|
||
print(f"Error loading settings")
|
||
self.clear()
|
||
finally:
|
||
# Ensure defaults are set even if file exists but is missing keys
|
||
self.set_defaults()
|
||
# Only save if file didn't exist or was invalid
|
||
if not loaded_from_file:
|
||
self.save()
|