feat: add MicroPython support for ESP32/ESP32-S3 boards via QEMU bridge
Extends MicroPython support (Phase 2) to ESP32 Xtensa boards running on QEMU. Firmware is downloaded from micropython.org, cached in IndexedDB, and loaded into QEMU. User code is injected via the raw-paste REPL protocol after the MicroPython REPL boots. - Create Esp32MicroPythonLoader.ts for firmware download/cache - Add raw-paste REPL injection (Ctrl+A → Ctrl+E → code → Ctrl+D) to Esp32Bridge - Extend loadMicroPythonProgram in store for ESP32 path - Add ESP32 default MicroPython content (GPIO 2 blink) - Simplify SerialMonitor Ctrl+C/D to work for all MicroPython boards - Bundle fallback firmware for ESP32 and ESP32-S3 - Add all ESP32 board variants to SerialMonitor tab maps Closes #3 (Phase 2) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>feature/micropython-rp2040
parent
990ae4be8c
commit
7c9fd0bf2b
Binary file not shown.
Binary file not shown.
|
|
@ -4,43 +4,69 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
||||||
import { useSimulatorStore, getBoardSimulator } from '../../store/useSimulatorStore';
|
import { useSimulatorStore } from '../../store/useSimulatorStore';
|
||||||
import { RP2040Simulator } from '../../simulation/RP2040Simulator';
|
|
||||||
import type { BoardKind } from '../../types/board';
|
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-devkit-c-v4': 'ESP32',
|
||||||
|
'esp32-cam': 'ESP32-CAM',
|
||||||
|
'wemos-lolin32-lite':'Lolin32',
|
||||||
'esp32-s3': 'ESP32-S3',
|
'esp32-s3': 'ESP32-S3',
|
||||||
|
'xiao-esp32-s3': 'XIAO-S3',
|
||||||
|
'arduino-nano-esp32':'Nano ESP32',
|
||||||
'esp32-c3': 'ESP32-C3',
|
'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-devkit-c-v4': '⬡',
|
||||||
|
'esp32-cam': '⬡',
|
||||||
|
'wemos-lolin32-lite':'⬡',
|
||||||
'esp32-s3': '⬡',
|
'esp32-s3': '⬡',
|
||||||
|
'xiao-esp32-s3': '⬡',
|
||||||
|
'arduino-nano-esp32':'⬡',
|
||||||
'esp32-c3': '⬡',
|
'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-devkit-c-v4': '#a5d6a7',
|
||||||
|
'esp32-cam': '#a5d6a7',
|
||||||
|
'wemos-lolin32-lite':'#a5d6a7',
|
||||||
'esp32-s3': '#a5d6a7',
|
'esp32-s3': '#a5d6a7',
|
||||||
|
'xiao-esp32-s3': '#a5d6a7',
|
||||||
|
'arduino-nano-esp32':'#a5d6a7',
|
||||||
'esp32-c3': '#a5d6a7',
|
'esp32-c3': '#a5d6a7',
|
||||||
|
'xiao-esp32-c3': '#a5d6a7',
|
||||||
|
'aitewinrobot-esp32c3-supermini': '#a5d6a7',
|
||||||
|
'attiny85': '#ffcc80',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SerialMonitor: React.FC = () => {
|
export const SerialMonitor: React.FC = () => {
|
||||||
|
|
@ -103,17 +129,14 @@ export const SerialMonitor: React.FC = () => {
|
||||||
handleSend();
|
handleSend();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// MicroPython REPL control characters
|
// MicroPython REPL control characters (works for RP2040 and ESP32)
|
||||||
if (resolvedTabId && e.ctrlKey) {
|
if (resolvedTabId && isMicroPython && e.ctrlKey) {
|
||||||
const sim = getBoardSimulator(resolvedTabId);
|
|
||||||
if (sim instanceof RP2040Simulator && sim.isMicroPythonMode()) {
|
|
||||||
if (e.key === 'c' || e.key === 'C') {
|
if (e.key === 'c' || e.key === 'C') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sim.serialWriteByte(0x03); // Ctrl+C — keyboard interrupt
|
serialWriteToBoard(resolvedTabId, '\x03'); // Ctrl+C — keyboard interrupt
|
||||||
} else if (e.key === 'd' || e.key === 'D') {
|
} else if (e.key === 'd' || e.key === 'D') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
sim.serialWriteByte(0x04); // Ctrl+D — soft reset
|
serialWriteToBoard(resolvedTabId, '\x04'); // Ctrl+D — soft reset
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [handleSend, resolvedTabId]);
|
}, [handleSend, resolvedTabId]);
|
||||||
|
|
@ -143,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
|
||||||
|
|
@ -156,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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,119 @@
|
||||||
|
/**
|
||||||
|
* 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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Map any ESP32-family board kind to firmware variant key */
|
||||||
|
function toFirmwareVariant(boardKind: BoardKind): 'esp32' | 'esp32-s3' {
|
||||||
|
if (boardKind === 'esp32-s3' || boardKind === 'xiao-esp32-s3' || boardKind === 'arduino-nano-esp32') {
|
||||||
|
return 'esp32-s3';
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
@ -32,6 +32,17 @@ while True:
|
||||||
time.sleep(1)
|
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
|
||||||
|
|
||||||
|
|
@ -297,7 +308,11 @@ export const useEditorStore = create<EditorState>((set, get) => ({
|
||||||
const mainId = `${groupId}-main`;
|
const mainId = `${groupId}-main`;
|
||||||
let fileName: string;
|
let fileName: string;
|
||||||
let content: string;
|
let content: string;
|
||||||
if (isMicroPython) {
|
const isEsp32 = groupId.includes('esp32');
|
||||||
|
if (isMicroPython && isEsp32) {
|
||||||
|
fileName = 'main.py';
|
||||||
|
content = DEFAULT_ESP32_MICROPYTHON_CONTENT;
|
||||||
|
} else if (isMicroPython) {
|
||||||
fileName = 'main.py';
|
fileName = 'main.py';
|
||||||
content = DEFAULT_MICROPYTHON_CONTENT;
|
content = DEFAULT_MICROPYTHON_CONTENT;
|
||||||
} else if (isPi) {
|
} else if (isPi) {
|
||||||
|
|
|
||||||
|
|
@ -587,10 +587,27 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
|
||||||
if (!board) return;
|
if (!board) return;
|
||||||
if (!BOARD_SUPPORTS_MICROPYTHON.has(board.boardKind)) 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);
|
const sim = getBoardSimulator(boardId);
|
||||||
if (!(sim instanceof RP2040Simulator)) return;
|
if (!(sim instanceof RP2040Simulator)) return;
|
||||||
|
|
||||||
await sim.loadMicroPython(files);
|
await sim.loadMicroPython(files);
|
||||||
|
}
|
||||||
|
|
||||||
set((s) => {
|
set((s) => {
|
||||||
const boards = s.boards.map((b) =>
|
const boards = s.boards.map((b) =>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,15 @@ export type LanguageMode = 'arduino' | 'micropython';
|
||||||
export const BOARD_SUPPORTS_MICROPYTHON = new Set<BoardKind>([
|
export const BOARD_SUPPORTS_MICROPYTHON = new Set<BoardKind>([
|
||||||
'raspberry-pi-pico',
|
'raspberry-pi-pico',
|
||||||
'pi-pico-w',
|
'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',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface BoardInstance {
|
export interface BoardInstance {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue