Import led-driver app: pico/ and esp32/ layout
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
106
pico/src/main.py
Normal file
106
pico/src/main.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# """
|
||||
# Pico: receive led-driver JSON from UART (one message per line). Runs Presets + patterns.
|
||||
# UART RX on D7 (GPIO1). Non-blocking so presets.tick() runs every loop.
|
||||
# """
|
||||
# from settings import Settings
|
||||
# from machine import UART, Pin
|
||||
# import utime
|
||||
# from presets import Presets
|
||||
# from utils import convert_and_reorder_colors
|
||||
# import json
|
||||
|
||||
# # UART (Pico XIAO: D7 = GPIO1)
|
||||
# UART_RX_PIN = 1
|
||||
# UART_BAUD = 115200
|
||||
# UART_ID = 0
|
||||
|
||||
# settings = Settings()
|
||||
# print(settings)
|
||||
|
||||
# presets = Presets(settings["led_pin"], settings["num_leds"])
|
||||
# presets.load()
|
||||
# presets.b = settings.get("brightness", 255)
|
||||
# startup_preset = settings.get("startup_preset")
|
||||
# if startup_preset:
|
||||
# presets.select(startup_preset)
|
||||
# print("Selected startup preset:", startup_preset)
|
||||
|
||||
# last_brightness_save = 0
|
||||
|
||||
# # Non-blocking UART
|
||||
# uart = UART(UART_ID, baudrate=UART_BAUD, rx=Pin(UART_RX_PIN), rxbuf=512, timeout=0)
|
||||
# uart_buf = bytearray()
|
||||
|
||||
# print("UART RX on pin %s, %s baud (one JSON object per line)" % (UART_RX_PIN, UART_BAUD))
|
||||
|
||||
|
||||
# def process_message(data):
|
||||
# """Handle one JSON message (led-driver protocol: v, b, presets, select, default, save)."""
|
||||
# if data.get("v") != "1":
|
||||
# return
|
||||
# global last_brightness_save
|
||||
# if "b" in data:
|
||||
# try:
|
||||
# presets.b = max(0, min(255, int(data["b"])))
|
||||
# settings["brightness"] = presets.b
|
||||
# now = utime.ticks_ms()
|
||||
# if utime.ticks_diff(now, last_brightness_save) >= 500:
|
||||
# settings.save()
|
||||
# last_brightness_save = now
|
||||
# except (TypeError, ValueError):
|
||||
# pass
|
||||
# if "presets" in data:
|
||||
# for id, preset_data in data["presets"].items():
|
||||
# if "c" in preset_data:
|
||||
# preset_data["c"] = convert_and_reorder_colors(preset_data["c"], settings)
|
||||
# presets.edit(id, preset_data)
|
||||
# print("Edited preset", id, preset_data.get("name", ""))
|
||||
# if settings.get("name") in data.get("select", {}):
|
||||
# select_list = data["select"][settings.get("name")]
|
||||
# if select_list:
|
||||
# preset_name = select_list[0]
|
||||
# step = select_list[1] if len(select_list) > 1 else None
|
||||
# presets.select(preset_name, step=step)
|
||||
# if "default" in data:
|
||||
# settings["startup_preset"] = data["default"]
|
||||
# print("Set startup preset to", data["default"])
|
||||
# settings.save()
|
||||
# if "save" in data:
|
||||
# presets.save()
|
||||
|
||||
|
||||
# while True:
|
||||
# presets.tick()
|
||||
# n = uart.any()
|
||||
# if n:
|
||||
# data_in = uart.read(n)
|
||||
# if data_in:
|
||||
# for b in data_in:
|
||||
# if b in (0x0A, 0x0D): # LF or CR
|
||||
# if uart_buf:
|
||||
# try:
|
||||
# msg = uart_buf.decode("utf-8").strip()
|
||||
# if msg:
|
||||
# data = json.loads(msg)
|
||||
# process_message(data)
|
||||
# except (ValueError, UnicodeError):
|
||||
# pass
|
||||
# uart_buf = bytearray()
|
||||
# else:
|
||||
# if len(uart_buf) < 1024:
|
||||
# uart_buf.append(b)
|
||||
# utime.sleep_ms(1)
|
||||
|
||||
from neopixel import NeoPixel
|
||||
from machine import Pin
|
||||
|
||||
from ws2812 import WS2812B
|
||||
|
||||
sm = 0
|
||||
pins = ((2,270), (3,271), (4,272), (0,273), (7,274), (6,275), (29,276), (28,277))
|
||||
for pin, num_leds in pins:
|
||||
print(pin, num_leds)
|
||||
np = WS2812B(num_leds, pin, sm, 0.1)
|
||||
sm += 1
|
||||
np.fill((8, 0, 0))
|
||||
np.show()
|
||||
16
pico/src/p2p.py
Normal file
16
pico/src/p2p.py
Normal file
@@ -0,0 +1,16 @@
|
||||
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))
|
||||
6
pico/src/patterns/__init__.py
Normal file
6
pico/src/patterns/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from .blink import Blink
|
||||
from .rainbow import Rainbow
|
||||
from .pulse import Pulse
|
||||
from .transition import Transition
|
||||
from .chase import Chase
|
||||
from .circle import Circle
|
||||
33
pico/src/patterns/blink.py
Normal file
33
pico/src/patterns/blink.py
Normal file
@@ -0,0 +1,33 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Blink:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Blink pattern: toggles LEDs on/off using preset delay, cycling through colors."""
|
||||
# Use provided colors, or default to white if none
|
||||
colors = preset.c if preset.c else [(255, 255, 255)]
|
||||
color_index = 0
|
||||
state = True # True = on, False = off
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
# Re-read delay each loop so live updates to preset.d take effect
|
||||
delay_ms = max(1, int(preset.d))
|
||||
if utime.ticks_diff(current_time, last_update) >= delay_ms:
|
||||
if state:
|
||||
base_color = colors[color_index % len(colors)]
|
||||
color = self.driver.apply_brightness(base_color, preset.b)
|
||||
self.driver.fill(color)
|
||||
# Advance to next color for the next "on" phase
|
||||
color_index += 1
|
||||
else:
|
||||
# "Off" phase: turn all LEDs off
|
||||
self.driver.fill((0, 0, 0))
|
||||
state = not state
|
||||
last_update = current_time
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
124
pico/src/patterns/chase.py
Normal file
124
pico/src/patterns/chase.py
Normal file
@@ -0,0 +1,124 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Chase:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Chase pattern: n1 LEDs of color0, n2 LEDs of color1, repeating.
|
||||
Moves by n3 on even steps, n4 on odd steps (n3/n4 can be positive or negative)"""
|
||||
colors = preset.c
|
||||
if len(colors) < 1:
|
||||
# Need at least 1 color
|
||||
return
|
||||
|
||||
# Access colors, delay, and n values from preset
|
||||
if not colors:
|
||||
return
|
||||
# If only one color provided, use it for both colors
|
||||
if len(colors) < 2:
|
||||
color0 = colors[0]
|
||||
color1 = colors[0]
|
||||
else:
|
||||
color0 = colors[0]
|
||||
color1 = colors[1]
|
||||
|
||||
color0 = self.driver.apply_brightness(color0, preset.b)
|
||||
color1 = self.driver.apply_brightness(color1, preset.b)
|
||||
|
||||
n1 = max(1, int(preset.n1)) # LEDs of color 0
|
||||
n2 = max(1, int(preset.n2)) # LEDs of color 1
|
||||
n3 = int(preset.n3) # Step movement on even steps (can be negative)
|
||||
n4 = int(preset.n4) # Step movement on odd steps (can be negative)
|
||||
|
||||
segment_length = n1 + n2
|
||||
|
||||
# Calculate position from step_count
|
||||
step_count = self.driver.step
|
||||
# Position alternates: step 0 adds n3, step 1 adds n4, step 2 adds n3, etc.
|
||||
if step_count % 2 == 0:
|
||||
# Even steps: (step_count//2) pairs of (n3+n4) plus one extra n3
|
||||
position = (step_count // 2) * (n3 + n4) + n3
|
||||
else:
|
||||
# Odd steps: ((step_count+1)//2) pairs of (n3+n4)
|
||||
position = ((step_count + 1) // 2) * (n3 + n4)
|
||||
|
||||
# Wrap position to keep it reasonable
|
||||
max_pos = self.driver.num_leds + segment_length
|
||||
position = position % max_pos
|
||||
if position < 0:
|
||||
position += max_pos
|
||||
|
||||
# If auto is False, run a single step and then stop
|
||||
if not preset.a:
|
||||
# Clear all LEDs
|
||||
self.driver.n.fill((0, 0, 0))
|
||||
|
||||
# Draw repeating pattern starting at position
|
||||
for i in range(self.driver.num_leds):
|
||||
# Calculate position in the repeating segment
|
||||
relative_pos = (i - position) % segment_length
|
||||
if relative_pos < 0:
|
||||
relative_pos = (relative_pos + segment_length) % segment_length
|
||||
|
||||
# Determine which color based on position in segment
|
||||
if relative_pos < n1:
|
||||
self.driver.n[i] = color0
|
||||
else:
|
||||
self.driver.n[i] = color1
|
||||
|
||||
self.driver.n.write()
|
||||
|
||||
# Increment step for next beat
|
||||
self.driver.step = step_count + 1
|
||||
|
||||
# Allow tick() to advance the generator once
|
||||
yield
|
||||
return
|
||||
|
||||
# Auto mode: continuous loop
|
||||
# Use transition_duration for timing and force the first update to happen immediately
|
||||
transition_duration = max(10, int(preset.d))
|
||||
last_update = utime.ticks_ms() - transition_duration
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
if utime.ticks_diff(current_time, last_update) >= transition_duration:
|
||||
# Calculate current position from step_count
|
||||
if step_count % 2 == 0:
|
||||
position = (step_count // 2) * (n3 + n4) + n3
|
||||
else:
|
||||
position = ((step_count + 1) // 2) * (n3 + n4)
|
||||
|
||||
# Wrap position
|
||||
max_pos = self.driver.num_leds + segment_length
|
||||
position = position % max_pos
|
||||
if position < 0:
|
||||
position += max_pos
|
||||
|
||||
# Clear all LEDs
|
||||
self.driver.n.fill((0, 0, 0))
|
||||
|
||||
# Draw repeating pattern starting at position
|
||||
for i in range(self.driver.num_leds):
|
||||
# Calculate position in the repeating segment
|
||||
relative_pos = (i - position) % segment_length
|
||||
if relative_pos < 0:
|
||||
relative_pos = (relative_pos + segment_length) % segment_length
|
||||
|
||||
# Determine which color based on position in segment
|
||||
if relative_pos < n1:
|
||||
self.driver.n[i] = color0
|
||||
else:
|
||||
self.driver.n[i] = color1
|
||||
|
||||
self.driver.n.write()
|
||||
|
||||
# Increment step
|
||||
step_count += 1
|
||||
self.driver.step = step_count
|
||||
last_update = current_time
|
||||
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
96
pico/src/patterns/circle.py
Normal file
96
pico/src/patterns/circle.py
Normal file
@@ -0,0 +1,96 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Circle:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Circle loading pattern - grows to n2, then tail moves forward at n3 until min length n4"""
|
||||
head = 0
|
||||
tail = 0
|
||||
|
||||
# Calculate timing from preset
|
||||
head_rate = max(1, int(preset.n1)) # n1 = head moves per second
|
||||
tail_rate = max(1, int(preset.n3)) # n3 = tail moves per second
|
||||
max_length = max(1, int(preset.n2)) # n2 = max length
|
||||
min_length = max(0, int(preset.n4)) # n4 = min length
|
||||
|
||||
head_delay = 1000 // head_rate # ms between head movements
|
||||
tail_delay = 1000 // tail_rate # ms between tail movements
|
||||
|
||||
last_head_move = utime.ticks_ms()
|
||||
last_tail_move = utime.ticks_ms()
|
||||
|
||||
phase = "growing" # "growing", "shrinking", or "off"
|
||||
|
||||
# Support up to two colors (like chase). If only one color is provided,
|
||||
# use black for the second; if none, default to white.
|
||||
colors = preset.c
|
||||
if not colors:
|
||||
base0 = base1 = (255, 255, 255)
|
||||
elif len(colors) == 1:
|
||||
base0 = colors[0]
|
||||
base1 = (0, 0, 0)
|
||||
else:
|
||||
base0 = colors[0]
|
||||
base1 = colors[1]
|
||||
|
||||
color0 = self.driver.apply_brightness(base0, preset.b)
|
||||
color1 = self.driver.apply_brightness(base1, preset.b)
|
||||
|
||||
while True:
|
||||
current_time = utime.ticks_ms()
|
||||
|
||||
# Background: use second color during the "off" phase, otherwise clear to black
|
||||
if phase == "off":
|
||||
self.driver.n.fill(color1)
|
||||
else:
|
||||
self.driver.n.fill((0, 0, 0))
|
||||
|
||||
# Calculate segment length
|
||||
segment_length = (head - tail) % self.driver.num_leds
|
||||
if segment_length == 0 and head != tail:
|
||||
segment_length = self.driver.num_leds
|
||||
|
||||
# Draw segment from tail to head as a solid color (no per-LED alternation)
|
||||
current_color = color0
|
||||
for i in range(segment_length + 1):
|
||||
led_pos = (tail + i) % self.driver.num_leds
|
||||
self.driver.n[led_pos] = current_color
|
||||
|
||||
# Move head continuously at n1 LEDs per second
|
||||
if utime.ticks_diff(current_time, last_head_move) >= head_delay:
|
||||
head = (head + 1) % self.driver.num_leds
|
||||
last_head_move = current_time
|
||||
|
||||
# Tail behavior based on phase
|
||||
if phase == "growing":
|
||||
# Growing phase: tail stays at 0 until max length reached
|
||||
if segment_length >= max_length:
|
||||
phase = "shrinking"
|
||||
elif phase == "shrinking":
|
||||
# Shrinking phase: move tail forward at n3 LEDs per second
|
||||
if utime.ticks_diff(current_time, last_tail_move) >= tail_delay:
|
||||
tail = (tail + 1) % self.driver.num_leds
|
||||
last_tail_move = current_time
|
||||
|
||||
# Check if we've reached min length
|
||||
current_length = (head - tail) % self.driver.num_leds
|
||||
if current_length == 0 and head != tail:
|
||||
current_length = self.driver.num_leds
|
||||
|
||||
# For min_length = 0, we need at least 1 LED (the head)
|
||||
if min_length == 0 and current_length <= 1:
|
||||
phase = "off" # All LEDs off for 1 step
|
||||
elif min_length > 0 and current_length <= min_length:
|
||||
phase = "growing" # Cycle repeats
|
||||
else: # phase == "off"
|
||||
# Off phase: second color fills the ring for 1 step, then restart
|
||||
tail = head # Reset tail to head position to start fresh
|
||||
phase = "growing"
|
||||
|
||||
self.driver.n.write()
|
||||
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
64
pico/src/patterns/pulse.py
Normal file
64
pico/src/patterns/pulse.py
Normal file
@@ -0,0 +1,64 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Pulse:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
self.driver.off()
|
||||
|
||||
# Get colors from preset
|
||||
colors = preset.c
|
||||
if not colors:
|
||||
colors = [(255, 255, 255)]
|
||||
|
||||
color_index = 0
|
||||
cycle_start = utime.ticks_ms()
|
||||
|
||||
# State machine based pulse using a single generator loop
|
||||
while True:
|
||||
# Read current timing parameters from preset
|
||||
attack_ms = max(0, int(preset.n1)) # Attack time in ms
|
||||
hold_ms = max(0, int(preset.n2)) # Hold time in ms
|
||||
decay_ms = max(0, int(preset.n3)) # Decay time in ms
|
||||
delay_ms = max(0, int(preset.d))
|
||||
|
||||
total_ms = attack_ms + hold_ms + decay_ms + delay_ms
|
||||
if total_ms <= 0:
|
||||
total_ms = 1
|
||||
|
||||
now = utime.ticks_ms()
|
||||
elapsed = utime.ticks_diff(now, cycle_start)
|
||||
|
||||
base_color = colors[color_index % len(colors)]
|
||||
|
||||
if elapsed < attack_ms and attack_ms > 0:
|
||||
# Attack: fade 0 -> 1
|
||||
factor = elapsed / attack_ms
|
||||
color = tuple(int(c * factor) for c in base_color)
|
||||
self.driver.fill(self.driver.apply_brightness(color, preset.b))
|
||||
elif elapsed < attack_ms + hold_ms:
|
||||
# Hold: full brightness
|
||||
self.driver.fill(self.driver.apply_brightness(base_color, preset.b))
|
||||
elif elapsed < attack_ms + hold_ms + decay_ms and decay_ms > 0:
|
||||
# Decay: fade 1 -> 0
|
||||
dec_elapsed = elapsed - attack_ms - hold_ms
|
||||
factor = max(0.0, 1.0 - (dec_elapsed / decay_ms))
|
||||
color = tuple(int(c * factor) for c in base_color)
|
||||
self.driver.fill(self.driver.apply_brightness(color, preset.b))
|
||||
elif elapsed < total_ms:
|
||||
# Delay phase: LEDs off between pulses
|
||||
self.driver.fill((0, 0, 0))
|
||||
else:
|
||||
# End of cycle, move to next color and restart timing
|
||||
color_index += 1
|
||||
cycle_start = now
|
||||
if not preset.a:
|
||||
break
|
||||
# Skip drawing this tick, start next cycle
|
||||
yield
|
||||
continue
|
||||
|
||||
# Yield once per tick
|
||||
yield
|
||||
51
pico/src/patterns/rainbow.py
Normal file
51
pico/src/patterns/rainbow.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import utime
|
||||
|
||||
|
||||
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)
|
||||
|
||||
def run(self, preset):
|
||||
step = self.driver.step % 256
|
||||
step_amount = max(1, int(preset.n1)) # n1 controls step increment
|
||||
|
||||
# If auto is False, run a single step and 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
|
||||
yield
|
||||
return
|
||||
|
||||
last_update = utime.ticks_ms()
|
||||
|
||||
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
|
||||
last_update = current_time
|
||||
# Yield once per tick so other logic can run
|
||||
yield
|
||||
57
pico/src/patterns/transition.py
Normal file
57
pico/src/patterns/transition.py
Normal file
@@ -0,0 +1,57 @@
|
||||
import utime
|
||||
|
||||
|
||||
class Transition:
|
||||
def __init__(self, driver):
|
||||
self.driver = driver
|
||||
|
||||
def run(self, preset):
|
||||
"""Transition between colors, blending over `delay` ms."""
|
||||
colors = preset.c
|
||||
if not colors:
|
||||
self.driver.off()
|
||||
yield
|
||||
return
|
||||
|
||||
# Only one color: just keep it on
|
||||
if len(colors) == 1:
|
||||
while True:
|
||||
self.driver.fill(self.driver.apply_brightness(colors[0], preset.b))
|
||||
yield
|
||||
return
|
||||
|
||||
color_index = 0
|
||||
start_time = utime.ticks_ms()
|
||||
|
||||
while True:
|
||||
if not colors:
|
||||
break
|
||||
|
||||
# Get current and next color based on live list
|
||||
c1 = colors[color_index % len(colors)]
|
||||
c2 = colors[(color_index + 1) % len(colors)]
|
||||
|
||||
duration = max(10, int(preset.d)) # At least 10ms
|
||||
now = utime.ticks_ms()
|
||||
elapsed = utime.ticks_diff(now, start_time)
|
||||
|
||||
if elapsed >= duration:
|
||||
# End of this transition step
|
||||
if not preset.a:
|
||||
# One-shot: transition from first to second color only
|
||||
self.driver.fill(self.driver.apply_brightness(c2, preset.b))
|
||||
break
|
||||
# Auto: move to next pair
|
||||
color_index = (color_index + 1) % len(colors)
|
||||
start_time = now
|
||||
yield
|
||||
continue
|
||||
|
||||
# Interpolate between c1 and c2
|
||||
factor = elapsed / duration
|
||||
interpolated = tuple(
|
||||
int(c1[i] + (c2[i] - c1[i]) * factor) for i in range(3)
|
||||
)
|
||||
self.driver.fill(self.driver.apply_brightness(interpolated, preset.b))
|
||||
|
||||
yield
|
||||
79
pico/src/preset.py
Normal file
79
pico/src/preset.py
Normal file
@@ -0,0 +1,79 @@
|
||||
class Preset:
|
||||
def __init__(self, data):
|
||||
# Set default values for all preset attributes
|
||||
self.p = "off"
|
||||
self.d = 100
|
||||
self.b = 127
|
||||
self.c = [(255, 255, 255)]
|
||||
self.a = True
|
||||
self.n1 = 0
|
||||
self.n2 = 0
|
||||
self.n3 = 0
|
||||
self.n4 = 0
|
||||
self.n5 = 0
|
||||
self.n6 = 0
|
||||
|
||||
# Override defaults with provided data
|
||||
self.edit(data)
|
||||
|
||||
def edit(self, data=None):
|
||||
if not data:
|
||||
return False
|
||||
for key, value in data.items():
|
||||
setattr(self, key, value)
|
||||
return True
|
||||
|
||||
@property
|
||||
def pattern(self):
|
||||
return self.p
|
||||
|
||||
@pattern.setter
|
||||
def pattern(self, value):
|
||||
self.p = value
|
||||
|
||||
@property
|
||||
def delay(self):
|
||||
return self.d
|
||||
|
||||
@delay.setter
|
||||
def delay(self, value):
|
||||
self.d = value
|
||||
|
||||
@property
|
||||
def brightness(self):
|
||||
return self.b
|
||||
|
||||
@brightness.setter
|
||||
def brightness(self, value):
|
||||
self.b = value
|
||||
|
||||
@property
|
||||
def colors(self):
|
||||
return self.c
|
||||
|
||||
@colors.setter
|
||||
def colors(self, value):
|
||||
self.c = value
|
||||
|
||||
@property
|
||||
def auto(self):
|
||||
return self.a
|
||||
|
||||
@auto.setter
|
||||
def auto(self, value):
|
||||
self.a = value
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"p": self.p,
|
||||
"d": self.d,
|
||||
"b": self.b,
|
||||
"c": self.c,
|
||||
"a": self.a,
|
||||
"n1": self.n1,
|
||||
"n2": self.n2,
|
||||
"n3": self.n3,
|
||||
"n4": self.n4,
|
||||
"n5": self.n5,
|
||||
"n6": self.n6,
|
||||
}
|
||||
131
pico/src/presets.py
Normal file
131
pico/src/presets.py
Normal file
@@ -0,0 +1,131 @@
|
||||
from machine import Pin
|
||||
from ws2812 import WS2812B
|
||||
from preset import Preset
|
||||
from patterns import Blink, Rainbow, Pulse, Transition, Chase, Circle
|
||||
import json
|
||||
|
||||
|
||||
class Presets:
|
||||
def __init__(self, pin, num_leds, state_machine=0):
|
||||
# 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
|
||||
# Global brightness (0–255), controlled via UART/JSON {"b": <value>}
|
||||
self.b = 255
|
||||
|
||||
self.generator = None
|
||||
self.presets = {}
|
||||
self.selected = None
|
||||
|
||||
# Register all pattern methods
|
||||
self.patterns = {
|
||||
"off": self.off,
|
||||
"on": self.on,
|
||||
"blink": Blink(self).run,
|
||||
"rainbow": Rainbow(self).run,
|
||||
"pulse": Pulse(self).run,
|
||||
"transition": Transition(self).run,
|
||||
"chase": Chase(self).run,
|
||||
"circle": Circle(self).run,
|
||||
}
|
||||
|
||||
def save(self):
|
||||
"""Save the presets to a file."""
|
||||
with open("presets.json", "w") as f:
|
||||
json.dump({name: preset.to_dict() for name, preset in self.presets.items()}, f)
|
||||
return True
|
||||
|
||||
def load(self):
|
||||
"""Load presets from a file."""
|
||||
try:
|
||||
with open("presets.json", "r") as f:
|
||||
data = json.load(f)
|
||||
except OSError:
|
||||
# Create an empty presets file if missing
|
||||
self.presets = {}
|
||||
self.save()
|
||||
return True
|
||||
|
||||
self.presets = {}
|
||||
for name, preset_data in data.items():
|
||||
if "c" in preset_data:
|
||||
preset_data["c"] = [tuple(color) for color in preset_data["c"]]
|
||||
self.presets[name] = Preset(preset_data)
|
||||
if self.presets:
|
||||
print("Loaded presets:")
|
||||
#for name in sorted(self.presets.keys()):
|
||||
# print(f" {name}: {self.presets[name].to_dict()}")
|
||||
return True
|
||||
|
||||
def edit(self, name, data):
|
||||
"""Create or update a preset with the given name."""
|
||||
if name in self.presets:
|
||||
# Update existing preset
|
||||
self.presets[name].edit(data)
|
||||
else:
|
||||
# Create new preset
|
||||
self.presets[name] = Preset(data)
|
||||
return True
|
||||
|
||||
def delete(self, name):
|
||||
if name in self.presets:
|
||||
del self.presets[name]
|
||||
return True
|
||||
return False
|
||||
|
||||
def tick(self):
|
||||
if self.generator is None:
|
||||
return
|
||||
try:
|
||||
next(self.generator)
|
||||
except StopIteration:
|
||||
self.generator = None
|
||||
|
||||
def select(self, preset_name, step=None):
|
||||
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
|
||||
|
||||
def update_num_leds(self, pin, num_leds):
|
||||
num_leds = int(num_leds)
|
||||
if isinstance(pin, Pin):
|
||||
self.n = WS2812B(pin, num_leds)
|
||||
else:
|
||||
self.n = WS2812B(num_leds, int(pin), 0, brightness=1.0)
|
||||
self.num_leds = num_leds
|
||||
|
||||
def apply_brightness(self, color, brightness_override=None):
|
||||
# Combine per-preset brightness (override) with global brightness self.b
|
||||
local = brightness_override if brightness_override is not None else 255
|
||||
# Scale preset brightness by global brightness
|
||||
effective_brightness = int(local * self.b / 255)
|
||||
return tuple(int(c * effective_brightness / 255) for c in color)
|
||||
|
||||
def fill(self, color=None):
|
||||
fill_color = color if color is not None else (0, 0, 0)
|
||||
for i in range(self.num_leds):
|
||||
self.n[i] = fill_color
|
||||
self.n.write()
|
||||
|
||||
def off(self, preset=None):
|
||||
self.fill((0, 0, 0))
|
||||
|
||||
def on(self, preset):
|
||||
colors = preset.c
|
||||
color = colors[0] if colors else (255, 255, 255)
|
||||
self.fill(self.apply_brightness(color, preset.b))
|
||||
94
pico/src/settings.py
Normal file
94
pico/src/settings.py
Normal file
@@ -0,0 +1,94 @@
|
||||
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()
|
||||
53
pico/src/utils.py
Normal file
53
pico/src/utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
def convert_and_reorder_colors(colors, settings_or_color_order):
|
||||
"""Convert hex color strings to RGB tuples and reorder based on device color order.
|
||||
|
||||
Args:
|
||||
colors: List of colors, either hex strings like "#FF0000" or RGB tuples like (255, 0, 0)
|
||||
settings_or_color_order: Either a Settings object or a color_order string (e.g., "rgb", "grb")
|
||||
|
||||
Returns:
|
||||
List of RGB tuples reordered according to device color order
|
||||
"""
|
||||
# Get channel order from settings or color_order string
|
||||
if hasattr(settings_or_color_order, 'get_rgb_channel_order'):
|
||||
# It's a Settings object
|
||||
channel_order = settings_or_color_order.get_rgb_channel_order()
|
||||
elif isinstance(settings_or_color_order, str):
|
||||
# It's a color_order string, convert to channel order
|
||||
color_order = settings_or_color_order.lower()
|
||||
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)
|
||||
}
|
||||
hex_indices = color_orders.get(color_order, (1, 3, 5))
|
||||
# Map hex string positions to RGB channel indices
|
||||
hex_to_channel = {1: 0, 3: 1, 5: 2}
|
||||
channel_order = tuple(hex_to_channel[pos] for pos in hex_indices)
|
||||
else:
|
||||
# Assume it's already a channel order tuple
|
||||
channel_order = settings_or_color_order
|
||||
|
||||
converted_colors = []
|
||||
for color in colors:
|
||||
# Convert "#RRGGBB" to (R, G, B)
|
||||
if isinstance(color, str) and color.startswith("#"):
|
||||
r = int(color[1:3], 16)
|
||||
g = int(color[3:5], 16)
|
||||
b = int(color[5:7], 16)
|
||||
rgb = (r, g, b)
|
||||
# Reorder based on device color order
|
||||
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
|
||||
converted_colors.append(reordered)
|
||||
elif isinstance(color, (list, tuple)) and len(color) == 3:
|
||||
# Already a tuple/list, just reorder
|
||||
rgb = tuple(color)
|
||||
reordered = (rgb[channel_order[0]], rgb[channel_order[1]], rgb[channel_order[2]])
|
||||
converted_colors.append(reordered)
|
||||
else:
|
||||
# Keep as-is if not recognized format
|
||||
converted_colors.append(color)
|
||||
return converted_colors
|
||||
Reference in New Issue
Block a user