Add Pico presets engine, patterns, and tests.
Wire the Pico to UART-driven preset selection, add pattern modules and presets data, remove old p2p/settings code, and update tests and LED driver. Made-with: Cursor
This commit is contained in:
148
pico/src/main.py
148
pico/src/main.py
@@ -1,99 +1,63 @@
|
|||||||
import sys
|
from machine import UART, Pin
|
||||||
# So "from ws2812 import WS2812B" finds pico/lib when run from device / or test/
|
import json
|
||||||
if "lib" not in sys.path:
|
from presets import Presets
|
||||||
sys.path.insert(0, "lib")
|
import gc
|
||||||
if "../lib" not in sys.path:
|
|
||||||
sys.path.insert(0, "../lib")
|
|
||||||
from ws2812 import WS2812B
|
|
||||||
import time
|
|
||||||
|
|
||||||
# --- Rainbow pattern (outside ws2812): pregen double buffer, show via head offset ---
|
uart = UART(0, baudrate=921600, rx=Pin(1, Pin.IN))
|
||||||
|
|
||||||
def hue_to_rgb(hue):
|
presets = Presets()
|
||||||
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
|
presets.load()
|
||||||
h = hue % 360
|
|
||||||
x = 1 - abs((h / 60) % 2 - 1)
|
|
||||||
if h < 60:
|
|
||||||
r, g, b = 1, x, 0
|
|
||||||
elif h < 120:
|
|
||||||
r, g, b = x, 1, 0
|
|
||||||
elif h < 180:
|
|
||||||
r, g, b = 0, 1, x
|
|
||||||
elif h < 240:
|
|
||||||
r, g, b = 0, x, 1
|
|
||||||
elif h < 300:
|
|
||||||
r, g, b = x, 0, 1
|
|
||||||
else:
|
|
||||||
r, g, b = 1, 0, x
|
|
||||||
return (int(r * 255), int(g * 255), int(b * 255))
|
|
||||||
|
|
||||||
|
print(presets.presets.keys())
|
||||||
|
|
||||||
def make_rainbow_double(num_leds, brightness=1.0):
|
presets.select("off")
|
||||||
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len).
|
|
||||||
head must be in 0..strip_len-1 so DMA reads double_buf[head:head+strip_len] with no copy."""
|
|
||||||
n = 2 * num_leds
|
|
||||||
double_buf = bytearray(n * 3)
|
|
||||||
for i in range(n):
|
|
||||||
hue = (i / n) * 360 * 2
|
|
||||||
r, g, b = hue_to_rgb(hue)
|
|
||||||
g = int(g * brightness) & 0xFF
|
|
||||||
r = int(r * brightness) & 0xFF
|
|
||||||
b = int(b * brightness) & 0xFF
|
|
||||||
o = i * 3
|
|
||||||
double_buf[o] = g
|
|
||||||
double_buf[o + 1] = r
|
|
||||||
double_buf[o + 2] = b
|
|
||||||
strip_len = num_leds * 3
|
|
||||||
return (double_buf, strip_len)
|
|
||||||
|
|
||||||
|
#print memory usage
|
||||||
|
print(f"Memory usage: {gc.mem_free()/1024} kB free")
|
||||||
|
|
||||||
def show_rainbow(strip, double_buf, strip_len, head):
|
i = 0
|
||||||
"""DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1."""
|
|
||||||
strip.show(double_buf, head)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Strips + rainbow buffers per strip ---
|
|
||||||
# Each strip can have a different length; buffers and phase are per-strip.
|
|
||||||
# Strip config must match pico/src/main.py pins.
|
|
||||||
STRIP_CONFIG = (
|
|
||||||
(7, 291),
|
|
||||||
(3, 290),
|
|
||||||
(6, 283),
|
|
||||||
(28, 278),
|
|
||||||
(29, 275),
|
|
||||||
(4, 270),
|
|
||||||
(0, 283),
|
|
||||||
(2, 290),
|
|
||||||
)
|
|
||||||
|
|
||||||
strips = []
|
|
||||||
sm = 0
|
|
||||||
for pin, num_leds in STRIP_CONFIG:
|
|
||||||
print(pin, num_leds)
|
|
||||||
ws = WS2812B(num_leds, pin, sm, brightness=0.2) # 1.0 so fill() is visible
|
|
||||||
strips.append(ws)
|
|
||||||
sm += 1
|
|
||||||
|
|
||||||
# Cumulative LED count before each strip; total ring size
|
|
||||||
cumulative_leds = [0]
|
|
||||||
for ws in strips[:-1]:
|
|
||||||
cumulative_leds.append(cumulative_leds[-1] + ws.num_leds)
|
|
||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
|
||||||
bytes_per_cycle = total_ring_leds * 3
|
|
||||||
|
|
||||||
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
|
|
||||||
now = time.ticks_ms()
|
|
||||||
rainbow_data = [make_rainbow_double(ws.num_leds, ws.brightness) for ws in strips]
|
|
||||||
# Global phase in bytes; each strip: head = (phase + cumulative_leds[i]*3) % strip_len[i]
|
|
||||||
print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
|
||||||
rainbow_head = 0
|
|
||||||
step = 3
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
now = time.ticks_ms()
|
presets.tick()
|
||||||
for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)):
|
if uart.any():
|
||||||
head = (rainbow_head + cumulative_leds[i] * 3) % strip_len
|
data = uart.readline()
|
||||||
show_rainbow(strip, double_buf, strip_len, head)
|
try:
|
||||||
rainbow_head = (rainbow_head + step) % bytes_per_cycle
|
data = json.loads(data)
|
||||||
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
except:
|
||||||
time.sleep_ms(10)
|
# Ignore malformed JSON lines
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Select a preset by name (existing behaviour)
|
||||||
|
preset_name = data.get("select")
|
||||||
|
if preset_name is not None:
|
||||||
|
presets.select(preset_name)
|
||||||
|
presets.tick()
|
||||||
|
|
||||||
|
# Create or update a preset:
|
||||||
|
# {"preset_edit": {"name": "<name>", "data": {<preset_dict>}}}
|
||||||
|
edit_payload = data.get("preset_edit")
|
||||||
|
if isinstance(edit_payload, dict):
|
||||||
|
name = edit_payload.get("name")
|
||||||
|
preset_data = edit_payload.get("data") or {}
|
||||||
|
if isinstance(name, str) and isinstance(preset_data, dict):
|
||||||
|
# Log the incoming preset payload for debugging
|
||||||
|
print("PRESET_EDIT", name, preset_data)
|
||||||
|
presets.edit(name, preset_data)
|
||||||
|
|
||||||
|
# Delete a preset:
|
||||||
|
# {"preset_delete": "<name>"}
|
||||||
|
delete_name = data.get("preset_delete")
|
||||||
|
if isinstance(delete_name, str):
|
||||||
|
print("PRESET_DELETE", delete_name)
|
||||||
|
presets.delete(delete_name)
|
||||||
|
|
||||||
|
# Persist all presets to flash:
|
||||||
|
# {"preset_save": true}
|
||||||
|
if data.get("preset_save"):
|
||||||
|
print("PRESET_SAVE")
|
||||||
|
presets.save()
|
||||||
|
|
||||||
|
print(data)
|
||||||
|
gc.collect()
|
||||||
|
#print used and free memory
|
||||||
|
print(f"Memory usage: {gc.mem_alloc()/1024} kB used, {gc.mem_free()/1024} kB free")
|
||||||
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import aioespnow
|
|
||||||
import json
|
|
||||||
|
|
||||||
async def p2p(settings, patterns):
|
|
||||||
e = aioespnow.AIOESPNow() # Returns AIOESPNow enhanced with async support
|
|
||||||
e.active(True)
|
|
||||||
async for mac, msg in e:
|
|
||||||
try:
|
|
||||||
data = json.loads(msg)
|
|
||||||
except:
|
|
||||||
print(f"Failed to load espnow data {msg}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
if "names" not in data or settings.get("name") in data.get("names", []):
|
|
||||||
await settings.set_settings(data.get("settings", {}), patterns, data.get("save", False))
|
|
||||||
@@ -4,3 +4,32 @@ from .pulse import Pulse
|
|||||||
from .transition import Transition
|
from .transition import Transition
|
||||||
from .chase import Chase
|
from .chase import Chase
|
||||||
from .circle import Circle
|
from .circle import Circle
|
||||||
|
from .roll import Roll
|
||||||
|
from .calibration import Calibration
|
||||||
|
from .test import Test
|
||||||
|
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
|
||||||
|
|||||||
9
pico/src/patterns/backbalance.py
Normal file
9
pico/src/patterns/backbalance.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Backbalance:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/backbend.py
Normal file
9
pico/src/patterns/backbend.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Backbend:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/backbendsplit.py
Normal file
9
pico/src/patterns/backbendsplit.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Backbendsplit:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/beat.py
Normal file
9
pico/src/patterns/beat.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Beat:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
38
pico/src/patterns/calibration.py
Normal file
38
pico/src/patterns/calibration.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
"""Calibration: strips 2 and 6 only. First 10 green, then alternating 10 blue / 10 red. 10% brightness."""
|
||||||
|
|
||||||
|
BRIGHTNESS = 0.10
|
||||||
|
BLOCK = 10
|
||||||
|
STRIPS_ON = (2, 6) # 0-based: 3rd and 7th strip only
|
||||||
|
|
||||||
|
GREEN = (0, 255, 0)
|
||||||
|
RED = (255, 0, 0)
|
||||||
|
BLUE = (0, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
def _scale(color, factor):
|
||||||
|
return tuple(int(c * factor) for c in color)
|
||||||
|
|
||||||
|
|
||||||
|
class Calibration:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
green = _scale(GREEN, BRIGHTNESS)
|
||||||
|
red = _scale(RED, BRIGHTNESS)
|
||||||
|
blue = _scale(BLUE, BRIGHTNESS)
|
||||||
|
on_set = set(STRIPS_ON)
|
||||||
|
for strip_idx, strip in enumerate(strips):
|
||||||
|
n = strip.num_leds
|
||||||
|
if strip_idx not in on_set:
|
||||||
|
strip.fill((0, 0, 0))
|
||||||
|
strip.show()
|
||||||
|
continue
|
||||||
|
for i in range(n):
|
||||||
|
if i < BLOCK:
|
||||||
|
strip.set(i, green)
|
||||||
|
else:
|
||||||
|
block = (i - BLOCK) // BLOCK
|
||||||
|
strip.set(i, blue if block % 2 == 0 else red)
|
||||||
|
strip.show()
|
||||||
9
pico/src/patterns/crouch.py
Normal file
9
pico/src/patterns/crouch.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Crouch:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/dismount.py
Normal file
9
pico/src/patterns/dismount.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Dismount:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/elbowhang.py
Normal file
9
pico/src/patterns/elbowhang.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Elbowhang:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/elbowhangspin.py
Normal file
9
pico/src/patterns/elbowhangspin.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Elbowhangspin:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/elbowhangsplit.py
Normal file
9
pico/src/patterns/elbowhangsplit.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Elbowhangsplit:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
63
pico/src/patterns/flare.py
Normal file
63
pico/src/patterns/flare.py
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import utime
|
||||||
|
|
||||||
|
|
||||||
|
class Flare:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
"""
|
||||||
|
Flare: on the strip used by the first roll head,
|
||||||
|
make the strip fade up to brightness over the delay time.
|
||||||
|
|
||||||
|
- c[0]: color1 for the first n1 LEDs
|
||||||
|
- c[1]: color2 for the rest of the strip
|
||||||
|
- n1: number of LEDs from the start of the strip that use color1
|
||||||
|
- d: fade-in duration in ms (time to reach full preset brightness b)
|
||||||
|
"""
|
||||||
|
strips = self.driver.strips
|
||||||
|
|
||||||
|
# Which strip to flare: last roll head, clamped to valid range
|
||||||
|
strip_idx = getattr(self.driver, "last_roll_head", 0)
|
||||||
|
if strip_idx < 0 or strip_idx >= len(strips):
|
||||||
|
strip_idx = 0
|
||||||
|
|
||||||
|
strip = strips[strip_idx]
|
||||||
|
n = strip.num_leds
|
||||||
|
|
||||||
|
colors = preset.c
|
||||||
|
base_c1 = colors[0] if len(colors) > 0 else (255, 255, 255)
|
||||||
|
base_c2 = colors[1] if len(colors) > 1 else (0, 0, 0)
|
||||||
|
|
||||||
|
count_c1 = max(0, min(int(preset.n1), n))
|
||||||
|
fade_ms = max(1, int(preset.d) or 1)
|
||||||
|
target_b = int(preset.b) if hasattr(preset, "b") else 255
|
||||||
|
|
||||||
|
start_time = utime.ticks_ms()
|
||||||
|
done = False
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = utime.ticks_ms()
|
||||||
|
elapsed = utime.ticks_diff(now, start_time)
|
||||||
|
|
||||||
|
if not done:
|
||||||
|
if elapsed >= fade_ms:
|
||||||
|
factor = 1.0
|
||||||
|
done = True
|
||||||
|
else:
|
||||||
|
factor = elapsed / fade_ms if fade_ms > 0 else 1.0
|
||||||
|
else:
|
||||||
|
factor = 1.0
|
||||||
|
|
||||||
|
# Effective per-preset brightness scaled over time
|
||||||
|
current_b = int(target_b * factor)
|
||||||
|
|
||||||
|
# Apply global + local brightness to both colors
|
||||||
|
c1 = self.driver.apply_brightness(base_c1, current_b)
|
||||||
|
c2 = self.driver.apply_brightness(base_c2, current_b)
|
||||||
|
|
||||||
|
for i in range(n):
|
||||||
|
strip.set(i, c1 if i < count_c1 else c2)
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
yield
|
||||||
9
pico/src/patterns/fluff.py
Normal file
9
pico/src/patterns/fluff.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Fluff:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/foothang.py
Normal file
9
pico/src/patterns/foothang.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Foothang:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/frontbalance.py
Normal file
9
pico/src/patterns/frontbalance.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Frontbalance:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
26
pico/src/patterns/grab.py
Normal file
26
pico/src/patterns/grab.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""Grab: from center of each strip, 10 LEDs each side (21 total) in purple."""
|
||||||
|
|
||||||
|
SPAN = 10 # LEDs on each side of center
|
||||||
|
PURPLE = (180, 0, 255)
|
||||||
|
|
||||||
|
|
||||||
|
class Grab:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
for strip_idx, strip in enumerate(strips):
|
||||||
|
n = strip.num_leds
|
||||||
|
mid = self.driver.strip_midpoints[strip_idx]
|
||||||
|
strip.fill((0, 0, 0))
|
||||||
|
start = max(0, mid - SPAN)
|
||||||
|
end = min(n, mid + SPAN + 1)
|
||||||
|
for i in range(start, end):
|
||||||
|
strip.set(i, preset.c[0])
|
||||||
|
|
||||||
|
for strip in strips:
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
31
pico/src/patterns/hook.py
Normal file
31
pico/src/patterns/hook.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
"""Hook: light strips n1 to n2 with a segment span long, offset from dead center."""
|
||||||
|
|
||||||
|
class Hook:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
midpoints = self.driver.strip_midpoints
|
||||||
|
n1 = max(0, int(preset.n1))
|
||||||
|
n2 = max(n1, int(preset.n2))
|
||||||
|
span = max(0, int(preset.n3))
|
||||||
|
offset = int(preset.n4) # positive = toward one end
|
||||||
|
color = preset.c[0] if preset.c else (0, 0, 0)
|
||||||
|
|
||||||
|
for strip_idx, strip in enumerate(strips):
|
||||||
|
strip.fill((0, 0, 0))
|
||||||
|
if n1 <= strip_idx <= n2:
|
||||||
|
mid = midpoints[strip_idx]
|
||||||
|
n = strip.num_leds
|
||||||
|
center = mid + offset
|
||||||
|
start = max(0, center - span)
|
||||||
|
end = min(n, center + span + 1)
|
||||||
|
for i in range(start, end):
|
||||||
|
strip.set(i, color)
|
||||||
|
|
||||||
|
for strip in strips:
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/invert.py
Normal file
9
pico/src/patterns/invert.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Invert:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/invertsplit.py
Normal file
9
pico/src/patterns/invertsplit.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Invertsplit:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/kneehang.py
Normal file
9
pico/src/patterns/kneehang.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Kneehang:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/legswoop.py
Normal file
9
pico/src/patterns/legswoop.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Legswoop:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
116
pico/src/patterns/lift.py
Normal file
116
pico/src/patterns/lift.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""Lift: opposite of Spin — arms contract from the ends toward the center. Preset color, n1 = rate."""
|
||||||
|
|
||||||
|
import utime
|
||||||
|
|
||||||
|
SPAN = 10 # LEDs on each side of center (match Grab)
|
||||||
|
LUT_SIZE = 256
|
||||||
|
|
||||||
|
|
||||||
|
class Lift:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
active_indices = (0, 4)
|
||||||
|
c0 = preset.c[0] if preset.c else (0, 0, 0)
|
||||||
|
c1 = preset.c[1] if len(preset.c) > 1 else c0
|
||||||
|
|
||||||
|
lut = []
|
||||||
|
for k in range(LUT_SIZE):
|
||||||
|
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
|
||||||
|
r = int(c0[0] + (c1[0] - c0[0]) * t)
|
||||||
|
g = int(c0[1] + (c1[1] - c0[1]) * t)
|
||||||
|
b = int(c0[2] + (c1[2] - c0[2]) * t)
|
||||||
|
lut.append((r, g, b))
|
||||||
|
|
||||||
|
midpoints = self.driver.strip_midpoints
|
||||||
|
rate = max(1, int(preset.n1) or 1)
|
||||||
|
delay_ms = max(1, int(preset.d) or 1)
|
||||||
|
margin = max(0, int(preset.n2) or 0)
|
||||||
|
|
||||||
|
left = {}
|
||||||
|
right = {}
|
||||||
|
for idx in active_indices:
|
||||||
|
if 0 <= idx < len(strips):
|
||||||
|
strip = strips[idx]
|
||||||
|
n = strip.num_leds
|
||||||
|
mid = midpoints[idx]
|
||||||
|
left[idx] = margin
|
||||||
|
right[idx] = n - margin
|
||||||
|
|
||||||
|
last_update = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||||
|
yield
|
||||||
|
continue
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
for idx in active_indices:
|
||||||
|
if idx < 0 or idx >= len(strips):
|
||||||
|
continue
|
||||||
|
strip = strips[idx]
|
||||||
|
n = strip.num_leds
|
||||||
|
mid = midpoints[idx]
|
||||||
|
|
||||||
|
step = max(1, rate // 2) if idx == 0 else rate
|
||||||
|
new_left = min(mid - SPAN, left[idx] + step)
|
||||||
|
new_right = max(mid + SPAN + 1, right[idx] - step)
|
||||||
|
|
||||||
|
left_len = max(0, (mid - SPAN) - new_left)
|
||||||
|
right_len = max(0, new_right - (mid + SPAN + 1))
|
||||||
|
bright = strip.brightness
|
||||||
|
ar = strip.ar
|
||||||
|
|
||||||
|
# Clear arm regions to black so contracted pixels turn off
|
||||||
|
for i in range(margin, mid - SPAN):
|
||||||
|
if 0 <= i < n:
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = ar[base + 1] = ar[base + 2] = 0
|
||||||
|
for i in range(mid + SPAN + 1, n - margin):
|
||||||
|
if 0 <= i < n:
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = ar[base + 1] = ar[base + 2] = 0
|
||||||
|
|
||||||
|
for j, i in enumerate(range(new_left, mid - SPAN)):
|
||||||
|
if 0 <= i < n:
|
||||||
|
t = 1 - j / (left_len - 1) if left_len > 1 else 0
|
||||||
|
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||||
|
r, g, b = lut[lut_idx]
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = int(g * bright)
|
||||||
|
ar[base + 1] = int(r * bright)
|
||||||
|
ar[base + 2] = int(b * bright)
|
||||||
|
|
||||||
|
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
|
||||||
|
if 0 <= i < n:
|
||||||
|
t = j / (right_len - 1) if right_len > 1 else 0
|
||||||
|
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||||
|
r, g, b = lut[lut_idx]
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = int(g * bright)
|
||||||
|
ar[base + 1] = int(r * bright)
|
||||||
|
ar[base + 2] = int(b * bright)
|
||||||
|
|
||||||
|
left[idx] = new_left
|
||||||
|
right[idx] = new_right
|
||||||
|
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
# Check if all arms have contracted to center - run once, then hold
|
||||||
|
all_done = True
|
||||||
|
for idx in active_indices:
|
||||||
|
if idx < 0 or idx >= len(strips):
|
||||||
|
continue
|
||||||
|
mid = midpoints[idx]
|
||||||
|
if left[idx] < mid - SPAN or right[idx] > mid + SPAN + 1:
|
||||||
|
all_done = False
|
||||||
|
break
|
||||||
|
if all_done:
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
yield
|
||||||
68
pico/src/patterns/point.py
Normal file
68
pico/src/patterns/point.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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
|
||||||
|
|
||||||
75
pico/src/patterns/pose.py
Normal file
75
pico/src/patterns/pose.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
class Pose:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
"""
|
||||||
|
Pose pattern: simple static bands that turn the hoop on
|
||||||
|
within the specified n ranges, across ALL strips.
|
||||||
|
|
||||||
|
Uses the preset's n values as inclusive ranges over the
|
||||||
|
logical ring (driver.n):
|
||||||
|
|
||||||
|
- n1–n2: color c[0]
|
||||||
|
- n3–n4: color c[1]
|
||||||
|
- n5–n6: color c[2]
|
||||||
|
- n7–n8: color c[3]
|
||||||
|
"""
|
||||||
|
# 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, max_len):
|
||||||
|
a = int(a)
|
||||||
|
b = int(b)
|
||||||
|
if a > b:
|
||||||
|
a, b = b, a
|
||||||
|
if b < 0 or a >= max_len:
|
||||||
|
return None
|
||||||
|
a = max(0, a)
|
||||||
|
b = min(max_len - 1, b)
|
||||||
|
if a > b:
|
||||||
|
return None
|
||||||
|
return a, b
|
||||||
|
|
||||||
|
# For Pose, apply the same ranges on EVERY strip:
|
||||||
|
# each color band is repeated across all strips.
|
||||||
|
for strip in self.driver.strips:
|
||||||
|
strip_len = strip.num_leds
|
||||||
|
|
||||||
|
ranges = []
|
||||||
|
r1 = norm_range(getattr(preset, "n1", 0), getattr(preset, "n2", -1), strip_len)
|
||||||
|
if r1:
|
||||||
|
ranges.append((r1[0], r1[1], c1))
|
||||||
|
r2 = norm_range(getattr(preset, "n3", 0), getattr(preset, "n4", -1), strip_len)
|
||||||
|
if r2:
|
||||||
|
ranges.append((r2[0], r2[1], c2))
|
||||||
|
r3 = norm_range(getattr(preset, "n5", 0), getattr(preset, "n6", -1), strip_len)
|
||||||
|
if r3:
|
||||||
|
ranges.append((r3[0], r3[1], c3))
|
||||||
|
r4 = norm_range(getattr(preset, "n7", 0), getattr(preset, "n8", -1), strip_len)
|
||||||
|
if r4:
|
||||||
|
ranges.append((r4[0], r4[1], c4))
|
||||||
|
|
||||||
|
# Static draw on this strip: last range wins on overlaps
|
||||||
|
for i in range(strip_len):
|
||||||
|
color = (0, 0, 0)
|
||||||
|
for start, end, c in ranges:
|
||||||
|
if start <= i <= end:
|
||||||
|
color = c
|
||||||
|
strip.set(i, color)
|
||||||
|
|
||||||
|
# Flush all strips
|
||||||
|
for strip in self.driver.strips:
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
@@ -1,51 +1,97 @@
|
|||||||
import utime
|
import utime
|
||||||
|
|
||||||
|
|
||||||
|
def _hue_to_rgb(hue):
|
||||||
|
"""Hue 0..360 -> (r, g, b). Simple HSV with S=V=1."""
|
||||||
|
h = hue % 360
|
||||||
|
x = 1 - abs((h / 60) % 2 - 1)
|
||||||
|
if h < 60:
|
||||||
|
r, g, b = 1, x, 0
|
||||||
|
elif h < 120:
|
||||||
|
r, g, b = x, 1, 0
|
||||||
|
elif h < 180:
|
||||||
|
r, g, b = 0, 1, x
|
||||||
|
elif h < 240:
|
||||||
|
r, g, b = 0, x, 1
|
||||||
|
elif h < 300:
|
||||||
|
r, g, b = x, 0, 1
|
||||||
|
else:
|
||||||
|
r, g, b = 1, 0, x
|
||||||
|
return (int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
|
||||||
|
|
||||||
|
def _make_rainbow_double(num_leds, brightness=1.0):
|
||||||
|
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len_bytes).
|
||||||
|
DMA reads double_buf[head:head+strip_len] with no copy."""
|
||||||
|
n = 2 * num_leds
|
||||||
|
double_buf = bytearray(n * 3)
|
||||||
|
for i in range(n):
|
||||||
|
hue = (i / n) * 360 * 2
|
||||||
|
r, g, b = _hue_to_rgb(hue)
|
||||||
|
double_buf[i * 3] = int(g * brightness) & 0xFF
|
||||||
|
double_buf[i * 3 + 1] = int(r * brightness) & 0xFF
|
||||||
|
double_buf[i * 3 + 2] = int(b * brightness) & 0xFF
|
||||||
|
strip_len_bytes = num_leds * 3
|
||||||
|
return (double_buf, strip_len_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_buffers(driver, preset, buffers_cache):
|
||||||
|
"""Build or refresh per-strip double buffers with current brightness. Returns (rainbow_data, cumulative_bytes)."""
|
||||||
|
effective = (preset.b * driver.b) / (255 * 255)
|
||||||
|
key = (preset.b, driver.b)
|
||||||
|
if buffers_cache.get("key") == key and buffers_cache.get("data"):
|
||||||
|
return buffers_cache["data"], buffers_cache["cumulative_bytes"]
|
||||||
|
strips = driver.strips
|
||||||
|
rainbow_data = [_make_rainbow_double(s.num_leds, effective) for s in strips]
|
||||||
|
cumulative_bytes = [0]
|
||||||
|
for s in strips:
|
||||||
|
cumulative_bytes.append(cumulative_bytes[-1] + s.num_leds * 3)
|
||||||
|
buffers_cache["key"] = key
|
||||||
|
buffers_cache["data"] = rainbow_data
|
||||||
|
buffers_cache["cumulative_bytes"] = cumulative_bytes
|
||||||
|
return rainbow_data, cumulative_bytes
|
||||||
|
|
||||||
|
|
||||||
class Rainbow:
|
class Rainbow:
|
||||||
def __init__(self, driver):
|
def __init__(self, driver):
|
||||||
self.driver = driver
|
self.driver = driver
|
||||||
|
self._buffers_cache = {}
|
||||||
def _wheel(self, pos):
|
|
||||||
if pos < 85:
|
|
||||||
return (pos * 3, 255 - pos * 3, 0)
|
|
||||||
elif pos < 170:
|
|
||||||
pos -= 85
|
|
||||||
return (255 - pos * 3, 0, pos * 3)
|
|
||||||
else:
|
|
||||||
pos -= 170
|
|
||||||
return (0, pos * 3, 255 - pos * 3)
|
|
||||||
|
|
||||||
def run(self, preset):
|
def run(self, preset):
|
||||||
step = self.driver.step % 256
|
step_amount = max(1, int(preset.n1)) # n1 = bytes to advance per frame (speed)
|
||||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
total_ring_bytes = self.driver.num_leds * 3
|
||||||
|
# Phase in bytes; driver.step kept in 0..255 for compatibility
|
||||||
|
phase = (self.driver.step * total_ring_bytes) // 256
|
||||||
|
|
||||||
# If auto is False, run a single step and then stop
|
rainbow_data, cumulative_bytes = _ensure_buffers(
|
||||||
|
self.driver, preset, self._buffers_cache
|
||||||
|
)
|
||||||
|
strips = self.driver.strips
|
||||||
|
|
||||||
|
def show_frame(phase):
|
||||||
|
for i, (strip, (double_buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
|
||||||
|
head = (phase + cumulative_bytes[i]) % strip_len_bytes
|
||||||
|
strip.show(double_buf, head)
|
||||||
|
self.driver.step = (phase * 256) // total_ring_bytes
|
||||||
|
|
||||||
|
# Single step then stop
|
||||||
if not preset.a:
|
if not preset.a:
|
||||||
for i in range(self.driver.num_leds):
|
show_frame(phase)
|
||||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
phase = (phase + step_amount) % total_ring_bytes
|
||||||
self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b)
|
self.driver.step = (phase * 256) // total_ring_bytes
|
||||||
self.driver.n.write()
|
|
||||||
# Increment step by n1 for next manual call
|
|
||||||
self.driver.step = (step + step_amount) % 256
|
|
||||||
# Allow tick() to advance the generator once
|
|
||||||
yield
|
yield
|
||||||
return
|
return
|
||||||
|
|
||||||
last_update = utime.ticks_ms()
|
last_update = utime.ticks_ms()
|
||||||
|
sleep_ms = max(1, int(preset.d))
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
current_time = utime.ticks_ms()
|
current_time = utime.ticks_ms()
|
||||||
sleep_ms = max(1, int(preset.d)) # Get delay from preset
|
|
||||||
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
if utime.ticks_diff(current_time, last_update) >= sleep_ms:
|
||||||
for i in range(self.driver.num_leds):
|
rainbow_data, cumulative_bytes = _ensure_buffers(
|
||||||
rc_index = (i * 256 // self.driver.num_leds) + step
|
self.driver, preset, self._buffers_cache
|
||||||
self.driver.n[i] = self.driver.apply_brightness(
|
)
|
||||||
self._wheel(rc_index & 255),
|
show_frame(phase)
|
||||||
preset.b,
|
phase = (phase + step_amount) % total_ring_bytes
|
||||||
)
|
|
||||||
self.driver.n.write()
|
|
||||||
step = (step + step_amount) % 256
|
|
||||||
self.driver.step = step
|
|
||||||
last_update = current_time
|
last_update = current_time
|
||||||
# Yield once per tick so other logic can run
|
|
||||||
yield
|
yield
|
||||||
|
|||||||
154
pico/src/patterns/roll.py
Normal file
154
pico/src/patterns/roll.py
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import utime
|
||||||
|
|
||||||
|
|
||||||
|
class Roll:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
"""Roll: moving band with gradient from color1 to color2 over the strips.
|
||||||
|
|
||||||
|
- 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)
|
||||||
|
- n4: direction (0 = clockwise, 1 = anti-clockwise)
|
||||||
|
- c[0]: color1 at the head strip
|
||||||
|
- c[1]: color2 at the tail strip
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
|
||||||
|
def draw(head):
|
||||||
|
# Remember head strip for flare
|
||||||
|
try:
|
||||||
|
self.driver.last_roll_head = head
|
||||||
|
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
|
||||||
|
else:
|
||||||
|
dist = (strip_idx - head) % n_segments
|
||||||
|
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
if not preset.a:
|
||||||
|
head = step % n_segments if n_segments > 0 else 0
|
||||||
|
draw(head)
|
||||||
|
self.driver.step = step + 1
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
|
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)
|
||||||
|
step += 1
|
||||||
|
|
||||||
|
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
|
||||||
|
yield
|
||||||
9
pico/src/patterns/seat.py
Normal file
9
pico/src/patterns/seat.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Seat:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
98
pico/src/patterns/spin.py
Normal file
98
pico/src/patterns/spin.py
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
"""Spin: continues from Grab — segment (10 each side of center) moves slowly up to the top. Preset color, n1 = rate."""
|
||||||
|
|
||||||
|
import utime
|
||||||
|
|
||||||
|
SPAN = 10 # LEDs on each side of center (match Grab)
|
||||||
|
LUT_SIZE = 256 # gradient lookup table entries
|
||||||
|
|
||||||
|
|
||||||
|
class Spin:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
active_indices = (0, 4)
|
||||||
|
c0 = preset.c[0]
|
||||||
|
c1 = preset.c[1]
|
||||||
|
|
||||||
|
# Precompute gradient LUT: t in [0,1] maps to (r,g,b)
|
||||||
|
lut = []
|
||||||
|
for k in range(LUT_SIZE):
|
||||||
|
t = k / (LUT_SIZE - 1) if LUT_SIZE > 1 else 1
|
||||||
|
r = int(c0[0] + (c1[0] - c0[0]) * t)
|
||||||
|
g = int(c0[1] + (c1[1] - c0[1]) * t)
|
||||||
|
b = int(c0[2] + (c1[2] - c0[2]) * t)
|
||||||
|
lut.append((r, g, b))
|
||||||
|
|
||||||
|
# For each active strip we expand from just outside the grab center
|
||||||
|
# left: from (mid - SPAN) down to 0
|
||||||
|
# right: from (mid + SPAN) up to end
|
||||||
|
midpoints = self.driver.strip_midpoints
|
||||||
|
rate = max(1, int(preset.n1) or 1)
|
||||||
|
delay_ms = max(1, int(preset.d) or 1)
|
||||||
|
margin = max(0, int(preset.n2) or 0)
|
||||||
|
|
||||||
|
# Track current extents of each arm
|
||||||
|
left = {}
|
||||||
|
right = {}
|
||||||
|
for idx in active_indices:
|
||||||
|
if 0 <= idx < len(strips):
|
||||||
|
mid = midpoints[idx]
|
||||||
|
left[idx] = mid - SPAN # inner edge of left arm
|
||||||
|
right[idx] = mid + SPAN + 1 # inner edge of right arm
|
||||||
|
|
||||||
|
last_update = utime.ticks_ms()
|
||||||
|
|
||||||
|
while True:
|
||||||
|
now = utime.ticks_ms()
|
||||||
|
if utime.ticks_diff(now, last_update) < delay_ms:
|
||||||
|
yield
|
||||||
|
continue
|
||||||
|
last_update = now
|
||||||
|
|
||||||
|
for idx in active_indices:
|
||||||
|
if idx < 0 or idx >= len(strips):
|
||||||
|
continue
|
||||||
|
strip = strips[idx]
|
||||||
|
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
|
||||||
|
new_left = max(margin, left[idx] - step)
|
||||||
|
new_right = min(n - margin, right[idx] + step)
|
||||||
|
|
||||||
|
# Left arm: c1 at outer, c0 at inner. Right arm: c0 at inner, c1 at outer.
|
||||||
|
left_len = max(0, (mid - SPAN) - new_left)
|
||||||
|
right_len = max(0, new_right - (mid + SPAN + 1))
|
||||||
|
bright = strip.brightness
|
||||||
|
ar = strip.ar
|
||||||
|
|
||||||
|
for j, i in enumerate(range(new_left, mid - SPAN)):
|
||||||
|
if 0 <= i < n:
|
||||||
|
t = 1 - j / (left_len - 1) if left_len > 1 else 0
|
||||||
|
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||||
|
r, g, b = lut[lut_idx]
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = int(g * bright)
|
||||||
|
ar[base + 1] = int(r * bright)
|
||||||
|
ar[base + 2] = int(b * bright)
|
||||||
|
|
||||||
|
for j, i in enumerate(range(mid + SPAN + 1, new_right)):
|
||||||
|
if 0 <= i < n:
|
||||||
|
t = j / (right_len - 1) if right_len > 1 else 0
|
||||||
|
lut_idx = min(int(t * (LUT_SIZE - 1)), LUT_SIZE - 1)
|
||||||
|
r, g, b = lut[lut_idx]
|
||||||
|
base = i * 3
|
||||||
|
ar[base] = int(g * bright)
|
||||||
|
ar[base + 1] = int(r * bright)
|
||||||
|
ar[base + 2] = int(b * bright)
|
||||||
|
|
||||||
|
left[idx] = new_left
|
||||||
|
right[idx] = new_right
|
||||||
|
|
||||||
|
# Show only on this strip
|
||||||
|
strip.show()
|
||||||
|
|
||||||
|
yield
|
||||||
9
pico/src/patterns/split.py
Normal file
9
pico/src/patterns/split.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Split:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
9
pico/src/patterns/straddle.py
Normal file
9
pico/src/patterns/straddle.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""Placeholder until implemented."""
|
||||||
|
|
||||||
|
class Straddle:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
while True:
|
||||||
|
yield
|
||||||
28
pico/src/patterns/test.py
Normal file
28
pico/src/patterns/test.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Test pattern: strip i has (i+1) LEDs on at the start (indices 0..i) plus the midpoint LED on. 50% red."""
|
||||||
|
|
||||||
|
BRIGHTNESS = 0.50
|
||||||
|
|
||||||
|
RED = (255, 0, 0)
|
||||||
|
|
||||||
|
|
||||||
|
def _scale(color, factor):
|
||||||
|
return tuple(int(c * factor) for c in color)
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def __init__(self, driver):
|
||||||
|
self.driver = driver
|
||||||
|
|
||||||
|
def run(self, preset):
|
||||||
|
strips = self.driver.strips
|
||||||
|
red = _scale(RED, BRIGHTNESS)
|
||||||
|
for strip_idx, strip in enumerate(strips):
|
||||||
|
n = strip.num_leds
|
||||||
|
mid = self.driver.strip_midpoints[strip_idx] # from STRIP_CONFIG
|
||||||
|
strip.fill((0, 0, 0))
|
||||||
|
# First (strip_idx + 1) LEDs on: indices 0..strip_idx
|
||||||
|
for i in range(min(strip_idx + 1, n)):
|
||||||
|
strip.set(i, red)
|
||||||
|
# Midpoint LED on
|
||||||
|
strip.set(mid, red)
|
||||||
|
strip.show()
|
||||||
@@ -12,6 +12,8 @@ class Preset:
|
|||||||
self.n4 = 0
|
self.n4 = 0
|
||||||
self.n5 = 0
|
self.n5 = 0
|
||||||
self.n6 = 0
|
self.n6 = 0
|
||||||
|
self.n7 = 0
|
||||||
|
self.n8 = 0
|
||||||
|
|
||||||
# Override defaults with provided data
|
# Override defaults with provided data
|
||||||
self.edit(data)
|
self.edit(data)
|
||||||
@@ -76,4 +78,6 @@ class Preset:
|
|||||||
"n4": self.n4,
|
"n4": self.n4,
|
||||||
"n5": self.n5,
|
"n5": self.n5,
|
||||||
"n6": self.n6,
|
"n6": self.n6,
|
||||||
|
"n7": self.n7,
|
||||||
|
"n8": self.n8,
|
||||||
}
|
}
|
||||||
|
|||||||
618
pico/src/presets.json
Normal file
618
pico/src/presets.json
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
{
|
||||||
|
"start": {
|
||||||
|
"p": "off",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"grab": {
|
||||||
|
"p": "grab",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[64,0,255]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"spin1": {
|
||||||
|
"p": "spin",
|
||||||
|
"d": 0,
|
||||||
|
"b": 100,
|
||||||
|
"c": [
|
||||||
|
[64,0,255],
|
||||||
|
[255,105,180]
|
||||||
|
],
|
||||||
|
"n1": 1,
|
||||||
|
"n2": 20,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"lift": {
|
||||||
|
"p": "lift",
|
||||||
|
"d": 0,
|
||||||
|
"b": 100,
|
||||||
|
"c": [
|
||||||
|
[64,0,255],
|
||||||
|
[255,105,180]
|
||||||
|
],
|
||||||
|
"n1": 1,
|
||||||
|
"n2": 20,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"flare": {
|
||||||
|
"p": "flare",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"hook": {
|
||||||
|
"p": "hook",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 7,
|
||||||
|
"n3": 15,
|
||||||
|
"n4": 15
|
||||||
|
},
|
||||||
|
"roll1": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 200,
|
||||||
|
"b": 100,
|
||||||
|
"c": [
|
||||||
|
[64,0,255],
|
||||||
|
[20,20,40]
|
||||||
|
],
|
||||||
|
"n1": 50,
|
||||||
|
"n2": 160,
|
||||||
|
"n3": 1,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"invertsplit": {
|
||||||
|
"p": "invertsplit",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"pose1": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,0,0],
|
||||||
|
[0,255,0],
|
||||||
|
[0,0,255],
|
||||||
|
[255,255,255]
|
||||||
|
],
|
||||||
|
"n1": 100,
|
||||||
|
"n2": 150,
|
||||||
|
"n3": 650,
|
||||||
|
"n4": 700,
|
||||||
|
"n5": 1200,
|
||||||
|
"n6": 1250,
|
||||||
|
"n7": 1750,
|
||||||
|
"n8": 1800
|
||||||
|
},
|
||||||
|
"pose2": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,105,180],
|
||||||
|
[64,0,255],
|
||||||
|
[255,165,0],
|
||||||
|
[0,255,255]
|
||||||
|
],
|
||||||
|
"n1": 150,
|
||||||
|
"n2": 200,
|
||||||
|
"n3": 700,
|
||||||
|
"n4": 750,
|
||||||
|
"n5": 1250,
|
||||||
|
"n6": 1300,
|
||||||
|
"n7": 1800,
|
||||||
|
"n8": 1850
|
||||||
|
},
|
||||||
|
"roll2": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"backbalance1": {
|
||||||
|
"p": "backbalance",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"beat1": {
|
||||||
|
"p": "beat",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"pose3": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,255,0],
|
||||||
|
[255,0,255],
|
||||||
|
[0,255,255],
|
||||||
|
[255,255,255]
|
||||||
|
],
|
||||||
|
"n1": 200,
|
||||||
|
"n2": 250,
|
||||||
|
"n3": 750,
|
||||||
|
"n4": 800,
|
||||||
|
"n5": 1300,
|
||||||
|
"n6": 1350,
|
||||||
|
"n7": 1850,
|
||||||
|
"n8": 1900
|
||||||
|
},
|
||||||
|
"roll3": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"crouch": {
|
||||||
|
"p": "crouch",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"pose4": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[64,0,255],
|
||||||
|
[255,105,180],
|
||||||
|
[255,255,255],
|
||||||
|
[255,140,0]
|
||||||
|
],
|
||||||
|
"n1": 250,
|
||||||
|
"n2": 300,
|
||||||
|
"n3": 800,
|
||||||
|
"n4": 850,
|
||||||
|
"n5": 1350,
|
||||||
|
"n6": 1400,
|
||||||
|
"n7": 1900,
|
||||||
|
"n8": 1950
|
||||||
|
},
|
||||||
|
"roll4": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"backbendsplit": {
|
||||||
|
"p": "backbendsplit",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"backbalance2": {
|
||||||
|
"p": "backbalance",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"backbalance3": {
|
||||||
|
"p": "backbalance",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"beat2": {
|
||||||
|
"p": "beat",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"straddle": {
|
||||||
|
"p": "straddle",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"beat3": {
|
||||||
|
"p": "beat",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"frontbalance1": {
|
||||||
|
"p": "frontbalance",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"pose5": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,0,127],
|
||||||
|
[0,127,255],
|
||||||
|
[127,255,0],
|
||||||
|
[255,255,255]
|
||||||
|
],
|
||||||
|
"n1": 300,
|
||||||
|
"n2": 350,
|
||||||
|
"n3": 850,
|
||||||
|
"n4": 900,
|
||||||
|
"n5": 1400,
|
||||||
|
"n6": 1450,
|
||||||
|
"n7": 1950,
|
||||||
|
"n8": 2000
|
||||||
|
},
|
||||||
|
"pose6": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,80,0],
|
||||||
|
[0,200,120],
|
||||||
|
[80,0,255],
|
||||||
|
[255,255,255]
|
||||||
|
],
|
||||||
|
"n1": 350,
|
||||||
|
"n2": 400,
|
||||||
|
"n3": 900,
|
||||||
|
"n4": 950,
|
||||||
|
"n5": 1450,
|
||||||
|
"n6": 1500,
|
||||||
|
"n7": 2000,
|
||||||
|
"n8": 2050
|
||||||
|
},
|
||||||
|
"elbowhang": {
|
||||||
|
"p": "elbowhang",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"elbowhangspin": {
|
||||||
|
"p": "elbowhangspin",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"spin2": {
|
||||||
|
"p": "spin",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"dismount": {
|
||||||
|
"p": "dismount",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"spin3": {
|
||||||
|
"p": "spin",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"fluff": {
|
||||||
|
"p": "fluff",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"spin4": {
|
||||||
|
"p": "spin",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"flare2": {
|
||||||
|
"p": "flare",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"elbowhangsplit2": {
|
||||||
|
"p": "elbowhangsplit",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"invert": {
|
||||||
|
"p": "invert",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"roll5": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"backbend": {
|
||||||
|
"p": "backbend",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"pose7": {
|
||||||
|
"p": "point",
|
||||||
|
"d": 0,
|
||||||
|
"b": 220,
|
||||||
|
"c": [
|
||||||
|
[255,0,0],
|
||||||
|
[255,165,0],
|
||||||
|
[255,255,0],
|
||||||
|
[255,255,255]
|
||||||
|
],
|
||||||
|
"n1": 400,
|
||||||
|
"n2": 450,
|
||||||
|
"n3": 950,
|
||||||
|
"n4": 1000,
|
||||||
|
"n5": 1500,
|
||||||
|
"n6": 1550,
|
||||||
|
"n7": 2050,
|
||||||
|
"n8": 2100
|
||||||
|
},
|
||||||
|
"roll6": {
|
||||||
|
"p": "roll",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"seat": {
|
||||||
|
"p": "seat",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"kneehang": {
|
||||||
|
"p": "kneehang",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"legswoop": {
|
||||||
|
"p": "legswoop",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"split": {
|
||||||
|
"p": "split",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"foothang": {
|
||||||
|
"p": "foothang",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"p": "off",
|
||||||
|
"d": 0,
|
||||||
|
"b": 0,
|
||||||
|
"c": [
|
||||||
|
[0,0,0]
|
||||||
|
],
|
||||||
|
"n1": 0,
|
||||||
|
"n2": 0,
|
||||||
|
"n3": 0,
|
||||||
|
"n4": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,92 @@
|
|||||||
from machine import Pin
|
from machine import Pin
|
||||||
from ws2812 import WS2812B
|
from ws2812 import WS2812B
|
||||||
from preset import Preset
|
from preset import Preset
|
||||||
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
|
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,
|
||||||
|
)
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
# Order: strips[0]=physical 1 … strips[7]=physical 8. (pin, num_leds, midpoint_index).
|
||||||
|
STRIP_CONFIG = (
|
||||||
|
(6, 291, 291 // 2), # 1
|
||||||
|
(29, 290, 290 // 2-1), # 2
|
||||||
|
(3, 283, 283 // 2), # 3
|
||||||
|
(28, 278, 278 // 2-1), # 4
|
||||||
|
(2, 278, 275 // 2), # 5 (bottom of hoop)
|
||||||
|
(0, 283, 278 // 2-1), # 6
|
||||||
|
(4, 290, 283 // 2), # 7
|
||||||
|
(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, pin, num_leds, state_machine=0):
|
def __init__(self):
|
||||||
|
|
||||||
|
self.strips = []
|
||||||
|
self.strip_midpoints = [] # midpoint LED index per strip (from STRIP_CONFIG)
|
||||||
|
|
||||||
|
state_machine = 0
|
||||||
|
for entry in STRIP_CONFIG:
|
||||||
|
pin, num_leds = entry[0], entry[1]
|
||||||
|
mid = entry[2] if len(entry) >= 3 else num_leds // 2
|
||||||
|
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
|
||||||
# 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)
|
||||||
num_leds = int(num_leds)
|
|
||||||
if isinstance(pin, Pin):
|
|
||||||
self.n = WS2812B(pin, num_leds) # NeoPixel-style (Pin, n)
|
|
||||||
else:
|
|
||||||
self.n = WS2812B(num_leds, int(pin), state_machine, brightness=1.0)
|
|
||||||
self.num_leds = num_leds
|
|
||||||
self.step = 0
|
self.step = 0
|
||||||
|
# Remember which strip was last used as the roll head (for flare, etc.)
|
||||||
|
self.last_roll_head = 0
|
||||||
# Global brightness (0–255), controlled via UART/JSON {"b": <value>}
|
# Global brightness (0–255), controlled via UART/JSON {"b": <value>}
|
||||||
self.b = 255
|
self.b = 255
|
||||||
|
|
||||||
@@ -32,8 +104,79 @@ class Presets:
|
|||||||
"transition": Transition(self).run,
|
"transition": Transition(self).run,
|
||||||
"chase": Chase(self).run,
|
"chase": Chase(self).run,
|
||||||
"circle": Circle(self).run,
|
"circle": Circle(self).run,
|
||||||
|
"roll": Roll(self).run,
|
||||||
|
"calibration": Calibration(self).run,
|
||||||
|
"test": Test(self).run,
|
||||||
|
"grab": Grab(self).run,
|
||||||
|
"spin": Spin(self).run,
|
||||||
|
"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,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# --- Strip geometry utilities -------------------------------------------------
|
||||||
|
|
||||||
|
def strip_length(self, strip_idx):
|
||||||
|
"""Return number of LEDs for a physical strip index."""
|
||||||
|
if 0 <= strip_idx < len(self.strips):
|
||||||
|
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):
|
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:
|
||||||
@@ -87,19 +230,26 @@ class Presets:
|
|||||||
self.generator = None
|
self.generator = None
|
||||||
|
|
||||||
def select(self, preset_name, step=None):
|
def select(self, preset_name, step=None):
|
||||||
|
if preset_name is None:
|
||||||
|
return False
|
||||||
|
print(f"Selecting preset: {preset_name}")
|
||||||
|
preset = None
|
||||||
|
pattern_key = preset_name
|
||||||
if preset_name in self.presets:
|
if preset_name in self.presets:
|
||||||
preset = self.presets[preset_name]
|
preset = self.presets[preset_name]
|
||||||
if preset.p in self.patterns:
|
pattern_key = preset.p
|
||||||
# Set step value if explicitly provided
|
if pattern_key not in self.patterns:
|
||||||
if step is not None:
|
return False
|
||||||
self.step = step
|
# Run by pattern name (works for saved presets and built-ins like calibration, off, test)
|
||||||
elif preset.p == "off" or self.selected != preset_name:
|
if preset is None:
|
||||||
self.step = 0
|
preset = Preset({"p": pattern_key}) if pattern_key != "off" else None
|
||||||
self.generator = self.patterns[preset.p](preset)
|
if step is not None:
|
||||||
self.selected = preset_name # Store the preset name, not the object
|
self.step = step
|
||||||
return True
|
elif pattern_key == "off" or self.selected != preset_name:
|
||||||
# If preset doesn't exist or pattern not found, default to "off"
|
self.step = 0
|
||||||
return False
|
self.generator = self.patterns[pattern_key](preset)
|
||||||
|
self.selected = preset_name
|
||||||
|
return True
|
||||||
|
|
||||||
def update_num_leds(self, pin, num_leds):
|
def update_num_leds(self, pin, num_leds):
|
||||||
num_leds = int(num_leds)
|
num_leds = int(num_leds)
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
import json
|
|
||||||
import ubinascii
|
|
||||||
import machine
|
|
||||||
|
|
||||||
class Settings(dict):
|
|
||||||
SETTINGS_FILE = "/settings.json"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__()
|
|
||||||
self.load() # Load settings from file during initialization
|
|
||||||
self.color_order = self.get_color_order(self["color_order"])
|
|
||||||
|
|
||||||
def _default_name(self):
|
|
||||||
"""Device name: use unique_id on Pico (no WiFi); use AP MAC on ESP32."""
|
|
||||||
try:
|
|
||||||
import network
|
|
||||||
mac = network.WLAN(network.AP_IF).config("mac")
|
|
||||||
return "led-%s" % ubinascii.hexlify(mac).decode()
|
|
||||||
except Exception:
|
|
||||||
return "led-%s" % ubinascii.hexlify(machine.unique_id()).decode()
|
|
||||||
|
|
||||||
def set_defaults(self):
|
|
||||||
self["led_pin"] = 10
|
|
||||||
self["num_leds"] = 50
|
|
||||||
self["color_order"] = "rgb"
|
|
||||||
self["name"] = self._default_name()
|
|
||||||
self["debug"] = False
|
|
||||||
self["startup_preset"] = None
|
|
||||||
self["brightness"] = 255
|
|
||||||
|
|
||||||
def save(self):
|
|
||||||
try:
|
|
||||||
j = json.dumps(self)
|
|
||||||
with open(self.SETTINGS_FILE, 'w') as file:
|
|
||||||
file.write(j)
|
|
||||||
print("Settings saved successfully.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error saving settings: {e}")
|
|
||||||
|
|
||||||
def load(self):
|
|
||||||
try:
|
|
||||||
with open(self.SETTINGS_FILE, 'r') as file:
|
|
||||||
loaded_settings = json.load(file)
|
|
||||||
self.update(loaded_settings)
|
|
||||||
print("Settings loaded successfully.")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading settings")
|
|
||||||
self.set_defaults()
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
def get_color_order(self, color_order):
|
|
||||||
"""Convert color order string to tuple of hex string indices."""
|
|
||||||
color_orders = {
|
|
||||||
"rgb": (1, 3, 5),
|
|
||||||
"rbg": (1, 5, 3),
|
|
||||||
"grb": (3, 1, 5),
|
|
||||||
"gbr": (3, 5, 1),
|
|
||||||
"brg": (5, 1, 3),
|
|
||||||
"bgr": (5, 3, 1)
|
|
||||||
}
|
|
||||||
return color_orders.get(color_order.lower(), (1, 3, 5)) # Default to RGB
|
|
||||||
|
|
||||||
def get_rgb_channel_order(self, color_order=None):
|
|
||||||
"""Convert color order string to RGB channel indices for reordering tuples.
|
|
||||||
Returns tuple of channel indices: (r_channel, g_channel, b_channel)
|
|
||||||
Example: 'grb' -> (1, 0, 2) means (G, R, B)"""
|
|
||||||
if color_order is None:
|
|
||||||
color_order = self.get("color_order", "rgb")
|
|
||||||
color_order = color_order.lower()
|
|
||||||
# Map hex string positions to RGB channel indices
|
|
||||||
# Position 1 (R in hex) -> channel 0, Position 3 (G) -> channel 1, Position 5 (B) -> channel 2
|
|
||||||
hex_to_channel = {1: 0, 3: 1, 5: 2}
|
|
||||||
hex_indices = self.get_color_order(color_order)
|
|
||||||
return tuple(hex_to_channel[pos] for pos in hex_indices)
|
|
||||||
|
|
||||||
# Example usage
|
|
||||||
def main():
|
|
||||||
settings = Settings()
|
|
||||||
print(f"Number of LEDs: {settings['num_leds']}")
|
|
||||||
settings['num_leds'] = 100
|
|
||||||
print(f"Updated number of LEDs: {settings['num_leds']}")
|
|
||||||
settings.save()
|
|
||||||
|
|
||||||
# Create a new Settings object to test loading
|
|
||||||
new_settings = Settings()
|
|
||||||
print(f"Loaded number of LEDs: {new_settings['num_leds']}")
|
|
||||||
print(settings)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Run the example
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
78
pico/test/chase.py
Normal file
78
pico/test/chase.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
# --- Chase test: pregenerated double buffer per strip, show via head offset (same as rainbow) ---
|
||||||
|
|
||||||
|
# (pin, num_leds) per strip — same config as rainbow
|
||||||
|
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
|
||||||
|
|
||||||
|
cumulative_leds = [0]
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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."""
|
||||||
|
n = 2 * num_leds
|
||||||
|
buf = bytearray(n * 3)
|
||||||
|
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)
|
||||||
|
else:
|
||||||
|
r = grn = b_ = 0
|
||||||
|
o = b * 3
|
||||||
|
buf[o] = grn
|
||||||
|
buf[o + 1] = r
|
||||||
|
buf[o + 2] = b_
|
||||||
|
return buf
|
||||||
|
|
||||||
|
|
||||||
|
# Pregenerate one double buffer per strip
|
||||||
|
chase_buffers = [
|
||||||
|
make_chase_double(ws.num_leds, cumulative_leds[i], total_ring_leds, CHASE_COLOR, TRAIL_LEN)
|
||||||
|
for i, ws in enumerate(strips)
|
||||||
|
]
|
||||||
|
|
||||||
|
chase_pos = 0
|
||||||
|
while True:
|
||||||
|
for i, strip in enumerate(strips):
|
||||||
|
# head in [0, strip_len) so DMA read head..head+num_leds*3 stays in double buffer (same as rainbow)
|
||||||
|
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)
|
||||||
@@ -28,13 +28,13 @@ def hue_to_rgb(hue):
|
|||||||
return (int(r * 255), int(g * 255), int(b * 255))
|
return (int(r * 255), int(g * 255), int(b * 255))
|
||||||
|
|
||||||
|
|
||||||
def make_rainbow_ring(total_leds, brightness=1.0):
|
def make_rainbow_double(num_leds, brightness=1.0):
|
||||||
"""Build one rainbow over the whole ring: 2 full hue cycles over total_leds (GRB).
|
"""Build 2 full rainbow cycles (2*num_leds pixels, GRB). Returns (double_buf, strip_len).
|
||||||
Returns (double_buf, ring_len_bytes). All strips sample from this so phase is continuous."""
|
head must be in 0..strip_len-1 so DMA reads double_buf[head:head+strip_len] with no copy."""
|
||||||
n = 2 * total_leds
|
n = 2 * num_leds
|
||||||
double_buf = bytearray(n * 3)
|
double_buf = bytearray(n * 3)
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
hue = ((i % total_leds) / total_leds) * 360 * 2
|
hue = (i / n) * 360 * 2
|
||||||
r, g, b = hue_to_rgb(hue)
|
r, g, b = hue_to_rgb(hue)
|
||||||
g = int(g * brightness) & 0xFF
|
g = int(g * brightness) & 0xFF
|
||||||
r = int(r * brightness) & 0xFF
|
r = int(r * brightness) & 0xFF
|
||||||
@@ -43,48 +43,27 @@ def make_rainbow_ring(total_leds, brightness=1.0):
|
|||||||
double_buf[o] = g
|
double_buf[o] = g
|
||||||
double_buf[o + 1] = r
|
double_buf[o + 1] = r
|
||||||
double_buf[o + 2] = b
|
double_buf[o + 2] = b
|
||||||
ring_len_bytes = total_leds * 3
|
strip_len = num_leds * 3
|
||||||
return (double_buf, ring_len_bytes)
|
return (double_buf, strip_len)
|
||||||
|
|
||||||
|
|
||||||
def make_strip_rainbow(num_leds, cumulative_leds, total_ring_leds, brightness=1.0):
|
def show_rainbow(strip, double_buf, strip_len, head):
|
||||||
"""Per-strip double buffer: pixel j has hue at global position (cumulative_leds + j) % total_ring_leds.
|
"""DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1."""
|
||||||
Use same head for all strips: head = rainbow_head % (2*num_leds*3)."""
|
strip.show(double_buf, head)
|
||||||
n = 2 * num_leds
|
|
||||||
buf = bytearray(n * 3)
|
|
||||||
for j in range(n):
|
|
||||||
global_pos = (cumulative_leds + j) % total_ring_leds
|
|
||||||
hue = (global_pos / total_ring_leds) * 360 * 2
|
|
||||||
r, g, b = hue_to_rgb(hue)
|
|
||||||
g = int(g * brightness) & 0xFF
|
|
||||||
r = int(r * brightness) & 0xFF
|
|
||||||
b = int(b * brightness) & 0xFF
|
|
||||||
o = j * 3
|
|
||||||
buf[o] = g
|
|
||||||
buf[o + 1] = r
|
|
||||||
buf[o + 2] = b
|
|
||||||
strip_len_bytes = num_leds * 3
|
|
||||||
return (buf, strip_len_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
def show_rainbow_segment(strip, buf, strip_len_bytes, head):
|
# --- Strips + rainbow buffers per strip ---
|
||||||
"""DMA reads strip's segment from buf at head."""
|
# Each strip can have a different length; buffers and phase are per-strip.
|
||||||
strip.show(buf, head)
|
# Strip config must match pico/src/main.py pins.
|
||||||
|
|
||||||
|
|
||||||
# --- Strips + one global ring rainbow (all strips in phase) ---
|
|
||||||
# Each strip can have a different length; one rainbow spans total_ring_leds so hue is continuous.
|
|
||||||
|
|
||||||
# (pin, num_leds) per strip — lengths differ per segment
|
|
||||||
STRIP_CONFIG = (
|
STRIP_CONFIG = (
|
||||||
(2, 291),
|
(7, 291),
|
||||||
(3, 290),
|
(3, 290),
|
||||||
(4, 283),
|
(6, 283),
|
||||||
(7, 278),
|
|
||||||
(0, 275),
|
|
||||||
(28, 278),
|
(28, 278),
|
||||||
(29, 283),
|
(29, 275),
|
||||||
(6, 290),
|
(4, 278),
|
||||||
|
(0, 283),
|
||||||
|
(2, 290),
|
||||||
)
|
)
|
||||||
|
|
||||||
strips = []
|
strips = []
|
||||||
@@ -102,24 +81,19 @@ for ws in strips[:-1]:
|
|||||||
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds
|
||||||
bytes_per_cycle = total_ring_leds * 3
|
bytes_per_cycle = total_ring_leds * 3
|
||||||
|
|
||||||
# Per-strip rainbow buffers: each strip's segment of the ring (same phase, no shared-buffer DMA)
|
# One rainbow double buffer per strip (length = 2 * num_leds for that strip)
|
||||||
now = time.ticks_ms()
|
now = time.ticks_ms()
|
||||||
rainbow_data = [
|
rainbow_data = [make_rainbow_double(ws.num_leds, ws.brightness) for ws in strips]
|
||||||
make_strip_rainbow(ws.num_leds, cumulative_leds[i], total_ring_leds, ws.brightness)
|
# Global phase in bytes; each strip: head = (phase + cumulative_leds[i]*3) % strip_len[i]
|
||||||
for i, ws in enumerate(strips)
|
|
||||||
]
|
|
||||||
print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
||||||
|
|
||||||
rainbow_head = 0
|
rainbow_head = 0
|
||||||
step = 3
|
step = 3
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
now = time.ticks_ms()
|
now = time.ticks_ms()
|
||||||
for i, (strip, (buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)):
|
for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)):
|
||||||
# Same head for all: each strip's buffer is already offset by cumulative_leds[i]
|
head = (rainbow_head + cumulative_leds[i] * 3) % strip_len
|
||||||
double_len_bytes = 2 * strip.num_leds * 3
|
show_rainbow(strip, double_buf, strip_len, head)
|
||||||
head = rainbow_head % double_len_bytes
|
|
||||||
show_rainbow_segment(strip, buf, strip_len_bytes, head)
|
|
||||||
rainbow_head = (rainbow_head + step) % bytes_per_cycle
|
rainbow_head = (rainbow_head + step) % bytes_per_cycle
|
||||||
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
#print(time.ticks_diff(time.ticks_ms(), now), "ms")
|
||||||
time.sleep_ms(10)
|
time.sleep_ms(10)
|
||||||
|
|||||||
81
pico/test/roll_strips.py
Normal file
81
pico/test/roll_strips.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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)
|
||||||
45
pico/test/test_serial.py
Normal file
45
pico/test/test_serial.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Serial loopback test – single file, runs on Pico and ESP32.
|
||||||
|
Wire TX to RX (Pico: GP0–GP1, ESP32: 17–18), then:
|
||||||
|
mpremote run pico/test/test_serial.py
|
||||||
|
|
||||||
|
For ESP32→Pico: run test_serial_send.py on ESP32, test_serial_receive.py on Pico; wire ESP32 TX (17) to Pico RX (1).
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from machine import UART, Pin
|
||||||
|
|
||||||
|
if "esp32" in sys.platform:
|
||||||
|
UART_ID, TX_PIN, RX_PIN, BAUD = 1, 17, 18, 115200
|
||||||
|
else:
|
||||||
|
UART_ID, TX_PIN, RX_PIN, BAUD = 0, 0, 1, 115200
|
||||||
|
|
||||||
|
READ_TIMEOUT_MS = 100
|
||||||
|
LINE_TERM = b"\n"
|
||||||
|
|
||||||
|
print("UART loopback: %s UART%d TX=%s RX=%s %d baud" % (sys.platform, UART_ID, TX_PIN, RX_PIN, BAUD))
|
||||||
|
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT), rx=Pin(RX_PIN, Pin.IN))
|
||||||
|
uart.read()
|
||||||
|
to_send = [b"hello", b"123", b"{\"v\":\"1\"}"]
|
||||||
|
errors = []
|
||||||
|
for msg in to_send:
|
||||||
|
uart.write(msg + LINE_TERM)
|
||||||
|
time.sleep_ms(20)
|
||||||
|
buf = bytearray()
|
||||||
|
deadline = time.ticks_add(time.ticks_ms(), READ_TIMEOUT_MS)
|
||||||
|
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
|
||||||
|
n = uart.any()
|
||||||
|
if n:
|
||||||
|
buf.extend(uart.read(n))
|
||||||
|
if LINE_TERM in buf:
|
||||||
|
break
|
||||||
|
time.sleep_ms(2)
|
||||||
|
got = bytes(buf).strip()
|
||||||
|
if got != msg:
|
||||||
|
errors.append((msg, got))
|
||||||
|
uart.deinit()
|
||||||
|
if errors:
|
||||||
|
print("FAIL loopback:", errors)
|
||||||
|
else:
|
||||||
|
print("PASS loopback: sent and received", to_send)
|
||||||
32
pico/test/test_serial_receive.py
Normal file
32
pico/test/test_serial_receive.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Serial receive test – single file. Run on Pico (RX side).
|
||||||
|
Wire: ESP32 TX (GPIO17) → Pico RX (GPIO1); GND ↔ GND. Run send test on ESP32.
|
||||||
|
mpremote run pico/test/test_serial_receive.py
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from machine import UART, Pin
|
||||||
|
|
||||||
|
if "esp32" in sys.platform:
|
||||||
|
UART_ID, TX_PIN, RX_PIN, BAUD = 1, 17, 18, 115200
|
||||||
|
else:
|
||||||
|
UART_ID, TX_PIN, RX_PIN, BAUD = 0, 0, 1, 115200
|
||||||
|
|
||||||
|
print("UART receive: %s UART%d TX=%s RX=%s %d baud (10 s)" % (sys.platform, UART_ID, TX_PIN, RX_PIN, BAUD))
|
||||||
|
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT), rx=Pin(RX_PIN, Pin.IN))
|
||||||
|
buf = bytearray()
|
||||||
|
deadline = time.ticks_add(time.ticks_ms(), 10000)
|
||||||
|
while time.ticks_diff(deadline, time.ticks_ms()) > 0:
|
||||||
|
n = uart.any()
|
||||||
|
if n:
|
||||||
|
buf.extend(uart.read(n))
|
||||||
|
while b"\n" in buf:
|
||||||
|
idx = buf.index(b"\n")
|
||||||
|
line = bytes(buf[:idx]).strip()
|
||||||
|
buf = buf[idx + 1:]
|
||||||
|
if line:
|
||||||
|
print("rx:", line.decode("utf-8", "replace"))
|
||||||
|
time.sleep_ms(10)
|
||||||
|
uart.deinit()
|
||||||
|
print("Receive test done.")
|
||||||
23
pico/test/test_serial_send.py
Normal file
23
pico/test/test_serial_send.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Serial send test – single file. Run on ESP32 (TX side).
|
||||||
|
Wire: ESP32 TX (GPIO17) → Pico RX (GPIO1); GND ↔ GND. Run receive test on Pico.
|
||||||
|
mpremote run pico/test/test_serial_send.py
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import sys
|
||||||
|
from machine import UART, Pin
|
||||||
|
|
||||||
|
if "esp32" in sys.platform:
|
||||||
|
UART_ID, TX_PIN, BAUD = 1, 17, 115200
|
||||||
|
else:
|
||||||
|
UART_ID, TX_PIN, BAUD = 0, 0, 115200
|
||||||
|
|
||||||
|
print("UART send: %s UART%d TX=%s %d baud" % (sys.platform, UART_ID, TX_PIN, BAUD))
|
||||||
|
uart = UART(UART_ID, baudrate=BAUD, tx=Pin(TX_PIN, Pin.OUT))
|
||||||
|
for line in [b"serial send test 1", b"serial send test 2", b"{\"v\":\"1\",\"b\":128}"]:
|
||||||
|
uart.write(line + b"\n")
|
||||||
|
print("sent:", line.decode("utf-8"))
|
||||||
|
time.sleep_ms(50)
|
||||||
|
uart.deinit()
|
||||||
|
print("Send test done.")
|
||||||
Reference in New Issue
Block a user