Compare commits
4 Commits
master
...
feature/mi
| Author | SHA1 | Date |
|---|---|---|
|
|
6e5e9778fc | |
|
|
0bc4c031d1 | |
|
|
7c9fd0bf2b | |
|
|
990ae4be8c |
|
|
@ -93,8 +93,16 @@ backend/app/services/esp32c3-rom.bin
|
|||
test/esp32-emulator/**/*.elf
|
||||
test/esp32-emulator/**/*.map
|
||||
test/esp32-emulator/out_*/
|
||||
|
||||
# Arduino-cli compilation by-products (not needed for tests)
|
||||
**/*.ino.eep
|
||||
**/*.ino.with_bootloader.bin
|
||||
**/*.ino.with_bootloader.hex
|
||||
**/*.ino.map
|
||||
**/*.ino.uf2
|
||||
.claude/settings.json
|
||||
|
||||
# Google Cloud service account credentials
|
||||
velxio-ba3355a41944.json
|
||||
marketing/*
|
||||
marketing/*
|
||||
docs/github-issues/*
|
||||
|
|
@ -0,0 +1,348 @@
|
|||
# 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** |
|
||||
|
|
@ -15,7 +15,9 @@
|
|||
"@xterm/xterm": "^6.0.0",
|
||||
"avr8js": "file:../wokwi-libs/avr8js",
|
||||
"axios": "^1.13.6",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"jszip": "^3.10.1",
|
||||
"littlefs": "^0.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
@ -3669,6 +3671,12 @@
|
|||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/idb-keyval": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz",
|
||||
"integrity": "sha512-yjD9nARJ/jb1g+CvD0tlhUHOrJ9Sy0P8T9MF3YaLlHnSRpwPfpTX0XIvpmw3gAJUmEu3FiICLBDPXVwyEvrleg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
|
@ -3997,6 +4005,12 @@
|
|||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/littlefs": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/littlefs/-/littlefs-0.1.0.tgz",
|
||||
"integrity": "sha512-mNwh4CHkuw83HCIAxzNE3KLV59A5IKkxiFbUUCPkA8gIZ9HeAOovFbrfdpAgVwLAX1tJrM5jP/I1cGPSFgBNZw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/locate-path": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@
|
|||
"@xterm/xterm": "^6.0.0",
|
||||
"avr8js": "file:../wokwi-libs/avr8js",
|
||||
"axios": "^1.13.6",
|
||||
"idb-keyval": "^6.2.2",
|
||||
"jszip": "^3.10.1",
|
||||
"littlefs": "^0.1.0",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-router-dom": "^7.13.1",
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -1,8 +1,8 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import { useEditorStore } from '../../store/useEditorStore';
|
||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||
import type { BoardKind } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS } from '../../types/board';
|
||||
import type { BoardKind, LanguageMode } from '../../types/board';
|
||||
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON } from '../../types/board';
|
||||
import { compileCode } from '../../services/compilation';
|
||||
import { CompileAllProgress } from './CompileAllProgress';
|
||||
import type { BoardCompileStatus } from './CompileAllProgress';
|
||||
|
|
@ -49,6 +49,8 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
boards,
|
||||
activeBoardId,
|
||||
compileBoardProgram,
|
||||
loadMicroPythonProgram,
|
||||
setBoardLanguageMode,
|
||||
startBoard,
|
||||
stopBoard,
|
||||
resetBoard,
|
||||
|
|
@ -112,6 +114,25 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
return;
|
||||
}
|
||||
|
||||
// MicroPython mode — no backend compilation needed
|
||||
if (activeBoard?.languageMode === 'micropython' && activeBoardId) {
|
||||
addLog({ timestamp: new Date(), type: 'info', message: 'MicroPython: loading firmware and user files...' });
|
||||
try {
|
||||
const groupFiles = useEditorStore.getState().getGroupFiles(activeBoard.activeFileGroupId);
|
||||
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
||||
await loadMicroPythonProgram(activeBoardId, pyFiles);
|
||||
addLog({ timestamp: new Date(), type: 'success', message: 'MicroPython firmware loaded successfully' });
|
||||
setMessage({ type: 'success', text: 'MicroPython ready' });
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
|
||||
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
||||
setMessage({ type: 'error', text: errMsg });
|
||||
} finally {
|
||||
setCompiling(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const fqbn = kind ? BOARD_KIND_FQBN[kind] : null;
|
||||
const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown';
|
||||
|
||||
|
|
@ -155,9 +176,37 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
}
|
||||
};
|
||||
|
||||
const handleRun = () => {
|
||||
const handleRun = async () => {
|
||||
if (activeBoardId) {
|
||||
const board = boards.find((b) => b.id === activeBoardId);
|
||||
|
||||
// MicroPython mode: auto-load firmware + files, then start
|
||||
if (board?.languageMode === 'micropython') {
|
||||
trackRunSimulation(board.boardKind);
|
||||
if (!board.compiledProgram) {
|
||||
// Need to load MicroPython first
|
||||
setCompiling(true);
|
||||
setMessage(null);
|
||||
addLog({ timestamp: new Date(), type: 'info', message: 'MicroPython: loading firmware and user files...' });
|
||||
try {
|
||||
const groupFiles = useEditorStore.getState().getGroupFiles(board.activeFileGroupId);
|
||||
const pyFiles = groupFiles.map((f) => ({ name: f.name, content: f.content }));
|
||||
await loadMicroPythonProgram(activeBoardId, pyFiles);
|
||||
addLog({ timestamp: new Date(), type: 'success', message: 'MicroPython firmware loaded' });
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : 'Failed to load MicroPython';
|
||||
addLog({ timestamp: new Date(), type: 'error', message: errMsg });
|
||||
setMessage({ type: 'error', text: errMsg });
|
||||
setCompiling(false);
|
||||
return;
|
||||
}
|
||||
setCompiling(false);
|
||||
}
|
||||
startBoard(activeBoardId);
|
||||
setMessage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3';
|
||||
if (isQemuBoard || board?.compiledProgram) {
|
||||
trackRunSimulation(board?.boardKind);
|
||||
|
|
@ -302,14 +351,39 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
<div className="editor-toolbar" ref={toolbarRef}>
|
||||
{/* Active board context pill */}
|
||||
{activeBoard && (
|
||||
<div
|
||||
className="tb-board-pill"
|
||||
style={{ borderColor: BOARD_PILL_COLOR[activeBoard.boardKind], color: BOARD_PILL_COLOR[activeBoard.boardKind] }}
|
||||
title={`Editing: ${BOARD_KIND_LABELS[activeBoard.boardKind]}`}
|
||||
>
|
||||
<span className="tb-board-pill-icon">{BOARD_PILL_ICON[activeBoard.boardKind]}</span>
|
||||
<span className="tb-board-pill-label">{BOARD_KIND_LABELS[activeBoard.boardKind]}</span>
|
||||
{activeBoard.running && <span className="tb-board-pill-running" title="Running" />}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
<div
|
||||
className="tb-board-pill"
|
||||
style={{ borderColor: BOARD_PILL_COLOR[activeBoard.boardKind], color: BOARD_PILL_COLOR[activeBoard.boardKind] }}
|
||||
title={`Editing: ${BOARD_KIND_LABELS[activeBoard.boardKind]}`}
|
||||
>
|
||||
<span className="tb-board-pill-icon">{BOARD_PILL_ICON[activeBoard.boardKind]}</span>
|
||||
<span className="tb-board-pill-label">{BOARD_KIND_LABELS[activeBoard.boardKind]}</span>
|
||||
{activeBoard.running && <span className="tb-board-pill-running" title="Running" />}
|
||||
</div>
|
||||
{BOARD_SUPPORTS_MICROPYTHON.has(activeBoard.boardKind) && (
|
||||
<select
|
||||
className="tb-lang-select"
|
||||
value={activeBoard.languageMode ?? 'arduino'}
|
||||
onChange={(e) => {
|
||||
if (activeBoardId) setBoardLanguageMode(activeBoardId, e.target.value as LanguageMode);
|
||||
}}
|
||||
title="Language mode"
|
||||
style={{
|
||||
background: '#2d2d2d',
|
||||
color: '#ccc',
|
||||
border: '1px solid #444',
|
||||
borderRadius: 4,
|
||||
padding: '2px 4px',
|
||||
fontSize: 11,
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
}}
|
||||
>
|
||||
<option value="arduino">Arduino C++</option>
|
||||
<option value="micropython">MicroPython</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -319,7 +393,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
onClick={handleCompile}
|
||||
disabled={compiling}
|
||||
className="tb-btn tb-btn-compile"
|
||||
title={compiling ? 'Compiling…' : 'Compile (Ctrl+B)'}
|
||||
title={compiling ? 'Loading…' : activeBoard?.languageMode === 'micropython' ? 'Load MicroPython' : 'Compile (Ctrl+B)'}
|
||||
>
|
||||
{compiling ? (
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="spin">
|
||||
|
|
@ -337,9 +411,14 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
|||
{/* Run */}
|
||||
<button
|
||||
onClick={handleRun}
|
||||
disabled={running || (!['raspberry-pi-3','esp32','esp32-s3'].includes(activeBoard?.boardKind ?? '') && !compiledHex && !activeBoard?.compiledProgram)}
|
||||
disabled={running || compiling || (
|
||||
!['raspberry-pi-3','esp32','esp32-s3'].includes(activeBoard?.boardKind ?? '')
|
||||
&& activeBoard?.languageMode !== 'micropython'
|
||||
&& !compiledHex
|
||||
&& !activeBoard?.compiledProgram
|
||||
)}
|
||||
className="tb-btn tb-btn-run"
|
||||
title="Run"
|
||||
title={activeBoard?.languageMode === 'micropython' ? 'Run MicroPython' : 'Run'}
|
||||
>
|
||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||
<polygon points="5,3 19,12 5,21" />
|
||||
|
|
|
|||
|
|
@ -9,37 +9,64 @@ import type { BoardKind } from '../../types/board';
|
|||
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||
|
||||
// Short labels for tabs
|
||||
const BOARD_SHORT_LABEL: Record<BoardKind, string> = {
|
||||
const BOARD_SHORT_LABEL: Partial<Record<BoardKind, string>> = {
|
||||
'arduino-uno': 'Uno',
|
||||
'arduino-nano': 'Nano',
|
||||
'arduino-mega': 'Mega',
|
||||
'raspberry-pi-pico': 'Pico',
|
||||
'pi-pico-w': 'Pico W',
|
||||
'raspberry-pi-3': 'Pi 3B',
|
||||
'esp32': 'ESP32',
|
||||
'esp32-s3': 'ESP32-S3',
|
||||
'esp32-c3': 'ESP32-C3',
|
||||
'esp32': 'ESP32',
|
||||
'esp32-devkit-c-v4': 'ESP32',
|
||||
'esp32-cam': 'ESP32-CAM',
|
||||
'wemos-lolin32-lite':'Lolin32',
|
||||
'esp32-s3': 'ESP32-S3',
|
||||
'xiao-esp32-s3': 'XIAO-S3',
|
||||
'arduino-nano-esp32':'Nano ESP32',
|
||||
'esp32-c3': 'ESP32-C3',
|
||||
'xiao-esp32-c3': 'XIAO-C3',
|
||||
'aitewinrobot-esp32c3-supermini': 'C3 Mini',
|
||||
'attiny85': 'ATtiny85',
|
||||
};
|
||||
|
||||
const BOARD_ICON: Record<BoardKind, string> = {
|
||||
const BOARD_ICON: Partial<Record<BoardKind, string>> = {
|
||||
'arduino-uno': '⬤',
|
||||
'arduino-nano': '▪',
|
||||
'arduino-mega': '▬',
|
||||
'raspberry-pi-pico': '◆',
|
||||
'pi-pico-w': '◆',
|
||||
'raspberry-pi-3': '⬛',
|
||||
'esp32': '⬡',
|
||||
'esp32-s3': '⬡',
|
||||
'esp32-c3': '⬡',
|
||||
'esp32': '⬡',
|
||||
'esp32-devkit-c-v4': '⬡',
|
||||
'esp32-cam': '⬡',
|
||||
'wemos-lolin32-lite':'⬡',
|
||||
'esp32-s3': '⬡',
|
||||
'xiao-esp32-s3': '⬡',
|
||||
'arduino-nano-esp32':'⬡',
|
||||
'esp32-c3': '⬡',
|
||||
'xiao-esp32-c3': '⬡',
|
||||
'aitewinrobot-esp32c3-supermini': '⬡',
|
||||
'attiny85': '▪',
|
||||
};
|
||||
|
||||
const BOARD_COLOR: Record<BoardKind, string> = {
|
||||
const BOARD_COLOR: Partial<Record<BoardKind, string>> = {
|
||||
'arduino-uno': '#4fc3f7',
|
||||
'arduino-nano': '#4fc3f7',
|
||||
'arduino-mega': '#4fc3f7',
|
||||
'raspberry-pi-pico': '#ce93d8',
|
||||
'pi-pico-w': '#ce93d8',
|
||||
'raspberry-pi-3': '#ef9a9a',
|
||||
'esp32': '#a5d6a7',
|
||||
'esp32-s3': '#a5d6a7',
|
||||
'esp32-c3': '#a5d6a7',
|
||||
'esp32': '#a5d6a7',
|
||||
'esp32-devkit-c-v4': '#a5d6a7',
|
||||
'esp32-cam': '#a5d6a7',
|
||||
'wemos-lolin32-lite':'#a5d6a7',
|
||||
'esp32-s3': '#a5d6a7',
|
||||
'xiao-esp32-s3': '#a5d6a7',
|
||||
'arduino-nano-esp32':'#a5d6a7',
|
||||
'esp32-c3': '#a5d6a7',
|
||||
'xiao-esp32-c3': '#a5d6a7',
|
||||
'aitewinrobot-esp32c3-supermini': '#a5d6a7',
|
||||
'attiny85': '#ffcc80',
|
||||
};
|
||||
|
||||
export const SerialMonitor: React.FC = () => {
|
||||
|
|
@ -94,12 +121,25 @@ export const SerialMonitor: React.FC = () => {
|
|||
setInputValue('');
|
||||
}, [resolvedTabId, inputValue, lineEnding, serialWriteToBoard]);
|
||||
|
||||
const isMicroPython = activeBoard?.languageMode === 'micropython';
|
||||
|
||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
return;
|
||||
}
|
||||
}, [handleSend]);
|
||||
// MicroPython REPL control characters (works for RP2040 and ESP32)
|
||||
if (resolvedTabId && isMicroPython && e.ctrlKey) {
|
||||
if (e.key === 'c' || e.key === 'C') {
|
||||
e.preventDefault();
|
||||
serialWriteToBoard(resolvedTabId, '\x03'); // Ctrl+C — keyboard interrupt
|
||||
} else if (e.key === 'd' || e.key === 'D') {
|
||||
e.preventDefault();
|
||||
serialWriteToBoard(resolvedTabId, '\x04'); // Ctrl+D — soft reset
|
||||
}
|
||||
}
|
||||
}, [handleSend, resolvedTabId]);
|
||||
|
||||
const handleTabClick = (boardId: string) => {
|
||||
setActiveTabId(boardId);
|
||||
|
|
@ -126,7 +166,7 @@ export const SerialMonitor: React.FC = () => {
|
|||
<div style={styles.tabStrip}>
|
||||
{boards.map((board) => {
|
||||
const isActive = board.id === resolvedTabId;
|
||||
const color = BOARD_COLOR[board.boardKind];
|
||||
const color = BOARD_COLOR[board.boardKind] ?? '#999';
|
||||
const hasUnread = (board.serialOutput.length) > (lastSeenLen[board.id] ?? 0);
|
||||
return (
|
||||
<button
|
||||
|
|
@ -139,9 +179,9 @@ export const SerialMonitor: React.FC = () => {
|
|||
title={BOARD_KIND_LABELS[board.boardKind]}
|
||||
>
|
||||
<span style={{ fontSize: 9, marginRight: 3, color: isActive ? color : '#888' }}>
|
||||
{BOARD_ICON[board.boardKind]}
|
||||
{BOARD_ICON[board.boardKind] ?? '●'}
|
||||
</span>
|
||||
{BOARD_SHORT_LABEL[board.boardKind]}
|
||||
{BOARD_SHORT_LABEL[board.boardKind] ?? board.boardKind}
|
||||
{hasUnread && !isActive && <span style={styles.unreadDot} />}
|
||||
</button>
|
||||
);
|
||||
|
|
@ -149,7 +189,10 @@ export const SerialMonitor: React.FC = () => {
|
|||
|
||||
{/* Right-side controls */}
|
||||
<div style={styles.tabControls}>
|
||||
{activeBoard?.serialBaudRate != null && activeBoard.serialBaudRate > 0 && (
|
||||
{isMicroPython && (
|
||||
<span style={{ color: '#ce93d8', fontSize: 11, fontWeight: 600 }}>MicroPython REPL</span>
|
||||
)}
|
||||
{activeBoard?.serialBaudRate != null && activeBoard.serialBaudRate > 0 && !isMicroPython && (
|
||||
<span style={styles.baudRate}>{activeBoard.serialBaudRate.toLocaleString()} baud</span>
|
||||
)}
|
||||
<label style={styles.autoscrollLabel}>
|
||||
|
|
@ -184,7 +227,7 @@ export const SerialMonitor: React.FC = () => {
|
|||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="Type message to send..."
|
||||
placeholder={isMicroPython ? 'Type Python expression... (Ctrl+C to interrupt)' : 'Type message to send...'}
|
||||
style={styles.input}
|
||||
disabled={!activeBoard?.running}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import type { BoardKind } from '../types/board';
|
|||
* Map any ESP32-family board kind to the 3 base QEMU machine types understood
|
||||
* by the backend esp_qemu_manager.
|
||||
*/
|
||||
function toQemuBoardType(kind: BoardKind): 'esp32' | 'esp32-s3' | 'esp32-c3' {
|
||||
export function toQemuBoardType(kind: BoardKind): 'esp32' | 'esp32-s3' | 'esp32-c3' {
|
||||
if (kind === 'esp32-s3' || kind === 'xiao-esp32-s3' || kind === 'arduino-nano-esp32') return 'esp32-s3';
|
||||
if (kind === 'esp32-c3' || kind === 'xiao-esp32-c3' || kind === 'aitewinrobot-esp32c3-supermini') return 'esp32-c3';
|
||||
return 'esp32'; // esp32, esp32-devkit-c-v4, esp32-cam, wemos-lolin32-lite
|
||||
|
|
@ -84,6 +84,11 @@ export class Esp32Bridge {
|
|||
private _pendingFirmware: string | null = null;
|
||||
private _pendingSensors: Array<Record<string, unknown>> = [];
|
||||
|
||||
// MicroPython raw-paste REPL injection
|
||||
private _pendingMicroPythonCode: string | null = null;
|
||||
private _serialBuffer = '';
|
||||
micropythonMode = false;
|
||||
|
||||
constructor(boardId: string, boardKind: BoardKind) {
|
||||
this.boardId = boardId;
|
||||
this.boardKind = boardKind;
|
||||
|
|
@ -134,6 +139,19 @@ export class Esp32Bridge {
|
|||
if (this.onSerialData) {
|
||||
for (const ch of text) this.onSerialData(ch, uart);
|
||||
}
|
||||
// Detect MicroPython REPL prompt and inject pending code
|
||||
if (this._pendingMicroPythonCode) {
|
||||
this._serialBuffer += text;
|
||||
if (this._serialBuffer.includes('>>>')) {
|
||||
this._injectCodeViaRawPaste(this._pendingMicroPythonCode);
|
||||
this._pendingMicroPythonCode = null;
|
||||
this._serialBuffer = '';
|
||||
}
|
||||
// Prevent buffer from growing indefinitely
|
||||
if (this._serialBuffer.length > 4096) {
|
||||
this._serialBuffer = this._serialBuffer.slice(-512);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'gpio_change': {
|
||||
|
|
@ -283,6 +301,68 @@ export class Esp32Bridge {
|
|||
this._send({ type: 'esp32_sensor_detach', data: { pin } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue user MicroPython code for injection after the REPL boots.
|
||||
* The code will be sent via raw-paste protocol once `>>>` is detected.
|
||||
*/
|
||||
setPendingMicroPythonCode(code: string): void {
|
||||
this._pendingMicroPythonCode = code;
|
||||
this._serialBuffer = '';
|
||||
this.micropythonMode = true;
|
||||
}
|
||||
|
||||
/** Check if this bridge is in MicroPython mode */
|
||||
isMicroPythonMode(): boolean {
|
||||
return this.micropythonMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inject code via MicroPython raw-paste REPL protocol:
|
||||
* \x01 (Ctrl+A) → enter raw REPL
|
||||
* \x05 (Ctrl+E) → enter raw-paste mode
|
||||
* <code bytes>
|
||||
* \x04 (Ctrl+D) → execute
|
||||
*/
|
||||
private _injectCodeViaRawPaste(code: string): void {
|
||||
console.log(`[Esp32Bridge:${this.boardId}] Injecting MicroPython code (${code.length} bytes) via raw-paste REPL`);
|
||||
|
||||
// Small delay to let the REPL fully initialize after printing >>>
|
||||
setTimeout(() => {
|
||||
// Step 1: Enter raw REPL (Ctrl+A)
|
||||
this.sendSerialBytes([0x01]);
|
||||
|
||||
setTimeout(() => {
|
||||
// Step 2: Enter raw-paste mode (Ctrl+E)
|
||||
this.sendSerialBytes([0x05]);
|
||||
|
||||
setTimeout(() => {
|
||||
// Step 3: Send code bytes in chunks (avoid overwhelming the serial)
|
||||
const encoder = new TextEncoder();
|
||||
const codeBytes = encoder.encode(code);
|
||||
const CHUNK_SIZE = 256;
|
||||
|
||||
let offset = 0;
|
||||
const sendChunk = () => {
|
||||
if (offset >= codeBytes.length) {
|
||||
// Step 4: Execute (Ctrl+D)
|
||||
setTimeout(() => {
|
||||
this.sendSerialBytes([0x04]);
|
||||
console.log(`[Esp32Bridge:${this.boardId}] MicroPython code injection complete`);
|
||||
}, 50);
|
||||
return;
|
||||
}
|
||||
const end = Math.min(offset + CHUNK_SIZE, codeBytes.length);
|
||||
const chunk = Array.from(codeBytes.slice(offset, end));
|
||||
this.sendSerialBytes(chunk);
|
||||
offset = end;
|
||||
setTimeout(sendChunk, 10);
|
||||
};
|
||||
sendChunk();
|
||||
}, 100);
|
||||
}, 100);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
private _send(payload: unknown): void {
|
||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||
this.socket.send(JSON.stringify(payload));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* Esp32MicroPythonLoader — Downloads and caches MicroPython firmware for ESP32 boards
|
||||
*
|
||||
* Supports ESP32 (Xtensa LX6) and ESP32-S3 (Xtensa LX7).
|
||||
* Firmware is cached in IndexedDB for fast subsequent loads.
|
||||
* Falls back to bundled firmware in public/firmware/ if remote download fails.
|
||||
*/
|
||||
|
||||
import { get as idbGet, set as idbSet } from 'idb-keyval';
|
||||
import type { BoardKind } from '../types/board';
|
||||
|
||||
interface FirmwareConfig {
|
||||
remote: string;
|
||||
cacheKey: string;
|
||||
fallback: string;
|
||||
}
|
||||
|
||||
const FIRMWARE_MAP: Record<string, FirmwareConfig> = {
|
||||
'esp32': {
|
||||
remote: 'https://micropython.org/resources/firmware/ESP32_GENERIC-20230426-v1.20.0.bin',
|
||||
cacheKey: 'micropython-esp32-v1.20.0',
|
||||
fallback: '/firmware/micropython-esp32.bin',
|
||||
},
|
||||
'esp32-s3': {
|
||||
remote: 'https://micropython.org/resources/firmware/ESP32_GENERIC_S3-20230426-v1.20.0.bin',
|
||||
cacheKey: 'micropython-esp32s3-v1.20.0',
|
||||
fallback: '/firmware/micropython-esp32s3.bin',
|
||||
},
|
||||
'esp32-c3': {
|
||||
remote: 'https://micropython.org/resources/firmware/ESP32_GENERIC_C3-20230426-v1.20.0.bin',
|
||||
cacheKey: 'micropython-esp32c3-v1.20.0',
|
||||
fallback: '/firmware/micropython-esp32c3.bin',
|
||||
},
|
||||
};
|
||||
|
||||
/** Map any ESP32-family board kind to firmware variant key */
|
||||
function toFirmwareVariant(boardKind: BoardKind): 'esp32' | 'esp32-s3' | 'esp32-c3' {
|
||||
if (boardKind === 'esp32-s3' || boardKind === 'xiao-esp32-s3' || boardKind === 'arduino-nano-esp32') {
|
||||
return 'esp32-s3';
|
||||
}
|
||||
if (boardKind === 'esp32-c3' || boardKind === 'xiao-esp32-c3' || boardKind === 'aitewinrobot-esp32c3-supermini') {
|
||||
return 'esp32-c3';
|
||||
}
|
||||
return 'esp32';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get MicroPython firmware binary for an ESP32 board.
|
||||
* Checks IndexedDB cache first, then remote, then bundled fallback.
|
||||
*/
|
||||
export async function getEsp32Firmware(
|
||||
boardKind: BoardKind,
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
): Promise<Uint8Array> {
|
||||
const variant = toFirmwareVariant(boardKind);
|
||||
const config = FIRMWARE_MAP[variant];
|
||||
if (!config) throw new Error(`No MicroPython firmware for board: ${boardKind}`);
|
||||
|
||||
// 1. Check IndexedDB cache
|
||||
try {
|
||||
const cached = await idbGet(config.cacheKey);
|
||||
if (cached instanceof Uint8Array && cached.length > 0) {
|
||||
console.log(`[ESP32-MicroPython] Firmware loaded from cache (${variant})`);
|
||||
return cached;
|
||||
}
|
||||
} catch {
|
||||
// IndexedDB unavailable
|
||||
}
|
||||
|
||||
// 2. Try remote download
|
||||
try {
|
||||
const response = await fetch(config.remote);
|
||||
if (response.ok) {
|
||||
const total = Number(response.headers.get('content-length') || 0);
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (reader) {
|
||||
const chunks: Uint8Array[] = [];
|
||||
let loaded = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
loaded += value.length;
|
||||
onProgress?.(loaded, total);
|
||||
}
|
||||
|
||||
const firmware = new Uint8Array(loaded);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
firmware.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
try { await idbSet(config.cacheKey, firmware); } catch { /* non-fatal */ }
|
||||
|
||||
console.log(`[ESP32-MicroPython] Firmware downloaded (${variant}, ${firmware.length} bytes)`);
|
||||
return firmware;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.warn(`[ESP32-MicroPython] Remote download failed for ${variant}, trying bundled fallback`);
|
||||
}
|
||||
|
||||
// 3. Fallback to bundled firmware
|
||||
const response = await fetch(config.fallback);
|
||||
if (!response.ok) {
|
||||
throw new Error(`MicroPython firmware not available for ${variant} (remote and bundled both failed)`);
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
const firmware = new Uint8Array(buffer);
|
||||
|
||||
try { await idbSet(config.cacheKey, firmware); } catch { /* non-fatal */ }
|
||||
|
||||
console.log(`[ESP32-MicroPython] Firmware loaded from bundled fallback (${variant}, ${firmware.length} bytes)`);
|
||||
return firmware;
|
||||
}
|
||||
|
||||
/** Convert Uint8Array to base64 string */
|
||||
export function uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
return btoa(binary);
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* MicroPythonLoader — Loads MicroPython firmware + user files into RP2040 flash
|
||||
*
|
||||
* 1. Parses UF2 firmware and writes it to flash
|
||||
* 2. Creates a LittleFS image with user .py files and writes it to flash at 0xa0000
|
||||
* 3. Caches the MicroPython firmware UF2 in IndexedDB for fast subsequent loads
|
||||
*/
|
||||
|
||||
import { get as idbGet, set as idbSet } from 'idb-keyval';
|
||||
import createLittleFS from 'littlefs';
|
||||
|
||||
// Flash geometry (matches rp2040js and MicroPython defaults)
|
||||
const FLASH_START_ADDRESS = 0x10000000;
|
||||
const MICROPYTHON_FS_FLASH_START = 0xa0000;
|
||||
const MICROPYTHON_FS_BLOCK_SIZE = 4096;
|
||||
const MICROPYTHON_FS_BLOCK_COUNT = 352;
|
||||
|
||||
// UF2 block constants
|
||||
const UF2_MAGIC_START0 = 0x0a324655;
|
||||
const UF2_MAGIC_START1 = 0x9e5d5157;
|
||||
const UF2_BLOCK_SIZE = 512;
|
||||
const UF2_PAYLOAD_SIZE = 256;
|
||||
const UF2_DATA_OFFSET = 32;
|
||||
const UF2_ADDR_OFFSET = 12;
|
||||
|
||||
// Firmware cache key for IndexedDB
|
||||
const FIRMWARE_CACHE_KEY = 'micropython-rp2040-uf2-v1.20.0';
|
||||
|
||||
// Bundled fallback path (placed in public/firmware/)
|
||||
const FIRMWARE_FALLBACK_PATH = '/firmware/micropython-rp2040.uf2';
|
||||
|
||||
// Remote firmware URL
|
||||
const FIRMWARE_REMOTE_URL =
|
||||
'https://micropython.org/resources/firmware/RPI_PICO-20230426-v1.20.0.uf2';
|
||||
|
||||
/**
|
||||
* Parse UF2 binary and write payload blocks into RP2040 flash.
|
||||
* UF2 format: 512-byte blocks, each with a 256-byte payload targeted at a flash address.
|
||||
*/
|
||||
export function loadUF2(uf2Data: Uint8Array, flash: Uint8Array): void {
|
||||
const view = new DataView(uf2Data.buffer, uf2Data.byteOffset, uf2Data.byteLength);
|
||||
|
||||
for (let offset = 0; offset + UF2_BLOCK_SIZE <= uf2Data.length; offset += UF2_BLOCK_SIZE) {
|
||||
const magic0 = view.getUint32(offset, true);
|
||||
const magic1 = view.getUint32(offset + 4, true);
|
||||
if (magic0 !== UF2_MAGIC_START0 || magic1 !== UF2_MAGIC_START1) {
|
||||
continue; // skip non-UF2 blocks
|
||||
}
|
||||
|
||||
const flashAddress = view.getUint32(offset + UF2_ADDR_OFFSET, true);
|
||||
const payload = uf2Data.subarray(offset + UF2_DATA_OFFSET, offset + UF2_DATA_OFFSET + UF2_PAYLOAD_SIZE);
|
||||
const flashOffset = flashAddress - FLASH_START_ADDRESS;
|
||||
|
||||
if (flashOffset >= 0 && flashOffset + UF2_PAYLOAD_SIZE <= flash.length) {
|
||||
flash.set(payload, flashOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a LittleFS filesystem image containing the user's Python files
|
||||
* and write it into RP2040 flash at the MicroPython filesystem offset.
|
||||
*/
|
||||
export async function loadUserFiles(
|
||||
files: Array<{ name: string; content: string }>,
|
||||
flash: Uint8Array,
|
||||
): Promise<void> {
|
||||
// Create a backing buffer for the LittleFS filesystem
|
||||
const fsBuffer = new Uint8Array(MICROPYTHON_FS_BLOCK_COUNT * MICROPYTHON_FS_BLOCK_SIZE);
|
||||
|
||||
// Initialize the littlefs WASM module
|
||||
const lfs = await createLittleFS({});
|
||||
|
||||
// Register flash read/write callbacks for the WASM module
|
||||
const flashRead = lfs.addFunction(
|
||||
(_cfg: number, block: number, off: number, buffer: number, size: number) => {
|
||||
const start = block * MICROPYTHON_FS_BLOCK_SIZE + off;
|
||||
lfs.HEAPU8.set(fsBuffer.subarray(start, start + size), buffer);
|
||||
return 0;
|
||||
},
|
||||
'iiiiii',
|
||||
);
|
||||
|
||||
const flashProg = lfs.addFunction(
|
||||
(_cfg: number, block: number, off: number, buffer: number, size: number) => {
|
||||
const start = block * MICROPYTHON_FS_BLOCK_SIZE + off;
|
||||
fsBuffer.set(lfs.HEAPU8.subarray(buffer, buffer + size), start);
|
||||
return 0;
|
||||
},
|
||||
'iiiiii',
|
||||
);
|
||||
|
||||
const flashErase = lfs.addFunction(
|
||||
(_cfg: number, _block: number) => 0,
|
||||
'iii',
|
||||
);
|
||||
|
||||
const flashSync = lfs.addFunction(() => 0, 'ii');
|
||||
|
||||
// Create LittleFS config and instance
|
||||
const config = lfs._new_lfs_config(
|
||||
flashRead, flashProg, flashErase, flashSync,
|
||||
MICROPYTHON_FS_BLOCK_COUNT, MICROPYTHON_FS_BLOCK_SIZE,
|
||||
);
|
||||
const lfsInstance = lfs._new_lfs();
|
||||
|
||||
// Format and mount
|
||||
lfs._lfs_format(lfsInstance, config);
|
||||
lfs._lfs_mount(lfsInstance, config);
|
||||
|
||||
// Write user files using cwrap for automatic string marshalling
|
||||
const writeFile = lfs.cwrap('lfs_write_file', 'number', ['number', 'string', 'string', 'number']);
|
||||
|
||||
for (const file of files) {
|
||||
const fileName = file.name;
|
||||
const content = file.content;
|
||||
writeFile(lfsInstance, fileName, content, content.length);
|
||||
}
|
||||
|
||||
// Unmount and free
|
||||
lfs._lfs_unmount(lfsInstance);
|
||||
lfs._free(lfsInstance);
|
||||
lfs._free(config);
|
||||
|
||||
// Copy the LittleFS image into RP2040 flash at the filesystem offset
|
||||
flash.set(fsBuffer, MICROPYTHON_FS_FLASH_START);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the MicroPython UF2 firmware binary.
|
||||
* Checks IndexedDB cache first, then tries remote download, then bundled fallback.
|
||||
*/
|
||||
export async function getFirmware(
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
): Promise<Uint8Array> {
|
||||
// 1. Check IndexedDB cache
|
||||
try {
|
||||
const cached = await idbGet(FIRMWARE_CACHE_KEY);
|
||||
if (cached instanceof Uint8Array && cached.length > 0) {
|
||||
console.log('[MicroPython] Firmware loaded from cache');
|
||||
return cached;
|
||||
}
|
||||
} catch {
|
||||
// IndexedDB unavailable, continue
|
||||
}
|
||||
|
||||
// 2. Try remote download
|
||||
try {
|
||||
const response = await fetch(FIRMWARE_REMOTE_URL);
|
||||
if (response.ok) {
|
||||
const total = Number(response.headers.get('content-length') || 0);
|
||||
const reader = response.body?.getReader();
|
||||
|
||||
if (reader) {
|
||||
const chunks: Uint8Array[] = [];
|
||||
let loaded = 0;
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
chunks.push(value);
|
||||
loaded += value.length;
|
||||
onProgress?.(loaded, total);
|
||||
}
|
||||
|
||||
const firmware = new Uint8Array(loaded);
|
||||
let offset = 0;
|
||||
for (const chunk of chunks) {
|
||||
firmware.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
// Cache for next time
|
||||
try {
|
||||
await idbSet(FIRMWARE_CACHE_KEY, firmware);
|
||||
} catch {
|
||||
// Cache write failure is non-fatal
|
||||
}
|
||||
|
||||
console.log(`[MicroPython] Firmware downloaded (${firmware.length} bytes)`);
|
||||
return firmware;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
console.warn('[MicroPython] Remote firmware download failed, trying bundled fallback');
|
||||
}
|
||||
|
||||
// 3. Fallback to bundled firmware
|
||||
const response = await fetch(FIRMWARE_FALLBACK_PATH);
|
||||
if (!response.ok) {
|
||||
throw new Error('MicroPython firmware not available (remote and bundled both failed)');
|
||||
}
|
||||
const buffer = await response.arrayBuffer();
|
||||
const firmware = new Uint8Array(buffer);
|
||||
|
||||
// Cache for next time
|
||||
try {
|
||||
await idbSet(FIRMWARE_CACHE_KEY, firmware);
|
||||
} catch {
|
||||
// non-fatal
|
||||
}
|
||||
|
||||
console.log(`[MicroPython] Firmware loaded from bundled fallback (${firmware.length} bytes)`);
|
||||
return firmware;
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import { RP2040, GPIOPinState, ConsoleLogger, LogLevel } from 'rp2040js';
|
||||
import { RP2040, GPIOPinState, ConsoleLogger, LogLevel, USBCDC } from 'rp2040js';
|
||||
import type { RPI2C } from 'rp2040js';
|
||||
import { PinManager } from './PinManager';
|
||||
import { bootromB1 } from './rp2040-bootrom';
|
||||
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
||||
|
||||
/**
|
||||
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
||||
|
|
@ -52,8 +53,10 @@ export class RP2040Simulator {
|
|||
private totalCycles = 0;
|
||||
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
||||
private pioStepAccum = 0;
|
||||
private usbCDC: USBCDC | null = null;
|
||||
private micropythonMode = false;
|
||||
|
||||
/** Serial output callback — fires for each byte the Pico sends on UART0 */
|
||||
/** Serial output callback — fires for each byte the Pico sends on UART0 (or USBCDC in MicroPython mode) */
|
||||
public onSerialData: ((char: string) => void) | null = null;
|
||||
|
||||
/**
|
||||
|
|
@ -97,6 +100,88 @@ export class RP2040Simulator {
|
|||
console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
|
||||
}
|
||||
|
||||
/**
|
||||
* Load MicroPython firmware + user .py files into RP2040 flash.
|
||||
* Uses USBCDC for serial (REPL) instead of UART.
|
||||
*/
|
||||
async loadMicroPython(
|
||||
files: Array<{ name: string; content: string }>,
|
||||
onProgress?: (loaded: number, total: number) => void,
|
||||
): Promise<void> {
|
||||
console.log('[RP2040] Loading MicroPython firmware...');
|
||||
|
||||
// 1. Get MicroPython UF2 firmware (cached in IndexedDB)
|
||||
const firmware = await getFirmware(onProgress);
|
||||
|
||||
// 2. Create fresh RP2040 instance
|
||||
this.rp2040 = new RP2040();
|
||||
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
||||
this.rp2040.loadBootrom(bootromB1);
|
||||
|
||||
// 3. Load UF2 firmware into flash
|
||||
loadUF2(firmware, this.rp2040.flash);
|
||||
console.log(`[RP2040] MicroPython UF2 loaded (${firmware.length} bytes)`);
|
||||
|
||||
// 4. Create LittleFS with user files and load into flash
|
||||
await loadUserFiles(files, this.rp2040.flash);
|
||||
console.log(`[RP2040] LittleFS loaded with ${files.length} file(s)`);
|
||||
|
||||
// Keep a flash copy for reset
|
||||
this.flashCopy = new Uint8Array(this.rp2040.flash);
|
||||
|
||||
// 5. Set up USBCDC for serial REPL (instead of UART)
|
||||
this.usbCDC = new USBCDC(this.rp2040.usbCtrl);
|
||||
this.usbCDC.onDeviceConnected = () => {
|
||||
// Send newline to trigger the REPL prompt
|
||||
this.usbCDC!.sendSerialByte('\r'.charCodeAt(0));
|
||||
this.usbCDC!.sendSerialByte('\n'.charCodeAt(0));
|
||||
};
|
||||
this.usbCDC.onSerialData = (buffer: Uint8Array) => {
|
||||
for (const byte of buffer) {
|
||||
if (this.onSerialData) {
|
||||
this.onSerialData(String.fromCharCode(byte));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 6. Set PC to flash start
|
||||
this.rp2040.core.PC = 0x10000000;
|
||||
|
||||
// 7. Wire peripherals (I2C, SPI, ADC, PIO, GPIO — same as Arduino mode)
|
||||
// But skip UART serial wiring since MicroPython uses USBCDC
|
||||
this.rp2040.uart[1].onByte = (value: number) => {
|
||||
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
||||
};
|
||||
this.wireI2C(0);
|
||||
this.wireI2C(1);
|
||||
this.rp2040.spi[0].onTransmit = (v: number) => { this.rp2040!.spi[0].completeTransmit(v); };
|
||||
this.rp2040.spi[1].onTransmit = (v: number) => { this.rp2040!.spi[1].completeTransmit(v); };
|
||||
this.rp2040.adc.channelValues[0] = 2048;
|
||||
this.rp2040.adc.channelValues[1] = 2048;
|
||||
this.rp2040.adc.channelValues[2] = 2048;
|
||||
this.rp2040.adc.channelValues[3] = 2048;
|
||||
this.rp2040.adc.channelValues[4] = 876;
|
||||
|
||||
// Patch PIO (same as initMCU)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const pio of (this.rp2040 as any).pio) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pio.run = function (this: any) {
|
||||
if (this.runTimer) { clearTimeout(this.runTimer); this.runTimer = null; }
|
||||
};
|
||||
}
|
||||
this.pioStepAccum = 0;
|
||||
|
||||
this.setupGpioListeners();
|
||||
this.micropythonMode = true;
|
||||
console.log('[RP2040] MicroPython ready');
|
||||
}
|
||||
|
||||
/** Returns true if currently in MicroPython mode */
|
||||
isMicroPythonMode(): boolean {
|
||||
return this.micropythonMode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getADC(): any {
|
||||
return this.rp2040?.adc ?? null;
|
||||
|
|
@ -359,9 +444,46 @@ export class RP2040Simulator {
|
|||
this.totalCycles = 0;
|
||||
this.scheduledPinChanges = [];
|
||||
if (this.rp2040 && this.flashCopy) {
|
||||
this.initMCU(this.flashCopy);
|
||||
// Re-register any previously added I2C devices
|
||||
// (devices are kept in i2cDevices maps which persist across reset)
|
||||
if (this.micropythonMode) {
|
||||
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
|
||||
this.rp2040 = new RP2040();
|
||||
this.rp2040.logger = new ConsoleLogger(LogLevel.Error);
|
||||
this.rp2040.loadBootrom(bootromB1);
|
||||
this.rp2040.flash.set(this.flashCopy);
|
||||
this.rp2040.core.PC = 0x10000000;
|
||||
|
||||
// Re-wire USBCDC
|
||||
this.usbCDC = new USBCDC(this.rp2040.usbCtrl);
|
||||
this.usbCDC.onDeviceConnected = () => {
|
||||
this.usbCDC!.sendSerialByte('\r'.charCodeAt(0));
|
||||
this.usbCDC!.sendSerialByte('\n'.charCodeAt(0));
|
||||
};
|
||||
this.usbCDC.onSerialData = (buffer: Uint8Array) => {
|
||||
for (const byte of buffer) {
|
||||
if (this.onSerialData) this.onSerialData(String.fromCharCode(byte));
|
||||
}
|
||||
};
|
||||
|
||||
// Re-wire peripherals (skipping UART0 serial)
|
||||
this.rp2040.uart[1].onByte = (value: number) => {
|
||||
if (this.onSerialData) this.onSerialData(String.fromCharCode(value));
|
||||
};
|
||||
this.wireI2C(0);
|
||||
this.wireI2C(1);
|
||||
this.rp2040.spi[0].onTransmit = (v: number) => { this.rp2040!.spi[0].completeTransmit(v); };
|
||||
this.rp2040.spi[1].onTransmit = (v: number) => { this.rp2040!.spi[1].completeTransmit(v); };
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
for (const pio of (this.rp2040 as any).pio) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
pio.run = function (this: any) {
|
||||
if (this.runTimer) { clearTimeout(this.runTimer); this.runTimer = null; }
|
||||
};
|
||||
}
|
||||
this.pioStepAccum = 0;
|
||||
this.setupGpioListeners();
|
||||
} else {
|
||||
this.initMCU(this.flashCopy);
|
||||
}
|
||||
console.log('[RP2040] CPU reset');
|
||||
}
|
||||
}
|
||||
|
|
@ -443,12 +565,30 @@ export class RP2040Simulator {
|
|||
}
|
||||
|
||||
/**
|
||||
* Send text to UART0 RX (as if typed in Serial Monitor).
|
||||
* Send text to UART0 RX (or USBCDC in MicroPython mode).
|
||||
*/
|
||||
serialWrite(text: string): void {
|
||||
if (!this.rp2040) return;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
this.rp2040.uart[0].feedByte(text.charCodeAt(i));
|
||||
if (this.micropythonMode && this.usbCDC) {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
this.usbCDC.sendSerialByte(text.charCodeAt(i));
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
this.rp2040.uart[0].feedByte(text.charCodeAt(i));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a raw byte to the serial interface (for control characters like Ctrl+C).
|
||||
*/
|
||||
serialWriteByte(byte: number): void {
|
||||
if (!this.rp2040) return;
|
||||
if (this.micropythonMode && this.usbCDC) {
|
||||
this.usbCDC.sendSerialByte(byte);
|
||||
} else {
|
||||
this.rp2040.uart[0].feedByte(byte);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,28 @@ void loop() {
|
|||
delay(1000);
|
||||
}`;
|
||||
|
||||
const DEFAULT_MICROPYTHON_CONTENT = `# MicroPython Blink for Raspberry Pi Pico
|
||||
from machine import Pin
|
||||
import time
|
||||
|
||||
led = Pin(25, Pin.OUT)
|
||||
|
||||
while True:
|
||||
led.toggle()
|
||||
time.sleep(1)
|
||||
`;
|
||||
|
||||
const DEFAULT_ESP32_MICROPYTHON_CONTENT = `# MicroPython Blink for ESP32
|
||||
from machine import Pin
|
||||
import time
|
||||
|
||||
led = Pin(2, Pin.OUT) # Built-in LED on GPIO 2
|
||||
|
||||
while True:
|
||||
led.toggle()
|
||||
time.sleep(1)
|
||||
`;
|
||||
|
||||
const DEFAULT_PY_CONTENT = `import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
|
|
@ -79,7 +101,7 @@ interface EditorState {
|
|||
loadFiles: (files: { name: string; content: string }[]) => void;
|
||||
|
||||
// File group management
|
||||
createFileGroup: (groupId: string, initialFiles?: { name: string; content: string }[]) => void;
|
||||
createFileGroup: (groupId: string, languageModeOrFiles?: string | { name: string; content: string }[]) => void;
|
||||
deleteFileGroup: (groupId: string) => void;
|
||||
setActiveGroup: (groupId: string) => void;
|
||||
getGroupFiles: (groupId: string) => WorkspaceFile[];
|
||||
|
|
@ -263,10 +285,14 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
|
||||
// ── File group management ─────────────────────────────────────────────────
|
||||
|
||||
createFileGroup: (groupId: string, initialFiles?: { name: string; content: string }[]) => {
|
||||
createFileGroup: (groupId: string, languageModeOrFiles?: string | { name: string; content: string }[]) => {
|
||||
set((s) => {
|
||||
if (s.fileGroups[groupId]) return s; // already exists
|
||||
|
||||
// Resolve overloaded parameter
|
||||
const initialFiles = Array.isArray(languageModeOrFiles) ? languageModeOrFiles : undefined;
|
||||
const languageMode = typeof languageModeOrFiles === 'string' ? languageModeOrFiles : undefined;
|
||||
|
||||
let files: WorkspaceFile[];
|
||||
if (initialFiles && initialFiles.length > 0) {
|
||||
files = initialFiles.map((f, i) => ({
|
||||
|
|
@ -276,15 +302,27 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
|||
modified: false,
|
||||
}));
|
||||
} else {
|
||||
// Determine default file by group name convention
|
||||
// Determine default file by group name convention or language mode
|
||||
const isPi = groupId.includes('raspberry-pi-3');
|
||||
const isMicroPython = languageMode === 'micropython';
|
||||
const mainId = `${groupId}-main`;
|
||||
files = [{
|
||||
id: mainId,
|
||||
name: isPi ? 'script.py' : 'sketch.ino',
|
||||
content: isPi ? DEFAULT_PY_CONTENT : DEFAULT_INO_CONTENT,
|
||||
modified: false,
|
||||
}];
|
||||
let fileName: string;
|
||||
let content: string;
|
||||
const isEsp32 = groupId.includes('esp32');
|
||||
if (isMicroPython && isEsp32) {
|
||||
fileName = 'main.py';
|
||||
content = DEFAULT_ESP32_MICROPYTHON_CONTENT;
|
||||
} else if (isMicroPython) {
|
||||
fileName = 'main.py';
|
||||
content = DEFAULT_MICROPYTHON_CONTENT;
|
||||
} else if (isPi) {
|
||||
fileName = 'script.py';
|
||||
content = DEFAULT_PY_CONTENT;
|
||||
} else {
|
||||
fileName = 'sketch.ino';
|
||||
content = DEFAULT_INO_CONTENT;
|
||||
}
|
||||
files = [{ id: mainId, name: fileName, content, modified: false }];
|
||||
}
|
||||
|
||||
const firstId = files[0]?.id ?? `${groupId}-main`;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import { PinManager } from '../simulation/PinManager';
|
|||
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
|
||||
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
||||
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
|
||||
import type { BoardKind, BoardInstance } from '../types/board';
|
||||
import type { BoardKind, BoardInstance, LanguageMode } from '../types/board';
|
||||
import { BOARD_SUPPORTS_MICROPYTHON } from '../types/board';
|
||||
import { calculatePinPosition } from '../utils/pinPositionCalculator';
|
||||
import { useOscilloscopeStore } from './useOscilloscopeStore';
|
||||
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
|
||||
|
|
@ -181,6 +182,8 @@ interface SimulatorState {
|
|||
setBoardPosition: (pos: { x: number; y: number }, boardId?: string) => void;
|
||||
setActiveBoardId: (boardId: string) => void;
|
||||
compileBoardProgram: (boardId: string, program: string) => void;
|
||||
loadMicroPythonProgram: (boardId: string, files: Array<{ name: string; content: string }>) => Promise<void>;
|
||||
setBoardLanguageMode: (boardId: string, mode: LanguageMode) => void;
|
||||
startBoard: (boardId: string) => void;
|
||||
stopBoard: (boardId: string) => void;
|
||||
resetBoard: (boardId: string) => void;
|
||||
|
|
@ -298,6 +301,7 @@ const INITIAL_BOARD: BoardInstance = {
|
|||
serialBaudRate: 0,
|
||||
serialMonitorOpen: false,
|
||||
activeFileGroupId: `group-${INITIAL_BOARD_ID}`,
|
||||
languageMode: 'arduino' as LanguageMode,
|
||||
};
|
||||
|
||||
// ── Store ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -459,6 +463,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
serialOutput: '', serialBaudRate: 0,
|
||||
serialMonitorOpen: false,
|
||||
activeFileGroupId: `group-${id}`,
|
||||
languageMode: 'arduino',
|
||||
};
|
||||
|
||||
set((s) => ({ boards: [...s.boards, newBoard] }));
|
||||
|
|
@ -577,6 +582,68 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
|||
});
|
||||
},
|
||||
|
||||
loadMicroPythonProgram: async (boardId: string, files: Array<{ name: string; content: string }>) => {
|
||||
const board = get().boards.find((b) => b.id === boardId);
|
||||
if (!board) return;
|
||||
if (!BOARD_SUPPORTS_MICROPYTHON.has(board.boardKind)) return;
|
||||
|
||||
if (isEsp32Kind(board.boardKind)) {
|
||||
// ESP32 path: load MicroPython firmware via QEMU bridge, inject code via raw-paste REPL
|
||||
const { getEsp32Firmware, uint8ArrayToBase64 } = await import('../simulation/Esp32MicroPythonLoader');
|
||||
const esp32Bridge = getEsp32Bridge(boardId);
|
||||
if (!esp32Bridge) return;
|
||||
|
||||
const firmware = await getEsp32Firmware(board.boardKind);
|
||||
const b64 = uint8ArrayToBase64(firmware);
|
||||
esp32Bridge.loadFirmware(b64);
|
||||
|
||||
// Queue code injection for after REPL boots
|
||||
const mainFile = files.find(f => f.name === 'main.py') ?? files[0];
|
||||
if (mainFile) {
|
||||
esp32Bridge.setPendingMicroPythonCode(mainFile.content);
|
||||
}
|
||||
} else {
|
||||
// RP2040 path: load firmware + filesystem in browser
|
||||
const sim = getBoardSimulator(boardId);
|
||||
if (!(sim instanceof RP2040Simulator)) return;
|
||||
await sim.loadMicroPython(files);
|
||||
}
|
||||
|
||||
set((s) => {
|
||||
const boards = s.boards.map((b) =>
|
||||
b.id === boardId ? { ...b, compiledProgram: 'micropython-loaded' } : b
|
||||
);
|
||||
const isActive = s.activeBoardId === boardId;
|
||||
return {
|
||||
boards,
|
||||
...(isActive ? { compiledHex: 'micropython-loaded', hexEpoch: s.hexEpoch + 1 } : {}),
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setBoardLanguageMode: (boardId: string, mode: LanguageMode) => {
|
||||
const board = get().boards.find((b) => b.id === boardId);
|
||||
if (!board) return;
|
||||
|
||||
// Only allow MicroPython for supported boards
|
||||
if (mode === 'micropython' && !BOARD_SUPPORTS_MICROPYTHON.has(board.boardKind)) return;
|
||||
|
||||
// Stop any running simulation
|
||||
if (board.running) get().stopBoard(boardId);
|
||||
|
||||
// Clear compiled program since language changed
|
||||
set((s) => ({
|
||||
boards: s.boards.map((b) =>
|
||||
b.id === boardId ? { ...b, languageMode: mode, compiledProgram: null } : b
|
||||
),
|
||||
}));
|
||||
|
||||
// Replace file group with appropriate default files
|
||||
const editorStore = useEditorStore.getState();
|
||||
editorStore.deleteFileGroup(board.activeFileGroupId);
|
||||
editorStore.createFileGroup(board.activeFileGroupId, mode);
|
||||
},
|
||||
|
||||
startBoard: (boardId: string) => {
|
||||
const board = get().boards.find((b) => b.id === boardId);
|
||||
if (!board) return;
|
||||
|
|
|
|||
|
|
@ -12,11 +12,31 @@ export type BoardKind =
|
|||
| 'esp32-s3' // Xtensa LX7, QEMU backend
|
||||
| 'xiao-esp32-s3' // Seeed XIAO ESP32-S3, QEMU (esp32-s3)
|
||||
| 'arduino-nano-esp32' // Arduino Nano ESP32 (S3), QEMU (esp32-s3)
|
||||
| 'esp32-c3' // RISC-V RV32IMC, browser emulation (Esp32C3Simulator)
|
||||
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, browser emulation (Esp32C3Simulator)
|
||||
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, browser emulation (Esp32C3Simulator)
|
||||
| 'esp32-c3' // RISC-V RV32IMC, QEMU backend
|
||||
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, QEMU backend
|
||||
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, QEMU backend
|
||||
| 'attiny85'; // AVR ATtiny85, browser emulation (avr8js)
|
||||
|
||||
export type LanguageMode = 'arduino' | 'micropython';
|
||||
|
||||
export const BOARD_SUPPORTS_MICROPYTHON = new Set<BoardKind>([
|
||||
'raspberry-pi-pico',
|
||||
'pi-pico-w',
|
||||
// ESP32 Xtensa (QEMU bridge)
|
||||
'esp32',
|
||||
'esp32-devkit-c-v4',
|
||||
'esp32-cam',
|
||||
'wemos-lolin32-lite',
|
||||
// ESP32-S3 Xtensa (QEMU bridge)
|
||||
'esp32-s3',
|
||||
'xiao-esp32-s3',
|
||||
'arduino-nano-esp32',
|
||||
// ESP32-C3 RISC-V (QEMU bridge)
|
||||
'esp32-c3',
|
||||
'xiao-esp32-c3',
|
||||
'aitewinrobot-esp32c3-supermini',
|
||||
]);
|
||||
|
||||
export interface BoardInstance {
|
||||
id: string; // unique in canvas, e.g. 'arduino-uno', 'raspberry-pi-3'
|
||||
boardKind: BoardKind;
|
||||
|
|
@ -28,6 +48,7 @@ export interface BoardInstance {
|
|||
serialBaudRate: number;
|
||||
serialMonitorOpen: boolean;
|
||||
activeFileGroupId: string;
|
||||
languageMode: LanguageMode; // 'arduino' (default) or 'micropython'
|
||||
}
|
||||
|
||||
export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['avr8js', 'rp2040js', '@wokwi/elements'],
|
||||
include: ['avr8js', 'rp2040js', '@wokwi/elements', 'littlefs'],
|
||||
},
|
||||
test: {
|
||||
globals: true,
|
||||
|
|
|
|||
Loading…
Reference in New Issue