From 7ee15f8eac59c95e38b679d055e307d9075885ea Mon Sep 17 00:00:00 2001 From: Jimmy Date: Sun, 10 May 2026 06:55:59 +1200 Subject: [PATCH] Stop tracking workspace/; bundled-demos/ is the canonical demo source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 --- .gitignore | 7 +- Dockerfile | 13 +-- LED_TUTORIAL.md | 2 +- README.md | 24 ++--- src/editor_app/config.py | 7 ++ src/editor_app/main.py | 12 ++- src/editor_app/services/user_workspace.py | 43 ++++++++- .../static/bundled-demos}/led_patterns.py | 0 .../static/bundled-demos}/led_tutorial.py | 0 .../code => src/static/bundled-demos}/main.py | 0 .../static/bundled-demos}/neopixel_demo.py | 0 .../bundled-demos}/neopixel_time_test.py | 0 .../static/bundled-demos}/panel16_bounce.py | 0 .../bundled-demos}/panel16_matrix_rain.py | 0 .../bundled-demos}/panel16_rainbow_wave.py | 0 .../static/bundled-demos}/panel16_utils.py | 0 tests/test_api.py | 10 +-- tests/test_led_patterns.py | 3 +- workspace/code/adc_slider_demo.py | 55 ------------ workspace/code/pattern_chase_demo.py | 83 ----------------- workspace/code/pattern_rainbow_demo.py | 47 ---------- workspace/code/pattern_twinkle_demo.py | 54 ----------- workspace/code/pin_demo.py | 54 ----------- workspace/code/serial_demo.py | 90 ------------------- 24 files changed, 86 insertions(+), 418 deletions(-) rename {workspace/code => src/static/bundled-demos}/led_patterns.py (100%) rename {workspace/code => src/static/bundled-demos}/led_tutorial.py (100%) rename {workspace/code => src/static/bundled-demos}/main.py (100%) rename {workspace/code => src/static/bundled-demos}/neopixel_demo.py (100%) rename {workspace/code => src/static/bundled-demos}/neopixel_time_test.py (100%) rename {workspace/code => src/static/bundled-demos}/panel16_bounce.py (100%) rename {workspace/code => src/static/bundled-demos}/panel16_matrix_rain.py (100%) rename {workspace/code => src/static/bundled-demos}/panel16_rainbow_wave.py (100%) rename {workspace/code => src/static/bundled-demos}/panel16_utils.py (100%) delete mode 100644 workspace/code/adc_slider_demo.py delete mode 100644 workspace/code/pattern_chase_demo.py delete mode 100644 workspace/code/pattern_rainbow_demo.py delete mode 100644 workspace/code/pattern_twinkle_demo.py delete mode 100644 workspace/code/pin_demo.py delete mode 100644 workspace/code/serial_demo.py diff --git a/.gitignore b/.gitignore index 9a5a693..2c34bd4 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Dockerfile b/Dockerfile index c8a1215..f27bca3 100644 --- a/Dockerfile +++ b/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 diff --git a/LED_TUTORIAL.md b/LED_TUTORIAL.md index ceaa526..8643961 100644 --- a/LED_TUTORIAL.md +++ b/LED_TUTORIAL.md @@ -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 diff --git a/README.md b/README.md index 8aeceba..40af383 100644 --- a/README.md +++ b/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/.py` and re-run "Reset demos" in the editor (or restart the dev server with an empty `workspace/code/`). ## Dev auto-reload hook diff --git a/src/editor_app/config.py b/src/editor_app/config.py index 0d8b7b2..3584b95 100644 --- a/src/editor_app/config.py +++ b/src/editor_app/config.py @@ -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" diff --git a/src/editor_app/main.py b/src/editor_app/main.py index 9705541..065ce88 100644 --- a/src/editor_app/main.py +++ b/src/editor_app/main.py @@ -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: diff --git a/src/editor_app/services/user_workspace.py b/src/editor_app/services/user_workspace.py index bc6820d..9e459f9 100644 --- a/src/editor_app/services/user_workspace.py +++ b/src/editor_app/services/user_workspace.py @@ -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 `/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: diff --git a/workspace/code/led_patterns.py b/src/static/bundled-demos/led_patterns.py similarity index 100% rename from workspace/code/led_patterns.py rename to src/static/bundled-demos/led_patterns.py diff --git a/workspace/code/led_tutorial.py b/src/static/bundled-demos/led_tutorial.py similarity index 100% rename from workspace/code/led_tutorial.py rename to src/static/bundled-demos/led_tutorial.py diff --git a/workspace/code/main.py b/src/static/bundled-demos/main.py similarity index 100% rename from workspace/code/main.py rename to src/static/bundled-demos/main.py diff --git a/workspace/code/neopixel_demo.py b/src/static/bundled-demos/neopixel_demo.py similarity index 100% rename from workspace/code/neopixel_demo.py rename to src/static/bundled-demos/neopixel_demo.py diff --git a/workspace/code/neopixel_time_test.py b/src/static/bundled-demos/neopixel_time_test.py similarity index 100% rename from workspace/code/neopixel_time_test.py rename to src/static/bundled-demos/neopixel_time_test.py diff --git a/workspace/code/panel16_bounce.py b/src/static/bundled-demos/panel16_bounce.py similarity index 100% rename from workspace/code/panel16_bounce.py rename to src/static/bundled-demos/panel16_bounce.py diff --git a/workspace/code/panel16_matrix_rain.py b/src/static/bundled-demos/panel16_matrix_rain.py similarity index 100% rename from workspace/code/panel16_matrix_rain.py rename to src/static/bundled-demos/panel16_matrix_rain.py diff --git a/workspace/code/panel16_rainbow_wave.py b/src/static/bundled-demos/panel16_rainbow_wave.py similarity index 100% rename from workspace/code/panel16_rainbow_wave.py rename to src/static/bundled-demos/panel16_rainbow_wave.py diff --git a/workspace/code/panel16_utils.py b/src/static/bundled-demos/panel16_utils.py similarity index 100% rename from workspace/code/panel16_utils.py rename to src/static/bundled-demos/panel16_utils.py diff --git a/tests/test_api.py b/tests/test_api.py index 95fb6f6..0eefbe4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -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") diff --git a/tests/test_led_patterns.py b/tests/test_led_patterns.py index 7ecfb03..f3f0624 100644 --- a/tests/test_led_patterns.py +++ b/tests/test_led_patterns.py @@ -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 diff --git a/workspace/code/adc_slider_demo.py b/workspace/code/adc_slider_demo.py deleted file mode 100644 index 79e232f..0000000 --- a/workspace/code/adc_slider_demo.py +++ /dev/null @@ -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) diff --git a/workspace/code/pattern_chase_demo.py b/workspace/code/pattern_chase_demo.py deleted file mode 100644 index ecec0f3..0000000 --- a/workspace/code/pattern_chase_demo.py +++ /dev/null @@ -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() diff --git a/workspace/code/pattern_rainbow_demo.py b/workspace/code/pattern_rainbow_demo.py deleted file mode 100644 index 1622ebc..0000000 --- a/workspace/code/pattern_rainbow_demo.py +++ /dev/null @@ -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() diff --git a/workspace/code/pattern_twinkle_demo.py b/workspace/code/pattern_twinkle_demo.py deleted file mode 100644 index 349a651..0000000 --- a/workspace/code/pattern_twinkle_demo.py +++ /dev/null @@ -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() diff --git a/workspace/code/pin_demo.py b/workspace/code/pin_demo.py deleted file mode 100644 index d63aa68..0000000 --- a/workspace/code/pin_demo.py +++ /dev/null @@ -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) diff --git a/workspace/code/serial_demo.py b/workspace/code/serial_demo.py deleted file mode 100644 index ce186b9..0000000 --- a/workspace/code/serial_demo.py +++ /dev/null @@ -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 | 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 | 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))