diff --git a/pico/src/patterns/chase.py b/pico/src/patterns/chase.py index ace1058..85147d9 100644 --- a/pico/src/patterns/chase.py +++ b/pico/src/patterns/chase.py @@ -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 diff --git a/pico/test/chase.py b/pico/test/chase.py index 0becd82..26c3cd7 100644 --- a/pico/test/chase.py +++ b/pico/test/chase.py @@ -33,27 +33,28 @@ for ws in strips[:-1]: cumulative_leds.append(cumulative_leds[-1] + ws.num_leds) total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds -# Chase: trail length (0 = single LED), color (R,G,B) -TRAIL_LEN = 8 -CHASE_COLOR = (0, 255, 100) # cyan-green +# Chase: color1 n1 long, then color2 n2 long, stepping n3 pixels +COLOR1 = (255, 0, 0) # red +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): - """Pregenerate strip double buffer: when head shows index b first, that pixel is at - distance (2*cumulative_leds - b) % total_ring_leds from chase head. GRB order.""" +def make_chase_double(num_leds, cumulative_leds, total_ring_leds, color1, color2, n1, n2): + """Pregenerate strip double buffer with repeating segments: + color1 for n1 pixels, then color2 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): - dist = (2 * cumulative_leds - b) % total_ring_leds - if dist == 0: - r, grn, b_ = color[0], color[1], color[2] - elif trail_len and 0 < dist <= trail_len: - fade = 1.0 - (dist / (trail_len + 1)) - r = int(color[0] * fade) - grn = int(color[1] * fade) - b_ = int(color[2] * fade) + # 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, grn, b_ = color1[0], color1[1], color1[2] else: - r = grn = b_ = 0 + r, grn, b_ = color2[0], color2[1], color2[2] o = b * 3 buf[o] = grn 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 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) ] @@ -74,5 +75,5 @@ while True: strip_len = strip.num_leds * 3 head = (chase_pos + cumulative_leds[i]) * 3 % strip_len strip.show(chase_buffers[i], head) - chase_pos = (chase_pos + 1) % total_ring_leds - time.sleep_ms(20) + chase_pos = (chase_pos + STEP) % total_ring_leds + time.sleep_ms(40)