Stop tracking workspace/; bundled-demos/ is the canonical demo source
`workspace/` is runtime state (per-user folders, no-auth dev's `code/`) and shouldn't be in git. The same files were previously committed under both `workspace/code/` and `src/static/bundled-demos/`, which forced a Docker `diff -q` sync check and leaked user-scoped paths into version control. - /workspace/ added to .gitignore; all previously tracked files removed via `git rm --cached`. - src/static/bundled-demos/ becomes the single source of truth: panel16 demos, led_tutorial, led_patterns, neopixel demos, and main.py move here alongside the existing canonical demos. - New BUNDLED_DEMOS_DIR config; user_workspace seeders read from it. - main.py lifespan seeds WORKSPACE_ROOT/code/ on startup so a fresh clone running `pipenv run dev` still gets the full sample set (existing files never overwritten — user edits survive restarts). - Dockerfile drops `COPY workspace` and the diff sanity check. - README/LED_TUTORIAL repointed at the new canonical paths. - test_led_patterns loads led_patterns.py from bundled-demos. - test_api uses mkdir(exist_ok=True) for `code/` (startup pre-creates). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -176,6 +176,9 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# Editor / workspace (generated locally)
|
||||
workspace/users/
|
||||
# Editor runtime state — `workspace/` holds user files (auth-mode per-user
|
||||
# folders, no-auth dev's `code/`); the canonical demo source lives under
|
||||
# `src/static/bundled-demos/` and is what gets seeded into `workspace/`
|
||||
# on startup. Nothing under `workspace/` should ever be committed.
|
||||
/workspace/
|
||||
src/static/.reload-token
|
||||
|
||||
13
Dockerfile
13
Dockerfile
@@ -14,17 +14,8 @@ RUN pipenv install --system --deploy
|
||||
COPY src ./src
|
||||
COPY lib ./lib
|
||||
RUN mkdir -p src/static/bundled-lib && cp -f lib/*.py src/static/bundled-lib/
|
||||
COPY workspace ./workspace
|
||||
# Sanity-check: the canonical demos under `src/static/bundled-demos/` (used by
|
||||
# the editor's "Reset demos" button) must stay in sync with `workspace/code/`.
|
||||
# Files in `bundled-demos/` are committed to git and copied via `COPY src`
|
||||
# above; this step just fails the build if a checked-in copy drifted from the
|
||||
# canonical version, so the mismatch is caught here instead of at runtime.
|
||||
RUN for f in pattern_rainbow_demo.py pattern_twinkle_demo.py pattern_chase_demo.py \
|
||||
adc_slider_demo.py pin_demo.py serial_demo.py; do \
|
||||
diff -q "workspace/code/$f" "src/static/bundled-demos/$f" \
|
||||
|| { echo "ERROR: $f out of sync between workspace/code/ and src/static/bundled-demos/" >&2; exit 1; }; \
|
||||
done
|
||||
# `workspace/` is runtime/user state (gitignored) and is created on demand
|
||||
# at app startup — the image does not need to ship it.
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ This tutorial is for the browser editor's ESP32-style mocks:
|
||||
- `machine.Pin`
|
||||
- `neopixel.NeoPixel`
|
||||
|
||||
Use `workspace/code/led_tutorial.py` while reading this guide.
|
||||
Open `code/led_tutorial.py` in the editor while reading this guide. (Source of truth: `src/static/bundled-demos/led_tutorial.py` — the editor's `code/` folder is seeded from there on first run.)
|
||||
|
||||
## 1) Basic setup
|
||||
|
||||
|
||||
24
README.md
24
README.md
@@ -110,7 +110,7 @@ The browser runtime ships MicroPython-style stubs in repo `lib/` (they appear as
|
||||
- `utime` — `ticks_ms`, `ticks_diff`, `ticks_add`, `sleep_ms`, `sleep_us`, `sleep`
|
||||
- `micropython.const` — no-op helper for ported constant declarations
|
||||
|
||||
Use them from scripts in `workspace/code` like typical ESP32 / MicroPython examples:
|
||||
Use them from scripts in `code/` (your editor workspace, populated on first run from `src/static/bundled-demos/`) like typical ESP32 / MicroPython examples:
|
||||
|
||||
```python
|
||||
from machine import Pin
|
||||
@@ -132,18 +132,20 @@ Simulator modes:
|
||||
- rows zig-zag left/right.
|
||||
- The 16x16 popup closes automatically on **Stop** or when script execution finishes.
|
||||
|
||||
Tutorial files:
|
||||
Tutorial files (canonical source — committed under `src/static/bundled-demos/`; copies appear in your editor's `code/` folder on first run):
|
||||
|
||||
- `LED_TUTORIAL.md` - step-by-step NeoPixel tutorial
|
||||
- `workspace/code/led_tutorial.py` - runnable guided LED example
|
||||
- `workspace/code/led_patterns.py` - shared pattern helpers (used by automated tests); each `pattern_*_demo.py` duplicates what it needs and uses only Python stdlib + `machine` / `neopixel` / `time`
|
||||
- `workspace/code/pattern_rainbow_demo.py` - rainbow animation (self-contained)
|
||||
- `workspace/code/pattern_chase_demo.py` - Knight Rider–style bouncing scanner (self-contained)
|
||||
- `workspace/code/pattern_twinkle_demo.py` - twinkle animation (self-contained)
|
||||
- `workspace/code/panel16_utils.py` - helpers for 16x16 serpentine mapping
|
||||
- `workspace/code/panel16_rainbow_wave.py` - 16x16 rainbow wave
|
||||
- `workspace/code/panel16_bounce.py` - 16x16 bouncing pixel with trail
|
||||
- `workspace/code/panel16_matrix_rain.py` - 16x16 matrix rain effect
|
||||
- `led_tutorial.py` - runnable guided LED example
|
||||
- `led_patterns.py` - shared pattern helpers (used by automated tests); each `pattern_*_demo.py` duplicates what it needs and uses only Python stdlib + `machine` / `neopixel` / `time`
|
||||
- `pattern_rainbow_demo.py` - rainbow animation (self-contained)
|
||||
- `pattern_chase_demo.py` - Knight Rider–style bouncing scanner (self-contained)
|
||||
- `pattern_twinkle_demo.py` - twinkle animation (self-contained)
|
||||
- `panel16_utils.py` - helpers for 16x16 serpentine mapping
|
||||
- `panel16_rainbow_wave.py` - 16x16 rainbow wave
|
||||
- `panel16_bounce.py` - 16x16 bouncing pixel with trail
|
||||
- `panel16_matrix_rain.py` - 16x16 matrix rain effect
|
||||
|
||||
> `workspace/` is gitignored runtime state. To edit the **shipped** demo source, edit `src/static/bundled-demos/<file>.py` and re-run "Reset demos" in the editor (or restart the dev server with an empty `workspace/code/`).
|
||||
|
||||
## Dev auto-reload hook
|
||||
|
||||
|
||||
@@ -28,3 +28,10 @@ load_env_file(PROJECT_ROOT / ".env")
|
||||
|
||||
_default_workspace = PROJECT_ROOT / "workspace"
|
||||
WORKSPACE_ROOT = Path(os.environ.get("WORKSPACE_ROOT", str(_default_workspace))).resolve()
|
||||
|
||||
# Canonical demo source. Files here are the single source of truth for the
|
||||
# editor's "Reset demos" button and per-user account seeding. They ship with
|
||||
# the static bundle (`/static/bundled-demos/...`) so a static-only host
|
||||
# also exposes them. `workspace/` is intentionally NOT used for canonical
|
||||
# data — it is treated as runtime/user state and is gitignored.
|
||||
BUNDLED_DEMOS_DIR = STATIC_DIR / "bundled-demos"
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from editor_app.config import STATIC_DIR
|
||||
from editor_app.config import STATIC_DIR, WORKSPACE_ROOT
|
||||
from editor_app.db.models import Base
|
||||
from editor_app.db.session import get_engine
|
||||
from editor_app.deps import require_api_access
|
||||
@@ -14,11 +14,19 @@ from editor_app.routers.auth_routes import router as auth_router
|
||||
from editor_app.routers.files import router as files_router
|
||||
from editor_app.routers.frontend import router as frontend_router
|
||||
from editor_app.routers.users_admin import router as users_admin_router
|
||||
from editor_app.services import accounts
|
||||
from editor_app.services import accounts, user_workspace
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(_app: FastAPI):
|
||||
# `workspace/` is gitignored runtime state. On a fresh clone it doesn't
|
||||
# exist, and in no-auth dev mode the file tree's `code/` would otherwise
|
||||
# be empty — seed every bundled demo so `pipenv run dev` after `git
|
||||
# clone` Just Works without needing user accounts. Files already in
|
||||
# `code/` are left alone (user edits are preserved across restarts).
|
||||
WORKSPACE_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
user_workspace.seed_all_bundled_demos(WORKSPACE_ROOT)
|
||||
|
||||
engine = get_engine()
|
||||
Base.metadata.create_all(bind=engine)
|
||||
with engine.begin() as conn:
|
||||
|
||||
@@ -34,7 +34,13 @@ def user_workspace_root(user_id: int, username: str, workspace_root: Path | None
|
||||
|
||||
|
||||
def _seed_canonical_demos_into_code(code_dir: Path) -> None:
|
||||
src_root = config.PROJECT_ROOT.resolve() / "workspace" / "code"
|
||||
"""Copy bundled demos into a user's `code/` if missing.
|
||||
|
||||
Reads from `BUNDLED_DEMOS_DIR` (single source of truth, ships under
|
||||
`src/static/bundled-demos/`), never from `workspace/`, so this works
|
||||
even when `workspace/` is empty (gitignored runtime directory).
|
||||
"""
|
||||
src_root = config.BUNDLED_DEMOS_DIR.resolve()
|
||||
for filename in _CANONICAL_DEMO_FILENAMES:
|
||||
dst = code_dir / filename
|
||||
if dst.exists():
|
||||
@@ -45,7 +51,12 @@ def _seed_canonical_demos_into_code(code_dir: Path) -> None:
|
||||
|
||||
|
||||
def ensure_default_code_main(user_root: Path) -> None:
|
||||
"""Ensure code/ has main.py and self-contained NeoPixel demos (copied from repo workspace/code/)."""
|
||||
"""Ensure code/ has main.py and the canonical NeoPixel demos.
|
||||
|
||||
Demos are sourced from `BUNDLED_DEMOS_DIR` (the single committed home
|
||||
for sample scripts). Only files listed in `_CANONICAL_DEMO_FILENAMES`
|
||||
get auto-seeded — the rest are available via the editor's "Reset
|
||||
demos" button or a manual copy."""
|
||||
code_dir = user_root / "code"
|
||||
code_dir.mkdir(parents=True, exist_ok=True)
|
||||
main_py = code_dir / "main.py"
|
||||
@@ -54,6 +65,34 @@ def ensure_default_code_main(user_root: Path) -> None:
|
||||
_seed_canonical_demos_into_code(code_dir)
|
||||
|
||||
|
||||
def seed_all_bundled_demos(user_root: Path) -> None:
|
||||
"""Copy *every* file in `BUNDLED_DEMOS_DIR` into `<user_root>/code/`.
|
||||
|
||||
Used at app startup to populate a fresh `workspace/code/` with the
|
||||
full sample set so a no-auth dev install (`pipenv run dev` after
|
||||
`git clone`) has something to play with. Existing files are not
|
||||
overwritten — user edits are preserved.
|
||||
"""
|
||||
code_dir = user_root / "code"
|
||||
code_dir.mkdir(parents=True, exist_ok=True)
|
||||
main_py = code_dir / "main.py"
|
||||
if not main_py.exists():
|
||||
main_py.write_text(DEFAULT_MAIN_PY, encoding="utf-8")
|
||||
src_root = config.BUNDLED_DEMOS_DIR.resolve()
|
||||
if not src_root.is_dir():
|
||||
return
|
||||
for src in sorted(src_root.iterdir()):
|
||||
if not src.is_file() or not src.name.endswith(".py"):
|
||||
continue
|
||||
dst = code_dir / src.name
|
||||
if dst.exists():
|
||||
continue
|
||||
try:
|
||||
dst.write_text(src.read_text(encoding="utf-8"), encoding="utf-8")
|
||||
except (UnicodeDecodeError, OSError):
|
||||
continue
|
||||
|
||||
|
||||
def rename_user_workspace_leaf(
|
||||
user_id: int, old_username: str, new_username: str, workspace_root: Path | None = None
|
||||
) -> None:
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_save_file_collapses_duplicate_scoped_prefix(client, tmp_path):
|
||||
|
||||
def test_lib_folder_is_read_only_for_mutations(client, tmp_path):
|
||||
code_dir = tmp_path / "code"
|
||||
code_dir.mkdir()
|
||||
code_dir.mkdir(exist_ok=True)
|
||||
(code_dir / "main.py").write_text("print('ok')\n", encoding="utf-8")
|
||||
|
||||
save_blocked = client.post("/api/file/lib/new.txt", json={"content": "nope"})
|
||||
@@ -115,7 +115,7 @@ def test_read_file_non_utf8_returns_400(client, tmp_path):
|
||||
|
||||
def test_delete_file_success_and_errors(client, tmp_path):
|
||||
target = tmp_path / "code" / "delete-me.txt"
|
||||
target.parent.mkdir()
|
||||
target.parent.mkdir(exist_ok=True)
|
||||
target.write_text("x", encoding="utf-8")
|
||||
|
||||
ok = client.delete("/api/file/code/delete-me.txt")
|
||||
@@ -219,7 +219,7 @@ def test_folder_create_and_delete(client, tmp_path):
|
||||
|
||||
|
||||
def test_create_folder_collapses_duplicate_scoped_prefix(client, tmp_path):
|
||||
(tmp_path / "code").mkdir()
|
||||
(tmp_path / "code").mkdir(exist_ok=True)
|
||||
create = client.post("/api/folder/new/code/code/nested", json={"path": "ignored"})
|
||||
assert create.status_code == 200
|
||||
assert (tmp_path / "code" / "nested").is_dir()
|
||||
@@ -230,14 +230,14 @@ def test_folder_delete_errors(client, tmp_path):
|
||||
missing = client.delete("/api/folder/code/missing")
|
||||
assert missing.status_code == 404
|
||||
|
||||
(tmp_path / "code").mkdir()
|
||||
(tmp_path / "code").mkdir(exist_ok=True)
|
||||
(tmp_path / "code" / "file.txt").write_text("x", encoding="utf-8")
|
||||
not_dir = client.delete("/api/folder/code/file.txt")
|
||||
assert not_dir.status_code == 400
|
||||
|
||||
|
||||
def test_workspace_py_sources_returns_python_files(client, tmp_path):
|
||||
(tmp_path / "code").mkdir()
|
||||
(tmp_path / "code").mkdir(exist_ok=True)
|
||||
(tmp_path / "code" / "app.py").write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
response = client.get("/api/workspace/py-sources")
|
||||
|
||||
@@ -4,7 +4,8 @@ from pathlib import Path
|
||||
|
||||
def _load_patterns_module():
|
||||
repo_root = Path(__file__).resolve().parents[1]
|
||||
module_path = repo_root / "workspace" / "code" / "led_patterns.py"
|
||||
# Canonical home for shipped demos — `workspace/` is gitignored.
|
||||
module_path = repo_root / "src" / "static" / "bundled-demos" / "led_patterns.py"
|
||||
spec = importlib.util.spec_from_file_location("led_patterns", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
assert spec is not None and spec.loader is not None
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Knight Rider–style 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()
|
||||
@@ -1,47 +0,0 @@
|
||||
"""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 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):
|
||||
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()
|
||||
@@ -1,54 +0,0 @@
|
||||
"""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()
|
||||
@@ -1,54 +0,0 @@
|
||||
"""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)
|
||||
@@ -1,90 +0,0 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user