diff --git a/pico/lib/ws2812.py b/pico/lib/ws2812.py index 9b59957..05d84ab 100644 --- a/pico/lib/ws2812.py +++ b/pico/lib/ws2812.py @@ -1,70 +1,70 @@ - -import array, time -from machine import Pin -import rp2 -from time import sleep -import dma - -@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8) -def ws2812(): - T1 = 2 - T2 = 5 - T3 = 3 - wrap_target() - label("bitloop") - out(x, 1) .side(0) [T3 - 1] - jmp(not_x, "do_zero") .side(1) [T1 - 1] - jmp("bitloop") .side(1) [T2 - 1] - label("do_zero") - nop() .side(0) [T2 - 1] - wrap() - -class WS2812B: - def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False): - self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin)) - self.sm.active(1) - self.ar = bytearray(num_leds*3) - self.num_leds = num_leds - self.brightness = brightness - self.invert = invert - self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3) - - def show(self, array=None, offset=0): - if array is None: - array = self.ar - self.pio_dma.start_transfer(array, offset) - - def set(self, i, color): - self.ar[i*3] = int(color[1]*self.brightness) - self.ar[i*3+1] = int(color[0]*self.brightness) - self.ar[i*3+2] = int(color[2]*self.brightness) - - def fill(self, color): - for i in range(self.num_leds): - self.set(i, color) - - def busy(self): - return self.pio_dma.busy() - - BLACK = (0, 0, 0) - RED = (255, 0, 0) - YELLOW = (255, 150, 0) - GREEN = (0, 255, 0) - CYAN = (0, 255, 255) - BLUE = (0, 0, 255) - PURPLE = (180, 0, 255) - WHITE = (255, 255, 255) - COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE) - -if __name__ == "__main__": - num_leds, pin, sm, brightness = 293, 2, 0, 0.1 - ws0 = WS2812B(num_leds, pin, sm, brightness) - while True: - for color in ws0.COLORS: - ws0.fill(color) - ws0.show() - time.sleep(1) - - - - + +import array, time +from machine import Pin +import rp2 +from time import sleep +import dma + +@rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=8) +def ws2812(): + T1 = 2 + T2 = 5 + T3 = 3 + wrap_target() + label("bitloop") + out(x, 1) .side(0) [T3 - 1] + jmp(not_x, "do_zero") .side(1) [T1 - 1] + jmp("bitloop") .side(1) [T2 - 1] + label("do_zero") + nop() .side(0) [T2 - 1] + wrap() + +class WS2812B: + def __init__(self, num_leds, pin, state_machine, brightness=0.1, invert=False): + self.sm = rp2.StateMachine(state_machine, ws2812, freq=8_000_000, sideset_base=Pin(pin)) + self.sm.active(1) + self.ar = bytearray(num_leds*3) + self.num_leds = num_leds + self.brightness = brightness + self.invert = invert + self.pio_dma = dma.PIO_DMA_Transfer(state_machine+4, state_machine, 8, num_leds*3) + + def show(self, array=None, offset=0): + if array is None: + array = self.ar + self.pio_dma.start_transfer(array, offset) + + def set(self, i, color): + self.ar[i*3] = int(color[1]*self.brightness) + self.ar[i*3+1] = int(color[0]*self.brightness) + self.ar[i*3+2] = int(color[2]*self.brightness) + + def fill(self, color): + for i in range(self.num_leds): + self.set(i, color) + + def busy(self): + return self.pio_dma.busy() + + BLACK = (0, 0, 0) + RED = (255, 0, 0) + YELLOW = (255, 150, 0) + GREEN = (0, 255, 0) + CYAN = (0, 255, 255) + BLUE = (0, 0, 255) + PURPLE = (180, 0, 255) + WHITE = (255, 255, 255) + COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE) + +if __name__ == "__main__": + num_leds, pin, sm, brightness = 293, 2, 0, 0.1 + ws0 = WS2812B(num_leds, pin, sm, brightness) + while True: + for color in ws0.COLORS: + ws0.fill(color) + ws0.show() + time.sleep(1) + + + + diff --git a/pico/src/main.py b/pico/src/main.py index 2d8ed6c..b74847b 100644 --- a/pico/src/main.py +++ b/pico/src/main.py @@ -1,99 +1,63 @@ -import sys -# So "from ws2812 import WS2812B" finds pico/lib when run from device / or test/ -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 +from machine import UART, Pin +import json +from presets import Presets +import gc -# --- 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): - """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)) +presets = Presets() +presets.load() +print(presets.presets.keys()) -def make_rainbow_double(num_leds, brightness=1.0): - """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) +presets.select("off") +#print memory usage +print(f"Memory usage: {gc.mem_free()/1024} kB free") -def show_rainbow(strip, double_buf, strip_len, head): - """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 - +i = 0 while True: - now = time.ticks_ms() - for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)): - head = (rainbow_head + cumulative_leds[i] * 3) % strip_len - show_rainbow(strip, double_buf, strip_len, head) - rainbow_head = (rainbow_head + step) % bytes_per_cycle - #print(time.ticks_diff(time.ticks_ms(), now), "ms") - time.sleep_ms(10) + presets.tick() + if uart.any(): + data = uart.readline() + try: + data = json.loads(data) + except: + # 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": "", "data": {}}} + 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": ""} + 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") + \ No newline at end of file diff --git a/pico/src/p2p.py b/pico/src/p2p.py deleted file mode 100644 index a00f6a2..0000000 --- a/pico/src/p2p.py +++ /dev/null @@ -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)) \ No newline at end of file diff --git a/pico/src/patterns/__init__.py b/pico/src/patterns/__init__.py index 83b9dac..9be8479 100644 --- a/pico/src/patterns/__init__.py +++ b/pico/src/patterns/__init__.py @@ -4,3 +4,32 @@ from .pulse import Pulse from .transition import Transition from .chase import Chase 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 diff --git a/pico/src/patterns/backbalance.py b/pico/src/patterns/backbalance.py new file mode 100644 index 0000000..2fb01e9 --- /dev/null +++ b/pico/src/patterns/backbalance.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Backbalance: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/backbend.py b/pico/src/patterns/backbend.py new file mode 100644 index 0000000..60a318d --- /dev/null +++ b/pico/src/patterns/backbend.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Backbend: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/backbendsplit.py b/pico/src/patterns/backbendsplit.py new file mode 100644 index 0000000..ec62857 --- /dev/null +++ b/pico/src/patterns/backbendsplit.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Backbendsplit: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/beat.py b/pico/src/patterns/beat.py new file mode 100644 index 0000000..a54aba3 --- /dev/null +++ b/pico/src/patterns/beat.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Beat: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/calibration.py b/pico/src/patterns/calibration.py new file mode 100644 index 0000000..2f2d874 --- /dev/null +++ b/pico/src/patterns/calibration.py @@ -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() diff --git a/pico/src/patterns/crouch.py b/pico/src/patterns/crouch.py new file mode 100644 index 0000000..8d5909f --- /dev/null +++ b/pico/src/patterns/crouch.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Crouch: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/dismount.py b/pico/src/patterns/dismount.py new file mode 100644 index 0000000..166d203 --- /dev/null +++ b/pico/src/patterns/dismount.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Dismount: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/elbowhang.py b/pico/src/patterns/elbowhang.py new file mode 100644 index 0000000..9ff46af --- /dev/null +++ b/pico/src/patterns/elbowhang.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Elbowhang: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/elbowhangspin.py b/pico/src/patterns/elbowhangspin.py new file mode 100644 index 0000000..857fb56 --- /dev/null +++ b/pico/src/patterns/elbowhangspin.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Elbowhangspin: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/elbowhangsplit.py b/pico/src/patterns/elbowhangsplit.py new file mode 100644 index 0000000..45d61a2 --- /dev/null +++ b/pico/src/patterns/elbowhangsplit.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Elbowhangsplit: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/flare.py b/pico/src/patterns/flare.py new file mode 100644 index 0000000..8dedc7a --- /dev/null +++ b/pico/src/patterns/flare.py @@ -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 diff --git a/pico/src/patterns/fluff.py b/pico/src/patterns/fluff.py new file mode 100644 index 0000000..ce372e5 --- /dev/null +++ b/pico/src/patterns/fluff.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Fluff: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/foothang.py b/pico/src/patterns/foothang.py new file mode 100644 index 0000000..20016e5 --- /dev/null +++ b/pico/src/patterns/foothang.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Foothang: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/frontbalance.py b/pico/src/patterns/frontbalance.py new file mode 100644 index 0000000..152b062 --- /dev/null +++ b/pico/src/patterns/frontbalance.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Frontbalance: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/grab.py b/pico/src/patterns/grab.py new file mode 100644 index 0000000..e95fe67 --- /dev/null +++ b/pico/src/patterns/grab.py @@ -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 diff --git a/pico/src/patterns/hook.py b/pico/src/patterns/hook.py new file mode 100644 index 0000000..535a20a --- /dev/null +++ b/pico/src/patterns/hook.py @@ -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 diff --git a/pico/src/patterns/invert.py b/pico/src/patterns/invert.py new file mode 100644 index 0000000..6512917 --- /dev/null +++ b/pico/src/patterns/invert.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Invert: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/invertsplit.py b/pico/src/patterns/invertsplit.py new file mode 100644 index 0000000..89f65d2 --- /dev/null +++ b/pico/src/patterns/invertsplit.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Invertsplit: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/kneehang.py b/pico/src/patterns/kneehang.py new file mode 100644 index 0000000..d0653e1 --- /dev/null +++ b/pico/src/patterns/kneehang.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Kneehang: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/legswoop.py b/pico/src/patterns/legswoop.py new file mode 100644 index 0000000..fc0446c --- /dev/null +++ b/pico/src/patterns/legswoop.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Legswoop: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/lift.py b/pico/src/patterns/lift.py new file mode 100644 index 0000000..1e7db61 --- /dev/null +++ b/pico/src/patterns/lift.py @@ -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 diff --git a/pico/src/patterns/point.py b/pico/src/patterns/point.py new file mode 100644 index 0000000..68f58b0 --- /dev/null +++ b/pico/src/patterns/point.py @@ -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 + diff --git a/pico/src/patterns/pose.py b/pico/src/patterns/pose.py new file mode 100644 index 0000000..e9d18d6 --- /dev/null +++ b/pico/src/patterns/pose.py @@ -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 diff --git a/pico/src/patterns/rainbow.py b/pico/src/patterns/rainbow.py index 64c54e9..e5358c1 100644 --- a/pico/src/patterns/rainbow.py +++ b/pico/src/patterns/rainbow.py @@ -1,51 +1,97 @@ 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: def __init__(self, driver): self.driver = driver - - 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) + self._buffers_cache = {} def run(self, preset): - step = self.driver.step % 256 - step_amount = max(1, int(preset.n1)) # n1 controls step increment + step_amount = max(1, int(preset.n1)) # n1 = bytes to advance per frame (speed) + 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: - for i in range(self.driver.num_leds): - rc_index = (i * 256 // self.driver.num_leds) + step - self.driver.n[i] = self.driver.apply_brightness(self._wheel(rc_index & 255), preset.b) - 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 + show_frame(phase) + phase = (phase + step_amount) % total_ring_bytes + self.driver.step = (phase * 256) // total_ring_bytes yield return last_update = utime.ticks_ms() + sleep_ms = max(1, int(preset.d)) while True: 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: - for i in range(self.driver.num_leds): - rc_index = (i * 256 // self.driver.num_leds) + step - self.driver.n[i] = self.driver.apply_brightness( - self._wheel(rc_index & 255), - preset.b, - ) - self.driver.n.write() - step = (step + step_amount) % 256 - self.driver.step = step + rainbow_data, cumulative_bytes = _ensure_buffers( + self.driver, preset, self._buffers_cache + ) + show_frame(phase) + phase = (phase + step_amount) % total_ring_bytes last_update = current_time - # Yield once per tick so other logic can run yield diff --git a/pico/src/patterns/roll.py b/pico/src/patterns/roll.py new file mode 100644 index 0000000..1a848b1 --- /dev/null +++ b/pico/src/patterns/roll.py @@ -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 diff --git a/pico/src/patterns/seat.py b/pico/src/patterns/seat.py new file mode 100644 index 0000000..967b59c --- /dev/null +++ b/pico/src/patterns/seat.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Seat: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/spin.py b/pico/src/patterns/spin.py new file mode 100644 index 0000000..f3b9f87 --- /dev/null +++ b/pico/src/patterns/spin.py @@ -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 diff --git a/pico/src/patterns/split.py b/pico/src/patterns/split.py new file mode 100644 index 0000000..37f82c9 --- /dev/null +++ b/pico/src/patterns/split.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Split: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/straddle.py b/pico/src/patterns/straddle.py new file mode 100644 index 0000000..ddb6f43 --- /dev/null +++ b/pico/src/patterns/straddle.py @@ -0,0 +1,9 @@ +"""Placeholder until implemented.""" + +class Straddle: + def __init__(self, driver): + self.driver = driver + + def run(self, preset): + while True: + yield diff --git a/pico/src/patterns/test.py b/pico/src/patterns/test.py new file mode 100644 index 0000000..a05faeb --- /dev/null +++ b/pico/src/patterns/test.py @@ -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() diff --git a/pico/src/preset.py b/pico/src/preset.py index d00babc..5eec952 100644 --- a/pico/src/preset.py +++ b/pico/src/preset.py @@ -12,6 +12,8 @@ class Preset: self.n4 = 0 self.n5 = 0 self.n6 = 0 + self.n7 = 0 + self.n8 = 0 # Override defaults with provided data self.edit(data) @@ -76,4 +78,6 @@ class Preset: "n4": self.n4, "n5": self.n5, "n6": self.n6, + "n7": self.n7, + "n8": self.n8, } diff --git a/pico/src/presets.json b/pico/src/presets.json new file mode 100644 index 0000000..7a69e06 --- /dev/null +++ b/pico/src/presets.json @@ -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 + } +} diff --git a/pico/src/presets.py b/pico/src/presets.py index 9888e54..df8ba3b 100644 --- a/pico/src/presets.py +++ b/pico/src/presets.py @@ -1,20 +1,92 @@ from machine import Pin from ws2812 import WS2812B 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 +# 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: - 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) - 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 + # 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": } self.b = 255 @@ -32,8 +104,79 @@ class Presets: "transition": Transition(self).run, "chase": Chase(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): """Save the presets to a file.""" with open("presets.json", "w") as f: @@ -87,19 +230,26 @@ class Presets: self.generator = 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: preset = self.presets[preset_name] - if preset.p in self.patterns: - # Set step value if explicitly provided - if step is not None: - self.step = step - elif preset.p == "off" or self.selected != preset_name: - self.step = 0 - self.generator = self.patterns[preset.p](preset) - self.selected = preset_name # Store the preset name, not the object - return True - # If preset doesn't exist or pattern not found, default to "off" - return False + pattern_key = preset.p + if pattern_key not in self.patterns: + return False + # Run by pattern name (works for saved presets and built-ins like calibration, off, test) + if preset is None: + preset = Preset({"p": pattern_key}) if pattern_key != "off" else None + if step is not None: + self.step = step + elif pattern_key == "off" or self.selected != preset_name: + self.step = 0 + self.generator = self.patterns[pattern_key](preset) + self.selected = preset_name + return True def update_num_leds(self, pin, num_leds): num_leds = int(num_leds) diff --git a/pico/src/settings.py b/pico/src/settings.py deleted file mode 100644 index 4447da8..0000000 --- a/pico/src/settings.py +++ /dev/null @@ -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() diff --git a/pico/test/chase.py b/pico/test/chase.py new file mode 100644 index 0000000..0becd82 --- /dev/null +++ b/pico/test/chase.py @@ -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) diff --git a/pico/test/rainbow.py b/pico/test/rainbow.py index 16164fc..a4ad3b5 100644 --- a/pico/test/rainbow.py +++ b/pico/test/rainbow.py @@ -28,13 +28,13 @@ def hue_to_rgb(hue): return (int(r * 255), int(g * 255), int(b * 255)) -def make_rainbow_ring(total_leds, brightness=1.0): - """Build one rainbow over the whole ring: 2 full hue cycles over total_leds (GRB). - Returns (double_buf, ring_len_bytes). All strips sample from this so phase is continuous.""" - n = 2 * total_leds +def make_rainbow_double(num_leds, brightness=1.0): + """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 % total_leds) / total_leds) * 360 * 2 + hue = (i / n) * 360 * 2 r, g, b = hue_to_rgb(hue) g = int(g * 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 + 1] = r double_buf[o + 2] = b - ring_len_bytes = total_leds * 3 - return (double_buf, ring_len_bytes) + strip_len = num_leds * 3 + return (double_buf, strip_len) -def make_strip_rainbow(num_leds, cumulative_leds, total_ring_leds, brightness=1.0): - """Per-strip double buffer: pixel j has hue at global position (cumulative_leds + j) % total_ring_leds. - Use same head for all strips: head = rainbow_head % (2*num_leds*3).""" - 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(strip, double_buf, strip_len, head): + """DMA reads directly from double_buf at head; no copy. head in 0..strip_len-1.""" + strip.show(double_buf, head) -def show_rainbow_segment(strip, buf, strip_len_bytes, head): - """DMA reads strip's segment from buf at head.""" - strip.show(buf, head) - - -# --- 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 +# --- 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 = ( - (2, 291), + (7, 291), (3, 290), - (4, 283), - (7, 278), - (0, 275), + (6, 283), (28, 278), - (29, 283), - (6, 290), + (29, 275), + (4, 278), + (0, 283), + (2, 290), ) strips = [] @@ -102,24 +81,19 @@ for ws in strips[:-1]: total_ring_leds = cumulative_leds[-1] + strips[-1].num_leds 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() -rainbow_data = [ - make_strip_rainbow(ws.num_leds, cumulative_leds[i], total_ring_leds, ws.brightness) - for i, ws in enumerate(strips) -] +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: now = time.ticks_ms() - for i, (strip, (buf, strip_len_bytes)) in enumerate(zip(strips, rainbow_data)): - # Same head for all: each strip's buffer is already offset by cumulative_leds[i] - double_len_bytes = 2 * strip.num_leds * 3 - head = rainbow_head % double_len_bytes - show_rainbow_segment(strip, buf, strip_len_bytes, head) + for i, (strip, (double_buf, strip_len)) in enumerate(zip(strips, rainbow_data)): + head = (rainbow_head + cumulative_leds[i] * 3) % strip_len + show_rainbow(strip, double_buf, strip_len, head) rainbow_head = (rainbow_head + step) % bytes_per_cycle #print(time.ticks_diff(time.ticks_ms(), now), "ms") time.sleep_ms(10) diff --git a/pico/test/roll_strips.py b/pico/test/roll_strips.py new file mode 100644 index 0000000..ab67aa3 --- /dev/null +++ b/pico/test/roll_strips.py @@ -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) diff --git a/pico/test/test_serial.py b/pico/test/test_serial.py new file mode 100644 index 0000000..712d52c --- /dev/null +++ b/pico/test/test_serial.py @@ -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) diff --git a/pico/test/test_serial_receive.py b/pico/test/test_serial_receive.py new file mode 100644 index 0000000..d65eb0c --- /dev/null +++ b/pico/test/test_serial_receive.py @@ -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.") diff --git a/pico/test/test_serial_send.py b/pico/test/test_serial_send.py new file mode 100644 index 0000000..b29e625 --- /dev/null +++ b/pico/test/test_serial_send.py @@ -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.")