Compare commits
7 Commits
52a5f0f8c4
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 292c5bde01 | |||
| a0687cff57 | |||
| 5f457b3ae7 | |||
| 3e58f4e97e | |||
| 47c19eecf1 | |||
| 47c17dba36 | |||
| e75723e2e7 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
||||
settings.json
|
||||
settings.json
|
||||
__pycache__/
|
||||
@@ -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"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -28,6 +28,8 @@ class WS2812B:
|
||||
self.brightness = brightness
|
||||
self.invert = invert
|
||||
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3)
|
||||
self.dma.start_transfer(self.ar)
|
||||
|
||||
|
||||
def show(self, array=None, offset=0):
|
||||
if array is None:
|
||||
|
||||
@@ -4,32 +4,16 @@ 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
|
||||
from .scale_test import ScaleTest
|
||||
from .grab import Grab
|
||||
from .spin import Spin
|
||||
from .lift import Lift
|
||||
from .flare import Flare
|
||||
from .hook import Hook
|
||||
from .invertsplit import Invertsplit
|
||||
from .pose import Pose
|
||||
from .backbalance import Backbalance
|
||||
from .beat import Beat
|
||||
from .crouch import Crouch
|
||||
from .backbendsplit import Backbendsplit
|
||||
from .straddle import Straddle
|
||||
from .frontbalance import Frontbalance
|
||||
from .elbowhang import Elbowhang
|
||||
from .elbowhangspin import Elbowhangspin
|
||||
from .dismount import Dismount
|
||||
from .fluff import Fluff
|
||||
from .elbowhangsplit import Elbowhangsplit
|
||||
from .invert import Invert
|
||||
from .backbend import Backbend
|
||||
from .seat import Seat
|
||||
from .kneehang import Kneehang
|
||||
from .legswoop import Legswoop
|
||||
from .split import Split
|
||||
from .foothang import Foothang
|
||||
from .point import Point
|
||||
from .segments import Segments
|
||||
from .segments_transition import SegmentsTransition
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbend:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Backbendsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Beat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 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:
|
||||
# Clear all LEDs
|
||||
self.driver.n.fill((0, 0, 0))
|
||||
step_count = int(self.driver.step)
|
||||
chase_pos = head_from_step(step_count)
|
||||
|
||||
# Draw repeating pattern starting at position
|
||||
for i in range(self.driver.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
|
||||
if relative_pos < n1:
|
||||
self.driver.n[i] = color0
|
||||
else:
|
||||
self.driver.n[i] = color1
|
||||
|
||||
self.driver.n.write()
|
||||
|
||||
# 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)
|
||||
|
||||
# Clear all LEDs
|
||||
self.driver.n.fill((0, 0, 0))
|
||||
show_frame(chase_pos)
|
||||
|
||||
# Draw repeating pattern starting at position
|
||||
for i in range(self.driver.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
|
||||
|
||||
self.driver.n.write()
|
||||
|
||||
# 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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Crouch:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Dismount:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
84
pico/src/patterns/double_circle.py
Normal file
84
pico/src/patterns/double_circle.py
Normal 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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangspin:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Elbowhangsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Fluff:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Foothang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Frontbalance:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invert:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Invertsplit:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Kneehang:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Legswoop:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,68 +0,0 @@
|
||||
class Point:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""
|
||||
Point pattern: color bands defined by n ranges.
|
||||
|
||||
- n1–n2: LEDs with color1 (c[0])
|
||||
- n3–n4: LEDs with color2 (c[1])
|
||||
- n5–n6: LEDs with color3 (c[2])
|
||||
- n7–n8: LEDs with color4 (c[3])
|
||||
|
||||
All indices are along the logical ring (driver.n), inclusive ranges.
|
||||
"""
|
||||
num_leds = self.driver.num_leds
|
||||
|
||||
# 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
|
||||
c1 = self.driver.apply_brightness(colors[0], preset.b)
|
||||
c2 = self.driver.apply_brightness(colors[1], preset.b)
|
||||
c3 = self.driver.apply_brightness(colors[2], preset.b)
|
||||
c4 = self.driver.apply_brightness(colors[3], preset.b)
|
||||
|
||||
# Helper to normalize and clamp a range
|
||||
def norm_range(a, b):
|
||||
a = int(a)
|
||||
b = int(b)
|
||||
if a > b:
|
||||
a, b = b, a
|
||||
if b < 0 or a >= num_leds:
|
||||
return None
|
||||
a = max(0, a)
|
||||
b = min(num_leds - 1, b)
|
||||
if a > b:
|
||||
return None
|
||||
return a, b
|
||||
|
||||
ranges = []
|
||||
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1))
|
||||
if r1:
|
||||
ranges.append((r1[0], r1[1], c1))
|
||||
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1))
|
||||
if r2:
|
||||
ranges.append((r2[0], r2[1], c2))
|
||||
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1))
|
||||
if r3:
|
||||
ranges.append((r3[0], r3[1], c3))
|
||||
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1))
|
||||
if r4:
|
||||
ranges.append((r4[0], r4[1], c4))
|
||||
|
||||
# Static draw: last range wins on overlaps
|
||||
for i in range(num_leds):
|
||||
color = (0, 0, 0)
|
||||
for start, end, c in ranges:
|
||||
if start <= i <= end:
|
||||
color = c
|
||||
self.driver.n[i] = color
|
||||
self.driver.n.write()
|
||||
|
||||
while True:
|
||||
yield
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import utime
|
||||
import math
|
||||
|
||||
|
||||
class Roll:
|
||||
@@ -6,149 +7,105 @@ class Roll:
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Roll: moving band with gradient from color1 to color2 over the strips.
|
||||
"""Roll: all strips show a shared color gradient palette, cycling out of phase.
|
||||
|
||||
- n1: offset from start of strip (effective start = start + n1)
|
||||
- n2: offset from end of strip (effective end = end - n2, inclusive)
|
||||
- n3: number of full rotations before stopping (0 = infinite)
|
||||
- All strips participate; each frame shows N discrete colors
|
||||
(one per strip), from color1 to color2.
|
||||
- Over time, each strip cycles through all colors, out of phase with the
|
||||
others, creating a smooth rolling band around the hoop.
|
||||
- n4: direction (0 = clockwise, 1 = anti-clockwise)
|
||||
- c[0]: color1 at the head strip
|
||||
- c[1]: color2 at the tail strip
|
||||
- c[0]: head color (full intensity)
|
||||
- c[1]: tail color (usually darker or off)
|
||||
"""
|
||||
colors = preset.c
|
||||
color1_raw = colors[0] if colors else (255, 255, 255)
|
||||
color2_raw = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||
color1 = self.driver.apply_brightness(color1_raw, preset.b)
|
||||
color2 = self.driver.apply_brightness(color2_raw, preset.b)
|
||||
base1 = colors[0] if colors else (255, 255, 255)
|
||||
base2 = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||
color1 = self.driver.apply_brightness(base1, preset.b)
|
||||
color2 = self.driver.apply_brightness(base2, preset.b)
|
||||
|
||||
n_segments = self.driver.n.num_strips if hasattr(self.driver.n, "num_strips") else 1
|
||||
# Margins from the start and end of each strip
|
||||
start_margin = max(0, int(getattr(preset, "n1", 0)))
|
||||
end_margin = max(0, int(getattr(preset, "n2", 0)))
|
||||
|
||||
# Debug info to see why roll might be black
|
||||
try:
|
||||
print(
|
||||
"ROLL preset",
|
||||
"p=", getattr(preset, "p", None),
|
||||
"b=", getattr(preset, "b", None),
|
||||
"colors_raw=", color1_raw, color2_raw,
|
||||
"colors_bright=", color1, color2,
|
||||
)
|
||||
print(
|
||||
"ROLL n1..n4",
|
||||
getattr(preset, "n1", None),
|
||||
getattr(preset, "n2", None),
|
||||
getattr(preset, "n3", None),
|
||||
getattr(preset, "n4", None),
|
||||
"n_segments=", n_segments,
|
||||
"start_margin=", start_margin,
|
||||
"end_margin=", end_margin,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# n3: number of rotations (0 = infinite)
|
||||
max_rotations = int(getattr(preset, "n3", 0)) or 0
|
||||
# n3: number of full rotations before stopping (0 = continuous)
|
||||
max_rotations = int(getattr(preset, "n3", 0) or 0)
|
||||
# n4: direction (0=cw, 1=ccw); default clockwise if missing
|
||||
clockwise = int(getattr(preset, "n4", 0)) == 0
|
||||
|
||||
step = self.driver.step
|
||||
delay_ms = max(1, int(preset.d) or 1)
|
||||
last_update = utime.ticks_ms()
|
||||
rotations_done = 0
|
||||
|
||||
def scale_color(c, f):
|
||||
return tuple(int(x * f) for x in c)
|
||||
# Precompute one shared buffer per brightness level (one per strip),
|
||||
# using the longest strip length so any strip can DMA from it safely.
|
||||
strips_list = self.driver.strips
|
||||
if not strips_list or n_segments <= 0:
|
||||
while True:
|
||||
yield
|
||||
|
||||
def lerp_color(c1, c2, t):
|
||||
"""Linear gradient between two colors."""
|
||||
if t <= 0:
|
||||
return c1
|
||||
if t >= 1:
|
||||
return c2
|
||||
return (
|
||||
int(c1[0] + (c2[0] - c1[0]) * t),
|
||||
int(c1[1] + (c2[1] - c1[1]) * t),
|
||||
int(c1[2] + (c2[2] - c1[2]) * t),
|
||||
)
|
||||
max_leds = max(s.num_leds for s in strips_list)
|
||||
|
||||
def draw(head):
|
||||
# Remember head strip for flare
|
||||
# Build N discrete color buffers forming a gradient from color1 to color2.
|
||||
buffers = []
|
||||
for j in range(n_segments):
|
||||
if n_segments > 1:
|
||||
t = j / (n_segments - 1)
|
||||
else:
|
||||
t = 0.0
|
||||
# Linear interpolation between color1 and color2
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * t)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * t)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * t)
|
||||
|
||||
buf = bytearray(max_leds * 3)
|
||||
for i in range(max_leds):
|
||||
o = i * 3
|
||||
buf[o] = g & 0xFF
|
||||
buf[o + 1] = r & 0xFF
|
||||
buf[o + 2] = b & 0xFF
|
||||
buffers.append(buf)
|
||||
|
||||
def draw(step_index):
|
||||
# Each strip picks a buffer index offset by its strip index so that:
|
||||
# - all brightness levels are visible simultaneously (one per strip)
|
||||
# - over time, each strip cycles through all brightness levels
|
||||
try:
|
||||
self.driver.last_roll_head = head
|
||||
self.driver.last_roll_head = step_index % n_segments
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
strips_list = self.driver.strips
|
||||
|
||||
for strip_idx, strip in enumerate(strips_list):
|
||||
if strip_idx < 0 or strip_idx >= n_segments:
|
||||
continue
|
||||
|
||||
# Distance from head along direction, 0..n_segments-1
|
||||
if clockwise:
|
||||
dist = (head - strip_idx) % n_segments
|
||||
buf_index = (step_index + strip_idx) % n_segments
|
||||
else:
|
||||
dist = (strip_idx - head) % n_segments
|
||||
buf_index = (step_index - strip_idx) % n_segments
|
||||
buf = buffers[buf_index]
|
||||
|
||||
# Color gradient from color1 at the head strip to color2 at the tail strip
|
||||
if n_segments > 1:
|
||||
t = dist / (n_segments - 1)
|
||||
else:
|
||||
t = 0.0
|
||||
c_strip = lerp_color(color1, color2, t)
|
||||
|
||||
n = strip.num_leds
|
||||
# Effective segment per strip:
|
||||
# start = 0 + start_margin
|
||||
# end = (n - 1) - end_margin (inclusive)
|
||||
width = n - start_margin - end_margin
|
||||
if width <= 0:
|
||||
# If margins are too large, fall back to full strip
|
||||
seg_s = 0
|
||||
seg_e = n
|
||||
else:
|
||||
seg_s = max(0, min(n, start_margin))
|
||||
seg_e = min(n, n - end_margin)
|
||||
|
||||
# Debug for first strip/head to see segment
|
||||
try:
|
||||
if strip_idx == 0 and head == 0:
|
||||
print("ROLL seg strip0 n=", n, "seg_s=", seg_s, "seg_e=", seg_e)
|
||||
except Exception:
|
||||
pass
|
||||
for i in range(n):
|
||||
if seg_s <= i < seg_e:
|
||||
strip.set(i, c_strip)
|
||||
else:
|
||||
strip.set(i, (0, 0, 0))
|
||||
strip.show()
|
||||
# Show the shared buffer; WS2812B will read num_leds*3 bytes.
|
||||
strip.show(buf, 0)
|
||||
|
||||
if not preset.a:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
draw(head)
|
||||
draw(step)
|
||||
self.driver.step = step + 1
|
||||
yield
|
||||
return
|
||||
|
||||
# Auto mode: advance based on preset.d (ms) for smooth, controllable speed
|
||||
delay_ms = max(10, int(getattr(preset, "d", 60)) or 10)
|
||||
last_update = utime.ticks_ms() - delay_ms
|
||||
rotations_done = 0
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
||||
head = step % n_segments if n_segments > 0 else 0
|
||||
if not clockwise and n_segments > 0:
|
||||
head = (n_segments - 1 - head)
|
||||
|
||||
draw(head)
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
draw(step)
|
||||
step += 1
|
||||
|
||||
self.driver.step = step
|
||||
last_update = now
|
||||
# Count full rotations if requested: one rotation per n_segments steps
|
||||
if max_rotations > 0 and n_segments > 0 and (step % n_segments) == 0:
|
||||
rotations_done += 1
|
||||
if rotations_done >= max_rotations:
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
return
|
||||
|
||||
self.driver.step = step
|
||||
last_update = current_time
|
||||
# Hold the final frame and stop advancing; keep yielding so
|
||||
# the generator stays alive without changing the LEDs.
|
||||
while True:
|
||||
yield
|
||||
yield
|
||||
|
||||
56
pico/src/patterns/scale_test.py
Normal file
56
pico/src/patterns/scale_test.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import utime
|
||||
|
||||
|
||||
RED = (255, 0, 0)
|
||||
|
||||
|
||||
class ScaleTest:
|
||||
"""
|
||||
Animated test for the scale() helper.
|
||||
|
||||
A single red pixel moves along the reference strip (strip 0). For each other
|
||||
strip, the position is mapped using:
|
||||
|
||||
n2 = scale(l1, l2, n1)
|
||||
|
||||
so that all lit pixels stay aligned by proportional position along the strips.
|
||||
"""
|
||||
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
strips = self.driver.strips
|
||||
if not strips:
|
||||
return
|
||||
|
||||
src_strip_idx = 0
|
||||
l1 = self.driver.strip_length(src_strip_idx)
|
||||
if l1 <= 0:
|
||||
return
|
||||
|
||||
step = self.driver.step
|
||||
delay_ms = max(1, int(getattr(preset, "d", 30) or 30))
|
||||
last_update = utime.ticks_ms()
|
||||
color = self.driver.apply_brightness(RED, getattr(preset, "b", 255))
|
||||
|
||||
while True:
|
||||
now = utime.ticks_ms()
|
||||
if utime.ticks_diff(now, last_update) >= delay_ms:
|
||||
n1 = step % l1
|
||||
|
||||
# Clear all strips
|
||||
for strip in strips:
|
||||
strip.fill((0, 0, 0))
|
||||
|
||||
# Light mapped position on each strip using Presets.set/show
|
||||
for dst_strip_idx, _ in enumerate(strips):
|
||||
self.driver.set(dst_strip_idx, n1, color)
|
||||
self.driver.show(dst_strip_idx)
|
||||
|
||||
step += 1
|
||||
self.driver.step = step
|
||||
last_update = now
|
||||
|
||||
yield
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Seat:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
18
pico/src/patterns/segments.py
Normal file
18
pico/src/patterns/segments.py
Normal 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()
|
||||
|
||||
136
pico/src/patterns/segments_transition.py
Normal file
136
pico/src/patterns/segments_transition.py
Normal 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 n1–n8 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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Split:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -1,9 +0,0 @@
|
||||
"""Placeholder until implemented."""
|
||||
|
||||
class Straddle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
while True:
|
||||
yield
|
||||
@@ -2,15 +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, Invertsplit, Pose,
|
||||
Backbalance, Beat, Crouch, Backbendsplit, Straddle,
|
||||
Frontbalance, Elbowhang, Elbowhangspin, Dismount, Fluff,
|
||||
Elbowhangsplit, Invert, Backbend, Seat, Kneehang,
|
||||
Legswoop, Split, Foothang, 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
|
||||
@@ -23,53 +65,9 @@ STRIP_CONFIG = (
|
||||
(7, 291, 290 // 2-1), # 8
|
||||
)
|
||||
|
||||
class StripRing:
|
||||
"""Treat multiple WS2812B strips as one logical ring. Equal weight per strip (scale by strip length)."""
|
||||
|
||||
def __init__(self, strips):
|
||||
self.strips = strips
|
||||
self._cumul = [0]
|
||||
for s in strips:
|
||||
self._cumul.append(self._cumul[-1] + s.num_leds)
|
||||
self.num_leds = self._cumul[-1]
|
||||
self.num_strips = len(strips)
|
||||
|
||||
def _strip_and_local(self, i):
|
||||
if i < 0 or i >= self.num_leds:
|
||||
raise IndexError(i)
|
||||
for s in range(len(self.strips)):
|
||||
if i < self._cumul[s + 1]:
|
||||
return s, i - self._cumul[s]
|
||||
return len(self.strips) - 1, i - self._cumul[-2]
|
||||
|
||||
def __setitem__(self, i, color):
|
||||
s, local = self._strip_and_local(i)
|
||||
self.strips[s].set(local, color)
|
||||
|
||||
def fill(self, color):
|
||||
for s in self.strips:
|
||||
s.fill(color)
|
||||
|
||||
def write(self):
|
||||
for s in self.strips:
|
||||
s.show()
|
||||
|
||||
def position(self, i):
|
||||
"""Normalized position 0..1 with equal span per strip (longer strips get same angular span)."""
|
||||
s, local = self._strip_and_local(i)
|
||||
strip_len = self.strips[s].num_leds
|
||||
frac = (local / strip_len) if strip_len else 0
|
||||
return (s + frac) / self.num_strips
|
||||
|
||||
def segment(self, i):
|
||||
"""Segment index 0..num_strips-1 (strip index) so segments align with physical strips."""
|
||||
s, _ = self._strip_and_local(i)
|
||||
return s
|
||||
|
||||
|
||||
class Presets:
|
||||
def __init__(self):
|
||||
|
||||
self.scale_map = []
|
||||
self.strips = []
|
||||
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
|
||||
|
||||
@@ -80,10 +78,14 @@ class Presets:
|
||||
self.strip_midpoints.append(mid)
|
||||
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
|
||||
state_machine += 1
|
||||
# Single logical strip over all 8 strips for patterns (n[i], .fill(), .write())
|
||||
self.n = StripRing(self.strips)
|
||||
self.num_leds = self.n.num_leds
|
||||
self.scale_map.append(self.create_scale_map(num_leds))
|
||||
|
||||
# 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
|
||||
@@ -104,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,
|
||||
@@ -112,27 +115,10 @@ class Presets:
|
||||
"lift": Lift(self).run,
|
||||
"flare": Flare(self).run,
|
||||
"hook": Hook(self).run,
|
||||
"invertsplit": Invertsplit(self).run,
|
||||
"pose": Pose(self).run,
|
||||
"backbalance": Backbalance(self).run,
|
||||
"beat": Beat(self).run,
|
||||
"crouch": Crouch(self).run,
|
||||
"backbendsplit": Backbendsplit(self).run,
|
||||
"straddle": Straddle(self).run,
|
||||
"frontbalance": Frontbalance(self).run,
|
||||
"elbowhang": Elbowhang(self).run,
|
||||
"elbowhangspin": Elbowhangspin(self).run,
|
||||
"dismount": Dismount(self).run,
|
||||
"fluff": Fluff(self).run,
|
||||
"elbowhangsplit": Elbowhangsplit(self).run,
|
||||
"invert": Invert(self).run,
|
||||
"backbend": Backbend(self).run,
|
||||
"seat": Seat(self).run,
|
||||
"kneehang": Kneehang(self).run,
|
||||
"legswoop": Legswoop(self).run,
|
||||
"split": Split(self).run,
|
||||
"foothang": Foothang(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 -------------------------------------------------
|
||||
@@ -143,40 +129,6 @@ class Presets:
|
||||
return self.strips[strip_idx].num_leds
|
||||
return 0
|
||||
|
||||
def strip_index_to_angle(self, strip_idx, index):
|
||||
"""Map an LED index on a given strip to a normalized angle 0..1.
|
||||
|
||||
This accounts for different strip lengths so that the same angle value
|
||||
corresponds to the same physical angle on all concentric strips.
|
||||
"""
|
||||
n = self.strip_length(strip_idx)
|
||||
if n <= 0:
|
||||
return 0.0
|
||||
index = int(index) % n
|
||||
return index / float(n)
|
||||
|
||||
def strip_angle_to_index(self, strip_idx, angle):
|
||||
"""Map a normalized angle 0..1 to an LED index on a given strip.
|
||||
|
||||
Use this when you want patterns to align by angle instead of raw index,
|
||||
despite strips having different circumferences.
|
||||
"""
|
||||
n = self.strip_length(strip_idx)
|
||||
if n <= 0:
|
||||
return 0
|
||||
# Normalize angle into [0,1)
|
||||
angle = float(angle)
|
||||
angle = angle - int(angle)
|
||||
if angle < 0.0:
|
||||
angle += 1.0
|
||||
return int(angle * n) % n
|
||||
|
||||
def remap_strip_index(self, src_strip_idx, dst_strip_idx, src_index):
|
||||
"""Remap an index from one strip to another so they align by angle."""
|
||||
angle = self.strip_index_to_angle(src_strip_idx, src_index)
|
||||
return self.strip_angle_to_index(dst_strip_idx, angle)
|
||||
|
||||
|
||||
def save(self):
|
||||
"""Save the presets to a file."""
|
||||
with open("presets.json", "w") as f:
|
||||
@@ -251,14 +203,6 @@ class Presets:
|
||||
self.selected = preset_name
|
||||
return True
|
||||
|
||||
def update_num_leds(self, pin, num_leds):
|
||||
num_leds = int(num_leds)
|
||||
if isinstance(pin, Pin):
|
||||
self.n = WS2812B(pin, num_leds)
|
||||
else:
|
||||
self.n = WS2812B(num_leds, int(pin), 0, brightness=1.0)
|
||||
self.num_leds = num_leds
|
||||
|
||||
def apply_brightness(self, color, brightness_override=None):
|
||||
# Combine per-preset brightness (override) with global brightness self.b
|
||||
local = brightness_override if brightness_override is not None else 255
|
||||
@@ -266,16 +210,36 @@ class Presets:
|
||||
effective_brightness = int(local * self.b / 255)
|
||||
return tuple(int(c * effective_brightness / 255) for c in color)
|
||||
|
||||
def fill(self, color=None):
|
||||
fill_color = color if color is not None else (0, 0, 0)
|
||||
for i in range(self.num_leds):
|
||||
self.n[i] = fill_color
|
||||
self.n.write()
|
||||
|
||||
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:
|
||||
strip.fill(color)
|
||||
|
||||
def fill_n(self, color, n1, n2):
|
||||
for i in range(n1, n2):
|
||||
for strip_idx in range(8):
|
||||
self.set(strip_idx, i, color)
|
||||
|
||||
|
||||
def set(self, strip, index, color):
|
||||
if index >= self.strips[0].num_leds:
|
||||
return False
|
||||
self.strips[strip].set(self.scale_map[strip][index], color)
|
||||
return True
|
||||
|
||||
def create_scale_map(self, num_leds):
|
||||
ref_len = STRIP_CONFIG[0][1]
|
||||
return [int(i * num_leds / ref_len) for i in range(ref_len)]
|
||||
|
||||
def show_all(self):
|
||||
for strip in self.strips:
|
||||
strip.show()
|
||||
@@ -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)
|
||||
|
||||
114
pico/test/roll.py
Normal file
114
pico/test/roll.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
On-device visual test for the Roll pattern via Presets.
|
||||
|
||||
This exercises src/patterns/roll.py (gradient from color1 to color2 across strips),
|
||||
not the low-level WS2812 driver.
|
||||
|
||||
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/roll.py :
|
||||
mpremote connect <device> run roll.py
|
||||
"""
|
||||
|
||||
import utime
|
||||
|
||||
from presets import Presets, Preset
|
||||
|
||||
|
||||
def make_roll_preset(name, color1, color2, delay_ms=60, brightness=255, direction=0):
|
||||
"""
|
||||
Helper to build a Preset for the 'roll' pattern.
|
||||
|
||||
- color1: head color (full intensity)
|
||||
- color2: tail color (end of gradient)
|
||||
- direction: 0 = clockwise, 1 = anti-clockwise
|
||||
"""
|
||||
data = {
|
||||
"p": "roll",
|
||||
"c": [color1, color2],
|
||||
"b": brightness,
|
||||
"d": delay_ms,
|
||||
"n4": direction,
|
||||
"a": True, # animated
|
||||
}
|
||||
return name, Preset(data)
|
||||
|
||||
|
||||
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 main():
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
|
||||
num_leds = presets.strip_length(0)
|
||||
if num_leds <= 0:
|
||||
print("No strips; aborting roll test.")
|
||||
return
|
||||
|
||||
print("Starting roll pattern gradient tests via Presets...")
|
||||
|
||||
# A few different roll presets to compare:
|
||||
roll_presets = []
|
||||
|
||||
# 1. White → off, clockwise (50% brightness, faster)
|
||||
roll_presets.append(
|
||||
make_roll_preset(
|
||||
"roll_white_off_cw",
|
||||
color1=(255, 255, 255),
|
||||
color2=(0, 0, 0),
|
||||
delay_ms=120,
|
||||
brightness=128,
|
||||
direction=0,
|
||||
)
|
||||
)
|
||||
|
||||
# 2. Warm white → cool blue, clockwise (50% brightness, faster)
|
||||
roll_presets.append(
|
||||
make_roll_preset(
|
||||
"roll_warm_cool_cw",
|
||||
color1=(255, 200, 100),
|
||||
color2=(0, 0, 255),
|
||||
delay_ms=130,
|
||||
brightness=128,
|
||||
direction=0,
|
||||
)
|
||||
)
|
||||
|
||||
# 3. Red → green, counter-clockwise (50% brightness, faster)
|
||||
roll_presets.append(
|
||||
make_roll_preset(
|
||||
"roll_red_green_ccw",
|
||||
color1=(255, 0, 0),
|
||||
color2=(0, 255, 0),
|
||||
delay_ms=110,
|
||||
brightness=128,
|
||||
direction=1,
|
||||
)
|
||||
)
|
||||
|
||||
# Register presets and run them one after another
|
||||
for name, preset_obj in roll_presets:
|
||||
presets.presets[name] = preset_obj
|
||||
|
||||
for name, _preset in roll_presets:
|
||||
print("Running roll preset:", name)
|
||||
presets.select(name)
|
||||
run_for(presets, duration_ms=8000)
|
||||
|
||||
print("Roll pattern Presets test finished. Turning off LEDs.")
|
||||
presets.select("off")
|
||||
presets.tick()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import math
|
||||
import sys
|
||||
if "lib" not in sys.path:
|
||||
sys.path.insert(0, "lib")
|
||||
if "../lib" not in sys.path:
|
||||
sys.path.insert(0, "../lib")
|
||||
from ws2812 import WS2812B
|
||||
import time
|
||||
|
||||
# --- Roll: N buffers (length = max strip), gradient full -> off; sequence through strips ---
|
||||
N_BUFFERS = 32 # more buffers = smoother transition
|
||||
|
||||
STRIP_CONFIG = (
|
||||
(2, 291),
|
||||
(3, 290),
|
||||
(4, 283),
|
||||
(7, 278),
|
||||
(0, 275),
|
||||
(28, 278),
|
||||
(29, 283),
|
||||
(6, 290),
|
||||
)
|
||||
|
||||
strips = []
|
||||
sm = 0
|
||||
for pin, num_leds in STRIP_CONFIG:
|
||||
print(pin, num_leds)
|
||||
ws = WS2812B(num_leds, pin, sm, brightness=1.0)
|
||||
strips.append(ws)
|
||||
sm += 1
|
||||
|
||||
num_strips = len(strips)
|
||||
max_leds = max(ws.num_leds for ws in strips)
|
||||
# Color when "on" (R, G, B); GRB order in buffer
|
||||
ROLL_COLOR = (0, 255, 120) # cyan-green
|
||||
|
||||
|
||||
def make_gradient_buffers(n_buffers, max_leds, color):
|
||||
"""Create n_buffers buffers, each max_leds long. Buffer 0 = full brightness, last = off.
|
||||
Gradient is logarithmic (perceptually smoother: more steps near full, fewer near off). GRB order."""
|
||||
out = []
|
||||
for j in range(n_buffers):
|
||||
# log gradient: scale = 255 * log(1 + (n - 1 - j)) / log(n) so 255 at j=0, 0 at j=n-1
|
||||
if n_buffers <= 1:
|
||||
scale = 255
|
||||
elif j >= n_buffers - 1:
|
||||
scale = 0
|
||||
else:
|
||||
# 1 + (n_buffers - 1 - j) runs from n_buffers down to 1
|
||||
scale = int(255 * math.log(1 + (n_buffers - 1 - j)) / math.log(n_buffers))
|
||||
scale = min(255, scale)
|
||||
buf = bytearray(max_leds * 3)
|
||||
r = (color[0] * scale) // 255
|
||||
g = (color[1] * scale) // 255
|
||||
b = (color[2] * scale) // 255
|
||||
for i in range(max_leds):
|
||||
o = i * 3
|
||||
buf[o] = g & 0xFF
|
||||
buf[o + 1] = r & 0xFF
|
||||
buf[o + 2] = b & 0xFF
|
||||
out.append(buf)
|
||||
return out
|
||||
|
||||
|
||||
# N buffers: first full, last off, gradient between
|
||||
buffers = make_gradient_buffers(N_BUFFERS, max_leds, ROLL_COLOR)
|
||||
|
||||
step = 0
|
||||
delay_ms = 50
|
||||
# Deadline-based loop: no extra pause at rotation wrap, smooth continuous roll
|
||||
next_ms = time.ticks_ms()
|
||||
|
||||
while True:
|
||||
for i, strip in enumerate(strips):
|
||||
buf_index = (step + i) % N_BUFFERS
|
||||
strip.show(buffers[buf_index], 0)
|
||||
step += 1 # unbounded; wrap only in index so no hitch at cycle end
|
||||
next_ms += delay_ms
|
||||
# Sleep until next frame time (handles drift, no pause at wrap)
|
||||
while time.ticks_diff(next_ms, time.ticks_ms()) > 0:
|
||||
time.sleep_ms(1)
|
||||
6
pico/test/test_all_on.py
Normal file
6
pico/test/test_all_on.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from neopixel import NeoPixel
|
||||
from machine import Pin
|
||||
|
||||
p = NeoPixel(Pin(6), 291)
|
||||
p.fill((255, 255, 255))
|
||||
p.write()
|
||||
71
pico/test/test_all_on_presets.py
Normal file
71
pico/test/test_all_on_presets.py
Normal file
@@ -0,0 +1,71 @@
|
||||
"""
|
||||
On-device test that turns all LEDs on via Presets and verifies strip 0 (pin 6).
|
||||
|
||||
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_all_on_presets.py :
|
||||
mpremote connect <device> run test_all_on_presets.py
|
||||
"""
|
||||
|
||||
from presets import Presets, Preset
|
||||
|
||||
|
||||
def verify_strip0_on(presets, expected_color):
|
||||
"""Check that every LED on strip 0 matches expected_color."""
|
||||
if not presets.strips:
|
||||
print("No strips; skipping strip-0 on test.")
|
||||
return
|
||||
|
||||
strip = presets.strips[0]
|
||||
r_exp, g_exp, b_exp = expected_color
|
||||
|
||||
for i in range(strip.num_leds):
|
||||
o = i * 3
|
||||
g = strip.ar[o]
|
||||
r = strip.ar[o + 1]
|
||||
b = strip.ar[o + 2]
|
||||
if (r, g, b) != (r_exp, g_exp, b_exp):
|
||||
raise AssertionError(
|
||||
"Strip 0 LED %d: got (%d,%d,%d), expected (%d,%d,%d)"
|
||||
% (i, r, g, b, r_exp, g_exp, b_exp)
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
presets = Presets()
|
||||
|
||||
if not presets.strips:
|
||||
print("No strips; skipping all-on-presets test.")
|
||||
return
|
||||
|
||||
# Full-brightness white via the built-in 'on' pattern.
|
||||
base_color = (255, 255, 255)
|
||||
brightness = 255
|
||||
|
||||
data = {
|
||||
"p": "on",
|
||||
"c": [base_color],
|
||||
"b": brightness,
|
||||
}
|
||||
|
||||
name = "test_all_on_presets"
|
||||
preset = Preset(data)
|
||||
presets.presets[name] = preset
|
||||
|
||||
presets.select(name)
|
||||
presets.tick()
|
||||
|
||||
# Compute the color actually written by the pattern after brightness scaling.
|
||||
expected_color = presets.apply_brightness(base_color, brightness)
|
||||
|
||||
verify_strip0_on(presets, expected_color)
|
||||
|
||||
print("test_all_on_presets: OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
184
pico/test/test_chase_via_presets.py
Normal file
184
pico/test/test_chase_via_presets.py
Normal file
@@ -0,0 +1,184 @@
|
||||
"""
|
||||
On-device test that exercises the Chase pattern 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_chase_via_presets.py :
|
||||
mpremote connect <device> run test_chase_via_presets.py
|
||||
"""
|
||||
|
||||
import utime
|
||||
|
||||
from presets import Presets, Preset
|
||||
|
||||
|
||||
def snapshot_strip_colors(presets, strip_idx=0, max_leds=32):
|
||||
"""Return a list of (r,g,b) tuples for the first max_leds of the given strip."""
|
||||
strip = presets.strips[strip_idx]
|
||||
num = min(strip.num_leds, max_leds)
|
||||
out = []
|
||||
for i in range(num):
|
||||
o = i * 3
|
||||
g = strip.ar[o]
|
||||
r = strip.ar[o + 1]
|
||||
b = strip.ar[o + 2]
|
||||
out.append((r, g, b))
|
||||
return out
|
||||
|
||||
|
||||
def expected_chase_color(i, num_leds, step_count, color0, color1, n1, n2, n3, n4):
|
||||
"""Mirror the position logic from patterns/chase.py for a single logical LED."""
|
||||
segment_length = n1 + n2
|
||||
|
||||
if step_count % 2 == 0:
|
||||
position = (step_count // 2) * (n3 + n4) + n3
|
||||
else:
|
||||
position = ((step_count + 1) // 2) * (n3 + n4)
|
||||
|
||||
max_pos = num_leds + segment_length
|
||||
position = position % max_pos
|
||||
if position < 0:
|
||||
position += max_pos
|
||||
|
||||
relative_pos = (i - position) % segment_length
|
||||
if relative_pos < 0:
|
||||
relative_pos = (relative_pos + segment_length) % segment_length
|
||||
|
||||
return color0 if relative_pos < n1 else color1
|
||||
|
||||
|
||||
def test_chase_single_step_via_presets():
|
||||
presets = Presets()
|
||||
|
||||
num_leds = presets.num_leds
|
||||
if num_leds <= 0:
|
||||
print("No strips; skipping chase test.")
|
||||
return
|
||||
|
||||
# Simple alternating colors with known lengths.
|
||||
base_color0 = (10, 0, 0)
|
||||
base_color1 = (0, 0, 20)
|
||||
|
||||
# Use full brightness so apply_brightness is identity.
|
||||
brightness = 255
|
||||
|
||||
n1 = 2
|
||||
n2 = 3
|
||||
# Same step size on even/odd for easier reasoning.
|
||||
n3 = 1
|
||||
n4 = 1
|
||||
|
||||
data = {
|
||||
"p": "chase",
|
||||
"c": [base_color0, base_color1],
|
||||
"b": brightness,
|
||||
"d": 0,
|
||||
"a": False, # single-step mode
|
||||
"n1": n1,
|
||||
"n2": n2,
|
||||
"n3": n3,
|
||||
"n4": n4,
|
||||
}
|
||||
|
||||
name = "test_chase_pattern"
|
||||
preset = Preset(data)
|
||||
presets.presets[name] = preset
|
||||
|
||||
# Select and run one tick; this should render exactly one chase frame for step 0.
|
||||
presets.select(name, step=0)
|
||||
presets.tick()
|
||||
|
||||
# Colors after brightness scaling (driver.apply_brightness is used in the pattern).
|
||||
color0 = presets.apply_brightness(base_color0, brightness)
|
||||
color1 = presets.apply_brightness(base_color1, brightness)
|
||||
|
||||
# Snapshot first few LEDs of strip 0 and compare against expected pattern for step 0.
|
||||
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
|
||||
step_count = 0
|
||||
|
||||
for i, actual in enumerate(colors):
|
||||
expected = expected_chase_color(
|
||||
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
|
||||
)
|
||||
assert (
|
||||
actual == expected
|
||||
), "LED %d: got %r, expected %r" % (i, actual, expected)
|
||||
|
||||
print("test_chase_single_step_via_presets: OK")
|
||||
|
||||
|
||||
def test_chase_multiple_steps_via_presets():
|
||||
"""Render several steps and verify pattern advances correctly."""
|
||||
presets = Presets()
|
||||
|
||||
num_leds = presets.num_leds
|
||||
if num_leds <= 0:
|
||||
print("No strips; skipping chase multi-step test.")
|
||||
return
|
||||
|
||||
base_color0 = (10, 0, 0)
|
||||
base_color1 = (0, 0, 20)
|
||||
brightness = 255
|
||||
|
||||
n1 = 2
|
||||
n2 = 3
|
||||
n3 = 1
|
||||
n4 = 1
|
||||
|
||||
data = {
|
||||
"p": "chase",
|
||||
"c": [base_color0, base_color1],
|
||||
"b": brightness,
|
||||
"d": 0,
|
||||
"a": False,
|
||||
"n1": n1,
|
||||
"n2": n2,
|
||||
"n3": n3,
|
||||
"n4": n4,
|
||||
}
|
||||
|
||||
name = "test_chase_pattern_multi"
|
||||
preset = Preset(data)
|
||||
presets.presets[name] = preset
|
||||
|
||||
color0 = presets.apply_brightness(base_color0, brightness)
|
||||
color1 = presets.apply_brightness(base_color1, brightness)
|
||||
|
||||
# In non-auto mode (a=False), the Chase pattern advances one step per
|
||||
# invocation of the generator, and Presets is expected to call select()
|
||||
# again for each beat. Emulate that here by re-selecting with an
|
||||
# explicit step value for each frame we want to test.
|
||||
for step_count in range(4):
|
||||
presets.select(name, step=step_count)
|
||||
presets.tick()
|
||||
colors = snapshot_strip_colors(presets, strip_idx=0, max_leds=16)
|
||||
|
||||
for i, actual in enumerate(colors):
|
||||
expected = expected_chase_color(
|
||||
i, num_leds, step_count, color0, color1, n1, n2, n3, n4
|
||||
)
|
||||
assert (
|
||||
actual == expected
|
||||
), "step %d, LED %d: got %r, expected %r" % (
|
||||
step_count,
|
||||
i,
|
||||
actual,
|
||||
expected,
|
||||
)
|
||||
|
||||
print("test_chase_multiple_steps_via_presets: OK")
|
||||
|
||||
|
||||
def main():
|
||||
test_chase_single_step_via_presets()
|
||||
test_chase_multiple_steps_via_presets()
|
||||
# Give a brief pause so message is visible if run interactively.
|
||||
utime.sleep_ms(100)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
157
pico/test/test_double_circle.py
Normal file
157
pico/test/test_double_circle.py
Normal 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()
|
||||
|
||||
52
pico/test/test_fill_n.py
Normal file
52
pico/test/test_fill_n.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""
|
||||
On-device test for Presets.fill_n() 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_fill_n.py :
|
||||
mpremote connect <device> run test_fill_n.py
|
||||
|
||||
This script:
|
||||
- Instantiates Presets
|
||||
- Calls fill_n() with a simple range
|
||||
- Lets you visually confirm that all strips show the same proportional segment
|
||||
and that equal-length strip pairs have identical lit indices.
|
||||
"""
|
||||
|
||||
from presets import Presets
|
||||
|
||||
|
||||
def main():
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
|
||||
# Choose a simple test range on the reference strip (strip 0).
|
||||
ref_len = presets.strip_length(0)
|
||||
if ref_len <= 0:
|
||||
print("No strips or invalid length; aborting fill_n test.")
|
||||
return
|
||||
|
||||
# Use a central segment so it's easy to see.
|
||||
start = ref_len // 4
|
||||
end = 3 * ref_len // 4
|
||||
print("Running fill_n test from", start, "to", end, "on reference strip 0.")
|
||||
|
||||
color = (0, 50, 0) # dim green
|
||||
|
||||
# First, clear everything
|
||||
for strip in presets.strips:
|
||||
strip.fill((0, 0, 0))
|
||||
strip.show()
|
||||
|
||||
# Apply fill_n, which will use scale() internally.
|
||||
presets.fill_n(color, start, end)
|
||||
|
||||
print("fill_n test applied; visually inspect strips.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
264
pico/test/test_multi_patterns.py
Normal file
264
pico/test/test_multi_patterns.py
Normal 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()
|
||||
|
||||
100
pico/test/test_scale.py
Normal file
100
pico/test/test_scale.py
Normal file
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Test the Presets.scale() helper on-device with mpremote.
|
||||
|
||||
Usage (from project root):
|
||||
|
||||
mpremote connect <device> cp pico/src/*.py : &&
|
||||
mpremote connect <device> cp pico/src/patterns/*.py :patterns &&
|
||||
mpremote connect <device> cp pico/lib/*.py : &&
|
||||
mpremote connect <device> cp tests/test_scale.py : &&
|
||||
mpremote connect <device> run test_scale.py
|
||||
|
||||
This script:
|
||||
- Creates a minimal Presets instance
|
||||
- Runs a few numeric test cases for scale()
|
||||
- Optionally displays a short visual check on the LEDs
|
||||
"""
|
||||
|
||||
from presets import Presets
|
||||
|
||||
|
||||
def numeric_tests(presets):
|
||||
"""
|
||||
Numeric sanity checks for scale() using the actual strip config.
|
||||
|
||||
We treat strip 0 as the reference and print the mapped indices for
|
||||
a few positions on each other strip.
|
||||
"""
|
||||
print("Numeric scale() tests (from strip 0):")
|
||||
ref_len = presets.strip_length(0)
|
||||
if ref_len <= 0:
|
||||
print(" strip 0 length <= 0; skipping numeric tests.")
|
||||
return
|
||||
|
||||
test_positions = [0, ref_len // 2, ref_len - 1]
|
||||
for pos in test_positions:
|
||||
print(" pos on strip 0:", pos)
|
||||
for dst_idx in range(len(presets.strips)):
|
||||
dst_len = presets.strip_length(dst_idx)
|
||||
if dst_len <= 0:
|
||||
continue
|
||||
n2 = presets.scale(dst_idx, pos)
|
||||
print(" -> strip", dst_idx, "len", dst_len, "pos", n2)
|
||||
|
||||
|
||||
def visual_test(presets):
|
||||
"""
|
||||
Simple visual test:
|
||||
- Use strip 0 as reference
|
||||
- Move a pixel along strip 0
|
||||
- Map position to all other strips with scale()
|
||||
"""
|
||||
import utime
|
||||
|
||||
strips = presets.strips
|
||||
if not strips:
|
||||
print("No strips available for visual test.")
|
||||
return
|
||||
|
||||
src_strip_idx = 0
|
||||
l1 = presets.strip_length(src_strip_idx)
|
||||
if l1 <= 0:
|
||||
print("strip_length(0) <= 0; aborting visual test.")
|
||||
return
|
||||
|
||||
color = (50, 0, 0) # dim red so it doesn't blind you
|
||||
|
||||
# Run once across the full length of the reference strip,
|
||||
# jumping 10 LEDs at a time.
|
||||
step_size = 10
|
||||
steps = (l1 + step_size - 1) // step_size
|
||||
print("Starting visual scale() test with 10-LED jumps:", steps, "steps...")
|
||||
for step in range(steps):
|
||||
n1 = (step * step_size) % l1
|
||||
|
||||
# Clear all strips
|
||||
for strip in strips:
|
||||
strip.fill((0, 0, 0))
|
||||
|
||||
# Light mapped position on each strip using Presets.set/show
|
||||
for dst_strip_idx, _ in enumerate(strips):
|
||||
presets.set(dst_strip_idx, n1, color)
|
||||
presets.show(dst_strip_idx)
|
||||
|
||||
print("Visual test finished.")
|
||||
|
||||
|
||||
def main():
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
numeric_tests(presets)
|
||||
# Comment this in/out depending on whether you want the LEDs to run:
|
||||
try:
|
||||
visual_test(presets)
|
||||
except Exception as e:
|
||||
print("Visual test error:", e)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
130
pico/test/test_segments.py
Normal file
130
pico/test/test_segments.py
Normal file
@@ -0,0 +1,130 @@
|
||||
"""
|
||||
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_segments.py :
|
||||
mpremote connect <device> run test_segments.py
|
||||
|
||||
This script:
|
||||
- Instantiates Presets
|
||||
- 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_segments_preset(name, colors, n_values, brightness=255):
|
||||
"""
|
||||
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]
|
||||
"""
|
||||
# Pad or trim colors to 4 entries
|
||||
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", # pattern key for Segments
|
||||
"c": cs,
|
||||
"b": brightness,
|
||||
"n1": n1,
|
||||
"n2": n2,
|
||||
"n3": n3,
|
||||
"n4": n4,
|
||||
"n5": n5,
|
||||
"n6": n6,
|
||||
"n7": n7,
|
||||
"n8": n8,
|
||||
# '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 segments preset and hold it for wait_ms."""
|
||||
presets.presets[name] = preset_obj
|
||||
presets.select(name)
|
||||
# Segments 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:
|
||||
# Keep ticking in case other logic ever depends on it
|
||||
presets.tick()
|
||||
|
||||
|
||||
def main():
|
||||
presets = Presets()
|
||||
presets.load()
|
||||
|
||||
num_leds = presets.strip_length(0)
|
||||
if num_leds <= 0:
|
||||
print("No strips; aborting segments test.")
|
||||
return
|
||||
|
||||
print("Starting segments pattern test...")
|
||||
|
||||
quarter = num_leds // 4
|
||||
half = num_leds // 2
|
||||
|
||||
segments_presets = []
|
||||
|
||||
# 1. Single band: first quarter, red
|
||||
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
|
||||
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
|
||||
segments_presets.append(
|
||||
make_segments_preset(
|
||||
"segments_rgb_quarters",
|
||||
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,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
# Show each for ~4 seconds
|
||||
for name, preset_obj in segments_presets:
|
||||
print("Showing segments preset:", name)
|
||||
show_and_wait(presets, name, preset_obj, wait_ms=4000)
|
||||
|
||||
print("Segments pattern test finished. Turning off LEDs.")
|
||||
presets.select("off")
|
||||
presets.tick()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
128
pico/test/test_spin.py
Normal file
128
pico/test/test_spin.py
Normal 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()
|
||||
|
||||
Reference in New Issue
Block a user