Add preset persistence and startup default.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
18
src/main.py
18
src/main.py
@@ -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 (0–255) for this device
|
# Global brightness (0–255) 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()
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -27,7 +28,34 @@ class Presets:
|
|||||||
"chase": Chase(self).run,
|
"chase": Chase(self).run,
|
||||||
"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."""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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! ✓")
|
||||||
|
|||||||
Reference in New Issue
Block a user