Files
led-driver/src/patterns/radiate.py
Jimmy 55a97ac51c 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>
2026-05-16 21:14:54 +12:00

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