Compare commits

...

2 Commits

Author SHA1 Message Date
47c17dba36 Add mpremote tests for scaling, roll, and point
Made-with: Cursor
2026-03-05 20:25:57 +13:00
e75723e2e7 Refactor presets scaling and clean up patterns
Made-with: Cursor
2026-03-05 20:25:28 +13:00
29 changed files with 500 additions and 371 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
settings.json settings.json
__pycache__/

View File

@@ -28,6 +28,8 @@ class WS2812B:
self.brightness = brightness self.brightness = brightness
self.invert = invert self.invert = invert
self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3) 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): def show(self, array=None, offset=0):
if array is None: if array is None:

View File

@@ -7,29 +7,11 @@ from .circle import Circle
from .roll import Roll from .roll import Roll
from .calibration import Calibration from .calibration import Calibration
from .test import Test from .test import Test
from .scale_test import ScaleTest
from .grab import Grab from .grab import Grab
from .spin import Spin from .spin import Spin
from .lift import Lift from .lift import Lift
from .flare import Flare from .flare import Flare
from .hook import Hook from .hook import Hook
from .invertsplit import Invertsplit
from .pose import Pose 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 .point import Point

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbalance:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbend:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Backbendsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Beat:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Crouch:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Dismount:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhangspin:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Elbowhangsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Fluff:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Foothang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Frontbalance:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Invert:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Invertsplit:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Kneehang:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Legswoop:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -3,66 +3,16 @@ class Point:
self.driver = driver self.driver = driver
def run(self, preset): def run(self, preset):
""" # Apply preset/global brightness once per color
Point pattern: color bands defined by n ranges. c1 = self.driver.apply_brightness(preset.c[0], preset.b)
c2 = self.driver.apply_brightness(preset.c[1], preset.b)
- n1n2: LEDs with color1 (c[0]) c3 = self.driver.apply_brightness(preset.c[2], preset.b)
- n3n4: LEDs with color2 (c[1]) c4 = self.driver.apply_brightness(preset.c[3], preset.b)
- n5n6: LEDs with color3 (c[2])
- n7n8: 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 # Helper to normalize and clamp a range
def norm_range(a, b): self.driver.fill_n(c1, preset.n1, preset.n2)
a = int(a) self.driver.fill_n(c2, preset.n3, preset.n4)
b = int(b) self.driver.fill_n(c3, preset.n5, preset.n6)
if a > b: self.driver.fill_n(c4, preset.n7, preset.n8)
a, b = b, a self.driver.show_all()
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

View 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

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Seat:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Split:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -1,9 +0,0 @@
"""Placeholder until implemented."""
class Straddle:
def __init__(self, driver):
self.driver = driver
def run(self, preset):
while True:
yield

View File

@@ -3,11 +3,7 @@ from ws2812 import WS2812B
from preset import Preset from preset import Preset
from patterns import ( from patterns import (
Blink, Rainbow, Pulse, Transition, Chase, Circle, Roll, Calibration, Test, Blink, Rainbow, Pulse, Transition, Chase, Circle, Roll, Calibration, Test,
Grab, Spin, Lift, Flare, Hook, Invertsplit, Pose, Grab, Spin, Lift, Flare, Hook, Pose, Point,
Backbalance, Beat, Crouch, Backbendsplit, Straddle,
Frontbalance, Elbowhang, Elbowhangspin, Dismount, Fluff,
Elbowhangsplit, Invert, Backbend, Seat, Kneehang,
Legswoop, Split, Foothang, Point,
) )
import json import json
@@ -23,53 +19,9 @@ STRIP_CONFIG = (
(7, 291, 290 // 2-1), # 8 (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: class Presets:
def __init__(self): def __init__(self):
self.scale_map = []
self.strips = [] self.strips = []
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG) self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
@@ -80,9 +32,9 @@ class Presets:
self.strip_midpoints.append(mid) self.strip_midpoints.append(mid)
self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0)) self.strips.append(WS2812B(num_leds, pin, state_machine, brightness=1.0))
state_machine += 1 state_machine += 1
self.scale_map.append(self.create_scale_map(num_leds))
# Single logical strip over all 8 strips for patterns (n[i], .fill(), .write()) # Single logical strip over all 8 strips for patterns (n[i], .fill(), .write())
self.n = StripRing(self.strips)
self.num_leds = self.n.num_leds
# WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in) # WS2812B with brightness=1.0 so Presets.apply_brightness() does all scaling (NeoPixel drop-in)
self.step = 0 self.step = 0
# Remember which strip was last used as the roll head (for flare, etc.) # Remember which strip was last used as the roll head (for flare, etc.)
@@ -112,26 +64,7 @@ class Presets:
"lift": Lift(self).run, "lift": Lift(self).run,
"flare": Flare(self).run, "flare": Flare(self).run,
"hook": Hook(self).run, "hook": Hook(self).run,
"invertsplit": Invertsplit(self).run,
"pose": Pose(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, "point": Point(self).run,
} }
@@ -143,40 +76,6 @@ class Presets:
return self.strips[strip_idx].num_leds return self.strips[strip_idx].num_leds
return 0 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): def save(self):
"""Save the presets to a file.""" """Save the presets to a file."""
with open("presets.json", "w") as f: with open("presets.json", "w") as f:
@@ -251,14 +150,6 @@ class Presets:
self.selected = preset_name self.selected = preset_name
return True 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): def apply_brightness(self, color, brightness_override=None):
# Combine per-preset brightness (override) with global brightness self.b # Combine per-preset brightness (override) with global brightness self.b
local = brightness_override if brightness_override is not None else 255 local = brightness_override if brightness_override is not None else 255
@@ -266,12 +157,6 @@ class Presets:
effective_brightness = int(local * self.b / 255) effective_brightness = int(local * self.b / 255)
return tuple(int(c * effective_brightness / 255) for c in color) 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): def off(self, preset=None):
self.fill((0, 0, 0)) self.fill((0, 0, 0))
@@ -279,3 +164,27 @@ class Presets:
colors = preset.c colors = preset.c
color = colors[0] if colors else (255, 255, 255) color = colors[0] if colors else (255, 255, 255)
self.fill(self.apply_brightness(color, preset.b)) self.fill(self.apply_brightness(color, preset.b))
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()

52
pico/test/test_fill_n.py Normal file
View 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()

130
pico/test/test_point.py Normal file
View File

@@ -0,0 +1,130 @@
"""
On-device test for the Point pattern using mpremote.
Usage (from pico/ dir or project root with adjusted paths):
mpremote connect <device> cp src/*.py :
mpremote connect <device> cp src/patterns/*.py :patterns
mpremote connect <device> cp lib/*.py :
mpremote connect <device> cp test/test_point.py :
mpremote connect <device> run test_point.py
This script:
- Instantiates Presets
- Creates a few in-memory 'point' presets with different ranges/colors
- Selects each one so you can visually confirm the segments
"""
from presets import Presets, Preset
def make_point_preset(name, colors, n_values, brightness=255):
"""
Helper to build a Preset for the 'point' pattern.
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": "point",
"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 point; it's static
}
return name, Preset(data)
def show_and_wait(presets, name, preset_obj, wait_ms):
"""Select a static 'point' preset and hold it for wait_ms."""
presets.presets[name] = preset_obj
presets.select(name)
# Point 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 point test.")
return
print("Starting point pattern test...")
quarter = num_leds // 4
half = num_leds // 2
point_presets = []
# 1. Single band: first quarter, red
point_presets.append(
make_point_preset(
"point_red_q1",
colors=[(255, 0, 0)],
n_values=[0, quarter - 1, 0, -1, 0, -1, 0, -1],
)
)
# 2. Two bands: red first half, green second half
point_presets.append(
make_point_preset(
"point_red_green_halves",
colors=[(255, 0, 0), (0, 255, 0)],
n_values=[0, half - 1, half, num_leds - 1, 0, -1, 0, -1],
)
)
# 3. Three bands: R, G, B quarters
point_presets.append(
make_point_preset(
"point_rgb_quarters",
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 point_presets:
print("Showing point preset:", name)
show_and_wait(presets, name, preset_obj, wait_ms=4000)
print("Point pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

118
pico/test/test_roll.py Normal file
View File

@@ -0,0 +1,118 @@
"""
On-device test for the Roll 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_roll.py :
mpremote connect <device> run test_roll.py
This script:
- Instantiates Presets
- Creates a few in-memory roll 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_roll_preset(name, color1, color2, n1=0, n2=0, n3=0, n4=0, delay_ms=50, brightness=255):
"""Helper to build a Preset dict for the roll pattern."""
data = {
"p": "roll",
"c": [color1, color2],
"b": brightness,
"d": delay_ms,
"n1": n1,
"n2": n2,
"n3": n3,
"n4": n4,
"a": True, # animated
}
return name, Preset(data)
def run_preset(presets, name, preset_obj, duration_ms):
"""Run a given roll preset for duration_ms using the existing tick loop."""
presets.presets[name] = preset_obj
presets.select(name)
start = utime.ticks_ms()
while utime.ticks_diff(utime.ticks_ms(), start) < duration_ms:
presets.tick()
def main():
presets = Presets()
presets.load()
print("Starting roll pattern test...")
ref_len = presets.strip_length(0)
# Use some margins based on strip length to show different bands.
quarter = ref_len // 4
# Define a few different roll presets:
roll_presets = []
# 1. Full-strip, white -> off, clockwise, medium speed
roll_presets.append(
make_roll_preset(
"roll_full_cw",
color1=(255, 255, 255),
color2=(0, 0, 0),
n1=0,
n2=0,
n3=2, # 2 rotations then stop
n4=0, # clockwise
delay_ms=40,
brightness=255,
)
)
# 2. Inner band only, red -> blue, clockwise, slower
roll_presets.append(
make_roll_preset(
"roll_inner_band",
color1=(255, 0, 0),
color2=(0, 0, 255),
n1=quarter, # start margin
n2=quarter, # end margin
n3=2,
n4=0,
delay_ms=60,
brightness=255,
)
)
# 3. Full-strip, green -> off, counter-clockwise, faster
roll_presets.append(
make_roll_preset(
"roll_full_ccw",
color1=(0, 255, 0),
color2=(0, 0, 0),
n1=0,
n2=0,
n3=2,
n4=1, # counter-clockwise
delay_ms=30,
brightness=255,
)
)
# Run each roll preset for about 5 seconds
for name, preset_obj in roll_presets:
print("Running roll preset:", name)
run_preset(presets, name, preset_obj, duration_ms=5000)
print("Roll pattern test finished. Turning off LEDs.")
presets.select("off")
presets.tick()
if __name__ == "__main__":
main()

100
pico/test/test_scale.py Normal file
View 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()