Brad's personal working notes for PyDevices work across sibling repos
(pydevices-examples, cmods, mip, app templates, etc.).
Not contributor-facing and not published via Pages or RTD — lives here
so it can sync with the org clone without being a product/docs surface.
- Evaluate worker execution for the direct MicroPython WebAssembly host after the main-thread bridge and browser migrations are stable. Define framebuffer, input, audio, timer, and Python-buffer ownership across the worker boundary before implementation. (cmods, pydevices, dotgithub)
- Stop installing
pydevices-desktopover the network at Workbench VM boot, once its contents stabilize (audiodevis being reworked as of 2026-08-25). Workbench currently runsmip.install("pydevices-desktop", index="https://PyDevices.github.io/mip", target="lib")on every VM boot: measured at 56 requests (1 manifest + 55.mpyfiles) and most of the ~20 s connect. The Emscripten FS is in-memory, so it survives a soft reset but not a page reload — every fresh session re-downloads all 56, and there is no offline path despite shipping as an installable PWA. Only the device layer is involved: verified 2026-08-25 against the current wasm,palettesandpdwidgetsare frozen as source andpygraphicsandlvglas native modules, whiledisplaydev,board_config,appdevandeventsresolve tolib/*.mpyfrom the install (.frozenprecedeslibonsys.path, so anything frozen would have won). Two ways out, either is fine: freeze the rest of the modules into the wasm build (joining the four that already are), or ship a build-time tarball — ViperIDE already has that machinery in Workbench,gen_tarinbuild.pyplusloadVFS, as used forvm_vfs.tar.gzandtools_vfs.tar.gz. Freezing is the smaller runtime; the tarball keeps the package updatable without a wasm rebuild. Either also pins a known version, where the live index means the session silently changes whenever the index does. (pydevices, cmods, workbench)
-
Frozen self-installer for MicroPython (Unix +
micropython.exe) — seedocs/frozen-self-installer-notes.md(pydevices-examples, micropython-lib) -
Develop apps and freeze them into standalone executables — start with
spotapi_remotein the spotapi repo (spotapi — external, not a cloned sibling)- Research packaging alternatives: Electron (JS shell + web UI) and PyInstaller (CPython frozen binary) vs MicroPython frozen executables; pick what fits each app
-
Create a feature-accurate Minesweeper game modeled after Windows Minesweeper (pydevices-examples)
-
Create a joystick emulator to run on an MCU and appear as a joystick over USB (pydevices)
-
Create a dino game to feature in the HTML of the gallery index. Fixed-height display that scales to page width; stay 1×1 (hidden) until a Dino button is pressed, equivalent to Run on
micropython.html/pyodide.html(pydevices-examples) -
Verify that FINGERMOTION isn't needed (pydevices-examples)
-
Piano audio latency/glissando degradation, root-caused 2026-08-25:
audioif'saudiocore.get_buffer()returned a typed ('h') memoryview whoselen()is the sample count, whileaudiodev.sample_out.AudioOut's pump did byte math on it — under-counting 16-bit PCM by 2x and producing at twice realtime forever, saturating the SDL transport (500ms blocking writes, stall recycles) on desktop; I2S's blocking write happened to absorb it on the P4. Fixed inaudioif(byte view) and the CP oracle patch, with a self-calibrating fallback inAudioOutfor stale frozen firmware. Also added: a backpressure-aware pump (a primed/stalled transport's backlog now drains instead of riding ahead of every note forever) and tighter "low"-latency profiles.utils/audio.py/AudioEngineretired entirely (see below). Released as pydevices 0.3.4-0.3.6. (pydevices, cmods/audioif, pydevices-examples) -
pydevices-examples/lib/utils/audio.py(AudioEngine) retired:piano.pyandwidgets_locker_kiosk.py(its last consumer) migrated tosynthio.Synthesizer+audiodev.sample_out.AudioOut. (pydevices-examples) -
windisplaypointer coordinates were double-scaled (wndproc mapped by_scale, then the appdev pointer pipeline divided bytouch_scaleagain) — zero error at the window origin, growing with distance (~4 white keys off at the far end of the piano keyboard at scale 1.37).touch_scalenow stays 1.0 onWinDisplay, matchingSDLDisplay's contract. (pydevices, displaydev) -
Hinch GUI trio (
nano/micro/touch_gui_simpletest) andwidgets_locker_kioskstill out of the example matrix — revisit and get them green (or keep parked with a clear reason). (pydevices-examples) -
Enable Mermaid rendering on RTD sites —
```mermaidblocks currently show as plain text, not diagrams. At least: pydevices-examples, pdwidgets, palettes, pygraphics. (pydevices-examples, pdwidgets, palettes, pygraphics) -
Publish more / all of pydevices to micropython-lib. (pydevices, micropython-lib)
-
Create a patch in
cmodsto enable FFI onmicropython.exeso it can useuwin32.py/ hardware timers. (cmods, multimer) -
Off-heap buffer via FFI:
uwin32now hasVirtualAlloc/VirtualFree/buffer_aton both the ffi and ctypes paths, andWinDisplay._alloc_framebufferuses them with abytearrayfallback. Only WinDisplay uses it so far; SDL/PG could follow. (pydevices, displaydev) -
Add PyScript live examples in
palettesandpdwidgets. (palettes, pdwidgets, pydevices-examples) -
WinDisplay copies the whole framebuffer every frame: fixed, but by deleting the buffer rather than moving it off-heap. GDI reads RGB565 natively through a 16-bit
BI_BITFIELDSDIB (uwin32.bmi_rgb565), so_bgra, the per-pixel_rgb565_to_bgra_rowconversion loop, and thebytes()copy all went away together.uwin32.dib_bitsreturns a plain integer address, which both backends accept asLPCVOID, so nothing is marshalled per present._visiblewent too: an unscrolled frame blits straight from_buffer, and a scrolled one blits it as 2-4 bands. Resident 614400 -> 153600 B (8 -> 2 B/px); MicroPython allocation 367 KB/frame -> 3.5 KB/frame; full-frame blit 6.2 -> 7076 fps (CPython) and 37 -> 7287 fps (MicroPython). (pydevices, displaydev) -
WinDisplay allocates a new framebuffer on every rotation: fixed, but not via
_visible-- that buffer no longer exists._rotate_rgb565now rotates into a transient scratch and copies back, so_bufferkeeps its identity (and with it the cached DIB address and the off-heap block). Steady state is 2 B/px rather than the 4 B/px that keeping_visibleas scratch would have cost; the only allocation is the scratch, on an operation that is usually a one-off at startup. (pydevices, displaydev) -
WinDisplay: partial presents are disabled at fractional scales. GDI resamples each band against its own destination rectangle, so a banded repaint disagrees with a full one by 11-24 rows out of ~65 and leaves seams;
_can_bandtherefore restricts banding to whole-number scales, and scrolling at a fractional scale composes into a scratch buffer first. Sinceboard_configscale is routinely fitted to something fractional (2.0 -> 1.37 on this desktop), most real windows take the full-repaint path. Worth revisiting: snapping the window size so the fitted scale stays an integer would restore banding and drop the scroll scratch. (pydevices, displaydev) -
MicroPython ffi: passing a long-lived buffer to an
ffi.funccall costs ~12.5 us versus ~0.3 us for a freshly allocated one (measured onmicropython.exe1.28, win32, withPeekMessageW). Caching a MSG buffer inWinDisplay._pumpto avoid the per-poll allocation made the pump ~20x slower, so it deliberately allocates one per poll. Worth understanding -- it inverts the usual "reuse the buffer" advice and affects every ffi-based driver. (pydevices, uwin32)
- pygraphics text draws (
Draw.text/text16) are drastically slow on the P4'sFBDisplayframebuffer (~1.8s for onetext16call, measured 2026-08-25) and anymultimer/appdevtimer that fires during such a draw hard-wedges the board -- unreachable over serial, needs a hard reset. Minimal repro (mpftpprobe):app.every(40, noop)+ apygraphics.Drawonboard_config.display_drv+.text16(...);fill_rectalone is fast and safe,text/text16alone (no timer) is slow but doesn't wedge, the combination wedges. This blocked gettingpydevices-examples/piano.pyrunning on the P4 -- audio itself (synthio -> AudioOut -> ES8311, verified separately via a staged diagnostic) is not implicated. Likely suspects: a per-pixel PSRAM access pattern in the text blit colliding with whatever IRQ multimer's esp32 timer uses. Repro scripts are in that session's scratchpad (piano_interactive_probe*.py,p4_piano_diag*.py). (pydevices, pygraphics, displayif, multimer)
-
mpftp firmware flash --uf2— flash an already-built UF2 by copying it to the bootloader volume, instead of a serial write. Scope it to flashing existing UF2 output, not wrapping arbitrary binaries withuf2conv.py. mpftp already has both halves:_find_circuitpy_host_roots()for volume discovery and a workingbootloadercommand. Two hard requirements: mandatory post-copy verification (PowerShellCopy-Itemfails non-terminatingly — rc=0 with nothing written, which for a flash operation makes "succeeded" and "wrote nothing" indistinguishable), and a re-enumeration wait after the copy. Gate the whole path on actually detecting a bootloader volume. (mpftp) - Make an ESP32-S3 board UF2-capable after it was flashed with esptool. Viable, but it is a one-time destructive provisioning step, not a per-build option — see below. (mpftp, cmods)
On S3, UF2 is not in ROM and is not a bootloader in the RP2040 sense. tinyuf2 is an ordinary ESP-IDF app living in the factory partition; CircuitPython is demoted to ota_0. The IDF second-stage bootloader boots factory when otadata is blank, and tinyuf2 then chooses between presenting the drive and chaining to the app.
UF2_BOOTLOADER in ports/espressif/Makefile does exactly one thing — pick a partition table. It defaults to CIRCUITPY_USB_DEVICE, so every S3 board with native USB already gets the UF2 layout:
| with UF2 | no UF2 | |
|---|---|---|
ota_0 |
0x010000, 2048K | 0x010000, 2048K |
ota_1 |
0x210000, 2048K | 0x210000, 2048K |
uf2 (app, factory) |
0x410000, 256K | — |
| user fs (fat) | 0x450000, 3776K | 0x410000, 4032K |
tinyuf2's own release partition table is byte-identical to CircuitPython's partitions-8MB.csv (only the fat partition's name differs, ffat vs user_fs — irrelevant, since supervisor/internal_flash.c looks it up by type/subtype with a NULL label). The two projects are designed to interlock.
Steps on an already-esptool'd board:
- Get a tinyuf2 build for the exact board — separate repo (
adafruit/tinyuf2), not vendored in CircuitPython. Prebuilt zips exist for ~14 S3 boards; otherwise add a board definition there (flash size, PSRAM, status LED pins). - Match the flash size. The devkitc build is 8MB; a 4MB S3 uses
partitions-4MB-no-ota.csv, which dropsota_1entirely to make room. - Flash tinyuf2's four files (from its
flash_args) — or itscombined.binat 0x0:0x0 bootloader.bin 0x8000 partition-table.bin 0xe000 ota_data_initial.bin # this is what says "boot factory" 0x410000 tinyuf2.bin - Rebuild CircuitPython with
UF2_BOOTLOADER=1so it targets the matching layout andmakeemitsfirmware.uf2(family ID0xc47e5767for S3, base 0x0 — relative to the ota partition).
The cost: repartitioning destroys the CIRCUITPY filesystem — the fat partition moves and shrinks by 256K, and there is no in-place upgrade path. This is the opposite of RP2040, where a UF2 reflash preserves the filesystem. On a 4MB S3 the storage cut is 1216K -> 960K (21%), which is why espressif_esp32s3_lcd_ev and ..._v1.5 both set UF2_BOOTLOADER = 0.
What it buys: software-triggered bootloader entry — microcontroller.on_next_reset(RunMode.UF2) sets reset-reason hint 0x11F2 (APP_REQUEST_UF2_RESET_HINT), which survives reset; plus double-tap reset if the tinyuf2 board def enables it.
What it does not buy: recoverability. Unlike the RP2040 1200-baud touch — handled in the USB stack, and the thing that rescued the wedged board on 2026-08-20 — setting the hint requires the running VM to execute code. A badly wedged S3 still means holding BOOT and using esptool. So this is convenience, not a rescue path, and mpftp still needs esptool on ESP32 regardless.