Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ sent upstream.

What this fork changes:

- The virtual device is the PyDevices MicroPython WASM build (lvgl,
pdwidgets, pygraphics and palettes built in; `displaydev`, `board_config`
and the rest of `pydevices-desktop` install from mip at boot), reached from
- The virtual device is the PyDevices MicroPython WASM build, with the whole
PyDevices stack frozen in (lvgl, pdwidgets, pygraphics, palettes,
`displaydev`, the desktop `board_config` and the rest; nothing installs at
boot), reached from
its own toolbar button alongside USB/Bluetooth/WebREPL. It gets a real
display stage — resolution and shape presets including round watch faces,
live resize, audio, pointer and keyboard input — and boots with example
Expand Down
Binary file modified assets/pydevices/micropython.wasm
Binary file not shown.
80 changes: 46 additions & 34 deletions assets/pydevices/micropython.wasm.provenance
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
{
"binary": {
"mtime": "2026-09-26T07:55:58-05:00",
"mtime": "2026-10-05T17:03:35-05:00",
"name": "micropython.wasm",
"sha256": "a7ddaf21a96e81aac63497b48382e7aa0d098044ab15550b6389feedbd73a4bd",
"size": 5880566
"sha256": "4cc43ae8a82e709e3bfbc56272bab1396ea73e0c5b433bd93d39e49e9993bb39",
"size": 6714453
},
"format": 1,
"install_only": false,
"overlays": [
"0001-micropython-windows-enable-networking-sockets-select-ssl-msvc.patch",
"0002-micropython-unix-raise-MICROPY_SCHEDULER_DEPTH-for-desktop-SDL.patch",
"0003-micropython-windows-enable-ffi-modffi-libffi.patch",
"0004-micropython-webassembly-allow-Asyncify-in-runPythonAsync.patch",
"0005-micropython-webassembly-fix-node-hook-ccall-signatures.patch",
"0006-micropython-webassembly-support-soft-reinitialization.patch",
"0007-micropython-webassembly-inert-jsffi-callbacks-across-reinit.patch",
"0008-micropython-webassembly-fix-empty-single-input-lexer-eof-primer.patch"
"0008-micropython-webassembly-fix-empty-single-input-lexer-eof-primer.patch",
"0009-micropython-esp32s3-add-SPIRAM_OCT_DEBUG-variant.patch",
"0010-micropython-esp32-i2s-mck-pin.patch",
"0011-micropython-esp32-webrepl-ctrl-c-in-tight-loops.patch",
"0012-micropython-unix-variant-configurable-default-heap-size.patch",
"0013-micropython-windows-read-piped-stdin-in-the-repl.patch",
"0014-micropython-esp32-tinyusb-0.21.patch",
"0015-micropython-windows-timing-module-and-idle-console.patch",
"0016-micropython-esp32-machine-timer-fire-during-reinit.patch",
"0017-micropython-py-makeqstrdefs-incremental-build-header-qstr.patch"
],
"sources": {
"audiodsp": {
"committed": "2026-09-25T06:49:25-05:00",
"describe": "v0.0.3-279-g094dde5",
"committed": "2026-10-05T15:26:05-05:00",
"describe": "v0.0.3-309-g79616ba",
"dirty": false,
"head": "094dde5203dadcbf30fe5f2b6a88c16e413799b8",
"head": "79616bae165b63069ccecc6d335c00ca334b193c",
"path": "/home/brad/gh/pydevices/audiodsp"
},
"audioif": {
"committed": "2026-09-24T09:44:37-05:00",
"describe": "v0.1.1",
"committed": "2026-10-05T16:59:29-05:00",
"describe": "v0.1.1-1-gb6b35a4",
"dirty": false,
"head": "71c78419c7199258933ea555591ed4e6f20c0e89",
"head": "b6b35a48a0cd23ddc549f8c0643d77ffe778da28",
"path": "/home/brad/gh/pydevices/audioif"
},
"cameraif": {
Expand All @@ -44,55 +56,55 @@
"path": "/home/brad/gh/pydevices/circuitpython"
},
"displayif": {
"committed": "2026-09-25T18:57:38-05:00",
"describe": "6ea4d30",
"committed": "2026-10-05T15:58:30-05:00",
"describe": "370dad6",
"dirty": false,
"head": "6ea4d30e5562bd55c3aee43a422aec439ca095ba",
"head": "370dad6fb7119ed80a35d782302d4ae52a0f50c5",
"path": "/home/brad/gh/pydevices/displayif"
},
"lvgl-circuitpython": {
"committed": "2026-09-26T07:13:38-05:00",
"describe": "3b6e652",
"committed": "2026-09-26T18:07:40-05:00",
"describe": "caaf6dc",
"dirty": false,
"head": "3b6e652e1d37f1a1081d7f91e3a7dcd8738d8dd6",
"head": "caaf6dc8e3c5975fe61488814a9176bcd2cf138f",
"path": "/home/brad/gh/pydevices/lvgl-circuitpython"
},
"lvgl-micropython": {
"committed": "2026-09-26T07:13:33-05:00",
"describe": "1024064",
"committed": "2026-10-05T16:59:29-05:00",
"describe": "821fc45",
"dirty": false,
"head": "1024064e0e0ed4e986383bf2ad08416b3767d51b",
"head": "821fc459e6210cf0ee441fc9a65a13a223ab8d84",
"path": "/home/brad/gh/pydevices/lvgl-micropython"
},
"micropython": {
"committed": "2026-09-26T01:50:11-05:00",
"describe": "v1.29.0-1-g152121c",
"committed": "2026-10-05T16:10:55-05:00",
"describe": "v1.29.0-1-g8a22473",
"dirty": false,
"head": "152121ce6a427ef8a83c7b2f68e4bf7aa39b69cd",
"head": "8a224736515fca0ef6288059fa9635560858d5a5",
"path": "/home/brad/gh/pydevices/micropython"
},
"micropython-pydevices": {
"committed": "2026-09-26T06:17:21-05:00",
"describe": "6e29120",
"dirty": true,
"head": "6e29120c80995dca606ad344179e39c6d7523319",
"committed": "2026-10-05T16:59:10-05:00",
"describe": "3fa0688",
"dirty": false,
"head": "3fa068873cb135566356386d2810ed0e86a6758e",
"path": "/home/brad/gh/pydevices/micropython-pydevices"
},
"pygraphics": {
"committed": "2026-09-23T22:38:03-05:00",
"describe": "v0.0.32-84-g04f5b44",
"committed": "2026-09-26T19:35:02-05:00",
"describe": "v0.0.32-86-gb4d4c49",
"dirty": false,
"head": "04f5b44850fcb8aa83f6798eb68fb57fb1578d88",
"head": "b4d4c49cc2a22cdd2632f2eea2593b4cc4df4db1",
"path": "/home/brad/gh/pydevices/pygraphics"
},
"usbif": {
"committed": "2026-09-25T22:28:49-05:00",
"describe": "c37ce17",
"dirty": true,
"head": "c37ce17f281ded56da8f04b9f9a8c29a8cba298d",
"committed": "2026-09-26T17:28:03-05:00",
"describe": "4c01722",
"dirty": false,
"head": "4c01722e9e0474697dcb2734fb0953bb3019e99c",
"path": "/home/brad/gh/pydevices/usbif"
}
},
"stamped": "2026-09-26T07:55:59-05:00",
"stamped": "2026-10-05T17:03:36-05:00",
"target": "mp-wasm"
}
38 changes: 10 additions & 28 deletions src/pydevices/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export function setBootSize(width, height) {
bootSize = { width, height }
}

