implemented firmware flashing for esp32s3

main
Jake 2026-02-18 23:39:05 +08:00
parent 2c00738b07
commit fd6d061d36
5 changed files with 248 additions and 45 deletions

View File

@ -16,7 +16,7 @@
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
<span class="icon">&#9654;</span> Connect
</button>
<button id="btn-flash" title="Flash MicroPython firmware" disabled>
<button id="btn-flash" title="Flash MicroPython firmware">
<span class="icon">&#9889;</span> Flash FW
</button>
<button id="btn-run" title="Upload and run code" disabled>
@ -56,6 +56,19 @@
</div>
</main>
<!-- Flash progress overlay -->
<div id="flash-overlay" class="hidden">
<div id="flash-modal">
<h3>Flashing MicroPython Firmware</h3>
<div id="flash-log"></div>
<div id="flash-progress-bar">
<div id="flash-progress-fill"></div>
</div>
<span id="flash-progress-text">0%</span>
<button id="flash-close" class="hidden">Close</button>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

Binary file not shown.

View File

@ -91,7 +91,6 @@ const terminalInput = document.getElementById('terminal-input');
function setConnectedUI(connected) {
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
btnFlash.disabled = !connected;
btnRun.disabled = !connected;
btnStop.disabled = !connected;
btnSave.disabled = !connected;
@ -122,17 +121,53 @@ btnConnect.addEventListener('click', async () => {
}
});
const flashOverlay = document.getElementById('flash-overlay');
const flashLog = document.getElementById('flash-log');
const flashFill = document.getElementById('flash-progress-fill');
const flashPctText = document.getElementById('flash-progress-text');
const flashCloseBtn = document.getElementById('flash-close');
function showFlashOverlay() {
flashLog.textContent = '';
flashFill.style.width = '0%';
flashPctText.textContent = '0%';
flashCloseBtn.classList.add('hidden');
flashOverlay.classList.remove('hidden');
}
function appendFlashLog(msg) {
flashLog.textContent += msg;
flashLog.scrollTop = flashLog.scrollHeight;
}
function setFlashProgress(pct) {
flashFill.style.width = pct + '%';
flashPctText.textContent = pct + '%';
}
flashCloseBtn.addEventListener('click', () => {
flashOverlay.classList.add('hidden');
});
btnFlash.addEventListener('click', async () => {
try {
clearTerminal();
appendToTerminal('Starting firmware flash...\n');
if (isConnected()) {
await disconnect();
const port = await navigator.serial.requestPort();
await flashFirmware(port, (msg) => appendToTerminal(msg + '\n'));
appendToTerminal('Flash complete! Reconnect to use the device.\n');
setConnectedUI(false);
}
showFlashOverlay();
try {
await flashFirmware(
(msg) => appendFlashLog(msg),
(pct) => setFlashProgress(pct),
);
setFlashProgress(100);
appendFlashLog('\nFlash complete! You can now Connect to use the device.\n');
} catch (err) {
appendToTerminal(`\nFlash error: ${err.message}\n`);
appendFlashLog(`\nFlash error: ${err.message}\n`);
} finally {
flashCloseBtn.classList.remove('hidden');
}
});

View File

@ -1,51 +1,120 @@
// esptool-js firmware flasher wrapper
// Actual flashing will use the esptool-js ESPLoader when the user triggers it.
import { ESPLoader, Transport } from 'esptool-js';
export async function flashFirmware(port, onProgress) {
const { ESPLoader, Transport } = await import('esptool-js');
const FIRMWARE_URL = '/firmware/ESP32_GENERIC_S3-20251209-v1.27.0.bin';
function arrayBufferToBinaryString(buffer) {
const bytes = new Uint8Array(buffer);
const chunks = [];
const chunkSize = 8192;
for (let i = 0; i < bytes.length; i += chunkSize) {
const slice = bytes.subarray(i, i + chunkSize);
chunks.push(String.fromCharCode.apply(null, slice));
}
return chunks.join('');
}
export async function flashFirmware(onLog, onProgress) {
const port = await navigator.serial.requestPort();
// Buffer to track message state for proper newline insertion
let messageBuffer = '';
function normalizeAndLog(data) {
messageBuffer += data;
// Detect patterns that indicate message boundaries and insert newlines
// Pattern 1: "Writing at" should always start on a new line (unless already at start/newline)
messageBuffer = messageBuffer.replace(/([^\n])(Writing at)/g, '$1\n$2');
// Pattern 2: Messages ending with "%)" followed by "Writing at" need a newline
messageBuffer = messageBuffer.replace(/(%\))(Writing at)/g, '$1\n$2');
// Pattern 3: Messages ending with "." or "..." followed by capital letters (like "Leaving...")
messageBuffer = messageBuffer.replace(/(\.\.?)([A-Z])/g, '$1\n$2');
// Pattern 4: "Wrote" messages should be on their own line
messageBuffer = messageBuffer.replace(/([^\n])(Wrote \d+ bytes)/g, '$1\n$2');
// Split on newlines and process complete lines
const lines = messageBuffer.split('\n');
// Keep the last potentially incomplete line in buffer
messageBuffer = lines.pop() || '';
// Log all complete lines
for (const line of lines) {
if (line.trim()) {
onLog?.(line + '\n');
}
}
}
function flushBuffer() {
if (messageBuffer.trim()) {
onLog?.(messageBuffer + '\n');
messageBuffer = '';
}
}
const transport = new Transport(port);
const loader = new ESPLoader({
const esploader = new ESPLoader({
transport,
baudrate: 115200,
romBaudrate: 115200,
terminal: {
clean() {},
writeLine(data) { onProgress?.(data); },
write(data) { onProgress?.(data); },
writeLine(data) {
// Flush any buffered content first
flushBuffer();
onLog?.(data + (data.endsWith('\n') ? '' : '\n'));
},
write(data) {
normalizeAndLog(data);
},
},
});
await loader.main();
await loader.eraseFlash();
onLog?.('Connecting to ESP32-S3...\n');
const chip = await esploader.main();
flushBuffer();
onLog?.(`Detected: ${chip}\n`);
const input = document.createElement('input');
input.type = 'file';
input.accept = '.bin';
onLog?.('Fetching firmware...\n');
const resp = await fetch(FIRMWARE_URL);
if (!resp.ok) throw new Error(`Failed to fetch firmware: ${resp.status}`);
const firmwareBuf = await resp.arrayBuffer();
const firmwareBin = arrayBufferToBinaryString(firmwareBuf);
onLog?.(`Firmware size: ${(firmwareBuf.byteLength / 1024).toFixed(0)} KB\n`);
return new Promise((resolve, reject) => {
input.onchange = async () => {
try {
const file = input.files[0];
if (!file) return reject(new Error('No file selected'));
onLog?.('Erasing flash...\n');
await esploader.eraseFlash();
flushBuffer();
onLog?.('Erase complete.\n');
const data = await file.arrayBuffer();
const uint8 = new Uint8Array(data);
onProgress?.('Writing firmware...');
await loader.writeFlash({
fileArray: [{ data: uint8, address: 0x0 }],
onLog?.('Writing firmware at 0x0...\n');
await esploader.writeFlash({
fileArray: [{ data: firmwareBin, address: 0x0 }],
flashSize: 'keep',
flashMode: 'keep',
flashFreq: 'keep',
eraseAll: false,
compress: true,
reportProgress(fileIndex, written, total) {
const pct = Math.round((written / total) * 100);
onProgress?.(pct);
},
});
flushBuffer();
onProgress?.('Firmware flashed successfully!');
onLog?.('\nFirmware written successfully!\n');
onLog?.('Hard resetting device...\n');
await esploader.after('hard_reset');
flushBuffer();
try {
await transport.disconnect();
resolve();
} catch (err) {
reject(err);
} catch (_) {
// already closed
}
};
input.click();
});
onLog?.('Done! Device is ready.\n');
}

View File

@ -260,3 +260,89 @@ html, body {
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* --- Flash Progress Overlay --- */
.hidden { display: none !important; }
#flash-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(4px);
}
#flash-modal {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 28px 32px;
width: 520px;
max-width: 90vw;
max-height: 80vh;
display: flex;
flex-direction: column;
gap: 14px;
}
#flash-modal h3 {
margin: 0;
color: var(--accent);
font-size: 16px;
}
#flash-log {
font-family: 'Cascadia Code', 'Fira Code', 'Consolas', monospace;
font-size: 12px;
line-height: 1.5;
color: var(--text-secondary);
background: var(--bg-secondary);
border-radius: var(--radius);
padding: 10px 12px;
max-height: 200px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
#flash-progress-bar {
height: 10px;
background: var(--bg-secondary);
border-radius: 5px;
overflow: hidden;
}
#flash-progress-fill {
height: 100%;
width: 0%;
background: var(--accent);
border-radius: 5px;
transition: width 0.2s ease;
}
#flash-progress-text {
font-size: 13px;
color: var(--text-secondary);
text-align: center;
}
#flash-close {
align-self: flex-end;
background: var(--bg-surface);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 8px 20px;
cursor: pointer;
font-size: 13px;
transition: background 0.15s, border-color 0.15s;
}
#flash-close:hover {
background: var(--accent);
color: var(--bg-toolbar);
border-color: var(--accent);
}