Compare commits

...

3 Commits

Author SHA1 Message Date
5f457b3ae7 Refine calibration, chase, and spin patterns and add spin test
Increase calibration brightness, update chase to use the new logical ring abstraction, and make spin start from a cleared frame with symmetric arm speed, alongside a dedicated on-device spin test script.

Made-with: Cursor
2026-03-05 23:41:25 +13:00
3e58f4e97e Add segments and double_circle patterns with shared presets
Introduce double_circle and segments-based patterns on the Pico, refactor the Presets engine to expose a logical ring over all strips, and migrate presets/test code from the old point pattern to segments while switching to a top-level presets.json.

Made-with: Cursor
2026-03-05 23:41:13 +13:00
47c19eecf1 Clean up ESP32 buttons and UART tests
Remove unused buttons.json and legacy UART UART test scripts, and update the web UI preset dropdown for new Pico patterns.

Made-with: Cursor
2026-03-05 23:40:28 +13:00
18 changed files with 909 additions and 231 deletions

View File

@@ -1,58 +0,0 @@
{
"buttons": [
{"id": "start", "preset": "off"},
{"id": "grab", "preset": "grab"},
{"id": "spin1", "preset": "spin1"},
{"id": "lift", "preset": "lift"},
{"id": "flare", "preset": "flare"},
{"id": "hook", "preset": "hook"},
{"id": "roll1", "preset": "roll1"},
{"id": "invertsplit", "preset": "invertsplit"},
{"id": "pose1", "preset": "pose1"},
{"id": "pose1", "preset": "pose2"},
{"id": "roll2", "preset": "roll2"},
{"id": "backbalance1", "preset": "backbalance1"},
{"id": "beat1", "preset": "beat1"},
{"id": "pose3", "preset": "pose3"},
{"id": "roll3", "preset": "roll3"},
{"id": "crouch", "preset": "crouch"},
{"id": "pose4", "preset": "pose4"},
{"id": "roll4", "preset": "roll4"},
{"id": "backbendsplit", "preset": "backbendsplit"},
{"id": "backbalance2", "preset": "backbalance2"},
{"id": "backbalance3", "preset": "backbalance3"},
{"id": "beat2", "preset": "beat2"},
{"id": "straddle", "preset": "straddle"},
{"id": "beat3", "preset": "beat3"},
{"id": "frontbalance1", "preset": "frontbalance1"},
{"id": "pose5", "preset": "pose5"},
{"id": "pose6", "preset": "pose6"},
{"id": "elbowhang", "preset": "elbowhang"},
{"id": "elbowhangspin", "preset": "elbowhangspin"},
{"id": "spin2", "preset": "spin2"},
{"id": "dismount", "preset": "dismount"},
{"id": "spin3", "preset": "spin3"},
{"id": "fluff", "preset": "fluff"},
{"id": "spin4", "preset": "spin4"},
{"id": "flare2", "preset": "flare2"},
{"id": "elbowhang", "preset": "elbowhang"},
{"id": "elbowhangsplit2", "preset": "elbowhangsplit2"},
{"id": "invert", "preset": "invert"},
{"id": "roll5", "preset": "roll5"},
{"id": "backbend", "preset": "backbend"},
{"id": "pose7", "preset": "pose7"},
{"id": "roll6", "preset": "roll6"},
{"id": "seat", "preset": "seat"},
{"id": "kneehang", "preset": "kneehang"},
{"id": "legswoop", "preset": "legswoop"},
{"id": "split", "preset": "split"},
{"id": "foothang", "preset": "foothang"},
{"id": "end", "preset": "end"}
]
}

View File

@@ -65,7 +65,9 @@
<option value="legswoop">legswoop</option>
<option value="split">split</option>
<option value="foothang">foothang</option>
<option value="point">point</option>
<option value="segments">segments</option>
<option value="segments_transition">segments_transition</option>
<option value="double_circle">double_circle</option>
<option value="off">off</option>
<option value="on">on</option>
<option value="blink">blink</option>

View File

