implemented firmware flashing for esp32s3
parent
2c00738b07
commit
fd6d061d36
15
index.html
15
index.html
|
|
@ -16,7 +16,7 @@
|
|||
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
|
||||
<span class="icon">▶</span> Connect
|
||||
</button>
|
||||
<button id="btn-flash" title="Flash MicroPython firmware" disabled>
|
||||
<button id="btn-flash" title="Flash MicroPython firmware">
|
||||
<span class="icon">⚡</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.
51
src/main.js
51
src/main.js
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue