Add 'Reset demos' button to refresh canonical demo files

Existing accounts (including admin) seeded before new demos shipped
had no easy way to pull in the latest copies — the registration-time
seeder is intentionally non-destructive. The new badge action fetches
src/static/bundled-demos/manifest.json, confirms the overwrite, and
re-copies each canonical demo into code/. Open tabs of those files are
refreshed in place so the user sees the new content immediately.

src/static/bundled-demos/ ships the six canonical files plus the
manifest so this works in local mode and on a static-only host. The
Dockerfile now mirrors workspace/code/<demo>.py into bundled-demos/
during the image build, keeping the two locations in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 06:35:03 +12:00
parent 655f8b78fd
commit 76129469a1
10 changed files with 507 additions and 1 deletions

View File

@@ -0,0 +1,55 @@
"""ADC slider demo — drag the sliders that appear under the editor.
Two simulated ADCs:
* pin 34 — sets the base hue of a rainbow
* pin 35 — sets overall brightness
The strip lights up while the script runs; the values update live (no need to
restart the script when you move the slider).
"""
import time
from machine import ADC, Pin
from neopixel import NeoPixel
NUM_LEDS = 16
strip = NeoPixel(Pin(5, Pin.OUT), NUM_LEDS)
hue_pot = ADC(Pin(34))
bri_pot = ADC(Pin(35))
def hsv_to_rgb(h, s, v):
h = h - int(h)
i = int(h * 6)
f = h * 6 - i
p = v * (1 - s)
q = v * (1 - f * s)
t = v * (1 - (1 - f) * s)
if i == 0:
r, g, b = v, t, p
elif i == 1:
r, g, b = q, v, p
elif i == 2:
r, g, b = p, v, t
elif i == 3:
r, g, b = p, q, v
elif i == 4:
r, g, b = t, p, v
else:
r, g, b = v, p, q
return int(r * 255), int(g * 255), int(b * 255)
print("Move the ADC sliders below the editor while this runs.")
while True:
base_hue = hue_pot.read_u16() / 65535
brightness = bri_pot.read_u16() / 65535
for i in range(NUM_LEDS):
h = (base_hue + i / NUM_LEDS) % 1.0
strip[i] = hsv_to_rgb(h, 1.0, brightness)
strip.write()
time.sleep(0.04)

View File

@@ -0,0 +1,10 @@
{
"files": [
"pattern_rainbow_demo.py",
"pattern_twinkle_demo.py",
"pattern_chase_demo.py",
"adc_slider_demo.py",
"pin_demo.py",
"serial_demo.py"
]
}

View File

@@ -0,0 +1,83 @@
"""Knight Riderstyle bouncing scanner — self-contained (stdlib + simulated hardware only)."""
import time
from machine import Pin
import neopixel
# --- helpers
def _clamp(channel: int) -> int:
return max(0, min(255, int(channel)))
def _bounce_head_index(led_count: int, frame: int) -> int:
if led_count <= 1:
return 0
span = led_count - 1
cycle = span * 2
if cycle <= 0:
return 0
t = frame % cycle
return t if t <= span else 2 * span - t
def _bounce_phase_tail_direction(led_count: int, frame: int) -> int:
if led_count <= 1:
return -1
span = led_count - 1
cycle = span * 2
if cycle <= 0:
return -1
t = frame % cycle
if t <= span:
return -1
return 1
def knight_rider_scanner_frame(
led_count: int,
frame: int,
head_color=(220, 0, 28),
tail_len: int = 8,
falloff_gamma: float = 2.6,
):
if led_count <= 0:
return []
out = [(0, 0, 0) for _ in range(led_count)]
tl = max(1, tail_len)
head = _bounce_head_index(led_count, frame)
direc = _bounce_phase_tail_direction(led_count, frame)
gamma = max(1.05, falloff_gamma)
for rk in reversed(range(tl)):
idx = head + direc * rk
if idx < 0 or idx >= led_count:
continue
w = max(0.0, float(tl - rk) / float(tl))
strength = w**gamma
out[idx] = tuple(_clamp(int(head_color[ch] * strength)) for ch in range(3))
return out
# --- demo
NUM_LEDS = 16
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
for frame in range(200):
frame_colors = knight_rider_scanner_frame(
len(np),
frame,
head_color=(220, 0, 36),
tail_len=10,
falloff_gamma=2.85,
)
for i, color in enumerate(frame_colors):
np[i] = color
np.write()
time.sleep(0.05)
np.fill((0, 0, 0))
np.write()

View File

@@ -0,0 +1,47 @@
"""Rainbow NeoPixel sweep — self-contained (stdlib + simulated hardware only)."""
import time
from machine import Pin
import neopixel
# --- helpers (same logic as bundled led_patterns.py, inlined here)
def _clamp(channel: int) -> int:
return max(0, min(255, int(channel)))
def wheel(pos: int):
"""Return rainbow RGB at position 0255."""
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):
if led_count <= 0:
return []
return [wheel((i * 256 // led_count + frame * step) & 255) for i in range(led_count)]
# --- demo
NUM_LEDS = 16
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
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()

View File

@@ -0,0 +1,54 @@
"""Twinkle NeoPixel demo — self-contained (stdlib + simulated hardware only)."""
import random
import time
from machine import Pin
import neopixel
# --- helpers
def _clamp(channel: int) -> int:
return max(0, min(255, int(channel)))
def twinkle_frame(
led_count: int,
frame: int,
base=(0, 0, 8),
sparkle=(255, 255, 180),
sparkles: int = 3,
seed: int = 1337,
):
if led_count <= 0:
return []
out = [tuple(_clamp(v) for v in base) for _ in range(led_count)]
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)
return out
# --- demo
NUM_LEDS = 16
np = neopixel.NeoPixel(Pin(4), NUM_LEDS)
for frame in range(120):
frame_colors = twinkle_frame(
len(np),
frame,
base=(0, 0, 6),
sparkle=(255, 210, 130),
sparkles=3,
)
for i, color in enumerate(frame_colors):
np[i] = color
np.write()
time.sleep(0.08)
np.fill((0, 0, 0))
np.write()

View File

@@ -0,0 +1,54 @@
"""Pin features demo.
A "Pins" panel appears below the editor while this script runs:
* Pin 2 (OUT) — blinks every 200 ms; the indicator follows along.
* Pin 4 (OUT) — chases through .on() / .off() / .toggle().
* Pin 0 (IN) — click the toggle button in the panel to flip its value.
When it goes 0 -> 1 we register an IRQ that toggles pin 2.
* Pin 13 (PWM) — duty sweeps up and down; the bar shows the live duty cycle.
"""
import time
from machine import Pin, PWM
led_a = Pin(2, Pin.OUT)
led_b = Pin(4, Pin.OUT)
button = Pin(0, Pin.IN, Pin.PULL_UP)
fader = PWM(Pin(13), freq=1000, duty_u16=0)
def on_button(pin):
print("[irq] button rising edge -> toggling pin 2")
led_a.toggle()
button.irq(handler=on_button, trigger=Pin.IRQ_RISING)
tick = 0
duty = 0
direction = 1024
while True:
led_a.value(tick % 2)
if tick % 4 == 0:
led_b.on()
elif tick % 4 == 2:
led_b.off()
duty += direction
if duty >= 65535:
duty = 65535
direction = -1024
elif duty <= 0:
duty = 0
direction = 1024
fader.duty_u16(duty)
button.value()
tick += 1
time.sleep(0.1)

View File

@@ -0,0 +1,90 @@
"""Serial in/out demo.
When this script runs, a "Serial monitor" pane appears below the editor.
Try this:
* type hello and press Enter -> Python echoes "echo: hello"
* type color red -> the strip turns red
* try color 0,128,255 -> any (r,g,b) tuple works
* type off -> strip blanks
* type bye -> script exits cleanly
Anything Python `write()`s to the UART shows up in green; what you type back
is shown in white.
"""
import time
from machine import Pin, UART
from neopixel import NeoPixel
NUM_LEDS = 16
strip = NeoPixel(Pin(5, Pin.OUT), NUM_LEDS)
uart = UART(0, baudrate=115200)
PALETTE = {
"red": (255, 0, 0),
"green": (0, 255, 0),
"blue": (0, 0, 255),
"white": (200, 200, 200),
"purple": (160, 0, 200),
"orange": (255, 110, 0),
}
def fill(color):
strip.fill(color)
strip.write()
def parse_color(arg):
arg = arg.strip().lower()
if arg in PALETTE:
return PALETTE[arg]
parts = [p for p in arg.replace(",", " ").split() if p]
if len(parts) == 3:
try:
return tuple(max(0, min(255, int(p))) for p in parts)
except ValueError:
return None
return None
uart.write("ready. commands: color <name|r,g,b> | off | bye\n")
fill((0, 0, 0))
running = True
while running:
line = uart.readline()
if line is None:
time.sleep(0.05)
continue
text = line.decode("utf-8", errors="replace").strip()
if not text:
continue
if text == "bye":
uart.write("goodbye!\n")
running = False
break
if text == "off":
fill((0, 0, 0))
uart.write("strip off\n")
continue
if text.startswith("color"):
rest = text[len("color"):].strip()
color = parse_color(rest) if rest else None
if color is None:
uart.write("usage: color <name> | color r,g,b\n")
else:
fill(color)
uart.write(f"strip = {color}\n")
continue
uart.write(f"echo: {text}\n")
fill((0, 0, 0))