From 42c14361e8cb82ef5e5c4fd1927b5ccddfe39764 Mon Sep 17 00:00:00 2001 From: Jimmy Date: Mon, 4 May 2026 22:48:54 +1200 Subject: [PATCH] feat(simulator): add GUI runner, stubs, and workspace assets Add host simulator scaffolding, examples, and docs so led-driver main can run end-to-end with MicroPython module stubs. Co-authored-by: Cursor --- .gitignore | 5 + README.md | 46 +++ .../led_driver_sim_hook.cpython-313.pyc | Bin 0 -> 625 bytes examples/blink.py | 19 ++ examples/chase.py | 25 ++ gui_main.py | 265 ++++++++++++++++++ led_driver_sim_hook.py | 17 ++ settings.default.json | 13 + stubs/__pycache__/machine.cpython-313.pyc | Bin 0 -> 1157 bytes stubs/__pycache__/neopixel.cpython-313.pyc | Bin 0 -> 2479 bytes stubs/__pycache__/network.cpython-313.pyc | Bin 0 -> 1607 bytes stubs/__pycache__/ubinascii.cpython-313.pyc | Bin 0 -> 346 bytes stubs/__pycache__/utime.cpython-313.pyc | Bin 0 -> 1119 bytes stubs/machine.py | 18 ++ stubs/neopixel.py | 37 +++ stubs/network.py | 38 +++ stubs/ubinascii.py | 6 + stubs/utime.py | 22 ++ workspace/patterns/.gitkeep | 1 + workspace/patterns/__init__.py | 1 + 20 files changed, 513 insertions(+) create mode 100644 .gitignore create mode 100644 __pycache__/led_driver_sim_hook.cpython-313.pyc create mode 100644 examples/blink.py create mode 100644 examples/chase.py create mode 100644 gui_main.py create mode 100644 led_driver_sim_hook.py create mode 100644 settings.default.json create mode 100644 stubs/__pycache__/machine.cpython-313.pyc create mode 100644 stubs/__pycache__/neopixel.cpython-313.pyc create mode 100644 stubs/__pycache__/network.cpython-313.pyc create mode 100644 stubs/__pycache__/ubinascii.cpython-313.pyc create mode 100644 stubs/__pycache__/utime.cpython-313.pyc create mode 100644 stubs/machine.py create mode 100644 stubs/neopixel.py create mode 100644 stubs/network.py create mode 100644 stubs/ubinascii.py create mode 100644 stubs/utime.py create mode 100644 workspace/patterns/.gitkeep create mode 100644 workspace/patterns/__init__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8022c72 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +workspace/settings.json +workspace/presets.json +workspace/patterns/* +!workspace/patterns/__init__.py +!workspace/patterns/.gitkeep diff --git a/README.md b/README.md index 5739f11..055a6a7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,48 @@ # led-simulator +Graphical **NeoPixel** simulator for host Python. It uses the same layout as device flash: + +- **`led-driver/src`** → flash root **`/`** (application modules, `patterns/`, `presets.json`, …) +- **`led-driver/lib`** → **`/lib`** (Microdot, utemplate, …) + +It runs the **whole application**: **`led-driver/src/main.py`** as **`__main__`** (`runpy.run_path(..., run_name="__main__")` → `asyncio.run(main(port=…))`). Startup matches the device — **`Settings`**, **`presets.load`**, **default preset** from settings, **`patterns/`**, **Microdot** **`/ws`**, **`presets_loop`**, **UDP hello**. Only **`machine`**, **`neopixel`**, **`network`**, **`utime`**, and **`ubinascii`** are stubbed (`stubs/`). + +The GUI sets **`LED_SIM_ROOT`** so **`main.py`** can register the Microdot app for **Stop**, and **`LED_SIM_PORT`** for the HTTP/WebSocket listen port (default **80** on device when unset). On the ESP32 those variables are unset. + +## Requirements + +- Python 3.10+ +- Tk (e.g. `python3-tk` on Debian/Ubuntu if the window fails to open) + +## Run + +From the repository root: + +```bash +pipenv install # optional; Microdot is vendored under led-driver/lib +python3 led-simulator/gui_main.py +``` + +## UI + +- **Start** / **Stop**: start or stop the full **`main.py`** process (same code path as the chip). +- **HTTP / WS port**: Microdot listen port; WebSocket URL **`ws://127.0.0.1:/ws`** (JSON **v1**). + +Configure the app like on flash: **`workspace/settings.json`** (patched as device settings path), **`presets.json`** and **`patterns/`** under **`led-simulator/workspace/`** when the simulator creates symlinks from **`src/`**, or edit files directly under **`led-driver/src/`** if you use a real **`patterns/`** directory there. + +Example pattern modules: **`led-simulator/examples/`** (`Blink`, `Chase`). Copy into **`patterns/`** as on the device. + +## Notes + +- Changing the port spinbox only takes effect on the next **Start**. +- If **`led-driver/src/patterns`** already exists as a normal directory, the simulator leaves it in place. + +## Shell (no GUI) + +```bash +cd led-driver/src +PYTHONPATH="../lib:../../led-simulator/stubs" LED_SIM_ROOT="../../led-simulator" LED_SIM_PORT=8765 \ + python3 main.py +``` + +(Adjust paths from your checkout root; **`LED_SIM_ROOT`** must point at **`led-simulator/`** so **Stop**-style hooks work if you use them.) diff --git a/__pycache__/led_driver_sim_hook.cpython-313.pyc b/__pycache__/led_driver_sim_hook.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b7d425c64f43bc80836f0556a36bda1b5fdf409 GIT binary patch literal 625 zcmZutu};G<5Vf0>6asB$gpdaYIzV@Xm^vUK5K;sSLsTVJ8*u9M?1IWvX6AlGKZ(Fl zi621Z2e=Ca5W>AD@#qykDuP4*yDuZA|7YV88-C=0R_f>*(}sqxnP8t3hzQRD4W<^vACi4p(+ literal 0 HcmV?d00001 diff --git a/examples/blink.py b/examples/blink.py new file mode 100644 index 0000000..2ee3734 --- /dev/null +++ b/examples/blink.py @@ -0,0 +1,19 @@ +"""Example pattern for led-simulator / led-driver (generator + yield per tick).""" + +import utime + + +class Blink: + def __init__(self, presets): + self.presets = presets + + def run(self, preset): + colors = preset.c if preset.c else [(255, 255, 255)] + idx = 0 + delay_ms = max(1, int(preset.d)) + while True: + rgb = colors[idx % len(colors)] + self.presets.fill(self.presets.apply_brightness(rgb, preset.b)) + yield + utime.sleep_ms(delay_ms) + idx += 1 diff --git a/examples/chase.py b/examples/chase.py new file mode 100644 index 0000000..baf222f --- /dev/null +++ b/examples/chase.py @@ -0,0 +1,25 @@ +"""Simple one-pixel chase (uses preset c[0] for the dot, preset n3 as step).""" + +import utime + + +class Chase: + def __init__(self, presets): + self.presets = presets + + def run(self, preset): + n = self.presets.num_leds + rgb = preset.c[0] if preset.c else (255, 64, 0) + raw_step = int(getattr(preset, "n3", 1)) + step = raw_step if raw_step != 0 else 1 + pos = 0 + delay_ms = max(1, int(preset.d)) + while True: + off = (0, 0, 0) + for i in range(n): + self.presets.n[i] = off + self.presets.n[pos % n] = self.presets.apply_brightness(rgb, preset.b) + self.presets.n.write() + yield + utime.sleep_ms(delay_ms) + pos += step diff --git a/gui_main.py b/gui_main.py new file mode 100644 index 0000000..b6ff8ce --- /dev/null +++ b/gui_main.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +""" +Graphical NeoPixel simulator for led-driver (CPython). + +Treats ``led-driver/src`` as flash root (``/``) and ``led-driver/lib`` as ``/lib``. +**Start** runs the full application: ``led-driver/src/main.py`` as ``__main__`` +(``asyncio.run(main(port=…))``) — same startup as the device: ``settings``, +``presets.json``, default preset, ``patterns/``, WebSocket, UDP hello, etc. +""" + +from __future__ import annotations + +import os +import queue +import runpy +import shutil +import sys +import threading +import tkinter as tk +from pathlib import Path +from tkinter import messagebox, ttk + +# --- Paths (before importing driver code) --- +SIM_ROOT = Path(__file__).resolve().parent +WORKSPACE = SIM_ROOT / "workspace" +STUBS_ROOT = SIM_ROOT / "stubs" +DRIVER_ROOT = SIM_ROOT.parent / "led-driver" +DRIVER_SRC = DRIVER_ROOT / "src" +DRIVER_LIB = DRIVER_ROOT / "lib" +DRIVER_MAIN = DRIVER_SRC / "main.py" +DEFAULT_SETTINGS = SIM_ROOT / "settings.default.json" + + +def _ensure_workspace_dirs(): + WORKSPACE.mkdir(parents=True, exist_ok=True) + (WORKSPACE / "patterns").mkdir(parents=True, exist_ok=True) + settings_path = WORKSPACE / "settings.json" + if not settings_path.is_file() and DEFAULT_SETTINGS.is_file(): + shutil.copy2(DEFAULT_SETTINGS, settings_path) + presets_path = WORKSPACE / "presets.json" + if not presets_path.is_file(): + presets_path.write_text("{}\n", encoding="utf-8") + + +def _install_driver_path_and_stubs(): + """``/lib`` then ``/`` then stubs (MicroPython search order, stubs win).""" + for p in (DRIVER_LIB, DRIVER_SRC, STUBS_ROOT): + sys.path.insert(0, str(p)) + + +def _patch_settings_path(): + _ensure_workspace_dirs() + import settings as led_settings + + led_settings.Settings.SETTINGS_FILE = str(WORKSPACE / "settings.json") + + +_install_driver_path_and_stubs() +_patch_settings_path() + +import neopixel as neopixel_stub # noqa: E402 + + +def _ensure_src_symlinks(): + """If flash root (``src``) has no ``patterns`` / ``presets.json``, link workspace copies.""" + _ensure_workspace_dirs() + pat_ws = WORKSPACE / "patterns" + pat_link = DRIVER_SRC / "patterns" + if not pat_link.exists(): + pat_link.symlink_to(pat_ws.resolve(), target_is_directory=True) + elif pat_link.is_symlink(): + pass + # Real directory: treat as on-device flash (do not replace). + + pr_ws = WORKSPACE / "presets.json" + if not pr_ws.is_file(): + pr_ws.write_text("{}\n", encoding="utf-8") + pr_link = DRIVER_SRC / "presets.json" + if not pr_link.exists(): + pr_link.symlink_to(pr_ws.resolve()) + + +class LedSimulatorApp: + def __init__(self): + _ensure_workspace_dirs() + + self.root = tk.Tk() + self.root.title("led-simulator — led-driver (full app)") + self.root.minsize(640, 280) + + self._pixel_queue: queue.Queue[list[tuple[int, int, int]] | None] = queue.Queue() + self._driver_thread: threading.Thread | None = None + self._driver_running = False + self._ws_port = tk.IntVar(value=8765) + self._status = tk.StringVar( + value="Idle — Start runs the full main.py (settings, presets, patterns/, /ws)." + ) + self._canvas_leds: list[int] = [] + + self._build_ui() + self._install_pixel_sink() + self.root.after(50, self._pump_pixels) + + self.root.protocol("WM_DELETE_WINDOW", self._on_quit) + + def _build_ui(self): + top = ttk.Frame(self.root, padding=8) + top.pack(fill=tk.X) + + row = ttk.Frame(top) + row.pack(fill=tk.X) + + ttk.Button(row, text="Start", command=self._on_start).pack(side=tk.LEFT, padx=(0, 6)) + ttk.Button(row, text="Stop", command=self._on_stop).pack(side=tk.LEFT, padx=(0, 16)) + ttk.Label(row, text="HTTP / WS port:").pack(side=tk.LEFT) + ttk.Spinbox(row, from_=1024, to=65535, textvariable=self._ws_port, width=8).pack( + side=tk.LEFT, padx=4 + ) + ttk.Label(row, text="(Microdot; path /ws)").pack(side=tk.LEFT, padx=8) + + hint = ttk.Label( + top, + text=( + "Flash root is led-driver/src: edit workspace/settings.json, presets.json, " + "and patterns/ (or use files already under src/)." + ), + wraplength=620, + justify=tk.LEFT, + ) + hint.pack(fill=tk.X, pady=(8, 0)) + + self._canvas = tk.Canvas(self.root, height=120, background="#1a1a1a", highlightthickness=0) + self._canvas.pack(fill=tk.BOTH, expand=True, padx=8, pady=(0, 8)) + + st = ttk.Label(self.root, textvariable=self._status, padding=(8, 0, 8, 8)) + st.pack(fill=tk.X) + + def _install_pixel_sink(self): + def sink(buf: list[tuple[int, int, int]]): + self._pixel_queue.put(buf) + + neopixel_stub.PIXEL_SINK = sink + + def _pump_pixels(self): + try: + while True: + buf = self._pixel_queue.get_nowait() + if buf is None: + self._draw_leds([(0, 0, 0)] * max(1, len(self._canvas_leds))) + else: + self._draw_leds(buf) + except queue.Empty: + pass + self.root.after(50, self._pump_pixels) + + def _draw_leds(self, buf: list[tuple[int, int, int]]): + self._canvas.delete("led") + n = len(buf) + if n == 0: + return + cw = self._canvas.winfo_width() or 700 + margin = 12 + usable = max(40, cw - 2 * margin) + gap = 4 + diam = max(6, min(22, (usable - (n - 1) * gap) // n)) + y = 50 + total_w = n * diam + (n - 1) * gap + x0 = max(margin, (cw - total_w) // 2) + self._canvas_leds = [] + for i, (r, g, b) in enumerate(buf): + x = x0 + i * (diam + gap) + fill = f"#{r:02x}{g:02x}{b:02x}" + outline = "#444444" if (r + g + b) > 40 else "#666666" + oid = self._canvas.create_oval( + x, y, x + diam, y + diam, fill=fill, outline=outline, width=1, tags="led" + ) + self._canvas_leds.append(oid) + + def _run_main_py_thread(self, port: int): + try: + try: + import led_driver_sim_hook as sim_hook + + sim_hook.clear_app() + except ImportError: + pass + + os.environ["LED_SIM_PORT"] = str(port) + os.environ["LED_SIM_ROOT"] = str(SIM_ROOT) + + runpy.run_path(str(DRIVER_MAIN), run_name="__main__") + except Exception as ex: # noqa: BLE001 + self.root.after(0, lambda e=ex: messagebox.showerror("Simulator", str(e))) + finally: + for key in ("LED_SIM_PORT", "LED_SIM_ROOT"): + os.environ.pop(key, None) + try: + import led_driver_sim_hook as sim_hook + + sim_hook.clear_app() + except ImportError: + pass + self._driver_running = False + self._pixel_queue.put(None) + self.root.after(0, lambda: self._status.set("Stopped.")) + + def _on_start(self): + if self._driver_running: + messagebox.showwarning("Simulator", "Already running — press Stop first.") + return + + for k in list(sys.modules): + if k.startswith("patterns."): + del sys.modules[k] + + _ensure_src_symlinks() + os.chdir(DRIVER_SRC) + + port = int(self._ws_port.get()) + self._driver_running = True + self.root.after( + 0, + lambda p=port: self._status.set( + f"Running full app (main.py) — ws://127.0.0.1:{p}/ws" + ), + ) + + self._driver_thread = threading.Thread( + target=self._run_main_py_thread, + args=(port,), + daemon=True, + ) + self._driver_thread.start() + + def _on_stop(self): + try: + import led_driver_sim_hook as sim_hook + + app = sim_hook.get_app() + if app is not None and getattr(app, "server", None): + app.shutdown() + except ImportError: + pass + + def _on_quit(self): + self._on_stop() + self.root.destroy() + + def run(self): + self.root.mainloop() + + +def main(): + if not DRIVER_SRC.is_dir() or not DRIVER_MAIN.is_file(): + print("led-driver/src/main.py not found next to led-simulator.", file=sys.stderr) + sys.exit(1) + if not DRIVER_LIB.is_dir(): + print("led-driver/lib not found next to led-simulator.", file=sys.stderr) + sys.exit(1) + app = LedSimulatorApp() + app.run() + + +if __name__ == "__main__": + main() diff --git a/led_driver_sim_hook.py b/led_driver_sim_hook.py new file mode 100644 index 0000000..174018c --- /dev/null +++ b/led_driver_sim_hook.py @@ -0,0 +1,17 @@ +"""Host-only: led-simulator registers the Microdot app so Stop can call shutdown().""" + +_app = None + + +def register_app(app): + global _app + _app = app + + +def get_app(): + return _app + + +def clear_app(): + global _app + _app = None diff --git a/settings.default.json b/settings.default.json new file mode 100644 index 0000000..c802665 --- /dev/null +++ b/settings.default.json @@ -0,0 +1,13 @@ +{ + "led_pin": 10, + "num_leds": 32, + "color_order": "rgb", + "name": "sim", + "debug": false, + "default": "on", + "brightness": 255, + "transport_type": "wifi", + "wifi_channel": 1, + "ssid": "", + "password": "" +} diff --git a/stubs/__pycache__/machine.cpython-313.pyc b/stubs/__pycache__/machine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d1b8591e2db1c2732cd2c08bae94e33af58feb6 GIT binary patch literal 1157 zcmZ8gJ8u&~5Z=9~*$ip&)VvHVvWyii8FvOr#4U1=7YjpPjYtVRrA3LX`rg zBBg(XJ4$|vSxPE*0fW*qb7xzGIo)?VvpX|8-^@*`)iMEj`RO~~p@jTEWiG~SF?|S& zA@PVHHFB4Dv`#$jkyh*qn%i`+84H`%GhOTBhG+ zA&-N7XEO{s4B96%o&k^9&^)V7`dXg~ z%d>^uGu$Pm@9=nnK_*ZhnPAA4t0`lUg`qM(w8Jd$b+m$DN%*2Zy#+8Ndn+M)3)yo* zjFC%^jA(15DUElO5hb0#HI*TQu&b;Aj~VZXDx4EGLJ2^c01k@UY91z0(CqUlI%*C? z(hvMpHjx8AiBpk;VIXpKK}iz{l+77ojlq#BF~(z_GNvj}Ut_N2nJSR!n2c9zpY8El z?R04hIuXkV*lV|h-f=q$z`M*CTs{kNU198f)(&Soml^AFA=40A6eqB)zkai;v==RL z1%jw`#$?(~xzE~ZD)>Q`29mK65mzzK3Iej21F|pg0GyCpMaS#yF}N3@R_tGU{_lgG4?`q&ubTG<}eY{xUlOy+9dWu~1h<~7IGE`7>Pj}2rm z4Hhav!~khyu<<5zX2=D?DI7wAL&$gluIMtLsw;poT?LHmF~Ec#=Pl!jY>S@=PqP|_ z5EZ!*5F0!@jvK+xcUU+p?7U`$Tf>cWv?v`?2`Lh*abH5lNmeS%%d6fJ?8`ON z_NnbF2HY9Oip#7qo09+%>-90xF%bi^} zV#}Fb)2HY9ZhbhVeA}`Ci(6BIClo390IoknN>LJuQYpH6DWnBCEQJIZz))c2DMCt^ z4q-)Us1ZS@M}CCW30TEF8mqblStX&*mKFKAD@yZ9?IT+*r5XW&l%>|FuNEVEhNpb4 zrxJRE?Q=cV&?9UQ%SCdoE!=gpEz$!p7#B}AR#?Tf3XZH|N>(wOtA7Jz6MoWkP22U# zc7^$hWxAfPdi9#ceA!~QFPpaK%hmD(fN677Yc|I5&c15eqin)gKPg*v=2Py|ai5O3 zF<1kNf$=oDr3}Mm9tf-oMA_CvlxP;S=3WQ5MpjapY4+XdgW=hWzoZU6O&xqJKN?<0 zE|wSCS9*8M4$XDmedqQU4=NA$J)-ji_p6Wf#mwT>AGR-CobH=C`bS6i^wB%+>|1m6 z_UB6-dsqHvycSoqwy6_0^Vjp!?5Fh3HI=;aK7Eop$lKvGm56gNz`o$72AvrY<2h*m zZAFUg=qK~EkflMcB#|q=6Xsov(Ug_(XJ#du?ZH<%0YE|R-8Uz%PtInZw(owD*!|zr zVWP$9&I;w|)RvmmLtj!o-WK)HP#L-;)We$9qc`(j+2nnZ>RYrE@-%EWRrZ1iYO}_| zH^cU&BE()4;jnB=MG4C4GnLaQwOGG|L8*-wIFg#<#|WzJHtcmFbOU5tcn16U#dt5m zc7z=WuOeU_6!kaYgxt_i#jzYvO(m?#fnJm`)3W^5NGLeO0PZ&cz=d>XioQT5kp>>f zbHfk&zCU%pb*Xo7sUsJO;iVTq9-VDdN7s_1r+51JZ2!IeclX~qgS9(gU{s&sV3_cL zfzW~44H($SZs1MiSwbISko~BVhjW|NIOLutDj4qv5BQrU&*sk$pE2}&;dGE}QI4oc zr;B?uN{%Xi=zd$2Lowb94(@9JqRc!^^{u3P?s&5&?&a_1Z-4o7`oNk@(tE_!vaw)HNmYU{Ct z+V|%+NgW8@!&e|tal;sODzN>bFa#gM9+tI$Wr=r#8@$KghB?fg_*<{Zl+s_xskIns R>$;ZwE4GUctP=q7>OX2t*lYj* literal 0 HcmV?d00001 diff --git a/stubs/__pycache__/network.cpython-313.pyc b/stubs/__pycache__/network.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..672533968b3ff2464b1181f7d6ca74ad733305ab GIT binary patch literal 1607 zcmZWp&yU+g6dotG^TSTFXj7%iF09oqqCSx97A-2(s$z>;Scy%mx+^LbY3g;H)LT0a zGY(aAAt$tmB7sl@|3di}IC1AdZ^0C)5~sbOYE!Al^4{2vy30iQJo9Ee-}kuZvL>gU*?H(ph(#E&Bqmuomb3CKe}(SwlPziaSlxM%Rt@Pa|eB5v@ZGp#3^dMtnT=~nv?&0t93WIV`=eK3d2-U67A+Y3HClV(Y~#7DiN^@y2k zUo&%YnR@-mWnJ5G;{vsv$otZTu*_Y*8<*yD9CtR1_)6a@OmAb+RN zP;mQLnwX+U^9pn(WLI)@LUzebTzR6ck~_Fkv|VWl5xvO$r=zl&RYPgKGa973!~BzW;s&;fH||{B%3(flud%PZ!xTAQxdsy zT%D$-c|6_L?ZgX$=C#NQEM#X9{%1PwAz1K4MFl~mCkc~Hc~n{d;6ii#{fo`@=K9*& zsB&RrqnZ8JM%rt$qr%0e_$3n4%Fz>-X*3-8?y=%2dfj9$E&gnXGPB~>>QA(lXN&XG zNQ;vdF%|S4z5;NG`gY*LB`Q-Igq@*}eVx)ThPI#1R4MIxj7PrLcl#ks7jEp(_VxBv zhF6W%5S9=OgjEFmfX1gO<&hnE4z;6*d3(di<&=s9g_EMJ7@na3yes@|fUn8)RCWCE zk(^WBd{LB@+6z6e)Q-v$fC}(Zg;=r!zT7qy_~g(|=zuiUy0t~Ouf@vN4S29K+2$u$ y}B>IEWu;s=K!APB}w{|eE6(X9;?%0X{5dR0xP+*Gy literal 0 HcmV?d00001 diff --git a/stubs/__pycache__/ubinascii.cpython-313.pyc b/stubs/__pycache__/ubinascii.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f157879b027530ea204b619e22f6bae536601b42 GIT binary patch literal 346 zcmey&%ge<81ecm`XSM?A#~=<2FhLog9e|9f48aUV48e@SOx}z|j4_NsEHGsVx`+uV z&W7N_SVhd~Oq$G9M!ty!3ci`iMfm}hB^mj73JIl2nR$uD$(fl63MKgp&M;xPsNPGE z5q_F1x7g$3Q}UDJ<8N`mByX{2q*ml)rd8hJEX{*5S2BDCX}IO0pOK%Ns-Kman_H=0 zP?VpQnp{$>pOc!Ro1C9lQk0*QlUfAk7H8&`<|LNn7wH$5lqMDH!yTkoPXy7EU;Z*vD>ZhZ4=-? zh{pJf1TG}RlLs#*9{f|N#Dl8`5-xf(^bf$9*X<%+oMa~R=DnSM-#61%I^9jczC8c# zzN3U3;Gttul(YsR*(N4o#H6M)MHw|^CYj1b!elc6Q8AMc6J`oxazQfG)xzBeE5V{) z(|ORC)8~Dr7hRkAQE{dbxSlhk4^*8>j=Nr?Ive=R82W|3SaOe!j_%liCblGGGI(ne z!Zs<#7t(Hm3BX@nG_zB*YThFoHJxgeg8Z;l&ss35X1!K;o_ z4`pw5*`T51agLwq;dp-0_niEa>v=2rI`fwtJK%X_Vf(d!`PHh!#4#q7M*(28pqYjQ(8Oaa$D^6X zH~&~aDMS^M<^V|tV;M&+EC-#k#E?qvPv~zbq3nXf*edMu{F)#5HP;SRt7uxq33KWr z5!eKaPz_xB2}hB8;r(fFehC6l(N1jyn`>{@_A-~>XPcR;d)l>615IsoR~c<{i&4XS z%mueBB-`XM%>jMD3#a21M4V8A8$`Q0Hc^MYSa!YHz&Ww=ZAcY&eqQ_?7JVM*F%RA2 zLoT${4ayL%A%#*f0zS+Ugs!)Fbsslj=b(NTJX)XqbmsM=om4Zef4JZ58T@>{sg3O_ zV}g{C3R9L<@omd$j~;(Q>|cA{D3a*M&R~W!j&!```ISc1xy|~ZhNI;u#c!EX`i None +PIXEL_SINK = None + + +class NeoPixel: + def __init__(self, pin, n): + self.pin = pin + self.n = n + self._buf = [(0, 0, 0)] * n + + def __setitem__(self, index, value): + if isinstance(value, (list, tuple)) and len(value) >= 3: + r, g, b = (int(value[0]), int(value[1]), int(value[2])) + self._buf[index] = ( + max(0, min(255, r)), + max(0, min(255, g)), + max(0, min(255, b)), + ) + + def __getitem__(self, index): + return self._buf[index] + + def fill(self, color): + c = (0, 0, 0) + if isinstance(color, (list, tuple)) and len(color) >= 3: + c = ( + max(0, min(255, int(color[0]))), + max(0, min(255, int(color[1]))), + max(0, min(255, int(color[2]))), + ) + self._buf = [c] * self.n + + def write(self): + if PIXEL_SINK is not None: + PIXEL_SINK(list(self._buf)) diff --git a/stubs/network.py b/stubs/network.py new file mode 100644 index 0000000..add5267 --- /dev/null +++ b/stubs/network.py @@ -0,0 +1,38 @@ +"""Minimal `network` stub so imports succeed (led-simulator).""" + +STA_IF = 0 +AP_IF = 1 + +# Fixed “STA” identity for hello.py / discovery on the host. +_SIM_MAC = b"\xaa\xbb\xcc\xdd\xee\xff" + + +class WLAN: + PM_NONE = 0 + + def __init__(self, interface): + self._interface = interface + self._active = False + + def active(self, is_active=None): + if is_active is None: + return self._active + self._active = bool(is_active) + return None + + def config(self, param=None, **kwargs): + if param == "mac": + return _SIM_MAC + return None + + def connect(self, *args, **kwargs): + return None + + def isconnected(self): + return True + + def ifconfig(self, config_tuple=None): + if config_tuple is None: + # ip, subnet, gateway, dns — enough for hello UDP targets + return ("192.168.1.100", "255.255.255.0", "192.168.1.1", "8.8.8.8") + return None diff --git a/stubs/ubinascii.py b/stubs/ubinascii.py new file mode 100644 index 0000000..b78bb5c --- /dev/null +++ b/stubs/ubinascii.py @@ -0,0 +1,6 @@ +"""Map MicroPython `ubinascii` to CPython `binascii`.""" + +import binascii + +hexlify = binascii.hexlify +unhexlify = binascii.unhexlify diff --git a/stubs/utime.py b/stubs/utime.py new file mode 100644 index 0000000..1ed55e5 --- /dev/null +++ b/stubs/utime.py @@ -0,0 +1,22 @@ +"""CPython stub for MicroPython `utime` (led-simulator).""" + +import time + +_MS_MASK = (1 << 30) - 1 + + +def sleep_ms(ms): + time.sleep(max(0, ms) / 1000.0) + + +def sleep(s): + time.sleep(s) + + +def ticks_ms(): + return int(time.monotonic() * 1000) & _MS_MASK + + +def ticks_diff(t1, t0): + """Approximate MicroPython ticks_diff for host monotonic ms.""" + return ((t1 - t0 + (1 << 29)) & _MS_MASK) - (1 << 29) diff --git a/workspace/patterns/.gitkeep b/workspace/patterns/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/workspace/patterns/.gitkeep @@ -0,0 +1 @@ + diff --git a/workspace/patterns/__init__.py b/workspace/patterns/__init__.py new file mode 100644 index 0000000..3885a94 --- /dev/null +++ b/workspace/patterns/__init__.py @@ -0,0 +1 @@ +# Host package mirror of device flash `patterns/` (pattern modules).