120 lines
3.8 KiB
TypeScript
120 lines
3.8 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|