@@ -1,64 +0,0 @@
"""
ESP32-C6 test: send JSON messages to Pico over UART (GPIO17).
Settings use strips = [[pin, num_leds], ...]. Run with Pico connected on RX.
Run with mpremote (from repo root):
./esp32/run_test_uart_json.sh
# or
mpremote run esp32/test/test_uart_send_json.py
# or with port
mpremote connect /dev/ttyUSB0 run esp32/test/test_uart_send_json.py
"""
import machine
import time
import json
UART_TX_PIN = 17
UART_BAUD = 921600
LED_PIN = 15
def send_json(uart, obj):
line = json.dumps(obj) + "\n"
uart.write(line)
print("TX:", line.strip())
def main():
uart = machine.UART(1, baudrate=UART_BAUD, tx=UART_TX_PIN)
led = machine.Pin(LED_PIN, machine.Pin.OUT)
# 1) Settings: one strip, pin 2, 10 LEDs (list of lists)
send_json(uart, {
"v": 1,
"settings": {
"strips": [[2, 10]],
"brightness": 30,
},
})
led.value(1)
time.sleep(0.2)
led.value(0)
time.sleep(0.3)
# 2) led-controller format: light + settings.color (hex)
send_json(uart, {"light": "strip1", "settings": {"color": "#FF0000"}, "save": False})
time.sleep(0.5)
# 3) led-controller format: light + settings.r,g,b
send_json(uart, {"light": "strip1", "settings": {"r": 0, "g": 255, "b": 0}, "save": False})
time.sleep(0.5)
# 4) led-controller format: blue (hex)
send_json(uart, {"light": "strip1", "settings": {"color": "#0000FF"}, "save": False})
time.sleep(0.5)
# 5) Off (existing format)
send_json(uart, {"v": 1, "off": True})
time.sleep(0.3)
print("Done. Pico: settings -> red (hex) -> green (r,g,b) -> blue (hex) -> off.")
if __name__ == "__main__":
main()

View File

@@ -1,32 +0,0 @@
"""
ESP32-C6 UART TX + LED test. Sends a few commands on GPIO17, blinks LED on GPIO15.
Run on device: exec(open('test/test_uart_tx').read()) or import test.test_uart_tx
Does not require Pico connected.
"""
import machine
import time
UART_TX_PIN = 17
LED_PIN = 15
def main():
uart = machine.UART(1, baudrate=115200, tx=UART_TX_PIN)
led = machine.Pin(LED_PIN, machine.Pin.OUT)
def send(cmd):
uart.write(cmd + "\n")
print("TX:", cmd)
# Blink and send a short command sequence
commands = ["off", "fill 255 0 0", "fill 0 255 0", "fill 0 0 255", "off"]
for i, cmd in enumerate(commands):
led.value(1)
send(cmd)
time.sleep(0.3)
led.value(0)
time.sleep(0.2)
print("Done. Connect Pico to see strip follow commands.")
if __name__ == "__main__":
main()

View File

@@ -4,6 +4,7 @@ from .pulse import Pulse
from .transition import Transition
from .chase import Chase
from .circle import Circle
from .double_circle import DoubleCircle
from .roll import Roll
from .calibration import Calibration
from .test import Test
@@ -14,4 +15,5 @@ from .lift import Lift
from .flare import Flare
from .hook import Hook
from .pose import Pose
from .point import Point
from .segments import Segments
from .segments_transition import SegmentsTransition

View File

@@ -1,6 +1,6 @@
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
BRIGHTNESS = 0.10
BRIGHTNESS = 1
BLOCK = 10
STRIPS_ON = (2, 6) # 0-based: 3rd and 7th strip only
@@ -22,6 +22,7 @@ class Calibration:
green = _scale(GREEN, BRIGHTNESS)
red = _scale(RED, BRIGHTNESS)
blue = _scale(BLUE, BRIGHTNESS)
blue = (0,0,0)
on_set = set(STRIPS_ON)
for strip_idx, strip in enumerate(strips):
n = strip.num_leds

View File

@@ -35,7 +35,7 @@ class Chase:
segment_length = n1 + n2
# Calculate position from step_count
step_count = self.driver.step
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
@@ -52,23 +52,23 @@ class Chase:
# If auto is False, run a single step and then stop
if not preset.a:
# Clear all LEDs
self.driver.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.driver.num_leds):
# 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
# Determine which color based on position in segment
if relative_pos < n1:
self.driver.n[i] = color0
else:
self.driver.n[i] = color1
color = color0 if relative_pos < n1 else color1
self.driver.n.write()
# 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
self.driver.step = step_count + 1
@@ -97,23 +97,23 @@ class Chase:
if position < 0:
position += max_pos
# Clear all LEDs
self.driver.n.fill((0, 0, 0))
# Draw repeating pattern starting at position
for i in range(self.driver.num_leds):
# 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
# Determine which color based on position in segment
if relative_pos < n1:
self.driver.n[i] = color0
else:
self.driver.n[i] = color1
color = color0 if relative_pos < n1 else color1
self.driver.n.write()
# 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

