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 <cursoragent@cursor.com>
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
workspace/settings.json
|
||||||
|
workspace/presets.json
|
||||||
|
workspace/patterns/*
|
||||||
|
!workspace/patterns/__init__.py
|
||||||
|
!workspace/patterns/.gitkeep
|
||||||
46
README.md
46
README.md
@@ -1,2 +1,48 @@
|
|||||||
# led-simulator
|
# 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:<port>/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.)
|
||||||
|
|||||||
BIN
__pycache__/led_driver_sim_hook.cpython-313.pyc
Normal file
BIN
__pycache__/led_driver_sim_hook.cpython-313.pyc
Normal file
Binary file not shown.
19
examples/blink.py
Normal file
19
examples/blink.py
Normal file
@@ -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
|
||||||
25
examples/chase.py
Normal file
25
examples/chase.py
Normal file
@@ -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
|
||||||
265
gui_main.py
Normal file
265
gui_main.py
Normal file
@@ -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()
|
||||||
17
led_driver_sim_hook.py
Normal file
17
led_driver_sim_hook.py
Normal file
@@ -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
|
||||||
13
settings.default.json
Normal file
13
settings.default.json
Normal file
@@ -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": ""
|
||||||
|
}
|
||||||
BIN
stubs/__pycache__/machine.cpython-313.pyc
Normal file
BIN
stubs/__pycache__/machine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
stubs/__pycache__/neopixel.cpython-313.pyc
Normal file
BIN
stubs/__pycache__/neopixel.cpython-313.pyc
Normal file
Binary file not shown.
BIN
stubs/__pycache__/network.cpython-313.pyc
Normal file
BIN
stubs/__pycache__/network.cpython-313.pyc
Normal file
Binary file not shown.
BIN
stubs/__pycache__/ubinascii.cpython-313.pyc
Normal file
BIN
stubs/__pycache__/ubinascii.cpython-313.pyc
Normal file
Binary file not shown.
BIN
stubs/__pycache__/utime.cpython-313.pyc
Normal file
BIN
stubs/__pycache__/utime.cpython-313.pyc
Normal file
Binary file not shown.
18
stubs/machine.py
Normal file
18
stubs/machine.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""CPython stub for MicroPython `machine` (led-simulator)."""
|
||||||
|
|
||||||
|
|
||||||
|
class Pin:
|
||||||
|
OUT = 1
|
||||||
|
IN = 0
|
||||||
|
|
||||||
|
def __init__(self, pin_id, mode=None, *, pull=None, value=None):
|
||||||
|
self.id = pin_id
|
||||||
|
self.mode = mode
|
||||||
|
|
||||||
|
|
||||||
|
class WDT:
|
||||||
|
def __init__(self, timeout=10000):
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def feed(self):
|
||||||
|
pass
|
||||||
37
stubs/neopixel.py
Normal file
37
stubs/neopixel.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
"""Host NeoPixel: keeps RGB buffer and notifies the GUI on `write()`."""
|
||||||
|
|
||||||
|
# Set by led-simulator main: callable(list[tuple[int,int,int]]) -> 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))
|
||||||
38
stubs/network.py
Normal file
38
stubs/network.py
Normal file
@@ -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
|
||||||
6
stubs/ubinascii.py
Normal file
6
stubs/ubinascii.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
"""Map MicroPython `ubinascii` to CPython `binascii`."""
|
||||||
|
|
||||||
|
import binascii
|
||||||
|
|
||||||
|
hexlify = binascii.hexlify
|
||||||
|
unhexlify = binascii.unhexlify
|
||||||
22
stubs/utime.py
Normal file
22
stubs/utime.py
Normal file
@@ -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)
|
||||||
1
workspace/patterns/.gitkeep
Normal file
1
workspace/patterns/.gitkeep
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
workspace/patterns/__init__.py
Normal file
1
workspace/patterns/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
# Host package mirror of device flash `patterns/` (pattern modules).
|
||||||
Reference in New Issue
Block a user