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">
|
<button id="btn-connect" title="Connect to ESP32 via Web Serial">
|
||||||
<span class="icon">▶</span> Connect
|
<span class="icon">▶</span> Connect
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-flash" title="Flash MicroPython firmware" disabled>
|
<button id="btn-flash" title="Flash MicroPython firmware">
|
||||||
<span class="icon">⚡</span> Flash FW
|
<span class="icon">⚡</span> Flash FW
|
||||||
</button>
|
</button>
|
||||||
<button id="btn-run" title="Upload and run code" disabled>
|
<button id="btn-run" title="Upload and run code" disabled>
|
||||||
|
|
@ -56,6 +56,19 @@
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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) {
|
function setConnectedUI(connected) {
|
||||||
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
btnConnect.textContent = connected ? '⏏ Disconnect' : '▶ Connect';
|
||||||
btnFlash.disabled = !connected;
|
|
||||||
btnRun.disabled = !connected;
|
btnRun.disabled = !connected;
|
||||||
btnStop.disabled = !connected;
|
btnStop.disabled = !connected;
|
||||||
btnSave.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 () => {
|
btnFlash.addEventListener('click', async () => {
|
||||||
try {
|
if (isConnected()) {
|
||||||
clearTerminal();
|
|
||||||
appendToTerminal('Starting firmware flash...\n');
|
|
||||||
await disconnect();
|
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);
|
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) {
|
} 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
|
import { ESPLoader, Transport } from 'esptool-js';
|
||||||
// Actual flashing will use the esptool-js ESPLoader when the user triggers it.
|
|
||||||
|
|
||||||
export async function flashFirmware(port, onProgress) {
|
const FIRMWARE_URL = '/firmware/ESP32_GENERIC_S3-20251209-v1.27.0.bin';
|
||||||
const { ESPLoader, Transport } = await import('esptool-js');
|
|
||||||
|
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 transport = new Transport(port);
|
||||||
const loader = new ESPLoader({
|
const esploader = new ESPLoader({
|
||||||
transport,
|
transport,
|
||||||
baudrate: 115200,
|
baudrate: 115200,
|
||||||
|
romBaudrate: 115200,
|
||||||
terminal: {
|
terminal: {
|
||||||
clean() {},
|
clean() {},
|
||||||
writeLine(data) { onProgress?.(data); },
|
writeLine(data) {
|
||||||
write(data) { onProgress?.(data); },
|
// Flush any buffered content first
|
||||||
|
flushBuffer();
|
||||||
|
onLog?.(data + (data.endsWith('\n') ? '' : '\n'));
|
||||||
|
},
|
||||||
|
write(data) {
|
||||||
|
normalizeAndLog(data);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await loader.main();
|
onLog?.('Connecting to ESP32-S3...\n');
|
||||||
await loader.eraseFlash();
|
const chip = await esploader.main();
|
||||||
|
flushBuffer();
|
||||||
|
onLog?.(`Detected: ${chip}\n`);
|
||||||
|
|
||||||
const input = document.createElement('input');
|
onLog?.('Fetching firmware...\n');
|
||||||
input.type = 'file';
|
const resp = await fetch(FIRMWARE_URL);
|
||||||
input.accept = '.bin';
|
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) => {
|
onLog?.('Erasing flash...\n');
|
||||||
input.onchange = async () => {
|
await esploader.eraseFlash();
|
||||||
try {
|
flushBuffer();
|
||||||
const file = input.files[0];
|
onLog?.('Erase complete.\n');
|
||||||
if (!file) return reject(new Error('No file selected'));
|
|
||||||
|
|
||||||
const data = await file.arrayBuffer();
|
onLog?.('Writing firmware at 0x0...\n');
|
||||||
const uint8 = new Uint8Array(data);
|
await esploader.writeFlash({
|
||||||
|
fileArray: [{ data: firmwareBin, address: 0x0 }],
|
||||||
onProgress?.('Writing firmware...');
|
flashSize: 'keep',
|
||||||
await loader.writeFlash({
|
flashMode: 'keep',
|
||||||
fileArray: [{ data: uint8, address: 0x0 }],
|
flashFreq: 'keep',
|
||||||
flashSize: 'keep',
|
eraseAll: false,
|
||||||
eraseAll: false,
|
compress: true,
|
||||||
compress: true,
|
reportProgress(fileIndex, written, total) {
|
||||||
});
|
const pct = Math.round((written / total) * 100);
|
||||||
|
onProgress?.(pct);
|
||||||
onProgress?.('Firmware flashed successfully!');
|
},
|
||||||
await transport.disconnect();
|
|
||||||
resolve();
|
|
||||||
} catch (err) {
|
|
||||||
reject(err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
input.click();
|
|
||||||
});
|
});
|
||||||
|
flushBuffer();
|
||||||
|
|
||||||
|
onLog?.('\nFirmware written successfully!\n');
|
||||||
|
onLog?.('Hard resetting device...\n');
|
||||||
|
await esploader.after('hard_reset');
|
||||||
|
flushBuffer();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transport.disconnect();
|
||||||
|
} catch (_) {
|
||||||
|
// already closed
|
||||||
|
}
|
||||||
|
|
||||||
|
onLog?.('Done! Device is ready.\n');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -260,3 +260,89 @@ html, body {
|
||||||
::-webkit-scrollbar-thumb:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background: var(--text-muted);
|
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