tweaks to esp32 flashing
parent
63db4556c5
commit
5a7cf002f5
|
|
@ -9,6 +9,7 @@ dist/
|
|||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
.cursor/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
|
|
|||
|
|
@ -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
|
||||
22
index.html
22
index.html
|
|
@ -262,6 +262,28 @@
|
|||
</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">×</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>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
78
src/main.js
78
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();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue