Switch chase to double-buffered ring chase
Made-with: Cursor
This commit is contained in:
@@ -1,123 +1,165 @@
|
|||||||
import utime
|
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:
|
class Chase:
|
||||||
def __init__(self, driver):
|
def __init__(self, driver):
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
|
self._buffers_cache = {}
|
||||||
|
|
||||||
def run(self, preset):
|
def run(self, preset):
|
||||||
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
|
"""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)"""
|
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)."""
|
||||||
colors = preset.c
|
colors = preset.c or []
|
||||||
if len(colors) < 1:
|
|
||||||
# Need at least 1 color
|
|
||||||
return
|
|
||||||
|
|
||||||
# Access colors, delay, and n values from preset
|
|
||||||
if not colors:
|
if not colors:
|
||||||
return
|
return
|
||||||
|
|
||||||
# If only one color provided, use it for both colors
|
# If only one color provided, use it for both colors
|
||||||
if len(colors) < 2:
|
if len(colors) < 2:
|
||||||
color0 = colors[0]
|
base0 = colors[0]
|
||||||
color1 = colors[0]
|
base1 = colors[0]
|
||||||
else:
|
else:
|
||||||
color0 = colors[0]
|
base0 = colors[0]
|
||||||
color1 = colors[1]
|
base1 = colors[1]
|
||||||
|
|
||||||
color0 = self.driver.apply_brightness(color0, preset.b)
|
# Apply preset/global brightness
|
||||||
color1 = self.driver.apply_brightness(color1, preset.b)
|
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
|
n1 = max(1, int(getattr(preset, "n1", 1)) or 1) # LEDs of color 0
|
||||||
n2 = max(1, int(preset.n2)) # LEDs of color 1
|
n2 = max(1, int(getattr(preset, "n2", 1)) or 1) # LEDs of color 1
|
||||||
n3 = int(preset.n3) # Step movement on even steps (can be negative)
|
n3 = int(getattr(preset, "n3", 0) or 0) # step on even steps
|
||||||
n4 = int(preset.n4) # Step movement on odd steps (can be negative)
|
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
|
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
|
||||||
step_count = int(self.driver.step)
|
self.driver, color0, color1, n1, n2, self._buffers_cache
|
||||||
# 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)
|
|
||||||
|
|
||||||
# Wrap position to keep it reasonable
|
strips = self.driver.strips
|
||||||
max_pos = self.driver.num_leds + segment_length
|
|
||||||
position = position % max_pos
|
|
||||||
if position < 0:
|
|
||||||
position += max_pos
|
|
||||||
|
|
||||||
# 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:
|
if not preset.a:
|
||||||
# Draw repeating pattern starting at position across all physical strips
|
step_count = int(self.driver.step)
|
||||||
num_leds = self.driver.num_leds
|
chase_pos = head_from_step(step_count)
|
||||||
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
|
|
||||||
|
|
||||||
# Determine which color based on position in segment
|
show_frame(chase_pos)
|
||||||
color = color0 if relative_pos < n1 else color1
|
|
||||||
|
|
||||||
# Apply this logical LED to every physical strip via driver.set()
|
# Advance step for next trigger
|
||||||
for strip_idx in range(num_strips):
|
|
||||||
self.driver.set(strip_idx, i, color)
|
|
||||||
|
|
||||||
self.driver.show_all()
|
|
||||||
|
|
||||||
# Increment step for next beat
|
|
||||||
self.driver.step = step_count + 1
|
self.driver.step = step_count + 1
|
||||||
|
|
||||||
# Allow tick() to advance the generator once
|
|
||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
# Auto mode: continuous loop
|
# Auto mode: continuous loop driven by delay d
|
||||||
# Use transition_duration for timing and force the first update to happen immediately
|
transition_duration = max(10, int(getattr(preset, "d", 50)) or 10)
|
||||||
transition_duration = max(10, int(preset.d))
|
|
||||||
last_update = utime.ticks_ms() - transition_duration
|
last_update = utime.ticks_ms() - transition_duration
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
current_time = utime.ticks_ms()
|
current_time = utime.ticks_ms()
|
||||||
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
||||||
# Calculate current position from step_count
|
# Rebuild buffers if geometry/colors changed
|
||||||
if step_count % 2 == 0:
|
chase_data, cumulative_leds, total_ring_leds = _ensure_chase_buffers(
|
||||||
position = (step_count // 2) * (n3 + n4) + n3
|
self.driver, color0, color1, n1, n2, self._buffers_cache
|
||||||
else:
|
)
|
||||||
position = ((step_count + 1) // 2) * (n3 + n4)
|
|
||||||
|
|
||||||
# Wrap position
|
step_count = int(self.driver.step)
|
||||||
max_pos = self.driver.num_leds + segment_length
|
chase_pos = head_from_step(step_count)
|
||||||
position = position % max_pos
|
|
||||||
if position < 0:
|
|
||||||
position += max_pos
|
|
||||||
|
|
||||||
# Draw repeating pattern starting at position across all physical strips
|
show_frame(chase_pos)
|
||||||
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
|
|
||||||
|
|
||||||
# Determine which color based on position in segment
|
# Advance step for next frame
|
||||||
color = color0 if relative_pos < n1 else color1
|
self.driver.step = step_count + 1
|
||||||
|
|
||||||
# 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
|
|
||||||
last_update = current_time
|
last_update = current_time
|
||||||
|
|
||||||
# Yield once per tick so other logic can run
|
# Yield once per tick so other logic can run
|
||||||
|
|||||||
@@ -33,27 +33,28 @@ for ws in strips[:-1]:
|
|||||||
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
||||||
|
|
||||||
# Chase: trail length (0 = single LED), color (R,G,B)
|
# Chase: color1 n1 long, then color2 n2 long, stepping n3 pixels
|
||||||
TRAIL_LEN = 8
|
COLOR1 = (255, 0, 0) # red
|
||||||
CHASE_COLOR = (0, 255, 100) # cyan-green
|
COLOR2 = (0, 0, 255) # blue
|
||||||
|
N1 = 24 # length of color1 segment
|
||||||
|
N2 = 24 # length of color2 segment
|
||||||
|
STEP = 1 # step size in pixels per frame
|
||||||
|
|
||||||
|
|
||||||
def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color, trail_len=0):
|
def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color1, color2, n1, n2):
|
||||||
"""Pregenerate strip double buffer: when head shows index b first, that pixel is at
|
"""Pregenerate strip double buffer with repeating segments:
|
||||||
distance (2*cumulative_leds - b) % total_ring_leds from chase head. GRB order."""
|
color1 for n1 pixels, then color2 for n2 pixels, around the full ring. GRB order."""
|
||||||
n = 2 * num_leds
|
n = 2 * num_leds
|
||||||
buf = bytearray(n * 3)
|
buf = bytearray(n * 3)
|
||||||
|
pattern_len = n1 + n2
|
||||||
for b in range(n):
|
for b in range(n):
|
||||||
dist = (2 * cumulative_leds - b) % total_ring_leds
|
# Position of this pixel along the logical ring
|
||||||
if dist == 0:
|
pos = (2 * cumulative_leds - b) % total_ring_leds
|
||||||
r, grn, b_ = color[0], color[1], color[2]
|
seg_pos = pos % pattern_len
|
||||||
elif trail_len and 0 < dist <= trail_len:
|
if seg_pos < n1:
|
||||||
fade = 1.0 - (dist / (trail_len + 1))
|
r, grn, b_ = color1[0], color1[1], color1[2]
|
||||||
r = int(color[0] * fade)
|
|
||||||
grn = int(color[1] * fade)
|
|
||||||
b_ = int(color[2] * fade)
|
|
||||||
else:
|
else:
|
||||||
r = grn = b_ = 0
|
r, grn, b_ = color2[0], color2[1], color2[2]
|
||||||
o = b * 3
|
o = b * 3
|
||||||
buf[o] = grn
|
buf[o] = grn
|
||||||
buf[o + 1] = r
|
buf[o + 1] = r
|
||||||
@@ -63,7 +64,7 @@ def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color, trail_l
|
|||||||
|
|
||||||
# Pregenerate one double buffer per strip
|
# Pregenerate one double buffer per strip
|
||||||
chase_buffers = [
|
chase_buffers = [
|
||||||
make_chase_double(ws.num_leds, cumulative_leds[i], total_ring_leds, CHASE_COLOR, TRAIL_LEN)
|
make_chase_double(ws.num_leds, cumulative_leds[i], total_ring_leds, COLOR1, COLOR2, N1, N2)
|
||||||
for i, ws in enumerate(strips)
|
for i, ws in enumerate(strips)
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -74,5 +75,5 @@ while True:
|
|||||||
strip_len = strip.num_leds * 3
|
strip_len = strip.num_leds * 3
|
||||||
head = (chase_pos + cumulative_leds[i]) * 3 % strip_len
|
head = (chase_pos + cumulative_leds[i]) * 3 % strip_len
|
||||||
strip.show(chase_buffers[i], head)
|
strip.show(chase_buffers[i], head)
|
||||||
chase_pos = (chase_pos + 1) % total_ring_leds
|
chase_pos = (chase_pos + STEP) % total_ring_leds
|
||||||
time.sleep_ms(20)
|
time.sleep_ms(40)
|
||||||
|
|||||||
Reference in New Issue
Block a user