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>
173 lines
7.0 KiB
Python
173 lines
7.0 KiB
Python
import utime
|
|
|
|
# When ``driver.debug`` is True (``settings["debug"]``), log at most this often (ms).
|
|
_RADIATE_DBG_INTERVAL_MS = 2500
|
|
|
|
|
|
class Radiate:
|
|
def __init__(self, driver):
|
|
self.driver = driver
|
|
self._color_step = 0
|
|
|
|
def run(self, preset):
|
|
"""Radiate from nodes every n1 LEDs, retriggering every delay (d).
|
|
|
|
- n1: node spacing in LEDs
|
|
- n2: outbound travel time in ms
|
|
- n3: return travel time in ms
|
|
- d: retrigger interval in ms
|
|
"""
|
|
colors = preset.c if preset.c else [(255, 255, 255)]
|
|
base_off = preset.background_or(colors)
|
|
|
|
spacing = max(1, int(preset.n1))
|
|
outward_ms = max(1, int(preset.n2))
|
|
return_ms = max(1, int(preset.n3))
|
|
max_dist = spacing // 2
|
|
|
|
lit_color = self.driver.apply_brightness(colors[self._color_step % max(1, len(colors))], preset.b)
|
|
off_color = self.driver.apply_brightness(base_off, preset.b)
|
|
|
|
now = utime.ticks_ms()
|
|
last_trigger = now
|
|
active_pulses = [now]
|
|
last_dbg = now
|
|
dbg_banner = False
|
|
|
|
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))
|
|
outward_ms = max(1, int(preset.n2))
|
|
return_ms = max(1, int(preset.n3))
|
|
max_dist = spacing // 2
|
|
on_color = colors[self._color_step % max(1, len(colors))]
|
|
lit_color = self.driver.apply_brightness(on_color, preset.b)
|
|
off_color = self.driver.apply_brightness(base_off, preset.b)
|
|
|
|
pulse_lifetime = outward_ms + return_ms
|
|
now = utime.ticks_ms()
|
|
age = utime.ticks_diff(now, cycle_start)
|
|
if age < 1:
|
|
age = 1
|
|
if age <= outward_ms:
|
|
front = (age * max_dist + outward_ms - 1) // outward_ms
|
|
elif age <= outward_ms + return_ms:
|
|
back_age = age - outward_ms
|
|
remaining = return_ms - back_age
|
|
front = (remaining * max_dist + return_ms - 1) // return_ms
|
|
else:
|
|
front = 0
|
|
|
|
lit_count = 0
|
|
for i in range(self.driver.num_leds):
|
|
offset = (i + (spacing // 2)) % spacing
|
|
dist = min(offset, spacing - offset)
|
|
lit = dist <= front
|
|
self.driver.n[i] = lit_color if lit else off_color
|
|
if lit:
|
|
lit_count += 1
|
|
self.driver.n.write()
|
|
|
|
if dbg:
|
|
if not dbg_banner:
|
|
dbg_banner = True
|
|
print(
|
|
"[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)
|
|
)
|
|
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:
|
|
self._color_step += 1
|
|
return
|
|
|
|
while True:
|
|
now = utime.ticks_ms()
|
|
dbg = bool(getattr(self.driver, "debug", False))
|
|
delay_ms = max(1, int(preset.d))
|
|
spacing = max(1, int(preset.n1))
|
|
outward_ms = max(1, int(preset.n2))
|
|
return_ms = max(1, int(preset.n3))
|
|
pulse_lifetime = outward_ms + return_ms
|
|
max_dist = spacing // 2
|
|
on_color = colors[self._color_step % max(1, len(colors))]
|
|
lit_color = self.driver.apply_brightness(on_color, preset.b)
|
|
off_color = self.driver.apply_brightness(base_off, preset.b)
|
|
|
|
if preset.a and utime.ticks_diff(now, last_trigger) >= delay_ms:
|
|
# Keep one pulse train at a time; replacing instead of appending
|
|
# prevents overlap from keeping color[0] continuously visible.
|
|
active_pulses = [now]
|
|
last_trigger = utime.ticks_add(last_trigger, delay_ms)
|
|
self._color_step += 1
|
|
|
|
# Drop pulses once their out-and-back lifetime ends.
|
|
kept = []
|
|
for start in active_pulses:
|
|
age = utime.ticks_diff(now, start)
|
|
if age < pulse_lifetime:
|
|
kept.append(start)
|
|
active_pulses = kept
|
|
|
|
lit_count = 0
|
|
for i in range(self.driver.num_leds):
|
|
# Nearest node distance for a repeating node grid every `spacing` LEDs.
|
|
offset = (i + (spacing // 2)) % spacing
|
|
dist = min(offset, spacing - offset)
|
|
|
|
lit = False
|
|
for start in active_pulses:
|
|
age = utime.ticks_diff(now, start)
|
|
# Auto: skip the exact trigger tick (age==0) so nodes are not stuck on.
|
|
if age <= 0:
|
|
continue
|
|
if age <= outward_ms:
|
|
# Integer-ceiling progression so peak can be reached even
|
|
# when tick timing skips the exact outward_ms boundary.
|
|
front = (age * max_dist + outward_ms - 1) // outward_ms
|
|
elif age <= outward_ms + return_ms:
|
|
back_age = age - outward_ms
|
|
remaining = return_ms - back_age
|
|
front = (remaining * max_dist + return_ms - 1) // return_ms
|
|
else:
|
|
continue
|
|
|
|
if dist <= front:
|
|
lit = True
|
|
break
|
|
|
|
self.driver.n[i] = lit_color if lit else off_color
|
|
if lit:
|
|
lit_count += 1
|
|
|
|
self.driver.n.write()
|
|
|
|
if dbg:
|
|
if not dbg_banner:
|
|
dbg_banner = True
|
|
print(
|
|
"[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)
|
|
)
|
|
pulse_age = -1
|
|
if active_pulses:
|
|
pulse_age = utime.ticks_diff(now, active_pulses[0])
|
|
if utime.ticks_diff(now, last_dbg) >= _RADIATE_DBG_INTERVAL_MS:
|
|
print(
|
|
"[radiate] pulses=%d first_age=%d lit=%d lifetime=%d"
|
|
% (len(active_pulses), pulse_age, lit_count, pulse_lifetime)
|
|
)
|
|
last_dbg = now
|
|
|
|
yield
|