Add preset persistence and startup default.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-02-08 18:48:44 +13:00
parent 7cc0a3b7d7
commit 3080548f47
5 changed files with 141 additions and 4 deletions

View File

@@ -10,6 +10,11 @@ settings = Settings()
print(settings) print(settings)
presets = Presets(settings["led_pin"], settings["num_leds"]) 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 = WDT(timeout=10000)
wdt.feed() wdt.feed()
@@ -31,7 +36,7 @@ while True:
# Only handle messages with the expected version. # Only handle messages with the expected version.
if data.get("v") != "1": if data.get("v") != "1":
continue continue
print(data) # print(data)
# Global brightness (0255) for this device # Global brightness (0255) for this device
if "b" in data: if "b" in data:
try: try:
@@ -39,11 +44,12 @@ while True:
except (TypeError, ValueError): except (TypeError, ValueError):
pass pass
if "presets" in data: 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 # Convert hex color strings to RGB tuples and reorder based on device color order
if "c" in preset_data: if "c" in preset_data:
preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings) 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", {}): if settings.get("name") in data.get("select", {}):
select_list = data["select"][settings.get("name")] select_list = data["select"][settings.get("name")]
# Select value is always a list: ["preset_name"] or ["preset_name", step] # Select value is always a list: ["preset_name"] or ["preset_name", step]
@@ -51,3 +57,9 @@ while True:
preset_name = select_list[0] preset_name = select_list[0]
step = select_list[1] if len(select_list) > 1 else None step = select_list[1] if len(select_list) > 1 else None
presets.select(preset_name, step=step) 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()

View File

@@ -22,3 +22,58 @@ class Preset:
for key, value in data.items(): for key, value in data.items():
setattr(self, key, value) setattr(self, key, value)
return True 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,
}

View File

@@ -2,6 +2,7 @@ from machine import Pin
from neopixel import NeoPixel from neopixel import NeoPixel
from preset import Preset from preset import Preset
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
import json
class Presets: class Presets:
@@ -28,6 +29,33 @@ class Presets:
"circle": Circle(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): def edit(self, name, data):
"""Create or update a preset with the given name.""" """Create or update a preset with the given name."""

View File

@@ -19,6 +19,7 @@ class Settings(dict):
self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}" self["name"] = f"led-{ubinascii.hexlify(network.WLAN(network.AP_IF).config('mac')).decode()}"
self["debug"] = False self["debug"] = False
self["startup_preset"] = None
def save(self): def save(self):
try: try:

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
"""Test ESPNow receive functionality - runs on MicroPython device.""" """Test ESPNow receive functionality - runs on MicroPython device."""
import json import json
import os
import utime import utime
from settings import Settings from settings import Settings
from presets import Presets from presets import Presets
@@ -621,6 +622,45 @@ def test_select_with_step():
print(" ✓ Step set correctly when switching presets") 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(): def main():
"""Run all tests.""" """Run all tests."""
print("=" * 60) print("=" * 60)
@@ -637,6 +677,7 @@ def main():
test_switch_presets() test_switch_presets()
test_beat_functionality() test_beat_functionality()
test_select_with_step() test_select_with_step()
test_preset_save_load()
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("All tests passed! ✓") print("All tests passed! ✓")