From 4879fcfe909e973558f661520275f206dd4b13bf Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sat, 9 May 2026 14:28:05 +1200 Subject: [PATCH] fix(patterns): use preset background fallback across animations Align pattern background rendering to use preset.background_or(...) and update pulse/radiate single-step behaviour to preserve visible frames and step progression. --- src/main.py | 7 ------- src/patterns/bar_graph.py | 2 +- src/patterns/blink.py | 5 +++-- src/patterns/chase.py | 2 +- src/patterns/circle.py | 4 ++-- src/patterns/clock_sweep.py | 2 +- src/patterns/comet_dual.py | 2 +- src/patterns/fireflies.py | 2 +- src/patterns/heartbeat.py | 2 +- src/patterns/marquee.py | 2 +- src/patterns/orbit.py | 2 +- src/patterns/pulse.py | 11 ++++++----- src/patterns/radiate.py | 7 ++++--- src/patterns/rain_drops.py | 2 +- src/patterns/scanner.py | 2 +- src/patterns/segment_chase.py | 2 +- src/patterns/snowfall.py | 2 +- src/patterns/strobe_burst.py | 2 +- src/patterns/twinkle.py | 2 +- src/preset.py | 3 +++ 20 files changed, 32 insertions(+), 33 deletions(-) diff --git a/src/main.py b/src/main.py index 8870de9..1f7bebb 100644 --- a/src/main.py +++ b/src/main.py @@ -173,13 +173,6 @@ async def presets_loop(): while True: presets.tick() wdt.feed() - 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()}) - last_mem_log = now - # tick() does not await; yield so UDP hello and HTTP/WebSocket can run. await asyncio.sleep(0) diff --git a/src/patterns/bar_graph.py b/src/patterns/bar_graph.py index 9b28485..ccdc895 100644 --- a/src/patterns/bar_graph.py +++ b/src/patterns/bar_graph.py @@ -16,7 +16,7 @@ class BarGraph: target = (self.driver.num_leds * level) // 100 lit = self.driver.apply_brightness(colors[0], preset.b) unlit = self.driver.apply_brightness( - colors[-1], + preset.background_or(colors), preset.b, ) for i in range(self.driver.num_leds): diff --git a/src/patterns/blink.py b/src/patterns/blink.py index 5d03742..8572adf 100644 --- a/src/patterns/blink.py +++ b/src/patterns/blink.py @@ -9,6 +9,7 @@ class Blink: """Blink pattern: toggles LEDs on/off using preset delay, cycling through colors.""" # Use provided colors, or default to white if none colors = preset.c if preset.c else [(255, 255, 255)] + bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b) color_index = 0 state = True # True = on, False = off last_update = utime.ticks_ms() @@ -25,8 +26,8 @@ class Blink: # Advance to next color for the next "on" phase color_index += 1 else: - # "Off" phase should actually be off. - self.driver.fill((0, 0, 0)) + # Inactive phase uses the preset background color. + self.driver.fill(bg_color) state = not state last_update = utime.ticks_add(last_update, delay_ms) # Yield once per tick so other logic can run diff --git a/src/patterns/chase.py b/src/patterns/chase.py index 490b1ae..a532848 100644 --- a/src/patterns/chase.py +++ b/src/patterns/chase.py @@ -26,7 +26,7 @@ class Chase: color0 = self.driver.apply_brightness(color0, preset.b) color1 = self.driver.apply_brightness(color1, preset.b) - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b) n1 = max(1, int(preset.n1)) # LEDs of color 0 n2 = max(1, int(preset.n2)) # LEDs of color 1 diff --git a/src/patterns/circle.py b/src/patterns/circle.py index c7c55f7..be10827 100644 --- a/src/patterns/circle.py +++ b/src/patterns/circle.py @@ -31,10 +31,10 @@ class Circle: base0 = base1 = (255, 255, 255) elif len(colors) == 1: base0 = colors[0] - base1 = colors[-1] + base1 = preset.background_or(colors) else: base0 = colors[0] - base1 = colors[-1] + base1 = preset.background_or(colors) color0 = self.driver.apply_brightness(base0, preset.b) color1 = self.driver.apply_brightness(base1, preset.b) diff --git a/src/patterns/clock_sweep.py b/src/patterns/clock_sweep.py index 546895d..567c3fa 100644 --- a/src/patterns/clock_sweep.py +++ b/src/patterns/clock_sweep.py @@ -15,7 +15,7 @@ class ClockSweep: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg = self.driver.apply_brightness(colors[-1], preset.b) + bg = self.driver.apply_brightness(preset.background_or(colors), preset.b) fg = self.driver.apply_brightness(colors[0], preset.b) for i in range(self.driver.num_leds): self.driver.n[i] = bg diff --git a/src/patterns/comet_dual.py b/src/patterns/comet_dual.py index 4614eed..280ad41 100644 --- a/src/patterns/comet_dual.py +++ b/src/patterns/comet_dual.py @@ -17,7 +17,7 @@ class CometDual: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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) diff --git a/src/patterns/fireflies.py b/src/patterns/fireflies.py index 2896233..09f56a2 100644 --- a/src/patterns/fireflies.py +++ b/src/patterns/fireflies.py @@ -16,7 +16,7 @@ class Fireflies: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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: diff --git a/src/patterns/heartbeat.py b/src/patterns/heartbeat.py index f7c1845..b4104f5 100644 --- a/src/patterns/heartbeat.py +++ b/src/patterns/heartbeat.py @@ -17,7 +17,7 @@ class Heartbeat: 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(colors[-1], 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) diff --git a/src/patterns/marquee.py b/src/patterns/marquee.py index 1c27cb7..fbcb85a 100644 --- a/src/patterns/marquee.py +++ b/src/patterns/marquee.py @@ -17,7 +17,7 @@ class Marquee: 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(colors[-1], 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 diff --git a/src/patterns/orbit.py b/src/patterns/orbit.py index 426a6f9..23a752e 100644 --- a/src/patterns/orbit.py +++ b/src/patterns/orbit.py @@ -15,7 +15,7 @@ class Orbit: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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 k in range(orbits): diff --git a/src/patterns/pulse.py b/src/patterns/pulse.py index e40384e..474a5df 100644 --- a/src/patterns/pulse.py +++ b/src/patterns/pulse.py @@ -6,19 +6,19 @@ class Pulse: self.driver = driver def run(self, preset): - self.driver.off() - # Get colors from preset colors = preset.c if not colors: colors = [(255, 255, 255)] + bg_base = preset.background_or(colors) + self.driver.fill(self.driver.apply_brightness(bg_base, preset.b)) - color_index = 0 + color_index = self.driver.step % max(1, len(colors)) cycle_start = utime.ticks_ms() # State machine based pulse using a single generator loop while True: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + bg_color = self.driver.apply_brightness(bg_base, preset.b) # Read current timing parameters from preset attack_ms = max(0, int(preset.n1)) # Attack time in ms hold_ms = max(0, int(preset.n2)) # Hold time in ms @@ -53,7 +53,8 @@ class Pulse: self.driver.fill(bg_color) else: # End of cycle, move to next color and restart timing - color_index += 1 + color_index = (color_index + 1) % max(1, len(colors)) + self.driver.step = color_index cycle_start = now if not preset.a: break diff --git a/src/patterns/radiate.py b/src/patterns/radiate.py index 4a4f92c..33b57ec 100644 --- a/src/patterns/radiate.py +++ b/src/patterns/radiate.py @@ -17,7 +17,7 @@ class Radiate: """ colors = preset.c if preset.c else [(255, 255, 255)] base_on = colors[0] - base_off = colors[-1] + base_off = preset.background_or(colors) spacing = max(1, int(preset.n1)) outward_ms = max(1, int(preset.n2)) @@ -34,8 +34,9 @@ class Radiate: dbg_banner = False if not preset.a: - # Single-step render uses only the first instant pulse. - active_pulses = [utime.ticks_ms()] + # Single-shot mode exits after one rendered frame. Seed the pulse + # slightly in the past so this frame is visible before returning. + active_pulses = [utime.ticks_add(utime.ticks_ms(), -1)] while True: now = utime.ticks_ms() diff --git a/src/patterns/rain_drops.py b/src/patterns/rain_drops.py index f072003..48af845 100644 --- a/src/patterns/rain_drops.py +++ b/src/patterns/rain_drops.py @@ -16,7 +16,7 @@ class RainDrops: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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 if random.randint(0, 255) < rate: diff --git a/src/patterns/scanner.py b/src/patterns/scanner.py index db38789..a619384 100644 --- a/src/patterns/scanner.py +++ b/src/patterns/scanner.py @@ -27,7 +27,7 @@ class Scanner: 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(colors[-1], 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: diff --git a/src/patterns/segment_chase.py b/src/patterns/segment_chase.py index 4ef69dd..7a048b9 100644 --- a/src/patterns/segment_chase.py +++ b/src/patterns/segment_chase.py @@ -24,7 +24,7 @@ class SegmentChase: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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 diff --git a/src/patterns/snowfall.py b/src/patterns/snowfall.py index ac8c306..cbccd67 100644 --- a/src/patterns/snowfall.py +++ b/src/patterns/snowfall.py @@ -16,7 +16,7 @@ class Snowfall: d = max(1, int(preset.d)) now = utime.ticks_ms() if utime.ticks_diff(now, last) >= d: - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + 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): diff --git a/src/patterns/strobe_burst.py b/src/patterns/strobe_burst.py index 731e61f..36ae8b2 100644 --- a/src/patterns/strobe_burst.py +++ b/src/patterns/strobe_burst.py @@ -16,7 +16,7 @@ class StrobeBurst: cooldown = max(1, int(preset.n3) if int(preset.n3) > 0 else 400) on_ms = max(1, int(preset.d) // 2) c = self.driver.apply_brightness(colors[0], preset.b) - bg_color = self.driver.apply_brightness(colors[-1], preset.b) + bg_color = self.driver.apply_brightness(preset.background_or(colors), preset.b) now = utime.ticks_ms() if state == "flash_on": diff --git a/src/patterns/twinkle.py b/src/patterns/twinkle.py index 3f17e6a..a5825f7 100644 --- a/src/patterns/twinkle.py +++ b/src/patterns/twinkle.py @@ -39,7 +39,7 @@ class Twinkle: """Twinkle: n1 activity, n2 density; n3/n4 min/max length of adjacent on/off runs.""" palette = self._palette(preset) num = self.driver.num_leds - bg_color = self.driver.apply_brightness(palette[-1], preset.b) + bg_color = self.driver.apply_brightness(preset.background_or(palette), preset.b) if num <= 0: while True: yield diff --git a/src/preset.py b/src/preset.py index 6c9f8ab..eef57fb 100644 --- a/src/preset.py +++ b/src/preset.py @@ -100,6 +100,9 @@ class Preset: def auto(self, value): self.a = value + def background_or(self, colors=None, default=(0, 0, 0)): + return default + def to_dict(self): return { "p": self.p,