conv. commit

This commit is contained in:
swaits committed 2026-04-08 10:59:57 -06:00
commit fd677e33a0
24 files changed
+5159

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
import type { FlashProgress } from "./types";
export function isWebSerialSupported(): boolean {
return typeof navigator !== "undefined" && "serial" in navigator;
}
export interface FlashCallbacks {
onProgress: (progress: FlashProgress) => void;
onLog: (message: string) => void;
}
interface FlashSession {
transport: InstanceType<Awaited<ReturnType<typeof loadEsptool>>["Transport"]>;
loader: InstanceType<Awaited<ReturnType<typeof loadEsptool>>["ESPLoader"]>;
}
async function loadEsptool() {
const mod = await import("esptool-js");
return { ESPLoader: mod.ESPLoader, Transport: mod.Transport };
}
export async function connect(callbacks: FlashCallbacks): Promise<FlashSession> {
if (!isWebSerialSupported()) {
throw new Error("Web Serial API is not supported in this browser. Use Chrome, Edge, or Opera.");
}
const { ESPLoader, Transport } = await loadEsptool();
callbacks.onLog("Requesting serial port...");
const port = await navigator.serial.requestPort();
callbacks.onLog("Opening connection...");
const transport = new Transport(port, true);
const loader = new ESPLoader({
transport,
baudrate: 460800,
romBaudrate: 115200,
terminal: {
clean: () => {},
writeLine: (data: string) => callbacks.onLog(data),
write: (data: string) => {
if (data.trim()) callbacks.onLog(data.trim());
},
},
});
callbacks.onLog("Connecting to ESP32...");
callbacks.onProgress({ phase: "Connecting", percent: 0, message: "Detecting chip..." });
await loader.main();
const chip = loader.chip.CHIP_NAME;
callbacks.onLog(`Detected: ${chip}`);
if (!chip.includes("ESP32-S3")) {
await transport.disconnect();
throw new Error(`Expected ESP32-S3 but detected ${chip}. Make sure you selected the correct board.`);
}
return { transport, loader };
}
export async function flash(
session: FlashSession,
firmware: Uint8Array,
callbacks: FlashCallbacks,
): Promise<void> {
callbacks.onLog("Reading flash ID...");
await session.loader.flashId();
callbacks.onLog("Preparing to flash...");
callbacks.onProgress({ phase: "Flashing", percent: 0, message: "Erasing flash..." });
// Pass firmware as Uint8Array directly — NOT as a binary string.
// pako's deflate() treats string input as UTF-8 text, which corrupts
// every byte > 0x7F (e.g. 0xE9 becomes 0xC3 0xA9). Uint8Array is
// handled as raw bytes.
const fileArray = [
{
data: firmware,
address: 0x0,
},
];
await session.loader.writeFlash({
fileArray,
flashSize: "keep",
flashMode: "keep",
flashFreq: "keep",
eraseAll: false,
compress: true,
reportProgress: (_fileIndex: number, written: number, total: number) => {
const percent = Math.round((written / total) * 100);
callbacks.onProgress({
phase: "Flashing",
percent,
message: `${formatBytes(written)} / ${formatBytes(total)}`,
});
},
calculateMD5Hash: (_image: string) => undefined,
} as never);
callbacks.onLog("Verifying and finalizing...");
callbacks.onProgress({ phase: "Finishing", percent: 100, message: "Verifying flash..." });
await session.loader.after("hard_reset" as never);
callbacks.onLog("Hard reset complete.");
}
export async function disconnect(session: FlashSession): Promise<void> {
try {
await session.transport.disconnect();
} catch {
// Best effort
}
}
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
const kb = bytes / 1024;
if (kb < 1024) return `${kb.toFixed(1)} KB`;
return `${(kb / 1024).toFixed(1)} MB`;
}