velxio/docs/MICROPYTHON_IMPLEMENTATION.md

349 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MicroPython Implementation — Technical Summary
> Issue #3 — Full MicroPython support for RP2040, ESP32, ESP32-S3, and ESP32-C3 boards.
> Branch: `feature/micropython-rp2040`
---
## Overview
Velxio supports MicroPython across **15 board variants** using two distinct execution strategies:
| Strategy | Boards | Execution | Serial |
|----------|--------|-----------|--------|
| **Browser-side** (rp2040js) | Raspberry Pi Pico, Pico W | In-browser at 125 MHz | USBCDC |
| **QEMU backend** (WebSocket) | ESP32, ESP32-S3, ESP32-C3 (13 variants) | Remote `qemu-system-xtensa` / `qemu-system-riscv32` | UART via WebSocket |
### Supported Boards
```
RP2040 (browser): raspberry-pi-pico, pi-pico-w
ESP32 Xtensa (QEMU): esp32, esp32-devkit-c-v4, esp32-cam, wemos-lolin32-lite
ESP32-S3 Xtensa (QEMU): esp32-s3, xiao-esp32-s3, arduino-nano-esp32
ESP32-C3 RISC-V (QEMU): esp32-c3, xiao-esp32-c3, aitewinrobot-esp32c3-supermini
```
---
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ User clicks "Run" with MicroPython mode │
│ │
│ EditorToolbar.handleRun() │
│ └── loadMicroPythonProgram(boardId, pyFiles) │
│ ├── RP2040? → sim.loadMicroPython(files) │
│ │ ├── getFirmware() [IndexedDB/remote/bundled]
│ │ ├── loadUF2() [parse UF2 → flash]
│ │ ├── loadUserFiles() [LittleFS → flash]
│ │ └── USBCDC serial [REPL via USB CDC]
│ │ │
│ └── ESP32? → getEsp32Firmware() │
│ ├── base64 encode │
│ ├── bridge.loadFirmware(b64) │
│ └── bridge.setPendingMicroPythonCode()
│ └── on ">>>" detected: │
│ _injectCodeViaRawPaste() │
│ \x01 → \x05 → code → \x04
└─────────────────────────────────────────────────────────────┘
```
---
## Phase 1: RP2040 MicroPython (Browser-Side)
### Files Created/Modified
| File | Action |
|------|--------|
| `frontend/src/simulation/MicroPythonLoader.ts` | **Created** — UF2 parser, LittleFS builder, firmware cache |
| `frontend/src/simulation/RP2040Simulator.ts` | Modified — `loadMicroPython()`, USBCDC serial, `micropythonMode` |
| `frontend/src/types/board.ts` | Modified — `LanguageMode` type, `BOARD_SUPPORTS_MICROPYTHON` set |
| `frontend/src/store/useSimulatorStore.ts` | Modified — `loadMicroPythonProgram`, `setBoardLanguageMode` |
| `frontend/src/store/useEditorStore.ts` | Modified — `DEFAULT_MICROPYTHON_CONTENT`, `createFileGroup` overload |
| `frontend/src/components/editor/EditorToolbar.tsx` | Modified — language selector, MicroPython compile/run flow |
| `frontend/src/components/simulator/SerialMonitor.tsx` | Modified — Ctrl+C/D, REPL label |
| `frontend/public/firmware/micropython-rp2040.uf2` | **Created** — bundled fallback (638 KB) |
### UF2 Firmware Loading
MicroPython for RP2040 uses UF2 format (USB Flashing Format). Each block is 512 bytes:
```
Offset Field
0 Magic 0 (0x0a324655)
4 Magic 1 (0x9e5d5157)
12 Flash address (little-endian)
32 Payload data (256 bytes)
```
Constants:
```typescript
FLASH_START_ADDRESS = 0x10000000 // RP2040 XIP flash base
MICROPYTHON_FS_FLASH_START = 0xa0000 // LittleFS starts at 640KB
MICROPYTHON_FS_BLOCK_SIZE = 4096
MICROPYTHON_FS_BLOCK_COUNT = 352 // 1.4 MB filesystem
```
### LittleFS Filesystem
User files (e.g., `main.py`) are written into a LittleFS image in memory using the `littlefs` WASM package:
1. Create backing buffer: `352 × 4096 = 1,441,792 bytes`
2. Register 4 WASM callbacks via `addFunction()`:
- `flashRead(cfg, block, off, buffer, size)` — reads from JS buffer
- `flashProg(cfg, block, off, buffer, size)` — writes to JS buffer
- `flashErase(cfg, block)` — no-op
- `flashSync()` — no-op
3. Format and mount: `_lfs_format()``_lfs_mount()`
4. Write files: `cwrap('lfs_write_file')` for each user file
5. Unmount and copy into flash at offset `0xa0000`
### USBCDC Serial (not UART)
MicroPython on RP2040 uses USB CDC for the REPL, not UART0:
```typescript
// In RP2040Simulator.loadMicroPython():
this.usbCDC = new USBCDC(rp2040.usbCtrl);
this.usbCDC.onDeviceConnected = () => this.usbCDC.sendSerialByte('\r'.charCodeAt(0));
this.usbCDC.onSerialData = (value) => { /* forward to SerialMonitor */ };
// Serial write routes through USBCDC when micropythonMode is true:
serialWrite(text: string): void {
if (this.micropythonMode && this.usbCDC) {
for (const ch of text) this.usbCDC.sendSerialByte(ch.charCodeAt(0));
} else {
this.rp2040?.uart[0].feedByte(...);
}
}
```
### Firmware Caching Strategy
All firmware uses a 3-tier loading strategy:
1. **IndexedDB cache** — instant load via `idb-keyval`
2. **Remote download** — from `micropython.org` with streaming progress
3. **Bundled fallback** — from `/firmware/` (Vite public directory)
Cache key: `micropython-rp2040-uf2-v1.20.0`
---
## Phase 2: ESP32/ESP32-S3 MicroPython (QEMU Backend)
### Files Created/Modified
| File | Action |
|------|--------|
| `frontend/src/simulation/Esp32MicroPythonLoader.ts` | **Created** — firmware download/cache for ESP32 variants |
| `frontend/src/simulation/Esp32Bridge.ts` | Modified — `setPendingMicroPythonCode()`, raw-paste injection |
| `frontend/src/store/useSimulatorStore.ts` | Modified — ESP32 path in `loadMicroPythonProgram` |
| `frontend/src/store/useEditorStore.ts` | Modified — `DEFAULT_ESP32_MICROPYTHON_CONTENT` |
| `frontend/src/components/simulator/SerialMonitor.tsx` | Modified — generic Ctrl+C/D for all boards |
| `frontend/public/firmware/micropython-esp32.bin` | **Created** — bundled fallback (1.5 MB) |
| `frontend/public/firmware/micropython-esp32s3.bin` | **Created** — bundled fallback (1.4 MB) |
### Firmware Variants
```typescript
FIRMWARE_MAP = {
'esp32': { remote: '.../ESP32_GENERIC-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32-v1.20.0' },
'esp32-s3': { remote: '.../ESP32_GENERIC_S3-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32s3-v1.20.0' },
'esp32-c3': { remote: '.../ESP32_GENERIC_C3-20230426-v1.20.0.bin', cacheKey: 'micropython-esp32c3-v1.20.0' },
}
```
Board kind → firmware variant mapping:
```
esp32, esp32-devkit-c-v4, esp32-cam, wemos-lolin32-lite → 'esp32'
esp32-s3, xiao-esp32-s3, arduino-nano-esp32 → 'esp32-s3'
esp32-c3, xiao-esp32-c3, aitewinrobot-esp32c3-supermini → 'esp32-c3'
```
### Raw-Paste REPL Protocol
Unlike RP2040 (which loads files into LittleFS), ESP32 boards inject user code into the MicroPython REPL after boot using the **raw-paste protocol**:
```
Step 1: Wait for ">>>" in serial output (REPL ready)
Step 2: Send \x01 (Ctrl+A) — enter raw REPL mode
Step 3: Send \x05 (Ctrl+E) — enter raw-paste mode
Step 4: Send code bytes in 256-byte chunks (10ms between chunks)
Step 5: Send \x04 (Ctrl+D) — execute the code
```
Implementation in `Esp32Bridge`:
```typescript
// State fields
private _pendingMicroPythonCode: string | null = null;
private _serialBuffer = '';
micropythonMode = false;
// In serial_output handler — detect REPL prompt
if (this._pendingMicroPythonCode && this._serialBuffer.includes('>>>')) {
this._injectCodeViaRawPaste(this._pendingMicroPythonCode);
this._pendingMicroPythonCode = null;
}
// Timing: 500ms initial delay → 100ms after Ctrl+A → 100ms after Ctrl+E
// → 10ms between 256-byte chunks → 50ms before Ctrl+D
```
### WebSocket Protocol Flow
```
Frontend Backend (QEMU)
│ │
│ start_esp32 { firmware_b64, board }
│──────────────────────────────────→│
│ │ QEMU boots MicroPython
│ │
│ serial_output { data: ">>>" } │
│←──────────────────────────────────│
│ │
│ esp32_serial_input { bytes: [0x01] } ← Ctrl+A (raw REPL)
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [0x05] } ← Ctrl+E (raw-paste)
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [...code...] } ← code chunks
│──────────────────────────────────→│
│ │
│ esp32_serial_input { bytes: [0x04] } ← Ctrl+D (execute)
│──────────────────────────────────→│
│ │
│ serial_output { data: "...output..." }
│←──────────────────────────────────│
```
---
## Phase 3: ESP32-C3 MicroPython
### Files Modified
| File | Action |
|------|--------|
| `frontend/src/types/board.ts` | Modified — added C3 boards to `BOARD_SUPPORTS_MICROPYTHON`, fixed comments |
| `frontend/src/simulation/Esp32MicroPythonLoader.ts` | Modified — added `esp32-c3` firmware variant |
| `frontend/public/firmware/micropython-esp32c3.bin` | **Created** — bundled fallback (1.4 MB) |
### Key Finding
ESP32-C3 boards were documented as "browser emulation (Esp32C3Simulator)" in the type definitions, but the actual runtime code routes them through `Esp32Bridge` (QEMU backend) — the `Esp32C3Simulator` is dead code. This was corrected.
The `isEsp32Kind()` function in `useSimulatorStore.ts` already included C3 boards, so the QEMU bridge infrastructure was already wired up. Only the MicroPython-specific additions were needed.
---
## Store Integration
### `loadMicroPythonProgram(boardId, files)`
```typescript
// In useSimulatorStore.ts
async loadMicroPythonProgram(boardId, files) {
if (isEsp32Kind(board.boardKind)) {
// ESP32 path: firmware → QEMU bridge → raw-paste injection
const firmware = await getEsp32Firmware(board.boardKind);
const b64 = uint8ArrayToBase64(firmware);
esp32Bridge.loadFirmware(b64);
esp32Bridge.setPendingMicroPythonCode(mainFile.content);
} else {
// RP2040 path: firmware + LittleFS in browser
await sim.loadMicroPython(files);
}
// Mark as loaded
board.compiledProgram = 'micropython-loaded';
}
```
### `setBoardLanguageMode(boardId, mode)`
When toggling between Arduino and MicroPython:
1. Stops any running simulation
2. Clears `compiledProgram`
3. Deletes old file group
4. Creates new file group with appropriate defaults:
- RP2040 MicroPython → `main.py` with `Pin(25)` blink
- ESP32 MicroPython → `main.py` with `Pin(2)` blink
- Arduino → `sketch.ino` with default Arduino code
---
## UI Integration
### Language Mode Selector
Appears in `EditorToolbar` next to the board name pill when `BOARD_SUPPORTS_MICROPYTHON.has(boardKind)`:
```
[Arduino C++ ▼] ↔ [MicroPython ▼]
```
Changing the selector:
- Switches `languageMode` on the `BoardInstance`
- Replaces editor files with language-appropriate defaults
- Updates compile button text: "Compile (Ctrl+B)" → "Load MicroPython"
### Serial Monitor
- **REPL label**: Shows "MicroPython REPL" in magenta when in MicroPython mode
- **Baud rate**: Hidden in MicroPython mode (REPL has no baud rate)
- **Ctrl+C**: Sends `\x03` (keyboard interrupt) — works for both RP2040 and ESP32
- **Ctrl+D**: Sends `\x04` (soft reset) — works for both RP2040 and ESP32
- **Placeholder**: "Type Python expression... (Ctrl+C to interrupt)"
### Run Button Behavior
For MicroPython boards, the Run button:
1. If firmware not yet loaded → auto-loads firmware first, then starts
2. If already loaded → starts simulation directly
3. Enabled even without `compiledProgram` (unlike Arduino mode)
---
## Firmware Files
| File | Size | Board | Version |
|------|------|-------|---------|
| `micropython-rp2040.uf2` | 638 KB | RP2040 | v1.20.0 (2023-04-26) |
| `micropython-esp32.bin` | 1.5 MB | ESP32 Xtensa | v1.20.0 (2023-04-26) |
| `micropython-esp32s3.bin` | 1.4 MB | ESP32-S3 Xtensa | v1.20.0 (2023-04-26) |
| `micropython-esp32c3.bin` | 1.4 MB | ESP32-C3 RISC-V | v1.20.0 (2023-04-26) |
All firmware uses MicroPython **v1.20.0** (stable release, 2023-04-26).
Firmware source: `https://micropython.org/resources/firmware/`
---
## Dependencies Added
| Package | Version | Purpose |
|---------|---------|---------|
| `littlefs` | 0.1.0 | LittleFS WASM for creating filesystem images (RP2040) |
| `idb-keyval` | 6.x | IndexedDB key-value wrapper for firmware caching |
---
## Git History
```
990ae4b feat: add MicroPython support for RP2040 boards (Pico / Pico W)
7c9fd0b feat: add MicroPython support for ESP32/ESP32-S3 boards via QEMU bridge
0bc4c03 feat: add MicroPython support for ESP32-C3 (RISC-V) boards
```
| Commit | Files Changed | Lines Added | Lines Removed |
|--------|:------------:|:-----------:|:------------:|
| `990ae4b` (RP2040) | 12 | +587 | -38 |
| `7c9fd0b` (ESP32/S3) | 8 | +296 | -33 |
| `0bc4c03` (ESP32-C3) | 3 | +16 | -4 |
| **Total** | **23** | **+899** | **-75** |