View File

@@ -0,0 +1,84 @@
class DoubleCircle:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
DoubleCircle: symmetric band around a center index on the logical ring.
- n1: center index on the logical ring (0-based, on reference strip 0)
- n2: radius of the band (max distance from center)
- n3: direction mode
0 → LEDs start ALL OFF and turn ON n4 LEDs at a time outward from n1 toward n1±n2
1 → LEDs start ALL ON within radius n2 and turn OFF n4 LEDs at a time inward toward n1
- n4: step size in LEDs per update
- c[0]: base color used for the band
"""
num_leds = self.driver.num_leds
if num_leds <= 0:
while True:
yield
colors = preset.c or []
base1 = colors[0] if len(colors) >= 1 else (255, 255, 255)
off = (0, 0, 0)
# Apply preset/global brightness
color_on = self.driver.apply_brightness(base1, preset.b)
color_off = off
# Center index and radius from preset; clamp center to ring length
center = int(getattr(preset, "n1", 0)) % num_leds
radius = max(1, int(getattr(preset, "n2", 0)) or 1)
mode = int(getattr(preset, "n3", 0) or 0) # 0 = grow band outward, 1 = shrink band inward
step_size = max(1, int(getattr(preset, "n4", 1)) or 1)
num_strips = len(self.driver.strips)
# Current "front" of the band, as a distance from center
# mode 0: grow band outward (0 → radius)
# mode 1: shrink band inward (radius → 0)
if mode == 0:
current = 0
else:
current = radius
while True:
# Draw current frame based on current radius
for i in range(num_leds):
# Shortest circular distance from i to center
forward = (i - center) % num_leds
backward = (center - i) % num_leds
dist = forward if forward < backward else backward
if dist > radius:
c = color_off
else:
if mode == 0:
# Grow outward: lit if within current radius
c = color_on if dist <= current else color_off
else:
# Shrink inward: lit if within current radius (band contracts toward center)
c = color_on if dist <= current else color_off
for strip_idx in range(num_strips):
self.driver.set(strip_idx, i, c)
self.driver.show_all()
# Update current radius for next frame
if mode == 0:
if current >= radius:
# Finished growing; hold final frame
while True:
yield
current = min(radius, current + step_size)
else:
if current <= 0:
# Finished shrinking; hold final frame
while True:
yield
current = max(0, current - step_size)
yield

View File

@@ -1,18 +0,0 @@
class Point:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
# Apply preset/global brightness once per color
c1 = self.driver.apply_brightness(preset.c[0], preset.b)
c2 = self.driver.apply_brightness(preset.c[1], preset.b)
c3 = self.driver.apply_brightness(preset.c[2], preset.b)
c4 = self.driver.apply_brightness(preset.c[3], preset.b)
# Helper to normalize and clamp a range
self.driver.fill_n(c1, preset.n1, preset.n2)
self.driver.fill_n(c2, preset.n3, preset.n4)
self.driver.fill_n(c3, preset.n5, preset.n6)
self.driver.fill_n(c4, preset.n7, preset.n8)
self.driver.show_all()

View File

@@ -0,0 +1,18 @@
class Segments:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
# Apply preset/global brightness once per color
ranges = [
(preset.n1, preset.n2),
(preset.n3, preset.n4),
(preset.n5, preset.n6),
(preset.n7, preset.n8),
]
for n, color in enumerate(preset.c):
self.driver.fill_n(color, ranges[n][0], ranges[n][1])
self.driver.show_all()

View File

@@ -0,0 +1,136 @@
import utime
class SegmentsTransition:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
"""
SegmentsTransition: fade from whatever is currently on the strips
to a new static Segments layout defined by n1n8 and c[0..3].
- Uses the existing strip buffers as the starting state.
- Target state matches the Segments pattern: up to 4 colored bands
along the logical reference strip, mapped to all physical strips.
- Transition duration is taken from preset.d (ms), minimum 50ms.
"""
strips = self.driver.strips
if not strips:
while True:
yield
# Snapshot starting GRB buffers (already scaled by per-strip brightness)
start_bufs = [bytes(strip.ar) for strip in strips]
# Prepare target buffers (same length as each strip's ar)
target_bufs = [bytearray(len(strip.ar)) for strip in strips]
# Base colors (up to 4), missing ones default to black
colors = list(preset.c) if getattr(preset, "c", None) else []
while len(colors) < 4:
colors.append((0, 0, 0))
# Apply preset/global brightness once per color
bright_colors = [
self.driver.apply_brightness(colors[0], preset.b),
self.driver.apply_brightness(colors[1], preset.b),
self.driver.apply_brightness(colors[2], preset.b),
self.driver.apply_brightness(colors[3], preset.b),
]
# Logical reference length for all strips (from scale_map[0])
ref_len = len(self.driver.scale_map[0]) if self.driver.scale_map else 0
if ref_len <= 0:
# Fallback: nothing to do, just hold current state
while True:
yield
# Helper to clamp and normalize a logical range [a, b] (inclusive) over ref_len.
# Returns (start, end_exclusive) suitable for range(start, end_exclusive).
def norm_range(a, b):
a = int(a)
b = int(b)
if a > b:
a, b = b, a
if b < 0 or a >= ref_len:
return None
a = max(0, a)
b = min(ref_len - 1, b)
if a > b:
return None
return a, b + 1
raw_ranges = [
(getattr(preset, "n1", 0), getattr(preset, "n2", -1), bright_colors[0]),
(getattr(preset, "n3", 0), getattr(preset, "n4", -1), bright_colors[1]),
(getattr(preset, "n5", 0), getattr(preset, "n6", -1), bright_colors[2]),
(getattr(preset, "n7", 0), getattr(preset, "n8", -1), bright_colors[3]),
]
# Build target buffers using the same logical indexing idea as Segments
for strip_idx, strip in enumerate(strips):
bright = strip.brightness
scale_map = self.driver.scale_map[strip_idx]
buf = target_bufs[strip_idx]
n_leds = strip.num_leds
# Start from black everywhere
for i in range(len(buf)):
buf[i] = 0
# Apply each logical range to this strip
for a, b, color in raw_ranges:
rng = norm_range(a, b)
if not rng:
continue
start, end = rng
r, g, bl = color
for logical_idx in range(start, end):
if logical_idx < 0 or logical_idx >= len(scale_map):
continue
phys_idx = scale_map[logical_idx]
if phys_idx < 0 or phys_idx >= n_leds:
continue
base = phys_idx * 3
if base + 2 >= len(buf):
continue
buf[base] = int(g * bright)
buf[base + 1] = int(r * bright)
buf[base + 2] = int(bl * bright)
# Duration in ms for the whole transition (slower by default)
# If preset.d is provided, use it; otherwise default to a slow 3000ms fade.
raw_d = int(getattr(preset, "d", 3000) or 3000)
duration = max(1000, raw_d) # enforce at least 1s for a clearly visible transition
start_time = utime.ticks_ms()
while True:
now = utime.ticks_ms()
elapsed = utime.ticks_diff(now, start_time)
if elapsed >= duration:
# Final frame: commit target buffers and hold, then update all strips together
for strip, target in zip(strips, target_bufs):
ar = strip.ar
for i in range(len(ar)):
ar[i] = target[i]
self.driver.show_all()
while True:
yield
# Interpolation factor in [0,1]
factor = elapsed / duration
inv = 1.0 - factor
# Blend from start to target in GRB space per byte
for idx, strip in enumerate(strips):
start_buf = start_bufs[idx]
target_buf = target_bufs[idx]
ar = strip.ar
for i in range(len(ar)):
ar[i] = int(start_buf[i] * inv + target_buf[i] * factor)
self.driver.show_all()
yield

View File

@@ -2,7 +2,7 @@
import utime
SPAN = 10 # LEDs on each side of center (match Grab)
SPAN = 0 # LEDs on each side of center (match Grab)
LUT_SIZE = 256 # gradient lookup table entries
@@ -12,6 +12,9 @@ class Spin:
def run(self, preset):
strips = self.driver.strips
self.driver.fill((0, 0, 0))
self.driver.show_all()
active_indices = (0, 4)
c0 = preset.c[0]
c1 = preset.c[1]
@@ -58,8 +61,8 @@ class Spin:
n = strip.num_leds
mid = midpoints[idx]
# Expand arms: inside (strip 1, idx 0) moves slower, outside (strip 5, idx 4) faster
step = max(1, rate // 2) if idx == 0 else rate
# Expand arms at the same rate on both sides
step = max(1, rate)
new_left = max(margin, left[idx] - step)
new_right = min(n - margin, right[idx] + step)

View File

@@ -2,11 +2,57 @@ from machine import Pin
from ws2812 import WS2812B
from preset import Preset
from patterns import (
Blink, Rainbow, Pulse, Transition, Chase, Circle, Roll, Calibration, Test,
Grab, Spin, Lift, Flare, Hook, Pose, Point,
Blink,
Rainbow,
Pulse,
Transition,
Chase,
Circle,
DoubleCircle,
Roll,
Calibration,
Test,
Grab,
Spin,
Lift,
Flare,
Hook,
Pose,
Segments,
SegmentsTransition,
)
import json
class _LogicalRing:
"""
Lightweight logical ring over all strips.
Used by patterns that expect driver.n (e.g. Circle, Roll legacy API).
"""
def __init__(self, driver):
self._driver = driver
self.num_strips = len(driver.strips)
def __len__(self):
return self._driver.num_leds
def fill(self, color):
# Apply color to all logical positions across all strips
for i in range(self._driver.num_leds):
for strip_idx in range(self.num_strips):
self._driver.set(strip_idx, i, color)
def __setitem__(self, index, color):
if index < 0 or index >= self._driver.num_leds:
return
for strip_idx in range(self.num_strips):
self._driver.set(strip_idx, index, color)
def write(self):
self._driver.show_all()
# Order: strips[0]=physical 1 … strips[7]=physical 8. (pin, num_leds, midpoint_index).
STRIP_CONFIG = (
(6, 291, 291 // 2), # 1
@@ -34,8 +80,12 @@ class Presets:
state_machine += 1
self.scale_map.append(self.create_scale_map(num_leds))
# Single logical strip over all 8 strips for patterns (n[i], .fill(), .write())
# Single logical strip using strip 0 as reference for patterns (n[i], .fill(), .write())
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
# Reference logical length for patterns that use driver.num_leds (Rainbow/Chase/Circle, etc.)
self.num_leds = self.strips[0].num_leds if self.strips else 0
# Legacy logical ring interface for patterns expecting driver.n
self.n = _LogicalRing(self)
self.step = 0
# Remember which strip was last used as the roll head (for flare, etc.)
self.last_roll_head = 0
@@ -56,6 +106,7 @@ class Presets:
"transition": Transition(self).run,
"chase": Chase(self).run,
"circle": Circle(self).run,
"double_circle": DoubleCircle(self).run,
"roll": Roll(self).run,
"calibration": Calibration(self).run,
"test": Test(self).run,
@@ -65,7 +116,9 @@ class Presets:
"flare": Flare(self).run,
"hook": Hook(self).run,
"pose": Pose(self).run,
"point": Point(self).run,
"segments": Segments(self).run,
"segments_transition": SegmentsTransition(self).run,
"point": Segments(self).run, # backwards-compatible alias
}
# --- Strip geometry utilities -------------------------------------------------
@@ -159,11 +212,13 @@ class Presets:
def off(self, preset=None):
self.fill((0, 0, 0))
self.show_all()
def on(self, preset):
colors = preset.c
color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.b))
self.show_all()
def fill(self, color):
for strip in self.strips:

View File

@@ -0,0 +1,157 @@
"""
On-device test for the double_circle pattern using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_double_circle.py :
mpremote connect <device> run test_double_circle.py
This script:
- Instantiates Presets
- Creates a few in-memory 'double_circle' presets with different centers, widths, and colors
- Selects each one so you can visually confirm the symmetric bands and color gradients
"""
from presets import Presets, Preset
def make_double_circle_preset(
name, center, half_width, colors, direction=0, step_size=1, brightness=255
):
"""
Helper to build a Preset for the 'double_circle' pattern.
center: logical index (0-based, on reference strip 0)
half_width: number of LEDs each side of center
colors: [color1, color2] where each color is (r,g,b)
"""
cs = list(colors)[:2]
while len(cs) < 2:
cs.append((0, 0, 0))
data = {
"p": "double_circle",
"c": cs,
"b": brightness,
"n1": center,
"n2": half_width,
"n3": direction,
"n4": step_size,
}
return name, Preset(data)
def show_and_wait(presets, name, preset_obj, wait_ms):
"""Select a static double_circle preset and hold it for wait_ms."""
presets.presets[name] = preset_obj
presets.select(name)
# DoubleCircle draws immediately in run(), then just yields; one tick is enough.
presets.tick()
import utime
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < wait_ms:
presets.tick()
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting double_circle test.")
return
print("Starting double_circle pattern test...")
quarter = num_leds // 4
half = num_leds // 2
dc_presets = []
# 1. Center at top (0), moderate width, color1 at center (n3=0)
dc_presets.append(
make_double_circle_preset(
"dc_top_red_to_blue",
center=0,
half_width=quarter,
colors=[(255, 0, 0), (0, 0, 255)],
direction=0,
)
)
# 2. Center at bottom (half), narrow band, color1 at endpoints (n3=1)
dc_presets.append(
make_double_circle_preset(
"dc_bottom_green_to_purple",
center=half,
half_width=quarter // 2,
colors=[(0, 255, 0), (128, 0, 128)],
direction=1,
)
)
# 3. Center at quarter, wide band, both directions for comparison
dc_presets.append(
make_double_circle_preset(
"dc_quarter_white_to_cyan_inward",
center=quarter,
half_width=half,
colors=[(255, 255, 255), (0, 255, 255)],
direction=0,
)
)
dc_presets.append(
make_double_circle_preset(
"dc_quarter_white_to_cyan_outward",
center=quarter,
half_width=half,
colors=[(255, 255, 255), (0, 255, 255)],
direction=1,
)
)
# 4. Explicit test: n1 = 50, n2 = 40 (half of 80) inward
dc_presets.append(
make_double_circle_preset(
"dc_n1_50_n2_40_inward",
center=50,
half_width=40,
colors=[(255, 100, 0), (0, 0, 0)],
direction=0,
)
)
# 5. Explicit test: n1 = num_leds//2, n2 = num_leds//4 outward, stepping as fast as possible
center_half = num_leds // 2
radius_quarter = max(1, num_leds // 4)
dc_presets.append(
make_double_circle_preset(
"dc_n1_half_n2_quarter_outward",
center=center_half,
half_width=radius_quarter,
colors=[(0, 150, 255), (0, 0, 0)],
direction=1,
step_size=radius_quarter, # jump to full radius in one step
)
)
# Show each for ~4 seconds
for name, preset_obj in dc_presets:
print("Showing double_circle preset:", name)
show_and_wait(presets, name, preset_obj, wait_ms=4000)
print("Double_circle pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,264 @@
"""
On-device test that exercises Segments and multiple SegmentsTransition presets via Presets.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_multi_patterns.py :
mpremote connect <device> run test_multi_patterns.py
"""
import utime
from presets import Presets, Preset
def run_for(presets, duration_ms):
"""Tick the current pattern for duration_ms."""
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
utime.sleep_ms(10)
def make_segments_preset(name, colors, n_values, brightness=255):
"""
Helper to build a Preset for the 'segments' pattern.
colors: list of up to 4 (r,g,b) tuples
n_values: list/tuple of 8 ints [n1..n8]
"""
cs = list(colors)[:4]
while len(cs) < 4:
cs.append((0, 0, 0))
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "segments",
"c": cs,
"b": brightness,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"n5": n5,
"n6": n6,
"n7": n7,
"n8": n8,
}
return name, Preset(data)
def make_segments_transition_preset(name, colors, n_values, duration_ms=1000, brightness=255):
"""
Helper to build a Preset for the 'segments_transition' pattern.
Starts from whatever is currently displayed and fades to the
new segments layout over duration_ms.
"""
cs = list(colors)[:4]
while len(cs) < 4:
cs.append((0, 0, 0))
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "segments_transition",
"c": cs,
"b": brightness,
"d": duration_ms,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"n5": n5,
"n6": n6,
"n7": n7,
"n8": n8,
}
return name, Preset(data)
def main():
presets = Presets()
presets.load()
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting multi-pattern test.")
return
print("Starting multi-pattern test with Presets...")
quarter = num_leds // 4
half = num_leds // 2
# 1. Static segments: simple R/G/B bands
name_static, preset_static = make_segments_preset(
"mp_segments_static",
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
n_values=[
0,
quarter - 1, # red
quarter,
2 * quarter - 1, # green
2 * quarter,
3 * quarter - 1, # blue
0,
-1,
],
)
# 2a. Segments transition: fade from previous buffer to new colors (slow)
name_trans1, preset_trans1 = make_segments_transition_preset(
"mp_segments_transition_1",
colors=[(255, 255, 255), (255, 0, 255), (0, 255, 255)],
n_values=[
0,
half - 1, # white on first half
half,
num_leds - 1, # magenta on second half
0,
-1, # cyan unused in this example
0,
-1,
],
duration_ms=3000,
)
# 2b. Segments transition: fade between two different segment layouts
name_trans2, preset_trans2 = make_segments_transition_preset(
"mp_segments_transition_2",
colors=[(255, 0, 0), (0, 0, 255)],
n_values=[
0,
quarter - 1, # red first quarter
quarter,
2 * quarter - 1, # blue second quarter
0,
-1,
0,
-1,
],
duration_ms=4000,
)
# 2c. Segments transition: thin moving band (center quarter only)
band_start = quarter // 2
band_end = band_start + quarter
name_trans3, preset_trans3 = make_segments_transition_preset(
"mp_segments_transition_3",
colors=[(0, 255, 0)],
n_values=[
band_start,
band_end - 1, # green band in the middle
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=5000,
)
# 2d. Segments transition: full-ring warm white fade
name_trans4, preset_trans4 = make_segments_transition_preset(
"mp_segments_transition_4",
colors=[(255, 200, 100)],
n_values=[
0,
num_leds - 1, # entire strip
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=6000,
)
# 2e. Segments transition: alternating warm/cool halves
name_trans5, preset_trans5 = make_segments_transition_preset(
"mp_segments_transition_5",
colors=[(255, 180, 100), (100, 180, 255)],
n_values=[
0,
half - 1, # warm first half
half,
num_leds - 1, # cool second half
0,
-1,
0,
-1,
],
duration_ms=5000,
)
# 2f. Segments transition: narrow red band near start
narrow_start = num_leds // 16
narrow_end = narrow_start + max(4, num_leds // 32)
name_trans6, preset_trans6 = make_segments_transition_preset(
"mp_segments_transition_6",
colors=[(255, 0, 0)],
n_values=[
narrow_start,
narrow_end - 1,
0,
-1,
0,
-1,
0,
-1,
],
duration_ms=4000,
)
# Register presets in Presets and run them in sequence
presets.presets[name_static] = preset_static
presets.presets[name_trans1] = preset_trans1
presets.presets[name_trans2] = preset_trans2
presets.presets[name_trans3] = preset_trans3
presets.presets[name_trans4] = preset_trans4
presets.presets[name_trans5] = preset_trans5
presets.presets[name_trans6] = preset_trans6
print("Showing static segments...")
presets.select(name_static)
presets.tick() # draw once
run_for(presets, 3000)
print("Running segments transition 1 (fading to new half/half layout)...")
presets.select(name_trans1)
run_for(presets, 3500)
print("Running segments transition 2 (fading to quarter-band layout)...")
presets.select(name_trans2)
run_for(presets, 4500)
print("Running segments transition 3 (fading to center green band)...")
presets.select(name_trans3)
run_for(presets, 5500)
print("Running segments transition 4 (fading to full warm white ring)...")
presets.select(name_trans4)
run_for(presets, 6500)
print("Running segments transition 5 (fading to warm/cool halves)...")
presets.select(name_trans5)
run_for(presets, 5500)
print("Running segments transition 6 (fading to narrow red band)...")
presets.select(name_trans6)
run_for(presets, 4500)
print("Multi-pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

View File

@@ -1,26 +1,26 @@
"""
On-device test for the Point pattern using mpremote.
On-device test for the Segments pattern using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_point.py :
mpremote connect <device> run test_point.py
mpremote connect <device> cp test/test_segments.py :
mpremote connect <device> run test_segments.py
This script:
- Instantiates Presets
- Creates a few in-memory 'point' presets with different ranges/colors
- Creates a few in-memory 'point' (Segments) presets with different ranges/colors
- Selects each one so you can visually confirm the segments
"""
from presets import Presets, Preset
def make_point_preset(name, colors, n_values, brightness=255):
def make_segments_preset(name, colors, n_values, brightness=255):
"""
Helper to build a Preset for the 'point' pattern.
Helper to build a Preset for the 'segments' pattern (key 'point').
colors: list of up to 4 (r,g,b) tuples
n_values: list/tuple of 8 ints [n1..n8]
@@ -32,7 +32,7 @@ def make_point_preset(name, colors, n_values, brightness=255):
n1, n2, n3, n4, n5, n6, n7, n8 = n_values
data = {
"p": "point",
"p": "segments", # pattern key for Segments
"c": cs,
"b": brightness,
"n1": n1,
@@ -43,16 +43,16 @@ def make_point_preset(name, colors, n_values, brightness=255):
"n6": n6,
"n7": n7,
"n8": n8,
# 'a' is not used by point; it's static
# 'a' is not used by segments; it's static
}
return name, Preset(data)
def show_and_wait(presets, name, preset_obj, wait_ms):
"""Select a static 'point' preset and hold it for wait_ms."""
"""Select a static segments preset and hold it for wait_ms."""
presets.presets[name] = preset_obj
presets.select(name)
# Point draws immediately in run(), then just yields; one tick is enough.
# Segments draws immediately in run(), then just yields; one tick is enough.
presets.tick()
import utime
@@ -69,38 +69,38 @@ def main():
num_leds = presets.strip_length(0)
if num_leds <= 0:
print("No strips; aborting point test.")
print("No strips; aborting segments test.")
return
print("Starting point pattern test...")
print("Starting segments pattern test...")
quarter = num_leds // 4
half = num_leds // 2
point_presets = []
segments_presets = []
# 1. Single band: first quarter, red
point_presets.append(
make_point_preset(
"point_red_q1",
segments_presets.append(
make_segments_preset(
"segments_red_q1",
colors=[(255, 0, 0)],
n_values=[0, quarter - 1, 0, -1, 0, -1, 0, -1],
)
)
# 2. Two bands: red first half, green second half
point_presets.append(
make_point_preset(
"point_red_green_halves",
segments_presets.append(
make_segments_preset(
"segments_red_green_halves",
colors=[(255, 0, 0), (0, 255, 0)],
n_values=[0, half - 1, half, num_leds - 1, 0, -1, 0, -1],
)
)
# 3. Three bands: R, G, B quarters
point_presets.append(
make_point_preset(
"point_rgb_quarters",
segments_presets.append(
make_segments_preset(
"segments_rgb_quarters",
colors=[(255, 0, 0), (0, 255, 0), (0, 0, 255)],
n_values=[
0,
@@ -116,11 +116,11 @@ def main():
)
# Show each for ~4 seconds
for name, preset_obj in point_presets:
print("Showing point preset:", name)
for name, preset_obj in segments_presets:
print("Showing segments preset:", name)
show_and_wait(presets, name, preset_obj, wait_ms=4000)
print("Point pattern test finished. Turning off LEDs.")
print("Segments pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()

128
pico/test/test_spin.py Normal file
View File

@@ -0,0 +1,128 @@
"""
On-device test for the Spin pattern using mpremote and Presets.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp presets.json :
mpremote connect <device> cp test/test_spin.py :
mpremote connect <device> run test_spin.py
This script:
- Instantiates Presets
- Creates a few in-memory spin presets with different parameters
- Runs each one for a short time so you can visually compare behaviour
"""
import utime
from presets import Presets, Preset
def make_spin_preset(
name,
color_inner,
color_outer,
rate=4,
delay_ms=30,
margin=0,
brightness=255,
):
"""Helper to build a Preset dict for the spin pattern."""
data = {
"p": "spin",
"c": [color_inner, color_outer],
"b": brightness,
"d": delay_ms,
"n1": rate, # expansion step per tick
"n2": margin, # margin from strip ends
"a": True,
}
return name, Preset(data)
def run_preset(presets, name, preset_obj, duration_ms):
"""Run a given spin preset for duration_ms using the existing tick loop."""
# Start each preset from a blank frame so both sides are balanced.
presets.select("off")
presets.tick()
presets.presets[name] = preset_obj
presets.select(name)
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
utime.sleep_ms(10)
def main():
presets = Presets()
presets.load()
# Ensure we start from a blank frame.
presets.select("off")
presets.tick()
print("Starting spin pattern test...")
# Use strip 0 length to derive a reasonable margin.
ref_len = presets.strip_length(0)
margin_small = ref_len // 16 if ref_len > 0 else 0
margin_large = ref_len // 8 if ref_len > 0 else 0
spin_presets = []
# 1. Slow spin, warm white to orange, small margin
spin_presets.append(
make_spin_preset(
"spin_slow_warm",
color_inner=(255, 200, 120),
color_outer=(255, 100, 0),
rate=2,
delay_ms=40,
margin=margin_small,
brightness=255,
)
)
# 2. Medium spin, cyan to magenta, larger margin
spin_presets.append(
make_spin_preset(
"spin_medium_cyan_magenta",
color_inner=(0, 255, 180),
color_outer=(255, 0, 180),
rate=4,
delay_ms=30,
margin=margin_large,
brightness=255,
)
)
# 3. Fast spin, white to off (fade outwards), no margin
spin_presets.append(
make_spin_preset(
"spin_fast_white",
color_inner=(255, 255, 255),
color_outer=(0, 0, 0),
rate=6,
delay_ms=20,
margin=0,
brightness=255,
)
)
# Run each spin preset for about 6 seconds
for name, preset_obj in spin_presets:
print("Running spin preset:", name)
run_preset(presets, name, preset_obj, duration_ms=6000)
print("Spin pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()