const MIP_INDEX = 'https://PyDevices.github.io/mip'

// The runtime is ~6 MB, so it is fetched on first connect rather than bundled.
// The import has to survive Rollup: an IIFE bundle cannot code-split, and a
// literal specifier would be inlined (or fail to resolve). Going through
Expand All @@ -61,19 +59,14 @@ async function loadPyDevicesMicroPython(mpOpts) {
}

/*
* Boot script, mirroring what the PyDevices simulator runs before handing over
* the REPL: tell displaydev how big the panel is, put the runtime's own
* directories on sys.path, then make sure the desktop support package is there.
*
* board_config is the entry point user code imports, so its presence is what
* decides whether the package still needs installing - checked as a file rather
* than an import, which would build a display as a side effect. The install
* needs the network; failing it leaves a usable REPL without the PyDevices
* libraries, and the next connect tries again.
* Boot script: put the runtime's own directories on sys.path, then tell
* displaydev how big the panel is.
*
* Order matters: displaydev arrives with the package, so the panel size can only
* be set once the install has run. Builds that freeze the libraries in will
* satisfy the import either way.
* The PyDevices stack - displaydev, appdev, the desktop board_config and the
* rest - is frozen into the WASM runtime, and workbench never installs any of
* it. If it is missing, this is a runtime built without it, and the import
* below fails loudly rather than reaching for the network. Installing other
* packages (examples and the like) is still the package manager's job.
*/
function bootScript(width, height) {
return `
Expand All @@ -82,20 +75,9 @@ import os, sys
# target by looking for a sys.path entry that ends with "/lib", and a
# relative "lib" left it refusing to install anything on the simulator.
sys.path[:] = [".", ".frozen", "/lib", "/utils"]
try:
os.stat("/lib/board_config.mpy")
except OSError:
try:
import mip
mip.install("pydevices-desktop", index=${JSON.stringify(MIP_INDEX)}, target="/lib")
except Exception as exc:
print("PyDevices libraries unavailable offline:", exc)
try:
from displaydev import env_set
env_set("PYDEVICES_WIDTH", ${Number(width)})
env_set("PYDEVICES_HEIGHT", ${Number(height)})
except ImportError:
print("displaydev unavailable - the display will use its own default size")
from displaydev import env_set
env_set("PYDEVICES_WIDTH", ${Number(width)})
env_set("PYDEVICES_HEIGHT", ${Number(height)})
os.chdir("/")
`
}
Expand Down
2 changes: 1 addition & 1 deletion src/vm_vfs/00_touch_paint.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
display driver, the same events. Connect hardware over USB, copy this file
across, and it runs unchanged.

