From 3080548f47ef5f1272c1aafe4beea7399c4e8afb Mon Sep 17 00:00:00 2001 From: jimmy Date: Sun, 8 Feb 2026 18:48:44 +1300 Subject: [PATCH] Add preset persistence and startup default. Co-authored-by: Cursor --- src/main.py | 18 ++++++++++-- src/preset.py | 55 +++++++++++++++++++++++++++++++++++++ src/presets.py | 30 +++++++++++++++++++- src/settings.py | 1 + test/test_espnow_receive.py | 41 +++++++++++++++++++++++++++ 5 files changed, 141 insertions(+), 4 deletions(-) diff --git a/src/main.py b/src/main.py index adcec2d..56d77ec 100644 --- a/src/main.py +++ b/src/main.py @@ -10,6 +10,11 @@ settings = Settings() print(settings) presets = Presets(settings["led_pin"], settings["num_leds"]) +presets.load() +startup_preset = settings.get("startup_preset") +if startup_preset: + presets.select(startup_preset) + print(f"Selected startup preset: {startup_preset}") wdt = WDT(timeout=10000) wdt.feed() @@ -31,7 +36,7 @@ while True: # Only handle messages with the expected version. if data.get("v") != "1": continue - print(data) + # print(data) # Global brightness (0–255) for this device if "b" in data: try: @@ -39,11 +44,12 @@ while True: except (TypeError, ValueError): pass if "presets" in data: - for name, preset_data in data["presets"].items(): + for id, preset_data in data["presets"].items(): # Convert hex color strings to RGB tuples and reorder based on device color order if "c" in preset_data: preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings) - presets.edit(name, preset_data) + presets.edit(id, preset_data) + print(f"Edited preset {id}: {preset_data.get('name', '')}") if settings.get("name") in data.get("select", {}): select_list = data["select"][settings.get("name")] # Select value is always a list: ["preset_name"] or ["preset_name", step] @@ -51,3 +57,9 @@ while True: preset_name = select_list[0] step = select_list[1] if len(select_list) > 1 else None presets.select(preset_name, step=step) + if "default" in data: + settings["startup_preset"] = data["default"] + print(f"Set startup preset to: {data['default']}") + settings.save() + if "save" in data: + presets.save() diff --git a/src/preset.py b/src/preset.py index 603e5d2..d00babc 100644 --- a/src/preset.py +++ b/src/preset.py @@ -22,3 +22,58 @@ class Preset: for key, value in data.items(): setattr(self, key, value) return True + + @property + def pattern(self): + return self.p + + @pattern.setter + def pattern(self, value): + self.p = value + + @property + def delay(self): + return self.d + + @delay.setter + def delay(self, value): + self.d = value + + @property + def brightness(self): + return self.b + + @brightness.setter + def brightness(self, value): + self.b = value + + @property + def colors(self): + return self.c + + @colors.setter + def colors(self, value): + self.c = value + + @property + def auto(self): + return self.a + + @auto.setter + def auto(self, value): + self.a = value + + def to_dict(self): + return { + "p": self.p, + "d": self.d, + "b": self.b, + "c": self.c, + "a": self.a, + "n1": self.n1, + "n2": self.n2, + "n3": self.n3, + "n4": self.n4, + "n5": self.n5, + "n6": self.n6, + } diff --git a/src/presets.py b/src/presets.py index dc45175..620cd8d 100644 --- a/src/presets.py +++ b/src/presets.py @@ -2,6 +2,7 @@ from machine import Pin from neopixel import NeoPixel from preset import Preset from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle +import json class Presets: @@ -27,7 +28,34 @@ class Presets: "chase": Chase(self).run, "circle": Circle(self).run, } - + + def save(self): + """Save the presets to a file.""" + with open("presets.json", "w") as f: + json.dump({name: preset.to_dict() for name, preset in self.presets.items()}, f) + return True + + def load(self): + """Load presets from a file.""" + try: + with open("presets.json", "r") as f: + data = json.load(f) + except OSError: + # Create an empty presets file if missing + self.presets = {} + self.save() + return True + + self.presets = {} + for name, preset_data in data.items(): + if "c" in preset_data: + preset_data["c"] = [tuple(color) for color in preset_data["c"]] + self.presets[name] = Preset(preset_data) + if self.presets: + print("Loaded presets:") + #for name in sorted(self.presets.keys()): + # print(f" {name}: {self.presets[name].to_dict()}") + return True def edit(self, name, data): """Create or update a preset with the given name.""" diff --git a/src/settings.py b/src/settings.py index 542a86c..a320dc6 100644 --- a/src/settings.py +++ b/src/settings.py @@ -19,6 +19,7 @@ class Settings(dict): self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}" self["debug"] = False + self["startup_preset"] = None def save(self): try: diff --git a/test/test_espnow_receive.py b/test/test_espnow_receive.py index 8e2bd9c..6e05e91 100644 --- a/test/test_espnow_receive.py +++ b/test/test_espnow_receive.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Test ESPNow receive functionality - runs on MicroPython device.""" import json +import os import utime from settings import Settings from presets import Presets @@ -621,6 +622,45 @@ def test_select_with_step(): print(" ✓ Step set correctly when switching presets") +def test_preset_save_load(): + """Test saving and loading presets to/from JSON.""" + print("\nTest 10: Preset save/load") + settings = Settings() + patterns = Presets(settings["led_pin"], settings["num_leds"]) + + patterns.edit("saved_preset", { + "p": "blink", + "d": 150, + "b": 200, + "c": [(1, 2, 3), (4, 5, 6)], + "a": False, + "n1": 1, + "n2": 2, + "n3": 3, + "n4": 4, + "n5": 5, + "n6": 6, + }) + assert patterns.save(), "Save should return True" + + reloaded = Presets(settings["led_pin"], settings["num_leds"]) + assert reloaded.load(), "Load should return True" + + preset = reloaded.presets.get("saved_preset") + assert preset is not None, "Preset should be loaded" + assert preset.p == "blink", "Pattern should be blink" + assert preset.d == 150, "Delay should be 150" + assert preset.b == 200, "Brightness should be 200" + assert preset.c == [(1, 2, 3), (4, 5, 6)], "Colors should be restored as tuples" + assert preset.a is False, "Auto should be False" + assert (preset.n1, preset.n2, preset.n3, preset.n4, preset.n5, preset.n6) == (1, 2, 3, 4, 5, 6), "n1-n6 should match" + try: + os.remove("presets.json") + except OSError: + pass + print(" ✓ Preset save/load works correctly") + + def main(): """Run all tests.""" print("=" * 60) @@ -637,6 +677,7 @@ def main(): test_switch_presets() test_beat_functionality() test_select_with_step() + test_preset_save_load() print("\n" + "=" * 60) print("All tests passed! ✓")