feat(patterns): merge pattern styles and add mode support
Consolidate legacy pattern ids into meteor, particles, sparkle, chase, and colour_cycle with n6/mode style selection; add pattern_modes helper, self-contained tests/all.py, and preset mode alias on wire. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import asyncio
|
||||
import gc
|
||||
import utime
|
||||
|
||||
from hello import broadcast_hello_udp
|
||||
from mem_stats import print_mem
|
||||
from wifi_sta import try_reconnect
|
||||
|
||||
_UDP_HELLO_ATTEMPT = 0
|
||||
@@ -16,8 +16,7 @@ async def presets_loop(presets, wdt):
|
||||
if bool(getattr(presets, "debug", False)):
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_mem_log) >= 5000:
|
||||
gc.collect()
|
||||
print("mem runtime:", {"free": gc.mem_free(), "alloc": gc.mem_alloc()})
|
||||
print_mem("runtime")
|
||||
last_mem_log = now
|
||||
# tick() does not await; yield so UDP hello and HTTP/WebSocket can run.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
37
src/main.py
37
src/main.py
@@ -1,6 +1,6 @@
|
||||
import print_timestamp # noqa: F401 — prefixes every print with [ticks_ms]
|
||||
from settings import Settings
|
||||
import machine
|
||||
import network
|
||||
import utime
|
||||
import asyncio
|
||||
import json
|
||||
@@ -10,8 +10,9 @@ from microdot.websocket import WebSocketError, with_websocket
|
||||
from presets import Presets
|
||||
from controller_messages import apply_startup_pattern, process_data
|
||||
from runtime_state import RuntimeState
|
||||
from background_tasks import udp_hello_loop_after_http_ready
|
||||
from wifi_sta import connect_until_up
|
||||
from background_tasks import presets_loop, udp_hello_loop_after_http_ready
|
||||
from mem_stats import print_mem
|
||||
from wifi_sta import boot_sta
|
||||
try:
|
||||
import uos as os
|
||||
except ImportError:
|
||||
@@ -25,9 +26,8 @@ machine.freq(160000000)
|
||||
|
||||
settings = Settings()
|
||||
|
||||
|
||||
|
||||
gc.collect()
|
||||
sta_if = boot_sta(settings, wdt)
|
||||
|
||||
presets = Presets(settings["led_pin"], settings["num_leds"])
|
||||
presets.load(settings)
|
||||
@@ -37,21 +37,6 @@ gc.collect()
|
||||
|
||||
apply_startup_pattern(settings, presets)
|
||||
|
||||
# On ESP32-C3, soft reboots can leave Wi-Fi driver state allocated.
|
||||
# Reset both interfaces and collect before bringing STA up.
|
||||
ap_if = network.WLAN(network.AP_IF)
|
||||
ap_if.active(False)
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
if sta_if.active():
|
||||
sta_if.active(False)
|
||||
utime.sleep_ms(100)
|
||||
gc.collect()
|
||||
sta_if.active(True)
|
||||
sta_if.config(pm=network.WLAN.PM_NONE)
|
||||
_boot_ssid = settings.get("ssid") or ""
|
||||
if _boot_ssid:
|
||||
connect_until_up(sta_if, _boot_ssid, settings.get("password") or "", wdt)
|
||||
|
||||
|
||||
def _print_network_ips(controller_ip=None):
|
||||
"""Always log STA address and led-controller (WS client) address when known."""
|
||||
@@ -64,6 +49,7 @@ def _print_network_ips(controller_ip=None):
|
||||
|
||||
|
||||
_print_network_ips()
|
||||
print_mem("startup")
|
||||
|
||||
runtime_state = RuntimeState()
|
||||
|
||||
@@ -94,6 +80,7 @@ async def ws_handler(request, ws):
|
||||
except Exception:
|
||||
controller_ip = None
|
||||
_print_network_ips(controller_ip)
|
||||
print_mem("ws connect")
|
||||
try:
|
||||
while True:
|
||||
data = await ws.receive()
|
||||
@@ -167,16 +154,8 @@ async def upload_pattern(request):
|
||||
}), 201, {"Content-Type": "application/json"}
|
||||
|
||||
|
||||
async def presets_loop():
|
||||
last_mem_log = utime.ticks_ms()
|
||||
while True:
|
||||
presets.tick()
|
||||
wdt.feed()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
|
||||
async def main(port=80):
|
||||
asyncio.create_task(presets_loop())
|
||||
asyncio.create_task(presets_loop(presets, wdt))
|
||||
asyncio.create_task(
|
||||
udp_hello_loop_after_http_ready(sta_if, settings, wdt, runtime_state)
|
||||
)
|
||||
|
||||
34
src/mem_stats.py
Normal file
34
src/mem_stats.py
Normal file
@@ -0,0 +1,34 @@
|
||||
"""GC / heap snapshot helpers for debug logging."""
|
||||
|
||||
import gc
|
||||
|
||||
|
||||
def snapshot():
|
||||
"""Return a dict of memory stats after ``gc.collect()``."""
|
||||
gc.collect()
|
||||
out = {
|
||||
"free": gc.mem_free(),
|
||||
"alloc": gc.mem_alloc(),
|
||||
}
|
||||
try:
|
||||
import esp32
|
||||
|
||||
blocks = esp32.idf_heap_info(esp32.HEAP_DATA)
|
||||
if blocks:
|
||||
block = blocks[0]
|
||||
if isinstance(block, dict):
|
||||
if "total_free_bytes" in block:
|
||||
out["idf_free"] = block["total_free_bytes"]
|
||||
largest = block.get("largest_free_block")
|
||||
if largest is None:
|
||||
largest = block.get("largest_free_block_in_bytes")
|
||||
if largest is not None:
|
||||
out["idf_largest"] = largest
|
||||
except Exception:
|
||||
pass
|
||||
return out
|
||||
|
||||
|
||||
def print_mem(label):
|
||||
"""Print one timestamped memory line (via ``print_timestamp`` when installed)."""
|
||||
print("mem %s:" % label, snapshot())
|
||||
@@ -1,12 +1,16 @@
|
||||
import math
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"northern_wave": 1}
|
||||
|
||||
|
||||
class Aurora:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(40, 200, 140), (80, 120, 255), (160, 80, 220)]
|
||||
def _run_bands(self, preset, colors):
|
||||
bands = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
|
||||
shimmer = max(0, min(255, int(preset.n2) if int(preset.n2) > 0 else 40))
|
||||
phase = self.driver.step % 256
|
||||
@@ -16,11 +20,17 @@ class Aurora:
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
for i in range(self.driver.num_leds):
|
||||
idx = ((i * bands) // max(1, self.driver.num_leds) + (phase // 32)) % len(colors)
|
||||
idx = (
|
||||
(i * bands) // max(1, self.driver.num_leds) + (phase // 32)
|
||||
) % len(colors)
|
||||
c = self.driver.apply_brightness(colors[idx], preset.b)
|
||||
w = (255 - abs(128 - ((i * 8 + phase) & 255)) * 2)
|
||||
w = 255 - abs(128 - ((i * 8 + phase) & 255)) * 2
|
||||
w = max(0, min(255, w + shimmer))
|
||||
self.driver.n[i] = ((c[0]*w)//255, (c[1]*w)//255, (c[2]*w)//255)
|
||||
self.driver.n[i] = (
|
||||
(c[0] * w) // 255,
|
||||
(c[1] * w) // 255,
|
||||
(c[2] * w) // 255,
|
||||
)
|
||||
self.driver.n.write()
|
||||
phase = (phase + 1) & 255
|
||||
self.driver.step = phase
|
||||
@@ -29,3 +39,57 @@ class Aurora:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
def _run_northern(self, preset, colors):
|
||||
period = max(4, int(preset.n1) if int(preset.n1) > 0 else 20)
|
||||
contrast = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 200))
|
||||
drift = max(1, int(preset.n3) if int(preset.n3) > 0 else 2)
|
||||
phase = 0
|
||||
last = utime.ticks_ms()
|
||||
ncols = len(colors)
|
||||
if ncols < 2:
|
||||
colors = list(colors) + [(120, 180, 255)]
|
||||
ncols = len(colors)
|
||||
twopi = 6.2831853
|
||||
|
||||
def lerp3(a, b, f):
|
||||
return (
|
||||
a[0] + ((b[0] - a[0]) * f) // 255,
|
||||
a[1] + ((b[1] - a[1]) * f) // 255,
|
||||
a[2] + ((b[2] - a[2]) * f) // 255,
|
||||
)
|
||||
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
t = (i * twopi / period) + (phase * twopi / 256.0)
|
||||
w = (math.sin(t) + 1.0) * 0.5
|
||||
u = w * (ncols - 1) * 256.0
|
||||
fi = int(u) >> 8
|
||||
frac = int(u) & 255
|
||||
if fi >= ncols - 1:
|
||||
fi = ncols - 2
|
||||
frac = 255
|
||||
peak = lerp3(colors[fi], colors[fi + 1], frac)
|
||||
peak = self.driver.apply_brightness(peak, preset.b)
|
||||
mixf = min(255, int(w * contrast * 2) >> 1)
|
||||
self.driver.n[i] = lerp3(bg, peak, mixf)
|
||||
self.driver.n.write()
|
||||
phase = (phase + drift) % 256
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
def run(self, preset):
|
||||
"""Aurora bands (n6=0) or sine northern wave (n6=1, legacy northern_wave)."""
|
||||
colors = preset.c if preset.c else [(40, 200, 140), (80, 120, 255), (160, 80, 220)]
|
||||
if style_mode(preset, 0, _LEGACY) == 1:
|
||||
colors = preset.c if preset.c else [(20, 55, 120), (60, 140, 220), (180, 220, 255)]
|
||||
yield from self._run_northern(preset, colors)
|
||||
return
|
||||
yield from self._run_bands(preset, colors)
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class BreathingDual:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 0, 140), (0, 120, 255)]
|
||||
phase_offset = max(0, min(255, int(preset.n1)))
|
||||
ease = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
phase = self.driver.step % 256
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
p1 = phase
|
||||
p2 = (phase + phase_offset) & 255
|
||||
t1 = 255 - abs(128 - p1) * 2
|
||||
t2 = 255 - abs(128 - p2) * 2
|
||||
if ease > 1:
|
||||
t1 = (t1 * t1) // 255
|
||||
t2 = (t2 * t2) // 255
|
||||
c1 = self.driver.apply_brightness(colors[0], preset.b)
|
||||
c2 = self.driver.apply_brightness(colors[1 % len(colors)] if len(colors) > 1 else colors[0], preset.b)
|
||||
half = self.driver.num_leds // 2
|
||||
for i in range(self.driver.num_leds):
|
||||
if i < half:
|
||||
self.driver.n[i] = ((c1[0]*t1)//255, (c1[1]*t1)//255, (c1[2]*t1)//255)
|
||||
else:
|
||||
self.driver.n[i] = ((c2[0]*t2)//255, (c2[1]*t2)//255, (c2[2]*t2)//255)
|
||||
self.driver.n.write()
|
||||
phase = (phase + 2) & 255
|
||||
self.driver.step = phase
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,13 +1,49 @@
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"marquee": 1}
|
||||
|
||||
|
||||
class Chase:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _run_marquee(self, preset, colors):
|
||||
on_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
|
||||
off_len = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
|
||||
step = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
|
||||
phase = self.driver.step % (on_len + off_len)
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
c = self.driver.apply_brightness(colors[0], preset.b)
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
m = (i + phase) % (on_len + off_len)
|
||||
self.driver.n[i] = c if m < on_len else bg_color
|
||||
self.driver.n.write()
|
||||
phase = (phase + step) % (on_len + off_len)
|
||||
self.driver.step = phase
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
def run(self, preset):
|
||||
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
|
||||
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
|
||||
"""Chase (n6=0) or marquee dashes (n6=1, legacy marquee).
|
||||
|
||||
Chase: n1/n2 segment lengths, n3/n4 step on even/odd beats.
|
||||
Marquee: n1 on length, n2 off length, n3 scroll step.
|
||||
"""
|
||||
if style_mode(preset, 0, _LEGACY) == 1:
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
yield from self._run_marquee(preset, colors)
|
||||
return
|
||||
|
||||
colors = preset.c
|
||||
if len(colors) < 1:
|
||||
# Need at least 1 color
|
||||
|
||||
@@ -1,11 +1,24 @@
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"rainbow": 1, "gradient_scroll": 0}
|
||||
|
||||
|
||||
class ColourCycle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _render(self, colors, phase, brightness):
|
||||
def _wheel(self, pos):
|
||||
if pos < 85:
|
||||
return (pos * 3, 255 - pos * 3, 0)
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (255 - pos * 3, 0, pos * 3)
|
||||
pos -= 170
|
||||
return (0, pos * 3, 255 - pos * 3)
|
||||
|
||||
def _render_gradient(self, colors, phase, brightness):
|
||||
num_leds = self.driver.num_leds
|
||||
color_count = len(colors)
|
||||
if num_leds <= 0 or color_count <= 0:
|
||||
@@ -15,14 +28,11 @@ class ColourCycle:
|
||||
return
|
||||
|
||||
full_span = color_count * 256
|
||||
# Match rainbow behaviour: phase is 0..255 and maps to one full-strip shift.
|
||||
phase_shift = (phase * full_span) // 256
|
||||
for i in range(num_leds):
|
||||
# Position around the colour loop, shifted by phase.
|
||||
pos = ((i * full_span) // num_leds + phase_shift) % full_span
|
||||
idx = pos // 256
|
||||
frac = pos & 255
|
||||
|
||||
c1 = colors[idx]
|
||||
c2 = colors[(idx + 1) % color_count]
|
||||
blended = (
|
||||
@@ -33,23 +43,55 @@ class ColourCycle:
|
||||
self.driver.n[i] = self.driver.apply_brightness(blended, brightness)
|
||||
self.driver.n.write()
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
phase = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1))
|
||||
def _render_rainbow(self, phase, brightness):
|
||||
num_leds = self.driver.num_leds
|
||||
for i in range(num_leds):
|
||||
rc_index = (i * 256 // max(1, num_leds)) + phase
|
||||
self.driver.n[i] = self.driver.apply_brightness(
|
||||
self._wheel(rc_index & 255), brightness
|
||||
)
|
||||
self.driver.n.write()
|
||||
|
||||
def run(self, preset):
|
||||
"""Scroll gradient (n6=0) or fixed spectrum wheel (n6=1, legacy rainbow).
|
||||
|
||||
n1: step rate
|
||||
n6: 0 gradient scroll, 1 rainbow wheel
|
||||
"""
|
||||
mode = style_mode(preset, 0, _LEGACY)
|
||||
step_amount = max(1, int(preset.n1) if int(preset.n1) > 0 else 1)
|
||||
phase = self.driver.step % 256
|
||||
|
||||
if mode == 1:
|
||||
if not preset.a:
|
||||
self._render_rainbow(phase, preset.b)
|
||||
self.driver.step = (phase + step_amount) % 256
|
||||
yield
|
||||
return
|
||||
last_update = utime.ticks_ms()
|
||||
while True:
|
||||
delay_ms = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
self._render_rainbow(phase, preset.b)
|
||||
phase = (phase + step_amount) % 256
|
||||
self.driver.step = phase
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
yield
|
||||
|
||||
colors = preset.c if preset.c else [(255, 0, 0), (0, 0, 255)]
|
||||
if not preset.a:
|
||||
self._render(colors, phase, preset.b)
|
||||
self._render_gradient(colors, phase, preset.b)
|
||||
self.driver.step = (phase + step_amount) % 256
|
||||
yield
|
||||
return
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
delay_ms = max(1, int(preset.d))
|
||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
||||
self._render(colors, phase, preset.b)
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
self._render_gradient(colors, phase, preset.b)
|
||||
phase = (phase + step_amount) % 256
|
||||
self.driver.step = phase
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class CometDual:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
tail = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
gap = max(0, int(preset.n3))
|
||||
p1 = 0
|
||||
p2 = self.driver.num_leds - 1 - gap
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
c1 = self.driver.apply_brightness(colors[0 % len(colors)], preset.b)
|
||||
c2 = self.driver.apply_brightness(colors[1 % len(colors)] if len(colors) > 1 else colors[0], preset.b)
|
||||
for t in range(tail):
|
||||
i1 = p1 - t
|
||||
if 0 <= i1 < self.driver.num_leds:
|
||||
s = (255 * (tail - t)) // max(1, tail)
|
||||
self.driver.n[i1] = ((c1[0]*s)//255, (c1[1]*s)//255, (c1[2]*s)//255)
|
||||
i2 = p2 + t
|
||||
if 0 <= i2 < self.driver.num_leds:
|
||||
s = (255 * (tail - t)) // max(1, tail)
|
||||
self.driver.n[i2] = ((c2[0]*s)//255, (c2[1]*s)//255, (c2[2]*s)//255)
|
||||
self.driver.n.write()
|
||||
p1 += speed
|
||||
p2 -= speed
|
||||
if p1 - tail > self.driver.num_leds and p2 + tail < 0:
|
||||
p1 = 0
|
||||
p2 = self.driver.num_leds - 1 - gap
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,35 +0,0 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
|
||||
class Fireflies:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 210, 80), (120, 255, 120)]
|
||||
count = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 8)
|
||||
bugs = [[random.randint(0, max(0, self.driver.num_leds - 1)), random.randint(0, 255)] for _ in range(count)]
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
for b in bugs:
|
||||
idx, ph = b
|
||||
tri = 255 - abs(128 - ph) * 2
|
||||
c = self.driver.apply_brightness(colors[idx % len(colors)], preset.b)
|
||||
self.driver.n[idx] = ((c[0]*tri)//255, (c[1]*tri)//255, (c[2]*tri)//255)
|
||||
b[1] = (ph + speed) & 255
|
||||
if random.randint(0, 31) == 0:
|
||||
b[0] = random.randint(0, max(0, self.driver.num_leds - 1))
|
||||
self.driver.n.write()
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,57 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class GradientScroll:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _render(self, colors, phase, brightness):
|
||||
num_leds = self.driver.num_leds
|
||||
color_count = len(colors)
|
||||
if num_leds <= 0 or color_count <= 0:
|
||||
return
|
||||
if color_count == 1:
|
||||
self.driver.fill(self.driver.apply_brightness(colors[0], brightness))
|
||||
return
|
||||
|
||||
full_span = color_count * 256
|
||||
phase_shift = (phase * full_span) // 256
|
||||
for i in range(num_leds):
|
||||
pos = ((i * full_span) // num_leds + phase_shift) % full_span
|
||||
idx = pos // 256
|
||||
frac = pos & 255
|
||||
|
||||
c1 = colors[idx]
|
||||
c2 = colors[(idx + 1) % color_count]
|
||||
blended = (
|
||||
c1[0] + ((c2[0] - c1[0]) * frac) // 256,
|
||||
c1[1] + ((c2[1] - c1[1]) * frac) // 256,
|
||||
c1[2] + ((c2[2] - c1[2]) * frac) // 256,
|
||||
)
|
||||
self.driver.n[i] = self.driver.apply_brightness(blended, brightness)
|
||||
self.driver.n.write()
|
||||
|
||||
def run(self, preset):
|
||||
"""Scrolling blended gradient.
|
||||
|
||||
n1: phase step amount (default 1)
|
||||
"""
|
||||
colors = preset.c if preset.c else [(255, 0, 0), (0, 0, 255)]
|
||||
phase = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1) if int(preset.n1) > 0 else 1)
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
delay_ms = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
self._render(colors, phase, preset.b)
|
||||
phase = (phase + step_amount) % 256
|
||||
self.driver.step = phase
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
|
||||
yield
|
||||
@@ -1,36 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Heartbeat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 0, 40)]
|
||||
phase = 0
|
||||
phase_start = utime.ticks_ms()
|
||||
did_manual_pulse = False
|
||||
while True:
|
||||
p1 = max(20, int(preset.n1) if int(preset.n1) > 0 else 120)
|
||||
p2 = max(20, int(preset.n2) if int(preset.n2) > 0 else 80)
|
||||
pause = max(20, int(preset.n3) if int(preset.n3) > 0 else 500)
|
||||
beat_gap = max(20, int(preset.d))
|
||||
colors = preset.c if preset.c else [(255, 0, 40)]
|
||||
lit_color = self.driver.apply_brightness(colors[0], preset.b)
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
phase_durations = (p1, beat_gap, p2, pause)
|
||||
phase_colors = (lit_color, bg_color, lit_color, bg_color)
|
||||
|
||||
now = utime.ticks_ms()
|
||||
while utime.ticks_diff(now, phase_start) >= phase_durations[phase]:
|
||||
phase_start = utime.ticks_add(phase_start, phase_durations[phase])
|
||||
phase = (phase + 1) % 4
|
||||
|
||||
self.driver.fill(phase_colors[phase])
|
||||
yield
|
||||
if not preset.a:
|
||||
if did_manual_pulse or phase == 0:
|
||||
self.driver.fill(bg_color)
|
||||
yield
|
||||
return
|
||||
did_manual_pulse = True
|
||||
@@ -1,69 +0,0 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
|
||||
class IceSparkle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(240, 248, 255), (200, 235, 255), (255, 255, 255)]
|
||||
rate = max(1, min(255, int(preset.n1) if int(preset.n1) > 0 else 55))
|
||||
decay = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 140))
|
||||
halo = max(0, min(3, int(preset.n3)))
|
||||
sparks = []
|
||||
cap = 28
|
||||
last = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg
|
||||
ns = []
|
||||
for s in sparks:
|
||||
lv = s["lv"] - decay
|
||||
if lv > 0:
|
||||
s["lv"] = lv
|
||||
ns.append(s)
|
||||
sparks = ns
|
||||
if len(sparks) < cap and random.randint(0, 255) < rate:
|
||||
sparks.append(
|
||||
{
|
||||
"p": random.randint(0, max(0, self.driver.num_leds - 1)),
|
||||
"lv": 255,
|
||||
"ci": random.randint(0, len(colors) - 1),
|
||||
}
|
||||
)
|
||||
for s in sparks:
|
||||
p = s["p"]
|
||||
lv = s["lv"]
|
||||
ci = s["ci"]
|
||||
base = colors[ci]
|
||||
for off in range(-halo, halo + 1):
|
||||
idx = p + off
|
||||
if 0 <= idx < self.driver.num_leds:
|
||||
dist = abs(off)
|
||||
fac = lv if dist == 0 else (lv * (halo - dist + 1)) // (halo + 1)
|
||||
lit = self.driver.apply_brightness(
|
||||
(
|
||||
(base[0] * fac) // 255,
|
||||
(base[1] * fac) // 255,
|
||||
(base[2] * fac) // 255,
|
||||
),
|
||||
preset.b,
|
||||
)
|
||||
o = self.driver.n[idx]
|
||||
self.driver.n[idx] = (
|
||||
min(255, o[0] + lit[0]),
|
||||
min(255, o[1] + lit[1]),
|
||||
min(255, o[2] + lit[2]),
|
||||
)
|
||||
self.driver.n.write()
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,31 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Marquee:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
on_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 3)
|
||||
off_len = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
|
||||
step = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
|
||||
phase = self.driver.step % (on_len + off_len)
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
c = self.driver.apply_brightness(colors[0], preset.b)
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
m = (i + phase) % (on_len + off_len)
|
||||
self.driver.n[i] = c if m < on_len else bg_color
|
||||
self.driver.n.write()
|
||||
phase = (phase + step) % (on_len + off_len)
|
||||
self.driver.step = phase
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
156
src/patterns/meteor.py
Normal file
156
src/patterns/meteor.py
Normal file
@@ -0,0 +1,156 @@
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"comet_dual": 1, "scanner": 2}
|
||||
|
||||
|
||||
class Meteor:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _fade(self, color, fade_amount):
|
||||
return (
|
||||
(color[0] * fade_amount) // 255,
|
||||
(color[1] * fade_amount) // 255,
|
||||
(color[2] * fade_amount) // 255,
|
||||
)
|
||||
|
||||
def _run_meteor(self, preset, colors, color_index, head, direction, last_update):
|
||||
tail_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 8)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
fade_amount = int(preset.n3) if int(preset.n3) > 0 else 192
|
||||
fade_amount = max(1, min(255, fade_amount))
|
||||
delay_ms = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||
return color_index, head, direction, last_update, False
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = self._fade(self.driver.n[i], fade_amount)
|
||||
base = colors[color_index % len(colors)]
|
||||
lit = self.driver.apply_brightness(base, preset.b)
|
||||
if 0 <= head < self.driver.num_leds:
|
||||
self.driver.n[head] = lit
|
||||
self.driver.n.write()
|
||||
head += direction * speed
|
||||
if head >= self.driver.num_leds + tail_len:
|
||||
head = self.driver.num_leds - 1
|
||||
direction = -1
|
||||
color_index += 1
|
||||
elif head < -tail_len:
|
||||
head = 0
|
||||
direction = 1
|
||||
color_index += 1
|
||||
return color_index, head, direction, utime.ticks_add(last_update, delay_ms), True
|
||||
|
||||
def _run_comet_dual(self, preset, colors, p1, p2, last):
|
||||
tail = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
gap = max(0, int(preset.n3))
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return p1, p2, last, False
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
c1 = self.driver.apply_brightness(colors[0 % len(colors)], preset.b)
|
||||
c2 = self.driver.apply_brightness(
|
||||
colors[1 % len(colors)] if len(colors) > 1 else colors[0], preset.b
|
||||
)
|
||||
for t in range(tail):
|
||||
i1 = p1 - t
|
||||
if 0 <= i1 < self.driver.num_leds:
|
||||
s = (255 * (tail - t)) // max(1, tail)
|
||||
self.driver.n[i1] = ((c1[0] * s) // 255, (c1[1] * s) // 255, (c1[2] * s) // 255)
|
||||
i2 = p2 + t
|
||||
if 0 <= i2 < self.driver.num_leds:
|
||||
s = (255 * (tail - t)) // max(1, tail)
|
||||
self.driver.n[i2] = ((c2[0] * s) // 255, (c2[1] * s) // 255, (c2[2] * s) // 255)
|
||||
self.driver.n.write()
|
||||
p1 += speed
|
||||
p2 -= speed
|
||||
if p1 - tail > self.driver.num_leds and p2 + tail < 0:
|
||||
p1 = 0
|
||||
p2 = self.driver.num_leds - 1 - gap
|
||||
return p1, p2, utime.ticks_add(last, d), True
|
||||
|
||||
def _run_scanner(self, preset, colors, color_index, center, direction, pause_frames, last_update):
|
||||
width = max(1, int(preset.n1) if int(preset.n1) > 0 else 4)
|
||||
end_pause = max(0, int(preset.n2))
|
||||
delay_ms = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||
return color_index, center, direction, pause_frames, last_update, False
|
||||
base = self.driver.apply_brightness(colors[color_index % len(colors)], preset.b)
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
dist = i - center
|
||||
if dist < 0:
|
||||
dist = -dist
|
||||
if dist > width:
|
||||
self.driver.n[i] = bg_color
|
||||
else:
|
||||
scale = ((width - dist) * 255) // max(1, width)
|
||||
self.driver.n[i] = (
|
||||
(base[0] * scale) // 255,
|
||||
(base[1] * scale) // 255,
|
||||
(base[2] * scale) // 255,
|
||||
)
|
||||
self.driver.n.write()
|
||||
if pause_frames > 0:
|
||||
pause_frames -= 1
|
||||
else:
|
||||
center += direction
|
||||
if center >= self.driver.num_leds - 1:
|
||||
center = self.driver.num_leds - 1
|
||||
direction = -1
|
||||
pause_frames = end_pause
|
||||
color_index += 1
|
||||
elif center <= 0:
|
||||
center = 0
|
||||
direction = 1
|
||||
pause_frames = end_pause
|
||||
color_index += 1
|
||||
return color_index, center, direction, pause_frames, utime.ticks_add(last_update, delay_ms), True
|
||||
|
||||
def run(self, preset):
|
||||
"""Moving lights: n6 style 0 meteor, 1 dual comet, 2 scanner (legacy ids still work)."""
|
||||
mode = style_mode(preset, 0, _LEGACY)
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
|
||||
if mode == 1:
|
||||
gap = max(0, int(preset.n3))
|
||||
p1, p2 = 0, self.driver.num_leds - 1 - gap
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
p1, p2, last, stepped = self._run_comet_dual(preset, colors, p1, p2, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
if mode == 2:
|
||||
color_index, center, direction, pause_frames = 0, 0, 1, 0
|
||||
last_update = utime.ticks_ms()
|
||||
while True:
|
||||
color_index, center, direction, pause_frames, last_update, stepped = (
|
||||
self._run_scanner(
|
||||
preset, colors, color_index, center, direction, pause_frames, last_update
|
||||
)
|
||||
)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
color_index, head, direction = 0, 0, 1
|
||||
last_update = utime.ticks_ms()
|
||||
while True:
|
||||
color_index, head, direction, last_update, stepped = self._run_meteor(
|
||||
preset, colors, color_index, head, direction, last_update
|
||||
)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,62 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class MeteorRain:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _fade(self, color, fade_amount):
|
||||
return (
|
||||
(color[0] * fade_amount) // 255,
|
||||
(color[1] * fade_amount) // 255,
|
||||
(color[2] * fade_amount) // 255,
|
||||
)
|
||||
|
||||
def run(self, preset):
|
||||
"""Single meteor with a fading tail.
|
||||
|
||||
n1: tail length (default 8)
|
||||
n2: speed in LEDs per frame (default 1)
|
||||
n3: fade amount per frame, 1..255 (default 192)
|
||||
"""
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
color_index = 0
|
||||
head = 0
|
||||
direction = 1
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
delay_ms = max(1, int(preset.d))
|
||||
tail_len = max(1, int(preset.n1) if int(preset.n1) > 0 else 8)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
fade_amount = int(preset.n3) if int(preset.n3) > 0 else 192
|
||||
fade_amount = max(1, min(255, fade_amount))
|
||||
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = self._fade(self.driver.n[i], fade_amount)
|
||||
|
||||
base = colors[color_index % len(colors)]
|
||||
lit = self.driver.apply_brightness(base, preset.b)
|
||||
if 0 <= head < self.driver.num_leds:
|
||||
self.driver.n[head] = lit
|
||||
self.driver.n.write()
|
||||
|
||||
head += direction * speed
|
||||
if head >= self.driver.num_leds + tail_len:
|
||||
head = self.driver.num_leds - 1
|
||||
direction = -1
|
||||
color_index += 1
|
||||
elif head < -tail_len:
|
||||
head = 0
|
||||
direction = 1
|
||||
color_index += 1
|
||||
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
|
||||
yield
|
||||
@@ -1,53 +0,0 @@
|
||||
import math
|
||||
import utime
|
||||
|
||||
|
||||
class NorthernWave:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(20, 55, 120), (60, 140, 220), (180, 220, 255)]
|
||||
period = max(4, int(preset.n1) if int(preset.n1) > 0 else 20)
|
||||
contrast = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 200))
|
||||
drift = max(1, int(preset.n3) if int(preset.n3) > 0 else 2)
|
||||
phase = 0
|
||||
last = utime.ticks_ms()
|
||||
ncols = len(colors)
|
||||
if ncols < 2:
|
||||
colors = list(colors) + [(120, 180, 255)]
|
||||
ncols = len(colors)
|
||||
twopi = 6.2831853
|
||||
|
||||
def lerp3(a, b, f):
|
||||
return (
|
||||
a[0] + ((b[0] - a[0]) * f) // 255,
|
||||
a[1] + ((b[1] - a[1]) * f) // 255,
|
||||
a[2] + ((b[2] - a[2]) * f) // 255,
|
||||
)
|
||||
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
t = (i * twopi / period) + (phase * twopi / 256.0)
|
||||
w = (math.sin(t) + 1.0) * 0.5
|
||||
u = w * (ncols - 1) * 256.0
|
||||
fi = int(u) >> 8
|
||||
frac = int(u) & 255
|
||||
if fi >= ncols - 1:
|
||||
fi = ncols - 2
|
||||
frac = 255
|
||||
peak = lerp3(colors[fi], colors[fi + 1], frac)
|
||||
peak = self.driver.apply_brightness(peak, preset.b)
|
||||
mixf = min(255, int(w * contrast * 2) >> 1)
|
||||
self.driver.n[i] = lerp3(bg, peak, mixf)
|
||||
self.driver.n.write()
|
||||
phase = (phase + drift) % 256
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
108
src/patterns/particles.py
Normal file
108
src/patterns/particles.py
Normal file
@@ -0,0 +1,108 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"starfall": 1}
|
||||
|
||||
|
||||
class Particles:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _run_snowfall(self, preset, colors, flakes, last):
|
||||
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 20)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return flakes, last, False
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
if random.randint(0, 255) < density:
|
||||
flakes.append([self.driver.num_leds - 1, random.randint(0, len(colors) - 1)])
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
nf = []
|
||||
for pos, ci in flakes:
|
||||
if 0 <= pos < self.driver.num_leds:
|
||||
self.driver.n[pos] = self.driver.apply_brightness(colors[ci], preset.b)
|
||||
pos -= speed
|
||||
if pos >= -1:
|
||||
nf.append([pos, ci])
|
||||
self.driver.n.write()
|
||||
return nf, utime.ticks_add(last, d), True
|
||||
|
||||
def _run_starfall(self, preset, colors, stars, last):
|
||||
rate = max(1, min(255, int(preset.n1) if int(preset.n1) > 0 else 14))
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
|
||||
tail = max(2, int(preset.n3) if int(preset.n3) > 0 else 10)
|
||||
max_stars = 4
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return stars, last, False
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg
|
||||
if len(stars) < max_stars and random.randint(0, 255) < rate:
|
||||
top = self.driver.num_leds - 1 + random.randint(
|
||||
0, min(8, self.driver.num_leds // 2)
|
||||
)
|
||||
stars.append({"h": float(top), "ci": random.randint(0, len(colors) - 1)})
|
||||
ns = []
|
||||
for s in stars:
|
||||
h = s["h"]
|
||||
ci = s["ci"]
|
||||
ih = int(h)
|
||||
for t in range(tail):
|
||||
idx = ih + t
|
||||
if 0 <= idx < self.driver.num_leds:
|
||||
fade = 255 - (t * 255 // max(1, tail - 1))
|
||||
base = colors[ci]
|
||||
lit = (
|
||||
(base[0] * fade) // 255,
|
||||
(base[1] * fade) // 255,
|
||||
(base[2] * fade) // 255,
|
||||
)
|
||||
lit = self.driver.apply_brightness(lit, preset.b)
|
||||
o = self.driver.n[idx]
|
||||
self.driver.n[idx] = (
|
||||
max(o[0], lit[0]),
|
||||
max(o[1], lit[1]),
|
||||
max(o[2], lit[2]),
|
||||
)
|
||||
h -= speed
|
||||
if h >= -tail:
|
||||
s["h"] = h
|
||||
ns.append(s)
|
||||
stars = ns
|
||||
self.driver.n.write()
|
||||
return stars, utime.ticks_add(last, d), True
|
||||
|
||||
def run(self, preset):
|
||||
"""Falling particles: n6 0 snowfall flakes, 1 starfall streaks."""
|
||||
mode = style_mode(preset, 0, _LEGACY)
|
||||
colors = preset.c if preset.c else [(255, 255, 255), (180, 220, 255)]
|
||||
last = utime.ticks_ms()
|
||||
|
||||
if mode == 1:
|
||||
colors = preset.c if preset.c else [
|
||||
(255, 255, 255),
|
||||
(200, 230, 255),
|
||||
(255, 248, 220),
|
||||
]
|
||||
stars = []
|
||||
while True:
|
||||
stars, last, stepped = self._run_starfall(preset, colors, stars, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
flakes = []
|
||||
while True:
|
||||
flakes, last, stepped = self._run_snowfall(preset, colors, flakes, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
18
src/patterns/pattern_modes.py
Normal file
18
src/patterns/pattern_modes.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Resolve pattern style from n6 or legacy preset pattern id (p)."""
|
||||
|
||||
|
||||
def style_mode(preset, default=0, legacy=None):
|
||||
legacy = legacy or {}
|
||||
p = getattr(preset, "p", "") or ""
|
||||
if p in legacy:
|
||||
return legacy[p]
|
||||
mode = getattr(preset, "mode", None)
|
||||
if mode is None and isinstance(preset, dict):
|
||||
mode = preset.get("mode")
|
||||
if mode is not None:
|
||||
try:
|
||||
return int(mode)
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
n6 = int(getattr(preset, "n6", 0) or 0)
|
||||
return n6 if n6 > 0 else default
|
||||
@@ -1,7 +1,7 @@
|
||||
import utime
|
||||
|
||||
# When ``driver.debug`` is True (``settings["debug"]``), log at most this often (ms).
|
||||
_RADIATE_DBG_INTERVAL_MS = 800
|
||||
_RADIATE_DBG_INTERVAL_MS = 2500
|
||||
|
||||
|
||||
class Radiate:
|
||||
@@ -37,6 +37,7 @@ class Radiate:
|
||||
if not preset.a:
|
||||
# Manual mode: one-shot pulse using the same ms-based timing as auto.
|
||||
cycle_start = utime.ticks_ms()
|
||||
last_dbg = cycle_start
|
||||
while True:
|
||||
dbg = bool(getattr(self.driver, "debug", False))
|
||||
spacing = max(1, int(preset.n1))
|
||||
@@ -78,10 +79,12 @@ class Radiate:
|
||||
"[radiate] debug on n1=%s n2=%s n3=%s d=%s auto=%s num_leds=%d"
|
||||
% (preset.n1, preset.n2, preset.n3, preset.d, preset.a, self.driver.num_leds)
|
||||
)
|
||||
print(
|
||||
"[radiate] manual frame age=%d/%d front=%d lit=%d"
|
||||
% (age, pulse_lifetime, front, lit_count)
|
||||
)
|
||||
if utime.ticks_diff(now, last_dbg) >= _RADIATE_DBG_INTERVAL_MS:
|
||||
print(
|
||||
"[radiate] manual frame age=%d/%d front=%d lit=%d"
|
||||
% (age, pulse_lifetime, front, lit_count)
|
||||
)
|
||||
last_dbg = now
|
||||
|
||||
yield
|
||||
if age >= pulse_lifetime:
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Rainbow:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _wheel(self, pos):
|
||||
if pos < 85:
|
||||
return (pos * 3, 255 - pos * 3, 0)
|
||||
elif pos < 170:
|
||||
pos -= 85
|
||||
return (255 - pos * 3, 0, pos * 3)
|
||||
else:
|
||||
pos -= 170
|
||||
return (0, pos * 3, 255 - pos * 3)
|
||||
|
||||
def run(self, preset):
|
||||
step = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
||||
|
||||
# If auto is False, run a single step and then stop
|
||||
if not preset.a:
|
||||
for i in range(self.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b)
|
||||
self.driver.n.write()
|
||||
# Increment step by n1 for next manual call
|
||||
self.driver.step = (step + step_amount) % 256
|
||||
# Allow tick() to advance the generator once
|
||||
yield
|
||||
return
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
sleep_ms = max(1, int(preset.d)) # Get delay from preset
|
||||
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
||||
for i in range(self.driver.num_leds):
|
||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
||||
self.driver.n[i] = self.driver.apply_brightness(
|
||||
self._wheel(rc_index & 255),
|
||||
preset.b,
|
||||
)
|
||||
self.driver.n.write()
|
||||
step = (step + step_amount) % 256
|
||||
self.driver.step = step
|
||||
last_update = utime.ticks_add(last_update, sleep_ms)
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
@@ -1,67 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Scanner:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Classic scanner eye with soft falloff.
|
||||
|
||||
n1: eye width (default 4)
|
||||
n2: end pause in frames (default 0)
|
||||
"""
|
||||
colors = preset.c if preset.c else [(255, 0, 0)]
|
||||
color_index = 0
|
||||
center = 0
|
||||
direction = 1
|
||||
pause_frames = 0
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
delay_ms = max(1, int(preset.d))
|
||||
width = max(1, int(preset.n1) if int(preset.n1) > 0 else 4)
|
||||
end_pause = max(0, int(preset.n2))
|
||||
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
base = colors[color_index % len(colors)]
|
||||
base = self.driver.apply_brightness(base, preset.b)
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
dist = i - center
|
||||
if dist < 0:
|
||||
dist = -dist
|
||||
if dist > width:
|
||||
self.driver.n[i] = bg_color
|
||||
else:
|
||||
scale = ((width - dist) * 255) // max(1, width)
|
||||
self.driver.n[i] = (
|
||||
(base[0] * scale) // 255,
|
||||
(base[1] * scale) // 255,
|
||||
(base[2] * scale) // 255,
|
||||
)
|
||||
self.driver.n.write()
|
||||
|
||||
if pause_frames > 0:
|
||||
pause_frames -= 1
|
||||
else:
|
||||
center += direction
|
||||
if center >= self.driver.num_leds - 1:
|
||||
center = self.driver.num_leds - 1
|
||||
direction = -1
|
||||
pause_frames = end_pause
|
||||
color_index += 1
|
||||
elif center <= 0:
|
||||
center = 0
|
||||
direction = 1
|
||||
pause_frames = end_pause
|
||||
color_index += 1
|
||||
|
||||
last_update = utime.ticks_add(last_update, delay_ms)
|
||||
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
|
||||
yield
|
||||
@@ -1,45 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class SegmentChase:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Independent moving segments (distinct from classic two-color chase).
|
||||
|
||||
n1: segment size (LEDs per segment)
|
||||
n2: step size (phase increment each frame)
|
||||
n3: per-segment phase offset
|
||||
n4: gap spacing inside segment (0 = solid segment)
|
||||
"""
|
||||
colors = preset.c if preset.c else [(255, 0, 0), (0, 0, 255)]
|
||||
seg = max(1, int(preset.n1) if int(preset.n1) > 0 else 4)
|
||||
phase_step = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
seg_offset = max(0, int(preset.n3))
|
||||
gap = max(0, int(preset.n4))
|
||||
phase = self.driver.step % 256
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
seg_idx = i // seg
|
||||
in_seg = i % seg
|
||||
local_phase = (phase + seg_idx * seg_offset) % seg
|
||||
lit_idx = (in_seg + local_phase) % seg
|
||||
if gap > 0 and lit_idx >= max(1, seg - gap):
|
||||
self.driver.n[i] = bg_color
|
||||
else:
|
||||
color_idx = seg_idx % len(colors)
|
||||
self.driver.n[i] = self.driver.apply_brightness(colors[color_idx], preset.b)
|
||||
self.driver.n.write()
|
||||
phase = (phase + phase_step) % seg
|
||||
self.driver.step = phase
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,37 +0,0 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
|
||||
class Snowfall:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 255, 255), (180, 220, 255)]
|
||||
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 20)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 1)
|
||||
flakes = []
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
if random.randint(0, 255) < density:
|
||||
flakes.append([self.driver.num_leds - 1, random.randint(0, len(colors)-1)])
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
nf = []
|
||||
for pos, ci in flakes:
|
||||
if 0 <= pos < self.driver.num_leds:
|
||||
self.driver.n[pos] = self.driver.apply_brightness(colors[ci], preset.b)
|
||||
pos -= speed
|
||||
if pos >= -1:
|
||||
nf.append([pos, ci])
|
||||
flakes = nf
|
||||
self.driver.n.write()
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
147
src/patterns/sparkle.py
Normal file
147
src/patterns/sparkle.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
from patterns.pattern_modes import style_mode
|
||||
|
||||
_LEGACY = {"sparkle_trail": 0, "ice_sparkle": 1, "fireflies": 2}
|
||||
|
||||
|
||||
class Sparkle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def _run_trail(self, preset, colors, last):
|
||||
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 24)
|
||||
decay = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 210))
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return last, False
|
||||
for i in range(self.driver.num_leds):
|
||||
r, g, b = self.driver.n[i]
|
||||
self.driver.n[i] = ((r * decay) // 255, (g * decay) // 255, (b * decay) // 255)
|
||||
sparks = max(1, self.driver.num_leds * density // 255)
|
||||
for _ in range(sparks):
|
||||
idx = random.randint(0, max(0, self.driver.num_leds - 1))
|
||||
c = self.driver.apply_brightness(
|
||||
colors[random.randint(0, len(colors) - 1)], preset.b
|
||||
)
|
||||
self.driver.n[idx] = c
|
||||
self.driver.n.write()
|
||||
return utime.ticks_add(last, d), True
|
||||
|
||||
def _run_ice(self, preset, colors, sparks, last):
|
||||
rate = max(1, min(255, int(preset.n1) if int(preset.n1) > 0 else 55))
|
||||
decay = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 140))
|
||||
halo = max(0, min(3, int(preset.n3)))
|
||||
cap = 28
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return sparks, last, False
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg
|
||||
ns = []
|
||||
for s in sparks:
|
||||
lv = s["lv"] - decay
|
||||
if lv > 0:
|
||||
s["lv"] = lv
|
||||
ns.append(s)
|
||||
sparks = ns
|
||||
if len(sparks) < cap and random.randint(0, 255) < rate:
|
||||
sparks.append(
|
||||
{
|
||||
"p": random.randint(0, max(0, self.driver.num_leds - 1)),
|
||||
"lv": 255,
|
||||
"ci": random.randint(0, len(colors) - 1),
|
||||
}
|
||||
)
|
||||
for s in sparks:
|
||||
p = s["p"]
|
||||
lv = s["lv"]
|
||||
ci = s["ci"]
|
||||
base = colors[ci]
|
||||
for off in range(-halo, halo + 1):
|
||||
idx = p + off
|
||||
if 0 <= idx < self.driver.num_leds:
|
||||
dist = abs(off)
|
||||
fac = lv if dist == 0 else (lv * (halo - dist + 1)) // (halo + 1)
|
||||
lit = self.driver.apply_brightness(
|
||||
(
|
||||
(base[0] * fac) // 255,
|
||||
(base[1] * fac) // 255,
|
||||
(base[2] * fac) // 255,
|
||||
),
|
||||
preset.b,
|
||||
)
|
||||
o = self.driver.n[idx]
|
||||
self.driver.n[idx] = (
|
||||
min(255, o[0] + lit[0]),
|
||||
min(255, o[1] + lit[1]),
|
||||
min(255, o[2] + lit[2]),
|
||||
)
|
||||
self.driver.n.write()
|
||||
return sparks, utime.ticks_add(last, d), True
|
||||
|
||||
def _run_fireflies(self, preset, colors, bugs, last):
|
||||
count = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 8)
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) < d:
|
||||
return bugs, last, False
|
||||
bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg_color
|
||||
for b in bugs:
|
||||
idx, ph = b
|
||||
tri = 255 - abs(128 - ph) * 2
|
||||
c = self.driver.apply_brightness(colors[idx % len(colors)], preset.b)
|
||||
self.driver.n[idx] = ((c[0] * tri) // 255, (c[1] * tri) // 255, (c[2] * tri) // 255)
|
||||
b[1] = (ph + speed) & 255
|
||||
if random.randint(0, 31) == 0:
|
||||
b[0] = random.randint(0, max(0, self.driver.num_leds - 1))
|
||||
self.driver.n.write()
|
||||
return bugs, utime.ticks_add(last, d), True
|
||||
|
||||
def run(self, preset):
|
||||
"""Sparkles: n6 0 trail decay, 1 ice burst+halo, 2 fireflies."""
|
||||
mode = style_mode(preset, 0, _LEGACY)
|
||||
colors = preset.c if preset.c else [(120, 120, 255)]
|
||||
last = utime.ticks_ms()
|
||||
|
||||
if mode == 2:
|
||||
colors = preset.c if preset.c else [(255, 210, 80), (120, 255, 120)]
|
||||
count = max(1, int(preset.n1) if int(preset.n1) > 0 else 6)
|
||||
bugs = [
|
||||
[random.randint(0, max(0, self.driver.num_leds - 1)), random.randint(0, 255)]
|
||||
for _ in range(count)
|
||||
]
|
||||
while True:
|
||||
bugs, last, stepped = self._run_fireflies(preset, colors, bugs, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
if mode == 1:
|
||||
colors = preset.c if preset.c else [
|
||||
(240, 248, 255),
|
||||
(200, 235, 255),
|
||||
(255, 255, 255),
|
||||
]
|
||||
sparks = []
|
||||
while True:
|
||||
sparks, last, stepped = self._run_ice(preset, colors, sparks, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
|
||||
while True:
|
||||
last, stepped = self._run_trail(preset, colors, last)
|
||||
if stepped and not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,31 +0,0 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
|
||||
class SparkleTrail:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(120, 120, 255)]
|
||||
density = max(1, int(preset.n1) if int(preset.n1) > 0 else 24)
|
||||
decay = max(1, min(255, int(preset.n2) if int(preset.n2) > 0 else 210))
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
for i in range(self.driver.num_leds):
|
||||
r,g,b = self.driver.n[i]
|
||||
self.driver.n[i] = ((r*decay)//255, (g*decay)//255, (b*decay)//255)
|
||||
sparks = max(1, self.driver.num_leds * density // 255)
|
||||
for _ in range(sparks):
|
||||
idx = random.randint(0, max(0, self.driver.num_leds - 1))
|
||||
c = self.driver.apply_brightness(colors[random.randint(0, len(colors)-1)], preset.b)
|
||||
self.driver.n[idx] = c
|
||||
self.driver.n.write()
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,65 +0,0 @@
|
||||
import random
|
||||
import utime
|
||||
|
||||
|
||||
class Starfall:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(255, 255, 255), (200, 230, 255), (255, 248, 220)]
|
||||
rate = max(1, min(255, int(preset.n1) if int(preset.n1) > 0 else 14))
|
||||
speed = max(1, int(preset.n2) if int(preset.n2) > 0 else 2)
|
||||
tail = max(2, int(preset.n3) if int(preset.n3) > 0 else 10)
|
||||
stars = []
|
||||
max_stars = 4
|
||||
last = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
bg = self.driver.apply_brightness(preset.background_or(colors), preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
self.driver.n[i] = bg
|
||||
if len(stars) < max_stars and random.randint(0, 255) < rate:
|
||||
top = self.driver.num_leds - 1 + random.randint(0, min(8, self.driver.num_leds // 2))
|
||||
stars.append(
|
||||
{
|
||||
"h": float(top),
|
||||
"ci": random.randint(0, len(colors) - 1),
|
||||
}
|
||||
)
|
||||
ns = []
|
||||
for s in stars:
|
||||
h = s["h"]
|
||||
ci = s["ci"]
|
||||
ih = int(h)
|
||||
for t in range(tail):
|
||||
idx = ih + t
|
||||
if 0 <= idx < self.driver.num_leds:
|
||||
fade = 255 - (t * 255 // max(1, tail - 1))
|
||||
base = colors[ci]
|
||||
lit = (
|
||||
(base[0] * fade) // 255,
|
||||
(base[1] * fade) // 255,
|
||||
(base[2] * fade) // 255,
|
||||
)
|
||||
lit = self.driver.apply_brightness(lit, preset.b)
|
||||
o = self.driver.n[idx]
|
||||
self.driver.n[idx] = (
|
||||
max(o[0], lit[0]),
|
||||
max(o[1], lit[1]),
|
||||
max(o[2], lit[2]),
|
||||
)
|
||||
h -= speed
|
||||
if h >= -tail:
|
||||
s["h"] = h
|
||||
ns.append(s)
|
||||
stars = ns
|
||||
self.driver.n.write()
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -1,32 +0,0 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Wave:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
colors = preset.c if preset.c else [(0, 180, 255)]
|
||||
wavelength = max(2, int(preset.n1) if int(preset.n1) > 0 else 12)
|
||||
amp = max(0, min(255, int(preset.n2) if int(preset.n2) > 0 else 180))
|
||||
drift = max(1, int(preset.n3) if int(preset.n3) > 0 else 1)
|
||||
phase = self.driver.step % 256
|
||||
last = utime.ticks_ms()
|
||||
while True:
|
||||
d = max(1, int(preset.d))
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last) >= d:
|
||||
base = self.driver.apply_brightness(colors[0], preset.b)
|
||||
for i in range(self.driver.num_leds):
|
||||
x = (i * 256 // wavelength + phase) & 255
|
||||
tri = 255 - abs(128 - x) * 2
|
||||
s = (tri * amp) // 255
|
||||
self.driver.n[i] = ((base[0]*s)//255, (base[1]*s)//255, (base[2]*s)//255)
|
||||
self.driver.n.write()
|
||||
phase = (phase + drift) % 256
|
||||
self.driver.step = phase
|
||||
last = utime.ticks_add(last, d)
|
||||
if not preset.a:
|
||||
yield
|
||||
return
|
||||
yield
|
||||
@@ -27,6 +27,7 @@ class Preset:
|
||||
"brightness": "b",
|
||||
"auto": "a",
|
||||
"background": "bg",
|
||||
"mode": "n6",
|
||||
}
|
||||
int_fields = {"d", "b", "n1", "n2", "n3", "n4", "n5", "n6"}
|
||||
allowed_fields = {"p", "c", "d", "b", "a", "bg", "n1", "n2", "n3", "n4", "n5", "n6"}
|
||||
|
||||
@@ -70,8 +70,29 @@ class Presets:
|
||||
except Exception as e:
|
||||
print("Pattern init failed:", module_name, e)
|
||||
|
||||
self._apply_pattern_aliases(loaded)
|
||||
return loaded
|
||||
|
||||
def _apply_pattern_aliases(self, loaded):
|
||||
"""Legacy pattern ids -> merged implementations (same generator)."""
|
||||
aliases = (
|
||||
("rainbow", "colour_cycle"),
|
||||
("gradient_scroll", "colour_cycle"),
|
||||
("meteor_rain", "meteor"),
|
||||
("comet_dual", "meteor"),
|
||||
("scanner", "meteor"),
|
||||
("snowfall", "particles"),
|
||||
("starfall", "particles"),
|
||||
("sparkle_trail", "sparkle"),
|
||||
("ice_sparkle", "sparkle"),
|
||||
("fireflies", "sparkle"),
|
||||
("marquee", "chase"),
|
||||
("northern_wave", "aurora"),
|
||||
)
|
||||
for old, new in aliases:
|
||||
if new in loaded and old not in loaded:
|
||||
loaded[old] = loaded[new]
|
||||
|
||||
def save(self):
|
||||
"""Save the presets to a file."""
|
||||
with open("presets.json", "w") as f:
|
||||
@@ -112,16 +133,24 @@ class Presets:
|
||||
"""Create or update a preset with the given name."""
|
||||
if name in self.presets:
|
||||
# Update existing preset
|
||||
was_auto = self.presets[name].a
|
||||
self.presets[name].edit(data)
|
||||
# Editing the live preset (e.g. toggling auto/manual) must reset runtime
|
||||
# state; re-select alone keeps step because preset name is unchanged.
|
||||
# Editing the live preset: auto still re-selects (one tick) so the strip
|
||||
# restarts without a separate select message (controller often sends both).
|
||||
# Manual must NOT call select() here — presets-only pushes (e.g. zone sequence
|
||||
# arming the first step) would otherwise run select's first tick and consume a
|
||||
# beat/step. Manual advances only on explicit select from the controller.
|
||||
if self.selected == name:
|
||||
self.step = 0
|
||||
self.generator = None
|
||||
self.fill((0, 0, 0))
|
||||
# Re-start pattern so manual/auto and other edits apply without a
|
||||
# separate select message (controller usually sends both).
|
||||
self.select(name)
|
||||
preset = self.presets[name]
|
||||
if preset.a:
|
||||
self.step = 0
|
||||
self.generator = None
|
||||
self.fill((0, 0, 0))
|
||||
self.select(name)
|
||||
elif was_auto:
|
||||
self.step = 0
|
||||
self.generator = None
|
||||
self.fill((0, 0, 0))
|
||||
else:
|
||||
if len(self.presets) >= MAX_PRESETS and name not in ("on", "off"):
|
||||
print("Preset limit reached:", MAX_PRESETS)
|
||||
|
||||
17
src/print_timestamp.py
Normal file
17
src/print_timestamp.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Install a builtins.print wrapper that prefixes each line with uptime (ms).
|
||||
|
||||
Import this module before other led-driver imports that print (e.g. first in main).
|
||||
"""
|
||||
|
||||
import builtins
|
||||
import utime
|
||||
|
||||
_original_print = builtins.print
|
||||
|
||||
|
||||
def _timestamped_print(*args, **kwargs):
|
||||
ts = utime.ticks_ms()
|
||||
return _original_print("[%d]" % ts, *args, **kwargs)
|
||||
|
||||
|
||||
builtins.print = _timestamped_print
|
||||
@@ -1,5 +1,7 @@
|
||||
"""STA connect helpers aligned with tests/test_wifi.py (status polling, fatal codes)."""
|
||||
|
||||
import gc
|
||||
import machine
|
||||
import utime
|
||||
import network
|
||||
|
||||
@@ -57,6 +59,40 @@ def _one_association_campaign(sta_if, ssid, password, wdt):
|
||||
return True
|
||||
|
||||
|
||||
def boot_sta(settings, wdt):
|
||||
"""Tear down and bring up STA. Call before large heap users (NeoPixel, patterns).
|
||||
|
||||
On ESP32-C3, soft reboots can leave the Wi-Fi driver allocated; init while the
|
||||
heap is still free. If re-init fails after a soft reboot, hard-reset once.
|
||||
"""
|
||||
sta_if = network.WLAN(network.STA_IF)
|
||||
try:
|
||||
if sta_if.active():
|
||||
try:
|
||||
sta_if.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
sta_if.active(False)
|
||||
except Exception:
|
||||
pass
|
||||
utime.sleep_ms(100)
|
||||
gc.collect()
|
||||
try:
|
||||
sta_if.active(True)
|
||||
except OSError as e:
|
||||
err = str(e)
|
||||
if "Out of Memory" in err or "WiFi" in err:
|
||||
if machine.reset_cause() == machine.SOFT_RESET:
|
||||
print("wifi_sta: init failed after soft reboot, hard reset:", err)
|
||||
machine.reset()
|
||||
raise
|
||||
sta_if.config(pm=network.WLAN.PM_NONE)
|
||||
ssid = settings.get("ssid") or ""
|
||||
if ssid:
|
||||
connect_until_up(sta_if, ssid, settings.get("password") or "", wdt)
|
||||
return sta_if
|
||||
|
||||
|
||||
def connect_until_up(sta_if, ssid, password, wdt):
|
||||
"""Boot: repeat campaigns until STA has a route (same strategy as tests/test_wifi.py)."""
|
||||
if not ssid:
|
||||
|
||||
Reference in New Issue
Block a user