Only needs pydevices-desktop, which the simulator installs at boot.
Only needs the PyDevices stack, which the simulator has frozen in.
"""

import board_config
Expand Down
31 changes: 31 additions & 0 deletions test/browser/boot-installs-nothing.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// SPDX-License-Identifier: MIT
//
// The PyDevices stack is frozen into the WASM runtime, so connecting the
// simulator installs nothing: no request to any mip index while it boots.
// Boot used to mip-install pydevices-desktop on every connect; this keeps
// that from coming back unnoticed.
import { test, expect } from '@playwright/test'
import { connectSimulator, focusTerminal, terminalText } from './helpers.js'

const MIP_INDEXES = [/pydevices\.github\.io\/mip/i, /micropython\.org\/pi\//i]

test('connecting the simulator installs nothing and the stack is there', async ({ page }) => {
const indexRequests = []
page.on('request', (request) => {
if (MIP_INDEXES.some((pattern) => pattern.test(request.url()))) {
indexRequests.push(request.url())
}
})

await connectSimulator(page)

await focusTerminal(page)
const before = await terminalText(page)
await page.keyboard.type('import os, board_peripherals, displaydev; print("stack-ok", os.listdir("/lib"))')
await page.keyboard.press('Enter')
await expect
.poll(async () => (await terminalText(page)).slice(before.length))
.toContain('stack-ok')

expect(indexRequests).toEqual([])
})
6 changes: 2 additions & 4 deletions test/browser/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@
import { expect } from '@playwright/test'

/*
* Connects the PyDevices virtual device and waits for a real prompt - not a
* fixed delay, since the mip install this triggers (~55 requests for
* pydevices-desktop) varies with network conditions far more than the WASM
* boot itself does.
* Connects the PyDevices virtual device and waits for a real prompt, not a
* fixed delay: the WASM boot's time varies with the machine.
*/
export async function connectSimulator(page) {
await page.goto('/')
Expand Down
4 changes: 2 additions & 2 deletions test/browser/playwright.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { defineConfig } from '@playwright/test'
export default defineConfig({
testDir: '.',
timeout: 60_000,
// The PyDevices VM installs pydevices-desktop from mip on every connect
// (~55 small requests) before it prints a prompt; give it room.
// The PyDevices VM boots the WASM runtime before it prints a prompt; give
// it room on a slow machine.
expect: { timeout: 45_000 },
fullyParallel: false,
workers: 1,
Expand Down
6 changes: 3 additions & 3 deletions test/browser/templates.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//
// Every example the VM ships runs clean and paints something. One connect
// and one page for the whole file, each example run as a step within it -
// booting the simulator (the mip install alone is ~55 requests) dominates
// the cost of this suite far more than running six short programs does, and
// nothing here mutates state a later example depends on.
// booting the simulator dominates the cost of this suite far more than
// running six short programs does, and nothing here mutates state a later
// example depends on.
import { test, expect } from '@playwright/test'
import { connectSimulator, runFile, distinctCanvasColors, clickCanvas } from './helpers.js'

Expand Down
Loading