Skip to content

Commit cfd8a51

Browse files
committed
feat(simulator): implement dynamic runtime engine switching between MicroPython WASM and Pyodide CPython
1 parent 8600972 commit cfd8a51

3 files changed

Lines changed: 6384 additions & 0 deletions

File tree

simulator/simulator.js

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
renderLineHighlight: "all",
111111
padding: { top: 12, bottom: 12 }
112112
});
113+
window.monacoEditor = monacoEditor;
113114

114115
// Shortcut: Ctrl+Enter / Cmd+Enter to Run
115116
monacoEditor.addCommand(monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter, function () {
@@ -131,6 +132,64 @@
131132
// Python Runtime Harness (Pyodide & MicroPython WASM)
132133
// =========================================================================
133134

135+
let micropythonInstance = null;
136+
let mpyPackagesInstalled = false;
137+
138+
async function getMicroPython() {
139+
if (micropythonInstance) return micropythonInstance;
140+
141+
setStatus("Booting MicroPython engine…", "busy");
142+
logConsole("[Runtime] Loading MicroPython WASM environment…\n", "info");
143+
144+
const { loadMicroPython } = await import("https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript/micropython.mjs");
145+
micropythonInstance = await loadMicroPython({
146+
stdout: (text) => logConsole(text + "\n"),
147+
stderr: (text) => logConsole(text + "\n", "error"),
148+
url: "https://cdn.jsdelivr.net/npm/@micropython/micropython-webassembly-pyscript/micropython.wasm"
149+
});
150+
151+
// Setup sys.path and PyScript/DOM shims
152+
try {
153+
micropythonInstance.runPython(`
154+
import sys, os
155+
if "." not in sys.path:
156+
sys.path.insert(0, ".")
157+
if "/lib" not in sys.path:
158+
sys.path.append("/lib")
159+
160+
# PyScript compatibility shims
161+
try:
162+
import js
163+
from js import document, window
164+
class _PS:
165+
pass
166+
ps = _PS()
167+
ps.document = document
168+
ps.window = window
169+
sys.modules["pyscript"] = ps
170+
except Exception:
171+
pass
172+
`);
173+
} catch (e) {
174+
console.warn("MicroPython init shim:", e);
175+
}
176+
177+
// Preload local mip.py into MicroPython filesystem
178+
try {
179+
const mipRes = await fetch("/vendor/pydevices-chrome/mip.py");
180+
if (mipRes.ok) {
181+
const mipCode = await mipRes.text();
182+
micropythonInstance.FS.writeFile("mip.py", mipCode);
183+
}
184+
} catch (e) {
185+
console.warn("Could not preload local mip.py for MicroPython:", e);
186+
}
187+
188+
setStatus("Ready", "ready");
189+
logConsole("[Runtime] MicroPython environment ready.\n", "success");
190+
return micropythonInstance;
191+
}
192+
134193
async function getPyodide() {
135194
if (pyodideInstance) return pyodideInstance;
136195

@@ -206,8 +265,49 @@ if "pyscript" not in sys.modules:
206265
logConsole(`\n--- Execution started (${new Date().toLocaleTimeString()}) ---\n`, "dim");
207266

208267
const t0 = performance.now();
268+
const runtime = elRuntimeSelect ? elRuntimeSelect.value : "pyodide";
209269

210270
try {
271+
if (runtime === "mpy") {
272+
const mp = await getMicroPython();
273+
274+
setCanvasResolution(currentResolution.width, currentResolution.height, currentResolution.shape);
275+
276+
// Set MicroPython display environment
277+
mp.runPython(`
278+
import sys, os
279+
if not hasattr(os, "environ"):
280+
os.environ = {}
281+
os.environ["PYDEVICES_WIDTH"] = "${currentResolution.width}"
282+
os.environ["PYDEVICES_HEIGHT"] = "${currentResolution.height}"
283+
os.environ["PYDEVICES_CANVAS_ID"] = "display_canvas"
284+
`);
285+
286+
mp.FS.writeFile("main.py", code);
287+
mp.runPython(code);
288+
289+
// Pump LVGL / display refresh
290+
try {
291+
mp.runPython(`
292+
import sys
293+
if "lvgl" in sys.modules:
294+
lv = sys.modules["lvgl"]
295+
if hasattr(lv, "task_handler"):
296+
lv.task_handler()
297+
if "board_config" in sys.modules:
298+
bc = sys.modules["board_config"]
299+
if hasattr(bc, "display_drv") and hasattr(bc.display_drv, "show"):
300+
bc.display_drv.show()
301+
`);
302+
} catch (e) {}
303+
304+
const elapsed = ((performance.now() - t0) / 1000).toFixed(2);
305+
logConsole(`--- Completed in ${elapsed}s ---\n`, "dim");
306+
setStatus("Ready", "ready");
307+
isRunning = false;
308+
return;
309+
}
310+
211311
const pyodide = await getPyodide();
212312

213313
// Always install pydevices-desktop for all scenarios
@@ -549,7 +649,59 @@ if "display_driver" in sys.modules:
549649
const promptStr = isMoreLines ? "... " : ">>> ";
550650
logConsole(`${promptStr}${line}\n`, "prompt");
551651

652+
const runtime = elRuntimeSelect ? elRuntimeSelect.value : "pyodide";
653+
552654
try {
655+
if (runtime === "mpy") {
656+
const mp = await getMicroPython();
657+
658+
const pyWrapper = `
659+
import sys
660+
661+
_line_input = ${JSON.stringify(line)}
662+
663+
try:
664+
_res = eval(_line_input)
665+
if _res is not None:
666+
print(repr(_res))
667+
except SyntaxError:
668+
try:
669+
_parts = [p.strip() for p in _line_input.split(";") if p.strip()]
670+
if len(_parts) > 1:
671+
for _p in _parts[:-1]:
672+
exec(_p)
673+
_last = _parts[-1]
674+
try:
675+
_res = eval(_last)
676+
if _res is not None:
677+
print(repr(_res))
678+
except SyntaxError:
679+
exec(_last)
680+
else:
681+
exec(_line_input)
682+
except Exception as _e:
683+
import sys
684+
sys.print_exception(_e) if hasattr(sys, "print_exception") else print(_e)
685+
except Exception as _e:
686+
import sys
687+
sys.print_exception(_e) if hasattr(sys, "print_exception") else print(_e)
688+
689+
try:
690+
if "lvgl" in sys.modules:
691+
_lv = sys.modules["lvgl"]
692+
if hasattr(_lv, "task_handler"):
693+
_lv.task_handler()
694+
if "board_config" in sys.modules:
695+
_bc = sys.modules["board_config"]
696+
if hasattr(_bc, "display_drv") and hasattr(_bc.display_drv, "show"):
697+
_bc.display_drv.show()
698+
except Exception:
699+
pass
700+
`;
701+
mp.runPython(pyWrapper);
702+
return;
703+
}
704+
553705
const pyodide = await getPyodide();
554706

555707
const pyWrapper = `
@@ -645,6 +797,14 @@ _more
645797
elResolutionSelect.addEventListener("change", (e) => handleResolutionChange(e.target.value));
646798
}
647799

800+
if (elRuntimeSelect) {
801+
elRuntimeSelect.addEventListener("change", (e) => {
802+
const val = e.target.value;
803+
const name = val === "mpy" ? "MicroPython" : "Pyodide (CPython)";
804+
logConsole(`[Runtime] Active execution engine set to ${name}.\n`, "info");
805+
});
806+
}
807+
648808
const themeToggle = document.getElementById("theme-toggle");
649809
if (themeToggle) {
650810
themeToggle.addEventListener("click", toggleTheme);

0 commit comments

Comments
 (0)