tweaks to esp32 flashing

main
Jake 2026-04-19 19:28:00 +08:00
parent 63db4556c5
commit 5a7cf002f5
9 changed files with 196 additions and 58 deletions

1
.gitignore vendored
View File

@ -9,6 +9,7 @@ dist/
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/
.cursor/
*.swp *.swp
*.swo *.swo
*~ *~

25
features.md Normal file
View File

@ -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

View File

@ -262,6 +262,28 @@
</div> </div>
</div> </div>
<!-- micro:bit firmware board picker overlay -->
<div id="microbit-flash-overlay" class="hidden">
<div id="microbit-flash-modal">
<div class="board-select-header">
<h3>Flash micro:bit MicroPython</h3>
<button id="microbit-flash-close" title="Close">&times;</button>
</div>
<p class="board-select-description">Choose your micro:bit hardware version, then continue to flash firmware.</p>
<div class="hex-upload-fields">
<label class="hex-field-label" for="microbit-variant-select">Board version</label>
<select id="microbit-variant-select">
<option value="v2">micro:bit v2</option>
<option value="v1">micro:bit v1</option>
</select>
</div>
<div class="board-select-actions">
<button id="microbit-flash-start">Flash</button>
</div>
<div id="microbit-flash-status" class="hex-upload-status"></div>
</div>
</div>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

1
readme.md Normal file
View File

@ -0,0 +1 @@
Realrobots.net blockly microcontroller IDE

View File

@ -15,10 +15,11 @@ export const microbit = {
id: 'microbit', id: 'microbit',
label: 'micro:bit', label: 'micro:bit',
firmware: { firmware: {
label: 'MicroPython (micro:bit v2)', label: 'MicroPython (micro:bit)',
url: 'https://micropython.org/resources/firmware/MICROBIT_V2.hex', url: import.meta.env.BASE_URL + 'firmware/micropython-microbit-v2.1.1.hex',
canFlashInBrowser: false, 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: [ categories: [
pinIoCategory, pinIoCategory,

View File

@ -25,6 +25,7 @@ import { connect, disconnect, isConnected, getPort, onData, writeString } from '
import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js'; import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './serial/repl.js';
import { flashFirmware } from './serial/flasher.js'; import { flashFirmware } from './serial/flasher.js';
import { flashPicoFirmware } from './serial/picoFlasher.js'; import { flashPicoFirmware } from './serial/picoFlasher.js';
import { flashFileToDrive } from './serial/driveFlasher.js';
import { appendToTerminal, clearTerminal } from './ui/terminal.js'; import { appendToTerminal, clearTerminal } from './ui/terminal.js';
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js'; import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js'; import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js';
@ -241,7 +242,8 @@ function updateDeviceUI() {
btnConnect.title = 'Connect to device via Web Serial'; btnConnect.title = 'Connect to device via Web Serial';
btnFlash.classList.remove('hidden'); btnFlash.classList.remove('hidden');
btnFlash.title = canFlashInBrowser() const flashMethod = getDevice()?.firmware?.flashMethod;
btnFlash.title = (canFlashInBrowser() || flashMethod === 'webusb' || flashMethod === 'microbit')
? 'Flash MicroPython firmware' ? 'Flash MicroPython firmware'
: 'Download firmware (drag to device)'; : 'Download firmware (drag to device)';
@ -394,6 +396,11 @@ const esp32FlashClose = document.getElementById('esp32-flash-close');
const esp32FlashStart = document.getElementById('esp32-flash-start'); const esp32FlashStart = document.getElementById('esp32-flash-start');
const esp32FlashVariant = document.getElementById('esp32-variant-select'); const esp32FlashVariant = document.getElementById('esp32-variant-select');
const esp32FlashStatus = document.getElementById('esp32-flash-status'); 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/'; 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() { function showFlashOverlay() {
flashLog.textContent = ''; flashLog.textContent = '';
flashFill.style.width = '0%'; flashFill.style.width = '0%';
@ -457,11 +475,29 @@ function closeEsp32FlashChooser() {
esp32FlashOverlay.classList.add('hidden'); 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); esp32FlashClose?.addEventListener('click', closeEsp32FlashChooser);
esp32FlashOverlay?.addEventListener('click', (event) => { esp32FlashOverlay?.addEventListener('click', (event) => {
if (event.target === esp32FlashOverlay) closeEsp32FlashChooser(); if (event.target === esp32FlashOverlay) closeEsp32FlashChooser();
}); });
microbitFlashClose?.addEventListener('click', closeMicrobitFlashChooser);
microbitFlashOverlay?.addEventListener('click', (event) => {
if (event.target === microbitFlashOverlay) closeMicrobitFlashChooser();
});
btnFlash.addEventListener('click', async () => { btnFlash.addEventListener('click', async () => {
if (isArduinoDevice()) return; if (isArduinoDevice()) return;
@ -492,6 +528,11 @@ btnFlash.addEventListener('click', async () => {
return; return;
} }
if (fw.flashMethod === 'microbit') {
openMicrobitFlashChooser();
return;
}
if (canFlashInBrowser()) { if (canFlashInBrowser()) {
openEsp32FlashChooser(); openEsp32FlashChooser();
return; 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 () => { esp32FlashStart?.addEventListener('click', async () => {
if (isConnected()) { if (isConnected()) {
await disconnect(); await disconnect();

View File

@ -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');
}

View File

@ -1,55 +1,10 @@
/** import { flashFileToDrive } from './driveFlasher.js';
* 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.
*/
export async function flashPicoFirmware(onLog, onProgress, options = {}) { export async function flashPicoFirmware(onLog, onProgress, options = {}) {
const { firmwareUrl } = options; await flashFileToDrive(onLog, onProgress, {
if (!firmwareUrl) throw new Error('No firmware URL provided'); firmwareUrl: options.firmwareUrl,
outputName: 'firmware.uf2',
onLog?.('Fetching firmware...\n'); driveHint: 'RPI-RP2 drive',
const resp = await fetch(firmwareUrl); });
if (!resp.ok) throw new Error(`Firmware fetch failed: ${resp.status}`); onLog?.('The Pico will reboot automatically with MicroPython.\n');
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');
} }

View File

@ -1287,7 +1287,8 @@ html, body {
/* --- Hex Upload Overlay (Arduino) --- */ /* --- Hex Upload Overlay (Arduino) --- */
#hex-upload-overlay, #hex-upload-overlay,
#esp32-flash-overlay { #esp32-flash-overlay,
#microbit-flash-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.7);
@ -1299,7 +1300,8 @@ html, body {
} }
#hex-upload-modal, #hex-upload-modal,
#esp32-flash-modal { #esp32-flash-modal,
#microbit-flash-modal {
background: var(--bg-surface); background: var(--bg-surface);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: 12px; border-radius: 12px;