Expand browser editor runtime and LED simulation workflows.
Add Docker deployment support, richer Selenium/LED pattern tests, in-browser diagnostics, responsive UI improvements, and 16x16 panel simulation tooling to speed iteration and hardware-style prototyping. Made-with: Cursor
This commit is contained in:
67
workspace/code/led_patterns.py
Normal file
67
workspace/code/led_patterns.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""LED pattern helpers inspired by embedded NeoPixel drivers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
|
||||
Color = tuple[int, int, int]
|
||||
|
||||
|
||||
def _clamp(channel: int) -> int:
|
||||
return max(0, min(255, int(channel)))
|
||||
|
||||
|
||||
def wheel(pos: int) -> Color:
|
||||
"""Return a rainbow color for position 0-255."""
|
||||
pos = 255 - (pos & 255)
|
||||
if pos < 85:
|
||||
return (_clamp(255 - pos * 3), 0, _clamp(pos * 3))
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (0, _clamp(pos * 3), _clamp(255 - pos * 3))
|
||||
pos -= 170
|
||||
return (_clamp(pos * 3), _clamp(255 - pos * 3), 0)
|
||||
|
||||
|
||||
def rainbow_frame(led_count: int, frame: int, step: int = 4) -> list[Color]:
|
||||
"""Generate one rainbow frame across all LEDs."""
|
||||
if led_count <= 0:
|
||||
return []
|
||||
return [wheel((i * 256 // led_count + frame * step) & 255) for i in range(led_count)]
|
||||
|
||||
|
||||
def chase_frame(
|
||||
led_count: int,
|
||||
frame: int,
|
||||
color: Color = (255, 120, 0),
|
||||
tail: Color = (16, 0, 0),
|
||||
) -> list[Color]:
|
||||
"""Generate a two-pixel chase pattern."""
|
||||
if led_count <= 0:
|
||||
return []
|
||||
out: list[Color] = [(0, 0, 0) for _ in range(led_count)]
|
||||
head = frame % led_count
|
||||
trail = (head - 1) % led_count
|
||||
out[trail] = tuple(_clamp(v) for v in tail) # type: ignore[assignment]
|
||||
out[head] = tuple(_clamp(v) for v in color) # type: ignore[assignment]
|
||||
return out
|
||||
|
||||
|
||||
def twinkle_frame(
|
||||
led_count: int,
|
||||
frame: int,
|
||||
base: Color = (0, 0, 8),
|
||||
sparkle: Color = (255, 255, 180),
|
||||
sparkles: int = 3,
|
||||
seed: int = 1337,
|
||||
) -> list[Color]:
|
||||
"""Generate deterministic twinkle frames for testing/replay."""
|
||||
if led_count <= 0:
|
||||
return []
|
||||
out: list[Color] = [tuple(_clamp(v) for v in base) for _ in range(led_count)] # type: ignore[list-item]
|
||||
rng = random.Random(seed + frame)
|
||||
for _ in range(min(max(0, sparkles), led_count)):
|
||||
idx = rng.randrange(led_count)
|
||||
out[idx] = tuple(_clamp(v) for v in sparkle) # type: ignore[assignment]
|
||||
return out
|
||||
50
workspace/code/led_tutorial.py
Normal file
50
workspace/code/led_tutorial.py
Normal file
@@ -0,0 +1,50 @@
|
||||
"""LED tutorial script for NeoPixel in the browser editor.
|
||||
|
||||
Run this file and watch the in-app NeoPixel simulator panel.
|
||||
"""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
|
||||
LED_COUNT = 12
|
||||
np = neopixel.NeoPixel(Pin(4), LED_COUNT)
|
||||
|
||||
|
||||
def show_step(title: str):
|
||||
print(f"\n--- {title} ---")
|
||||
|
||||
|
||||
show_step("Step 1: single colors")
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
time.sleep(0.2)
|
||||
np[0] = (255, 0, 0) # red
|
||||
np[1] = (0, 255, 0) # green
|
||||
np[2] = (0, 0, 255) # blue
|
||||
np.write()
|
||||
time.sleep(0.8)
|
||||
|
||||
show_step("Step 2: fill strip")
|
||||
np.fill((40, 0, 120))
|
||||
np.write()
|
||||
time.sleep(0.6)
|
||||
|
||||
show_step("Step 3: moving pixel")
|
||||
for i in range(len(np)):
|
||||
np.fill((0, 0, 0))
|
||||
np[i] = (255, 120, 0)
|
||||
np.write()
|
||||
time.sleep(0.06)
|
||||
|
||||
show_step("Step 4: simple pulse")
|
||||
for level in list(range(0, 200, 20)) + list(range(200, -1, -20)):
|
||||
np.fill((level, 0, level // 3))
|
||||
np.write()
|
||||
time.sleep(0.05)
|
||||
|
||||
show_step("Done")
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
print("Tutorial complete.")
|
||||
12
workspace/code/neopixel_demo.py
Normal file
12
workspace/code/neopixel_demo.py
Normal file
@@ -0,0 +1,12 @@
|
||||
"""Example ESP32 NeoPixel script using mock modules in browser."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), 8)
|
||||
np.fill((0, 0, 0))
|
||||
np[0] = (255, 0, 0)
|
||||
np[1] = (0, 255, 0)
|
||||
np[2] = (0, 0, 255)
|
||||
np.write()
|
||||
32
workspace/code/neopixel_time_test.py
Normal file
32
workspace/code/neopixel_time_test.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""NeoPixel time-based animation test for the browser simulator."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), 50)
|
||||
|
||||
|
||||
def wheel(pos: int) -> tuple[int, int, int]:
|
||||
"""Generate rainbow colors across 0-255 positions."""
|
||||
pos = 255 - (pos & 255)
|
||||
if pos < 85:
|
||||
return (255 - pos * 3, 0, pos * 3)
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (0, pos * 3, 255 - pos * 3)
|
||||
pos -= 170
|
||||
return (pos * 3, 255 - pos * 3, 0)
|
||||
|
||||
|
||||
print("Starting NeoPixel time test...")
|
||||
for frame in range(60):
|
||||
for i in range(len(np)):
|
||||
np[i] = wheel((i * 256 // len(np) + frame * 4) & 255)
|
||||
np.write()
|
||||
time.sleep(0.08)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
print("NeoPixel time test complete.")
|
||||
41
workspace/code/panel16_bounce.py
Normal file
41
workspace/code/panel16_bounce.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""16x16 bouncing pixel with fading trail."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
from panel16_utils import PANEL_H, PANEL_W, clamp8, xy_to_index
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), PANEL_W * PANEL_H)
|
||||
|
||||
trail = [[0 for _ in range(PANEL_W)] for _ in range(PANEL_H)]
|
||||
x = 0
|
||||
y = 0
|
||||
vx = 1
|
||||
vy = 1
|
||||
|
||||
for _frame in range(420):
|
||||
for yy in range(PANEL_H):
|
||||
for xx in range(PANEL_W):
|
||||
trail[yy][xx] = max(0, trail[yy][xx] - 14)
|
||||
|
||||
trail[y][x] = 255
|
||||
|
||||
for yy in range(PANEL_H):
|
||||
for xx in range(PANEL_W):
|
||||
v = trail[yy][xx]
|
||||
np[xy_to_index(xx, yy)] = (clamp8(v), clamp8(v // 2), clamp8(30))
|
||||
|
||||
np.write()
|
||||
time.sleep(0.02)
|
||||
|
||||
x += vx
|
||||
y += vy
|
||||
if x <= 0 or x >= PANEL_W - 1:
|
||||
vx *= -1
|
||||
if y <= 0 or y >= PANEL_H - 1:
|
||||
vy *= -1
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
37
workspace/code/panel16_matrix_rain.py
Normal file
37
workspace/code/panel16_matrix_rain.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""16x16 matrix-style rain animation."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import random
|
||||
import time
|
||||
|
||||
from panel16_utils import PANEL_H, PANEL_W, clamp8, xy_to_index
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), PANEL_W * PANEL_H)
|
||||
rng = random.Random(42)
|
||||
|
||||
heads = [rng.randrange(-PANEL_H, 0) for _ in range(PANEL_W)]
|
||||
|
||||
for _frame in range(320):
|
||||
for y in range(PANEL_H):
|
||||
for x in range(PANEL_W):
|
||||
np[xy_to_index(x, y)] = (0, 0, 0)
|
||||
|
||||
for x in range(PANEL_W):
|
||||
heads[x] += 1
|
||||
if heads[x] > PANEL_H + 6:
|
||||
heads[x] = rng.randrange(-PANEL_H, 0)
|
||||
|
||||
head_y = heads[x]
|
||||
for tail in range(8):
|
||||
y = head_y - tail
|
||||
if 0 <= y < PANEL_H:
|
||||
brightness = clamp8(255 - tail * 36)
|
||||
np[xy_to_index(x, y)] = (0, brightness, 0)
|
||||
|
||||
np.write()
|
||||
time.sleep(0.045)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
33
workspace/code/panel16_rainbow_wave.py
Normal file
33
workspace/code/panel16_rainbow_wave.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""16x16 rainbow wave animation."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
from panel16_utils import PANEL_H, PANEL_W, clamp8, xy_to_index
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), PANEL_W * PANEL_H)
|
||||
|
||||
|
||||
def wheel(pos: int) -> tuple[int, int, int]:
|
||||
pos = 255 - (pos & 255)
|
||||
if pos < 85:
|
||||
return (clamp8(255 - pos * 3), 0, clamp8(pos * 3))
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (0, clamp8(pos * 3), clamp8(255 - pos * 3))
|
||||
pos -= 170
|
||||
return (clamp8(pos * 3), clamp8(255 - pos * 3), 0)
|
||||
|
||||
|
||||
for frame in range(240):
|
||||
for y in range(PANEL_H):
|
||||
for x in range(PANEL_W):
|
||||
color_pos = ((x * 10) + (y * 6) + frame * 5) & 255
|
||||
np[xy_to_index(x, y)] = wheel(color_pos)
|
||||
np.write()
|
||||
time.sleep(0.04)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
26
workspace/code/panel16_utils.py
Normal file
26
workspace/code/panel16_utils.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Helpers for 16x16 serpentine NeoPixel panel animations.
|
||||
|
||||
Mapping matches the simulator's 16x16 mode:
|
||||
- first LED at top-right
|
||||
- row 0 goes right -> left
|
||||
- row 1 goes left -> right
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
PANEL_W = 16
|
||||
PANEL_H = 16
|
||||
|
||||
|
||||
def xy_to_index(x: int, y: int, width: int = PANEL_W) -> int:
|
||||
"""Map panel coordinate (x, y) to LED index."""
|
||||
x = int(x)
|
||||
y = int(y)
|
||||
if y % 2 == 0:
|
||||
return y * width + (width - 1 - x)
|
||||
return y * width + x
|
||||
|
||||
|
||||
def clamp8(v: int) -> int:
|
||||
return max(0, min(255, int(v)))
|
||||
20
workspace/code/pattern_chase_demo.py
Normal file
20
workspace/code/pattern_chase_demo.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Chase pattern demo using led_patterns helpers."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
from led_patterns import chase_frame
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), 24)
|
||||
|
||||
for frame in range(120):
|
||||
frame_colors = chase_frame(len(np), frame, color=(0, 220, 255), tail=(0, 40, 55))
|
||||
for i, color in enumerate(frame_colors):
|
||||
np[i] = color
|
||||
np.write()
|
||||
time.sleep(0.05)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
20
workspace/code/pattern_rainbow_demo.py
Normal file
20
workspace/code/pattern_rainbow_demo.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""Rainbow pattern demo using led_patterns helpers."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
from led_patterns import rainbow_frame
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), 256)
|
||||
|
||||
for frame in range(120):
|
||||
frame_colors = rainbow_frame(len(np), frame, step=5)
|
||||
for i, color in enumerate(frame_colors):
|
||||
np[i] = color
|
||||
np.write()
|
||||
time.sleep(0.05)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
26
workspace/code/pattern_twinkle_demo.py
Normal file
26
workspace/code/pattern_twinkle_demo.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Twinkle pattern demo using led_patterns helpers."""
|
||||
|
||||
from machine import Pin
|
||||
import neopixel
|
||||
import time
|
||||
|
||||
from led_patterns import twinkle_frame
|
||||
|
||||
|
||||
np = neopixel.NeoPixel(Pin(4), 36)
|
||||
|
||||
for frame in range(120):
|
||||
frame_colors = twinkle_frame(
|
||||
len(np),
|
||||
frame,
|
||||
base=(0, 0, 6),
|
||||
sparkle=(255, 210, 130),
|
||||
sparkles=5,
|
||||
)
|
||||
for i, color in enumerate(frame_colors):
|
||||
np[i] = color
|
||||
np.write()
|
||||
time.sleep(0.08)
|
||||
|
||||
np.fill((0, 0, 0))
|
||||
np.write()
|
||||
68
workspace/lib/led_patterns.py
Normal file
68
workspace/lib/led_patterns.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""Compatibility pattern helpers for NeoPixel demos.
|
||||
|
||||
This file mirrors `workspace/code/led_patterns.py` so imports like
|
||||
`from led_patterns import ...` work even in older worker sessions that only
|
||||
include `/workspace/lib` in `sys.path`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import random
|
||||
|
||||
|
||||
Color = tuple[int, int, int]
|
||||
|
||||
|
||||
def _clamp(channel: int) -> int:
|
||||
return max(0, min(255, int(channel)))
|
||||
|
||||
|
||||
def wheel(pos: int) -> Color:
|
||||
pos = 255 - (pos & 255)
|
||||
if pos < 85:
|
||||
return (_clamp(255 - pos * 3), 0, _clamp(pos * 3))
|
||||
if pos < 170:
|
||||
pos -= 85
|
||||
return (0, _clamp(pos * 3), _clamp(255 - pos * 3))
|
||||
pos -= 170
|
||||
return (_clamp(pos * 3), _clamp(255 - pos * 3), 0)
|
||||
|
||||
|
||||
def rainbow_frame(led_count: int, frame: int, step: int = 4) -> list[Color]:
|
||||
if led_count <= 0:
|
||||
return []
|
||||
return [wheel((i * 256 // led_count + frame * step) & 255) for i in range(led_count)]
|
||||
|
||||
|
||||
def chase_frame(
|
||||
led_count: int,
|
||||
frame: int,
|
||||
color: Color = (255, 120, 0),
|
||||
tail: Color = (16, 0, 0),
|
||||
) -> list[Color]:
|
||||
if led_count <= 0:
|
||||
return []
|
||||
out: list[Color] = [(0, 0, 0) for _ in range(led_count)]
|
||||
head = frame % led_count
|
||||
trail = (head - 1) % led_count
|
||||
out[trail] = tuple(_clamp(v) for v in tail) # type: ignore[assignment]
|
||||
out[head] = tuple(_clamp(v) for v in color) # type: ignore[assignment]
|
||||
return out
|
||||
|
||||
|
||||
def twinkle_frame(
|
||||
led_count: int,
|
||||
frame: int,
|
||||
base: Color = (0, 0, 8),
|
||||
sparkle: Color = (255, 255, 180),
|
||||
sparkles: int = 3,
|
||||
seed: int = 1337,
|
||||
) -> list[Color]:
|
||||
if led_count <= 0:
|
||||
return []
|
||||
out: list[Color] = [tuple(_clamp(v) for v in base) for _ in range(led_count)] # type: ignore[list-item]
|
||||
rng = random.Random(seed + frame)
|
||||
for _ in range(min(max(0, sparkles), led_count)):
|
||||
idx = rng.randrange(led_count)
|
||||
out[idx] = tuple(_clamp(v) for v in sparkle) # type: ignore[assignment]
|
||||
return out
|
||||
19
workspace/lib/machine.py
Normal file
19
workspace/lib/machine.py
Normal file
@@ -0,0 +1,19 @@
|
||||
"""Minimal MicroPython-style machine module mock for browser simulation."""
|
||||
|
||||
|
||||
class Pin:
|
||||
IN = 0
|
||||
OUT = 1
|
||||
PULL_UP = 2
|
||||
PULL_DOWN = 3
|
||||
|
||||
def __init__(self, pin_id: int, mode: int = OUT, value: int = 0):
|
||||
self.id = int(pin_id)
|
||||
self.mode = int(mode)
|
||||
self._value = 1 if value else 0
|
||||
|
||||
def value(self, new_value=None):
|
||||
if new_value is None:
|
||||
return self._value
|
||||
self._value = 1 if int(new_value) else 0
|
||||
return self._value
|
||||
56
workspace/lib/neopixel.py
Normal file
56
workspace/lib/neopixel.py
Normal file
@@ -0,0 +1,56 @@
|
||||
"""NeoPixel mock for Pyodide/browser execution.
|
||||
|
||||
Supports a useful subset of MicroPython's neopixel.NeoPixel API:
|
||||
- NeoPixel(pin, n, bpp=3, timing=1)
|
||||
- __setitem__, __getitem__, __len__
|
||||
- fill(color)
|
||||
- write() # prints current pixel buffer snapshot
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
|
||||
def _normalize_color(value, bpp: int):
|
||||
if not hasattr(value, "__iter__"):
|
||||
raise TypeError("Color must be an iterable, e.g. (r, g, b)")
|
||||
parts = [int(v) for v in value]
|
||||
if len(parts) != bpp:
|
||||
raise ValueError(f"Expected {bpp} color channels, got {len(parts)}")
|
||||
out = []
|
||||
for channel in parts:
|
||||
out.append(max(0, min(255, channel)))
|
||||
return tuple(out)
|
||||
|
||||
|
||||
class NeoPixel:
|
||||
def __init__(self, pin, n: int, bpp: int = 3, timing: int = 1):
|
||||
self.pin = pin
|
||||
self.n = int(n)
|
||||
self.bpp = int(bpp)
|
||||
self.timing = int(timing)
|
||||
self._buf = [tuple([0] * self.bpp) for _ in range(self.n)]
|
||||
|
||||
def __len__(self):
|
||||
return self.n
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self._buf[int(index)]
|
||||
|
||||
def __setitem__(self, index, color):
|
||||
idx = int(index)
|
||||
self._buf[idx] = _normalize_color(color, self.bpp)
|
||||
|
||||
def fill(self, color):
|
||||
c = _normalize_color(color, self.bpp)
|
||||
for i in range(self.n):
|
||||
self._buf[i] = c
|
||||
|
||||
def write(self):
|
||||
pin_id = getattr(self.pin, "id", self.pin)
|
||||
payload = {
|
||||
"type": "neopixel",
|
||||
"pin": pin_id,
|
||||
"pixels": [list(pixel) for pixel in self._buf],
|
||||
"bpp": self.bpp,
|
||||
}
|
||||
print("[neopixel-json]" + json.dumps(payload))
|
||||
Reference in New Issue
Block a user