Switch chase to double-buffered ring chase

Made-with: Cursor
This commit is contained in:
2026-03-06 01:00:25 +13:00
parent 5f457b3ae7
commit a0687cff57
2 changed files with 147 additions and 104 deletions

View File

@@ -1,123 +1,165 @@
import utime
def _make_chase_double(num_leds, cumulative_leds, total_ring_leds, color0, color1, n1, n2):
"""Pregenerate strip double buffer with repeating segments:
color0 for n1 pixels, then color1 for n2 pixels, around the full ring. GRB order."""
n = 2 * num_leds
buf = bytearray(n * 3)
pattern_len = n1 + n2
for b in range(n):
# Position of this pixel along the logical ring
pos = (2 * cumulative_leds - b) % total_ring_leds
seg_pos = pos % pattern_len
if seg_pos < n1:
r, g, b_ = color0
else:
r, g, b_ = color1
o = b * 3
buf[o] = g
buf[o + 1] = r
buf[o + 2] = b_
strip_len_bytes = num_leds * 3
return buf, strip_len_bytes
def _ensure_chase_buffers(driver, color0, color1, n1, n2, cache):
"""Build or refresh per-strip double buffers for the chase pattern."""
strips = driver.strips
key = (
color0,
color1,
int(n1),
int(n2),
tuple(s.num_leds for s in strips),
)
if cache.get("key") == key and cache.get("data") is not None:
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
if not strips:
cache["key"] = key
cache["data"] = []
cache["cumulative_leds"] = []
cache["total_ring_leds"] = 0
return cache["data"], cache["cumulative_leds"], cache["total_ring_leds"]
cumulative_leds = [0]
for s in strips[:-1]:
cumulative_leds.append(cumulative_leds[-1] + s.num_leds)
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
chase_data = []
for idx, s in enumerate(strips):
buf, strip_len_bytes = _make_chase_double(
s.num_leds,
cumulative_leds[idx],
total_ring_leds,
color0,
color1,
n1,
n2,
)
chase_data.append((buf, strip_len_bytes))
cache["key"] = key
cache["data"] = chase_data
cache["cumulative_leds"] = cumulative_leds
cache["total_ring_leds"] = total_ring_leds
return chase_data, cumulative_leds, total_ring_leds
class Chase:
def __init__(self, driver):
self.driver = driver
self._buffers_cache = {}
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)"""
colors = preset.c
if len(colors) < 1:
# Need at least 1 color
return
# Access colors, delay, and n values from preset
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating around the full ring.
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)."""
colors = preset.c or []
if not colors:
return
# If only one color provided, use it for both colors
if len(colors) < 2:
color0 = colors[0]
color1 = colors[0]
base0 = colors[0]
base1 = colors[0]
else:
color0 = colors[0]
color1 = colors[1]
base0 = colors[0]
base1 = colors[1]
color0 = self.driver.apply_brightness(color0, preset.b)
color1 = self.driver.apply_brightness(color1, preset.b)
# Apply preset/global brightness
color0 = self.driver.apply_brightness(base0, preset.b)
color1 = self.driver.apply_brightness(base1, preset.b)
n1 = max(1, int(preset.n1)) # LEDs of color 0
n2 = max(1, int(preset.n2)) # LEDs of color 1
n3 = int(preset.n3) # Step movement on even steps (can be negative)
n4 = int(preset.n4) # Step movement on odd steps (can be negative)
n1 = max(1, int(getattr(preset, "n1", 1)) or 1) # LEDs of color 0
n2 = max(1, int(getattr(preset, "n2", 1)) or 1) # LEDs of color 1
n3 = int(getattr(preset, "n3", 0) or 0) # step on even steps
n4 = int(getattr(preset, "n4", 0) or 0) # step on odd steps
segment_length = n1 + n2
if n3 == 0 and n4 == 0:
# Nothing to move; default to simple forward motion
n3 = 1
n4 = 1
# Calculate position from step_count
step_count = int(self.driver.step)
# Position alternates: step 0 adds n3, step 1 adds n4, step 2 adds n3, etc.
if step_count % 2 == 0:
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
position = (step_count // 2) * (n3 + n4) + n3
else:
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
position = ((step_count + 1) // 2) * (n3 + n4)
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
self.driver, color0, color1, n1, n2, self._buffers_cache
)
# Wrap position to keep it reasonable
max_pos = self.driver.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
strips = self.driver.strips
# If auto is False, run a single step and then stop
def show_frame(chase_pos):
for i, (strip, (buf, strip_len_bytes)) in enumerate(zip(strips, chase_data)):
# head in [0, strip_len_bytes) so DMA read head..head+strip_len_bytes stays in double buffer
head = ((chase_pos + cumulative_leds[i]) * 3) % strip_len_bytes
strip.show(buf, head)
# Helper to compute head position from current step_count
def head_from_step(step_count):
if step_count % 2 == 0:
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
pos = (step_count // 2) * (n3 + n4) + n3
else:
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
pos = ((step_count + 1) // 2) * (n3 + n4)
if total_ring_leds <= 0:
return 0
pos %= total_ring_leds
if pos < 0:
pos += total_ring_leds
return pos
# Single-step mode: render one frame, then stop
if not preset.a:
# Draw repeating pattern starting at position across all physical strips
num_leds = self.driver.num_leds
num_strips = len(self.driver.strips)
for i in range(num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
step_count = int(self.driver.step)
chase_pos = head_from_step(step_count)
# Determine which color based on position in segment
color = color0 if relative_pos < n1 else color1
show_frame(chase_pos)
# Apply this logical LED to every physical strip via driver.set()
for strip_idx in range(num_strips):
self.driver.set(strip_idx, i, color)
self.driver.show_all()
# Increment step for next beat
# Advance step for next trigger
self.driver.step = step_count + 1
# Allow tick() to advance the generator once
yield
return
# Auto mode: continuous loop
# Use transition_duration for timing and force the first update to happen immediately
transition_duration = max(10, int(preset.d))
# Auto mode: continuous loop driven by delay d
transition_duration = max(10, int(getattr(preset, "d", 50)) or 10)
last_update = utime.ticks_ms() - transition_duration
while True:
current_time = utime.ticks_ms()
if utime.ticks_diff(current_time, last_update) >= transition_duration:
# Calculate current position from step_count
if step_count % 2 == 0:
position = (step_count // 2) * (n3 + n4) + n3
else:
position = ((step_count + 1) // 2) * (n3 + n4)
# Rebuild buffers if geometry/colors changed
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
self.driver, color0, color1, n1, n2, self._buffers_cache
)
# Wrap position
max_pos = self.driver.num_leds + segment_length
position = position % max_pos
if position < 0:
position += max_pos
step_count = int(self.driver.step)
chase_pos = head_from_step(step_count)
# Draw repeating pattern starting at position across all physical strips
num_leds = self.driver.num_leds
num_strips = len(self.driver.strips)
for i in range(num_leds):
# Calculate position in the repeating segment
relative_pos = (i - position) % segment_length
if relative_pos < 0:
relative_pos = (relative_pos + segment_length) % segment_length
show_frame(chase_pos)
# Determine which color based on position in segment
color = color0 if relative_pos < n1 else color1
# Apply this logical LED to every physical strip via driver.set()
for strip_idx in range(num_strips):
self.driver.set(strip_idx, i, color)
self.driver.show_all()
# Increment step
step_count += 1
self.driver.step = step_count
# Advance step for next frame
self.driver.step = step_count + 1
last_update = current_time
# Yield once per tick so other logic can run