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/**/*.elf
|
||||||
test/esp32-emulator/**/*.map
|
test/esp32-emulator/**/*.map
|
||||||
test/esp32-emulator/out_*/
|
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
|
.claude/settings.json
|
||||||
|
|
||||||
# Google Cloud service account credentials
|
# Google Cloud service account credentials
|
||||||
velxio-ba3355a41944.json
|
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",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"avr8js": "file:../wokwi-libs/avr8js",
|
"avr8js": "file:../wokwi-libs/avr8js",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"littlefs": "^0.1.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"react-router-dom": "^7.13.1",
|
||||||
|
|
@ -3669,6 +3671,12 @@
|
||||||
"node": ">= 14"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|
@ -3997,6 +4005,12 @@
|
||||||
"immediate": "~3.0.5"
|
"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": {
|
"node_modules/locate-path": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,9 @@
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"avr8js": "file:../wokwi-libs/avr8js",
|
"avr8js": "file:../wokwi-libs/avr8js",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
|
"idb-keyval": "^6.2.2",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
|
"littlefs": "^0.1.0",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-router-dom": "^7.13.1",
|
"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 { useState, useCallback, useRef, useEffect } from 'react';
|
||||||
import { useEditorStore } from '../../store/useEditorStore';
|
import { useEditorStore } from '../../store/useEditorStore';
|
||||||
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||||
import type { BoardKind } from '../../types/board';
|
import type { BoardKind, LanguageMode } from '../../types/board';
|
||||||
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS } from '../../types/board';
|
import { BOARD_KIND_FQBN, BOARD_KIND_LABELS, BOARD_SUPPORTS_MICROPYTHON } from '../../types/board';
|
||||||
import { compileCode } from '../../services/compilation';
|
import { compileCode } from '../../services/compilation';
|
||||||
import { CompileAllProgress } from './CompileAllProgress';
|
import { CompileAllProgress } from './CompileAllProgress';
|
||||||
import type { BoardCompileStatus } from './CompileAllProgress';
|
import type { BoardCompileStatus } from './CompileAllProgress';
|
||||||
|
|
@ -49,6 +49,8 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
boards,
|
boards,
|
||||||
activeBoardId,
|
activeBoardId,
|
||||||
compileBoardProgram,
|
compileBoardProgram,
|
||||||
|
loadMicroPythonProgram,
|
||||||
|
setBoardLanguageMode,
|
||||||
startBoard,
|
startBoard,
|
||||||
stopBoard,
|
stopBoard,
|
||||||
resetBoard,
|
resetBoard,
|
||||||
|
|
@ -112,6 +114,25 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
return;
|
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 fqbn = kind ? BOARD_KIND_FQBN[kind] : null;
|
||||||
const boardLabel = kind ? BOARD_KIND_LABELS[kind] : 'Unknown';
|
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) {
|
if (activeBoardId) {
|
||||||
const board = boards.find((b) => b.id === 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';
|
const isQemuBoard = board?.boardKind === 'raspberry-pi-3' || board?.boardKind === 'esp32' || board?.boardKind === 'esp32-s3';
|
||||||
if (isQemuBoard || board?.compiledProgram) {
|
if (isQemuBoard || board?.compiledProgram) {
|
||||||
trackRunSimulation(board?.boardKind);
|
trackRunSimulation(board?.boardKind);
|
||||||
|
|
@ -302,14 +351,39 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
<div className="editor-toolbar" ref={toolbarRef}>
|
<div className="editor-toolbar" ref={toolbarRef}>
|
||||||
{/* Active board context pill */}
|
{/* Active board context pill */}
|
||||||
{activeBoard && (
|
{activeBoard && (
|
||||||
<div
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||||
className="tb-board-pill"
|
<div
|
||||||
style={{ borderColor: BOARD_PILL_COLOR[activeBoard.boardKind], color: BOARD_PILL_COLOR[activeBoard.boardKind] }}
|
className="tb-board-pill"
|
||||||
title={`Editing: ${BOARD_KIND_LABELS[activeBoard.boardKind]}`}
|
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>
|
<span className="tb-board-pill-icon">{BOARD_PILL_ICON[activeBoard.boardKind]}</span>
|
||||||
{activeBoard.running && <span className="tb-board-pill-running" title="Running" />}
|
<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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -319,7 +393,7 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
|
||||||
onClick={handleCompile}
|
onClick={handleCompile}
|
||||||
disabled={compiling}
|
disabled={compiling}
|
||||||
className="tb-btn tb-btn-compile"
|
className="tb-btn tb-btn-compile"
|
||||||
title={compiling ? 'Compiling…' : 'Compile (Ctrl+B)'}
|
title={compiling ? 'Loading…' : activeBoard?.languageMode === 'micropython' ? 'Load MicroPython' : 'Compile (Ctrl+B)'}
|
||||||
>
|
>
|
||||||
{compiling ? (
|
{compiling ? (
|
||||||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="spin">
|
<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 */}
|
{/* Run */}
|
||||||
<button
|
<button
|
||||||
onClick={handleRun}
|
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"
|
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">
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||||
<polygon points="5,3 19,12 5,21" />
|
<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';
|
import { BOARD_KIND_LABELS } from '../../types/board';
|
||||||
|
|
||||||
// Short labels for tabs
|
// Short labels for tabs
|
||||||
const BOARD_SHORT_LABEL: Record<BoardKind, string> = {
|
const BOARD_SHORT_LABEL: Partial<Record<BoardKind, string>> = {
|
||||||
'arduino-uno': 'Uno',
|
'arduino-uno': 'Uno',
|
||||||
'arduino-nano': 'Nano',
|
'arduino-nano': 'Nano',
|
||||||
'arduino-mega': 'Mega',
|
'arduino-mega': 'Mega',
|
||||||
'raspberry-pi-pico': 'Pico',
|
'raspberry-pi-pico': 'Pico',
|
||||||
|
'pi-pico-w': 'Pico W',
|
||||||
'raspberry-pi-3': 'Pi 3B',
|
'raspberry-pi-3': 'Pi 3B',
|
||||||
'esp32': 'ESP32',
|
'esp32': 'ESP32',
|
||||||
'esp32-s3': 'ESP32-S3',
|
'esp32-devkit-c-v4': 'ESP32',
|
||||||
'esp32-c3': 'ESP32-C3',
|
'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-uno': '⬤',
|
||||||
'arduino-nano': '▪',
|
'arduino-nano': '▪',
|
||||||
'arduino-mega': '▬',
|
'arduino-mega': '▬',
|
||||||
'raspberry-pi-pico': '◆',
|
'raspberry-pi-pico': '◆',
|
||||||
|
'pi-pico-w': '◆',
|
||||||
'raspberry-pi-3': '⬛',
|
'raspberry-pi-3': '⬛',
|
||||||
'esp32': '⬡',
|
'esp32': '⬡',
|
||||||
'esp32-s3': '⬡',
|
'esp32-devkit-c-v4': '⬡',
|
||||||
'esp32-c3': '⬡',
|
'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-uno': '#4fc3f7',
|
||||||
'arduino-nano': '#4fc3f7',
|
'arduino-nano': '#4fc3f7',
|
||||||
'arduino-mega': '#4fc3f7',
|
'arduino-mega': '#4fc3f7',
|
||||||
'raspberry-pi-pico': '#ce93d8',
|
'raspberry-pi-pico': '#ce93d8',
|
||||||
|
'pi-pico-w': '#ce93d8',
|
||||||
'raspberry-pi-3': '#ef9a9a',
|
'raspberry-pi-3': '#ef9a9a',
|
||||||
'esp32': '#a5d6a7',
|
'esp32': '#a5d6a7',
|
||||||
'esp32-s3': '#a5d6a7',
|
'esp32-devkit-c-v4': '#a5d6a7',
|
||||||
'esp32-c3': '#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 = () => {
|
export const SerialMonitor: React.FC = () => {
|
||||||
|
|
@ -94,12 +121,25 @@ export const SerialMonitor: React.FC = () => {
|
||||||
setInputValue('');
|
setInputValue('');
|
||||||
}, [resolvedTabId, inputValue, lineEnding, serialWriteToBoard]);
|
}, [resolvedTabId, inputValue, lineEnding, serialWriteToBoard]);
|
||||||
|
|
||||||
|
const isMicroPython = activeBoard?.languageMode === 'micropython';
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSend();
|
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) => {
|
const handleTabClick = (boardId: string) => {
|
||||||
setActiveTabId(boardId);
|
setActiveTabId(boardId);
|
||||||
|
|
@ -126,7 +166,7 @@ export const SerialMonitor: React.FC = () => {
|
||||||
<div style={styles.tabStrip}>
|
<div style={styles.tabStrip}>
|
||||||
{boards.map((board) => {
|
{boards.map((board) => {
|
||||||
const isActive = board.id === resolvedTabId;
|
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);
|
const hasUnread = (board.serialOutput.length) > (lastSeenLen[board.id] ?? 0);
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -139,9 +179,9 @@ export const SerialMonitor: React.FC = () => {
|
||||||
title={BOARD_KIND_LABELS[board.boardKind]}
|
title={BOARD_KIND_LABELS[board.boardKind]}
|
||||||
>
|
>
|
||||||
<span style={{ fontSize: 9, marginRight: 3, color: isActive ? color : '#888' }}>
|
<span style={{ fontSize: 9, marginRight: 3, color: isActive ? color : '#888' }}>
|
||||||
{BOARD_ICON[board.boardKind]}
|
{BOARD_ICON[board.boardKind] ?? '●'}
|
||||||
</span>
|
</span>
|
||||||
{BOARD_SHORT_LABEL[board.boardKind]}
|
{BOARD_SHORT_LABEL[board.boardKind] ?? board.boardKind}
|
||||||
{hasUnread && !isActive && <span style={styles.unreadDot} />}
|
{hasUnread && !isActive && <span style={styles.unreadDot} />}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|
@ -149,7 +189,10 @@ export const SerialMonitor: React.FC = () => {
|
||||||
|
|
||||||
{/* Right-side controls */}
|
{/* Right-side controls */}
|
||||||
<div style={styles.tabControls}>
|
<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>
|
<span style={styles.baudRate}>{activeBoard.serialBaudRate.toLocaleString()} baud</span>
|
||||||
)}
|
)}
|
||||||
<label style={styles.autoscrollLabel}>
|
<label style={styles.autoscrollLabel}>
|
||||||
|
|
@ -184,7 +227,7 @@ export const SerialMonitor: React.FC = () => {
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder="Type message to send..."
|
placeholder={isMicroPython ? 'Type Python expression... (Ctrl+C to interrupt)' : 'Type message to send...'}
|
||||||
style={styles.input}
|
style={styles.input}
|
||||||
disabled={!activeBoard?.running}
|
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
|
* Map any ESP32-family board kind to the 3 base QEMU machine types understood
|
||||||
* by the backend esp_qemu_manager.
|
* 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-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';
|
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
|
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 _pendingFirmware: string | null = null;
|
||||||
private _pendingSensors: Array<Record<string, unknown>> = [];
|
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) {
|
constructor(boardId: string, boardKind: BoardKind) {
|
||||||
this.boardId = boardId;
|
this.boardId = boardId;
|
||||||
this.boardKind = boardKind;
|
this.boardKind = boardKind;
|
||||||
|
|
@ -134,6 +139,19 @@ export class Esp32Bridge {
|
||||||
if (this.onSerialData) {
|
if (this.onSerialData) {
|
||||||
for (const ch of text) this.onSerialData(ch, uart);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case 'gpio_change': {
|
case 'gpio_change': {
|
||||||
|
|
@ -283,6 +301,68 @@ export class Esp32Bridge {
|
||||||
this._send({ type: 'esp32_sensor_detach', data: { pin } });
|
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 {
|
private _send(payload: unknown): void {
|
||||||
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
if (this.socket && this.socket.readyState === WebSocket.OPEN) {
|
||||||
this.socket.send(JSON.stringify(payload));
|
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 type { RPI2C } from 'rp2040js';
|
||||||
import { PinManager } from './PinManager';
|
import { PinManager } from './PinManager';
|
||||||
import { bootromB1 } from './rp2040-bootrom';
|
import { bootromB1 } from './rp2040-bootrom';
|
||||||
|
import { loadUF2, loadUserFiles, getFirmware } from './MicroPythonLoader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
* RP2040Simulator — Emulates Raspberry Pi Pico (RP2040) using rp2040js
|
||||||
|
|
@ -52,8 +53,10 @@ export class RP2040Simulator {
|
||||||
private totalCycles = 0;
|
private totalCycles = 0;
|
||||||
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = [];
|
||||||
private pioStepAccum = 0;
|
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;
|
public onSerialData: ((char: string) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -97,6 +100,88 @@ export class RP2040Simulator {
|
||||||
console.warn('[RP2040] loadHex() called on RP2040Simulator — use loadBinary() instead');
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
getADC(): any {
|
getADC(): any {
|
||||||
return this.rp2040?.adc ?? null;
|
return this.rp2040?.adc ?? null;
|
||||||
|
|
@ -359,9 +444,46 @@ export class RP2040Simulator {
|
||||||
this.totalCycles = 0;
|
this.totalCycles = 0;
|
||||||
this.scheduledPinChanges = [];
|
this.scheduledPinChanges = [];
|
||||||
if (this.rp2040 && this.flashCopy) {
|
if (this.rp2040 && this.flashCopy) {
|
||||||
this.initMCU(this.flashCopy);
|
if (this.micropythonMode) {
|
||||||
// Re-register any previously added I2C devices
|
// In MicroPython mode, restore the full flash snapshot (UF2 + LittleFS)
|
||||||
// (devices are kept in i2cDevices maps which persist across reset)
|
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');
|
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 {
|
serialWrite(text: string): void {
|
||||||
if (!this.rp2040) return;
|
if (!this.rp2040) return;
|
||||||
for (let i = 0; i < text.length; i++) {
|
if (this.micropythonMode && this.usbCDC) {
|
||||||
this.rp2040.uart[0].feedByte(text.charCodeAt(i));
|
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);
|
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
|
const DEFAULT_PY_CONTENT = `import RPi.GPIO as GPIO
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
@ -79,7 +101,7 @@ interface EditorState {
|
||||||
loadFiles: (files: { name: string; content: string }[]) => void;
|
loadFiles: (files: { name: string; content: string }[]) => void;
|
||||||
|
|
||||||
// File group management
|
// 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;
|
deleteFileGroup: (groupId: string) => void;
|
||||||
setActiveGroup: (groupId: string) => void;
|
setActiveGroup: (groupId: string) => void;
|
||||||
getGroupFiles: (groupId: string) => WorkspaceFile[];
|
getGroupFiles: (groupId: string) => WorkspaceFile[];
|
||||||
|
|
@ -263,10 +285,14 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||||
|
|
||||||
// ── File group management ─────────────────────────────────────────────────
|
// ── File group management ─────────────────────────────────────────────────
|
||||||
|
|
||||||
createFileGroup: (groupId: string, initialFiles?: { name: string; content: string }[]) => {
|
createFileGroup: (groupId: string, languageModeOrFiles?: string | { name: string; content: string }[]) => {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
if (s.fileGroups[groupId]) return s; // already exists
|
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[];
|
let files: WorkspaceFile[];
|
||||||
if (initialFiles && initialFiles.length > 0) {
|
if (initialFiles && initialFiles.length > 0) {
|
||||||
files = initialFiles.map((f, i) => ({
|
files = initialFiles.map((f, i) => ({
|
||||||
|
|
@ -276,15 +302,27 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||||
modified: false,
|
modified: false,
|
||||||
}));
|
}));
|
||||||
} else {
|
} 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 isPi = groupId.includes('raspberry-pi-3');
|
||||||
|
const isMicroPython = languageMode === 'micropython';
|
||||||
const mainId = `${groupId}-main`;
|
const mainId = `${groupId}-main`;
|
||||||
files = [{
|
let fileName: string;
|
||||||
id: mainId,
|
let content: string;
|
||||||
name: isPi ? 'script.py' : 'sketch.ino',
|
const isEsp32 = groupId.includes('esp32');
|
||||||
content: isPi ? DEFAULT_PY_CONTENT : DEFAULT_INO_CONTENT,
|
if (isMicroPython && isEsp32) {
|
||||||
modified: false,
|
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`;
|
const firstId = files[0]?.id ?? `${groupId}-main`;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import { PinManager } from '../simulation/PinManager';
|
||||||
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
|
import { VirtualDS1307, VirtualTempSensor, I2CMemoryDevice } from '../simulation/I2CBusManager';
|
||||||
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
import type { RP2040I2CDevice } from '../simulation/RP2040Simulator';
|
||||||
import type { Wire, WireInProgress, WireEndpoint } from '../types/wire';
|
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 { calculatePinPosition } from '../utils/pinPositionCalculator';
|
||||||
import { useOscilloscopeStore } from './useOscilloscopeStore';
|
import { useOscilloscopeStore } from './useOscilloscopeStore';
|
||||||
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
|
import { RaspberryPi3Bridge } from '../simulation/RaspberryPi3Bridge';
|
||||||
|
|
@ -181,6 +182,8 @@ interface SimulatorState {
|
||||||
setBoardPosition: (pos: { x: number; y: number }, boardId?: string) => void;
|
setBoardPosition: (pos: { x: number; y: number }, boardId?: string) => void;
|
||||||
setActiveBoardId: (boardId: string) => void;
|
setActiveBoardId: (boardId: string) => void;
|
||||||
compileBoardProgram: (boardId: string, program: 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;
|
startBoard: (boardId: string) => void;
|
||||||
stopBoard: (boardId: string) => void;
|
stopBoard: (boardId: string) => void;
|
||||||
resetBoard: (boardId: string) => void;
|
resetBoard: (boardId: string) => void;
|
||||||
|
|
@ -298,6 +301,7 @@ const INITIAL_BOARD: BoardInstance = {
|
||||||
serialBaudRate: 0,
|
serialBaudRate: 0,
|
||||||
serialMonitorOpen: false,
|
serialMonitorOpen: false,
|
||||||
activeFileGroupId: `group-${INITIAL_BOARD_ID}`,
|
activeFileGroupId: `group-${INITIAL_BOARD_ID}`,
|
||||||
|
languageMode: 'arduino' as LanguageMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Store ─────────────────────────────────────────────────────────────────
|
// ── Store ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -459,6 +463,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
serialOutput: '', serialBaudRate: 0,
|
serialOutput: '', serialBaudRate: 0,
|
||||||
serialMonitorOpen: false,
|
serialMonitorOpen: false,
|
||||||
activeFileGroupId: `group-${id}`,
|
activeFileGroupId: `group-${id}`,
|
||||||
|
languageMode: 'arduino',
|
||||||
};
|
};
|
||||||
|
|
||||||
set((s) => ({ boards: [...s.boards, newBoard] }));
|
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) => {
|
startBoard: (boardId: string) => {
|
||||||
const board = get().boards.find((b) => b.id === boardId);
|
const board = get().boards.find((b) => b.id === boardId);
|
||||||
if (!board) return;
|
if (!board) return;
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,31 @@ export type BoardKind =
|
||||||
| 'esp32-s3' // Xtensa LX7, QEMU backend
|
| 'esp32-s3' // Xtensa LX7, QEMU backend
|
||||||
| 'xiao-esp32-s3' // Seeed XIAO ESP32-S3, QEMU (esp32-s3)
|
| 'xiao-esp32-s3' // Seeed XIAO ESP32-S3, QEMU (esp32-s3)
|
||||||
| 'arduino-nano-esp32' // Arduino Nano ESP32 (S3), QEMU (esp32-s3)
|
| 'arduino-nano-esp32' // Arduino Nano ESP32 (S3), QEMU (esp32-s3)
|
||||||
| 'esp32-c3' // RISC-V RV32IMC, browser emulation (Esp32C3Simulator)
|
| 'esp32-c3' // RISC-V RV32IMC, QEMU backend
|
||||||
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, browser emulation (Esp32C3Simulator)
|
| 'xiao-esp32-c3' // Seeed XIAO ESP32-C3, QEMU backend
|
||||||
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, browser emulation (Esp32C3Simulator)
|
| 'aitewinrobot-esp32c3-supermini' // ESP32-C3 SuperMini, QEMU backend
|
||||||
| 'attiny85'; // AVR ATtiny85, browser emulation (avr8js)
|
| '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 {
|
export interface BoardInstance {
|
||||||
id: string; // unique in canvas, e.g. 'arduino-uno', 'raspberry-pi-3'
|
id: string; // unique in canvas, e.g. 'arduino-uno', 'raspberry-pi-3'
|
||||||
boardKind: BoardKind;
|
boardKind: BoardKind;
|
||||||
|
|
@ -28,6 +48,7 @@ export interface BoardInstance {
|
||||||
serialBaudRate: number;
|
serialBaudRate: number;
|
||||||
serialMonitorOpen: boolean;
|
serialMonitorOpen: boolean;
|
||||||
activeFileGroupId: string;
|
activeFileGroupId: string;
|
||||||
|
languageMode: LanguageMode; // 'arduino' (default) or 'micropython'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
|
export const BOARD_KIND_LABELS: Record<BoardKind, string> = {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['avr8js', 'rp2040js', '@wokwi/elements'],
|
include: ['avr8js', 'rp2040js', '@wokwi/elements', 'littlefs'],
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue