From 5a7cf002f5016aceac8fd28d9d5aa0f6bcf80fa0 Mon Sep 17 00:00:00 2001 From: Jake Date: Sun, 19 Apr 2026 19:28:00 +0800 Subject: [PATCH] tweaks to esp32 flashing --- .gitignore | 1 + features.md | 25 ++++++++++++ index.html | 22 +++++++++++ readme.md | 1 + src/devices/microbit.js | 7 ++-- src/main.js | 78 +++++++++++++++++++++++++++++++++++++- src/serial/driveFlasher.js | 55 +++++++++++++++++++++++++++ src/serial/picoFlasher.js | 59 ++++------------------------ src/style.css | 6 ++- 9 files changed, 196 insertions(+), 58 deletions(-) create mode 100644 features.md create mode 100644 readme.md create mode 100644 src/serial/driveFlasher.js diff --git a/.gitignore b/.gitignore index 597b97a..f3d326c 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ dist/ # IDE .vscode/ .idea/ +.cursor/ *.swp *.swo *~ diff --git a/features.md b/features.md new file mode 100644 index 0000000..354cc50 --- /dev/null +++ b/features.md @@ -0,0 +1,25 @@ +# Teach Real Hardware, Faster + +`esp32block` helps educators take students from drag-and-drop blocks to real embedded systems coding in minutes. It is classroom-friendly, browser-based, and built for rapid iteration across popular microcontroller boards. + +## Features + +- Block-based coding environment with instant generated MicroPython/Arduino code +- One-click firmware workflows for ESP32 families and RP2040/micro:bit bootloader drives +- Multi-board support: ESP32 variants, RP2040, micro:bit, Arduino Uno/Nano, and more +- Built-in serial monitor and terminal for live debugging in class +- Save, load, and manage projects directly in the app +- Toolbox customization to simplify lessons for different grade levels +- Robot hardware panel to map components and apply starter setup blocks +- Browser-first workflow: no heavy IDE installs for each student machine + +## Suggested Next Features + +- Classroom mode with student device roster and connection health status +- Guided lesson templates with prebuilt block sets and teacher notes +- Assignment mode with lockable toolboxes and step-by-step checkpoints +- Live code broadcast from teacher device to all student workspaces +- Auto-grading checks for block logic and generated code patterns +- Built-in simulator mode for lessons before hardware is connected +- Per-student progress tracking and printable assessment summaries +- Offline classroom package for unreliable school network environments diff --git a/index.html b/index.html index 08ac169..dd4b622 100644 --- a/index.html +++ b/index.html @@ -262,6 +262,28 @@ + + + diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..b6c2444 --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +Realrobots.net blockly microcontroller IDE \ No newline at end of file diff --git a/src/devices/microbit.js b/src/devices/microbit.js index 98cb060..3bae112 100644 --- a/src/devices/microbit.js +++ b/src/devices/microbit.js @@ -15,10 +15,11 @@ export const microbit = { id: 'microbit', label: 'micro:bit', firmware: { - label: 'MicroPython (micro:bit v2)', - url: 'https://micropython.org/resources/firmware/MICROBIT_V2.hex', + label: 'MicroPython (micro:bit)', + url: import.meta.env.BASE_URL + 'firmware/micropython-microbit-v2.1.1.hex', canFlashInBrowser: false, - instructions: 'Hold RESET, then drag the .hex file onto the MICROBIT drive.', + flashMethod: 'microbit', + instructions: 'Hold RESET while plugging in, then select the MICROBIT drive when prompted.', }, categories: [ pinIoCategory, diff --git a/src/main.js b/src/main.js index fd22e9b..5e5467c 100644 --- a/src/main.js +++ b/src/main.js @@ -25,6 +25,7 @@ import { connect, disconnect, isConnected, getPort, onData, writeString } from ' import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js'; import { flashFirmware } from './serial/flasher.js'; import { flashPicoFirmware } from './serial/picoFlasher.js'; +import { flashFileToDrive } from './serial/driveFlasher.js'; import { appendToTerminal, clearTerminal } from './ui/terminal.js'; import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js'; import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js'; @@ -241,7 +242,8 @@ function updateDeviceUI() { btnConnect.title = 'Connect to device via Web Serial'; btnFlash.classList.remove('hidden'); - btnFlash.title = canFlashInBrowser() + const flashMethod = getDevice()?.firmware?.flashMethod; + btnFlash.title = (canFlashInBrowser() || flashMethod === 'webusb' || flashMethod === 'microbit') ? 'Flash MicroPython firmware' : 'Download firmware (drag to device)'; @@ -394,6 +396,11 @@ const esp32FlashClose = document.getElementById('esp32-flash-close'); const esp32FlashStart = document.getElementById('esp32-flash-start'); const esp32FlashVariant = document.getElementById('esp32-variant-select'); const esp32FlashStatus = document.getElementById('esp32-flash-status'); +const microbitFlashOverlay = document.getElementById('microbit-flash-overlay'); +const microbitFlashClose = document.getElementById('microbit-flash-close'); +const microbitFlashStart = document.getElementById('microbit-flash-start'); +const microbitFlashVariant = document.getElementById('microbit-variant-select'); +const microbitFlashStatus = document.getElementById('microbit-flash-status'); const FW_BASE = import.meta.env.BASE_URL + 'firmware/'; @@ -421,6 +428,17 @@ const ESP32_FIRMWARE_OPTIONS = { }, }; +const MICROBIT_FIRMWARE_OPTIONS = { + v1: { + label: 'micro:bit v1', + url: FW_BASE + 'micropython-microbit-v1.1.1.hex', + }, + v2: { + label: 'micro:bit v2', + url: FW_BASE + 'micropython-microbit-v2.1.1.hex', + }, +}; + function showFlashOverlay() { flashLog.textContent = ''; flashFill.style.width = '0%'; @@ -457,11 +475,29 @@ function closeEsp32FlashChooser() { esp32FlashOverlay.classList.add('hidden'); } +function openMicrobitFlashChooser() { + if (!microbitFlashOverlay) return; + if (microbitFlashStatus) { + microbitFlashStatus.textContent = ''; + microbitFlashStatus.className = 'hex-upload-status'; + } + microbitFlashOverlay.classList.remove('hidden'); +} + +function closeMicrobitFlashChooser() { + if (!microbitFlashOverlay) return; + microbitFlashOverlay.classList.add('hidden'); +} + esp32FlashClose?.addEventListener('click', closeEsp32FlashChooser); esp32FlashOverlay?.addEventListener('click', (event) => { if (event.target === esp32FlashOverlay) closeEsp32FlashChooser(); }); +microbitFlashClose?.addEventListener('click', closeMicrobitFlashChooser); +microbitFlashOverlay?.addEventListener('click', (event) => { + if (event.target === microbitFlashOverlay) closeMicrobitFlashChooser(); +}); btnFlash.addEventListener('click', async () => { if (isArduinoDevice()) return; @@ -492,6 +528,11 @@ btnFlash.addEventListener('click', async () => { return; } + if (fw.flashMethod === 'microbit') { + openMicrobitFlashChooser(); + return; + } + if (canFlashInBrowser()) { openEsp32FlashChooser(); return; @@ -518,6 +559,41 @@ btnFlash.addEventListener('click', async () => { } }); +microbitFlashStart?.addEventListener('click', async () => { + const variantKey = microbitFlashVariant?.value || 'v2'; + const variant = MICROBIT_FIRMWARE_OPTIONS[variantKey]; + if (!variant) { + if (microbitFlashStatus) { + microbitFlashStatus.textContent = 'Invalid micro:bit version selected.'; + microbitFlashStatus.className = 'hex-upload-status status-err'; + } + return; + } + + closeMicrobitFlashChooser(); + showFlashOverlay(); + appendFlashLog(`Selected target: ${variant.label}\n`); + appendFlashLog('Put your micro:bit in bootloader mode if needed, then select the MICROBIT drive.\n'); + + try { + await flashFileToDrive( + (msg) => appendFlashLog(msg), + (pct) => setFlashProgress(pct), + { + firmwareUrl: variant.url, + outputName: 'firmware.hex', + driveHint: 'MICROBIT drive', + }, + ); + setFlashProgress(100); + appendFlashLog('\nFlash complete! You can now Connect to use the device.\n'); + } catch (err) { + appendFlashLog(`\nFlash error: ${err.message}\n`); + } finally { + flashCloseBtn.classList.remove('hidden'); + } +}); + esp32FlashStart?.addEventListener('click', async () => { if (isConnected()) { await disconnect(); diff --git a/src/serial/driveFlasher.js b/src/serial/driveFlasher.js new file mode 100644 index 0000000..c5a9589 --- /dev/null +++ b/src/serial/driveFlasher.js @@ -0,0 +1,55 @@ +/** + * Write a firmware file directly to a removable mass-storage drive + * selected by the user (e.g. MICROBIT or RPI-RP2). + */ +export async function flashFileToDrive(onLog, onProgress, options = {}) { + const { firmwareUrl, outputName = 'firmware.bin', driveHint = 'device drive' } = options; + if (!firmwareUrl) throw new Error('No firmware URL provided'); + + onLog?.('Fetching firmware...\n'); + const resp = await fetch(firmwareUrl); + if (!resp.ok) throw new Error(`Firmware fetch failed: ${resp.status}`); + const fileData = new Uint8Array(await resp.arrayBuffer()); + onLog?.(`Firmware: ${(fileData.length / 1024).toFixed(0)} KB\n\n`); + + onLog?.(`Select the ${driveHint} in the folder picker.\n\n`); + + let dirHandle; + try { + dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' }); + } catch (err) { + if (err?.name === 'AbortError') throw new Error('Cancelled - no folder selected'); + throw err; + } + + onLog?.(`Writing ${outputName} to ${dirHandle.name}...\n`); + const fileHandle = await dirHandle.getFileHandle(outputName, { create: true }); + const writable = await fileHandle.createWritable(); + + const CHUNK_SIZE = 64 * 1024; + let written = 0; + try { + while (written < fileData.length) { + const end = Math.min(written + CHUNK_SIZE, fileData.length); + await writable.write({ + type: 'write', + position: written, + data: fileData.slice(written, end), + }); + written = end; + onProgress?.(Math.round((written / fileData.length) * 95)); + } + await writable.close(); + } catch (err) { + // Some bootloaders unmount as soon as the file is fully written. + if (written >= fileData.length) { + onLog?.('(Drive disconnected - device is rebooting with new firmware)\n'); + } else { + throw err; + } + } + + onProgress?.(100); + onLog?.('Done!\n'); +} + diff --git a/src/serial/picoFlasher.js b/src/serial/picoFlasher.js index d2476b9..0408404 100644 --- a/src/serial/picoFlasher.js +++ b/src/serial/picoFlasher.js @@ -1,55 +1,10 @@ -/** - * Flash RP2040/RP2350 firmware by writing a UF2 file to the BOOTSEL - * mass-storage drive via the File System Access API. - * - * The device must be in BOOTSEL mode (hold BOOTSEL while plugging in). - * The user selects the RPI-RP2 drive in the browser's folder picker, - * and the UF2 bootloader handles the rest automatically. - */ +import { flashFileToDrive } from './driveFlasher.js'; export async function flashPicoFirmware(onLog, onProgress, options = {}) { - const { firmwareUrl } = options; - if (!firmwareUrl) throw new Error('No firmware URL provided'); - - onLog?.('Fetching firmware...\n'); - const resp = await fetch(firmwareUrl); - if (!resp.ok) throw new Error(`Firmware fetch failed: ${resp.status}`); - const uf2Data = new Uint8Array(await resp.arrayBuffer()); - onLog?.(`Firmware: ${(uf2Data.length / 1024).toFixed(0)} KB\n\n`); - - onLog?.('Select the RPI-RP2 drive in the folder picker.\n'); - onLog?.('(Hold BOOTSEL while plugging in if the drive isn\'t visible.)\n\n'); - - let dirHandle; - try { - dirHandle = await window.showDirectoryPicker({ id: 'pico-flash', mode: 'readwrite' }); - } catch (err) { - if (err.name === 'AbortError') throw new Error('Cancelled — no folder selected'); - throw err; - } - - onLog?.(`Writing firmware.uf2 to ${dirHandle.name} ...\n`); - const fileHandle = await dirHandle.getFileHandle('firmware.uf2', { create: true }); - const writable = await fileHandle.createWritable(); - - const CHUNK = 64 * 1024; - let written = 0; - try { - while (written < uf2Data.length) { - const end = Math.min(written + CHUNK, uf2Data.length); - await writable.write({ type: 'write', position: written, data: uf2Data.slice(written, end) }); - written = end; - onProgress?.(Math.round((written / uf2Data.length) * 95)); - } - await writable.close(); - } catch (err) { - if (written > 0 && written >= uf2Data.length) { - onLog?.('(Drive disconnected — Pico is rebooting with new firmware)\n'); - } else { - throw err; - } - } - - onProgress?.(100); - onLog?.('Done! The Pico will reboot automatically with MicroPython.\n'); + await flashFileToDrive(onLog, onProgress, { + firmwareUrl: options.firmwareUrl, + outputName: 'firmware.uf2', + driveHint: 'RPI-RP2 drive', + }); + onLog?.('The Pico will reboot automatically with MicroPython.\n'); } diff --git a/src/style.css b/src/style.css index dfbb7c7..d007881 100644 --- a/src/style.css +++ b/src/style.css @@ -1287,7 +1287,8 @@ html, body { /* --- Hex Upload Overlay (Arduino) --- */ #hex-upload-overlay, -#esp32-flash-overlay { +#esp32-flash-overlay, +#microbit-flash-overlay { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); @@ -1299,7 +1300,8 @@ html, body { } #hex-upload-modal, -#esp32-flash-modal { +#esp32-flash-modal, +#microbit-flash-modal { background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px;