esp-web-tools
Version:
Web tools for ESP devices
260 lines (227 loc) • 6.68 kB
text/typescript
import { Transport, ESPLoader } from "esptool-js";
import {
Build,
FlashError,
FlashState,
Manifest,
FlashStateType,
} from "./const";
import { sleep } from "./util/sleep";
/**
* Perform a hard reset using esploader's chip-specific reset logic.
* First asserts RTS to enter reset, then calls esploader.after() which
* uses the chip-specific reset procedure to release.
*/
const hardResetDevice = async (transport: Transport, esploader: ESPLoader) => {
await transport.setRTS(true);
await sleep(100);
await esploader.after();
};
export const flash = async (
onEvent: (state: FlashState) => void,
port: SerialPort,
manifestPath: string,
manifest: Manifest,
eraseFirst: boolean,
) => {
let build: Build | undefined;
let chipFamily: Build["chipFamily"];
const fireStateEvent = (stateUpdate: FlashState) =>
onEvent({
...stateUpdate,
manifest,
build,
chipFamily,
});
const transport = new Transport(port);
const portInfo = port.getInfo();
const isCdcUsbPort =
portInfo &&
portInfo.usbVendorId === 0x303a &&
portInfo.usbProductId !== undefined &&
[0x1001, 0x1002, 0x1003, 0x0002, 0x0003].includes(portInfo.usbProductId);
const esploader = new ESPLoader({
transport,
baudrate: 115200,
enableTracing: false,
});
// For debugging
(window as any).esploader = esploader;
fireStateEvent({
state: FlashStateType.INITIALIZING,
message: "Initializing...",
details: { done: false },
});
try {
await esploader.main();
await esploader.flashId();
} catch (err: any) {
console.error(err);
fireStateEvent({
state: FlashStateType.ERROR,
message:
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
details: { error: FlashError.FAILED_INITIALIZING, details: err },
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
chipFamily = esploader.chip.CHIP_NAME as any;
fireStateEvent({
state: FlashStateType.INITIALIZING,
message: `Initialized. Found ${chipFamily}`,
details: { done: true },
});
const detectedSerialType = isCdcUsbPort ? "cdc" : "uart";
build =
manifest.builds.find(
(b) => b.chipFamily === chipFamily && b.serialType === detectedSerialType,
) ||
manifest.builds.find(
(b) => b.chipFamily === chipFamily && b.serialType === undefined,
);
if (!build) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Your ${chipFamily} board is not supported.`,
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
fireStateEvent({
state: FlashStateType.PREPARING,
message: "Preparing installation...",
details: { done: false },
});
const manifestURL =
manifestPath.startsWith("blob:") || manifestPath.startsWith("data:")
? location.toString()
: new URL(manifestPath, location.toString()).toString();
const filePromises = build.parts.map(async (part) => {
const url = new URL(part.path, manifestURL).toString();
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(
`Downloading firmware ${part.path} failed: ${resp.status}`,
);
}
const reader = new FileReader();
const blob = await resp.blob();
return new Promise<ArrayBuffer>((resolve) => {
reader.addEventListener("load", () =>
resolve(reader.result as ArrayBuffer),
);
reader.readAsArrayBuffer(blob);
});
});
const fileArray: Array<{ data: Uint8Array; address: number }> = [];
let totalSize = 0;
for (let part = 0; part < filePromises.length; part++) {
try {
const buffer = await filePromises[part];
const data = new Uint8Array(buffer, 0, buffer.byteLength);
fileArray.push({ data, address: build.parts[part].offset });
totalSize += data.length;
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: err.message,
details: {
error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
details: err.message,
},
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
}
fireStateEvent({
state: FlashStateType.PREPARING,
message: "Installation prepared",
details: { done: true },
});
if (eraseFirst) {
fireStateEvent({
state: FlashStateType.ERASING,
message: "Erasing device...",
details: { done: false },
});
await esploader.eraseFlash();
fireStateEvent({
state: FlashStateType.ERASING,
message: "Device erased",
details: { done: true },
});
}
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: 0%`,
details: {
bytesTotal: totalSize,
bytesWritten: 0,
percentage: 0,
},
});
let totalWritten = 0;
try {
await esploader.writeFlash({
fileArray,
flashSize: "keep",
flashMode: "keep",
flashFreq: "keep",
eraseAll: false,
compress: true,
// report progress
reportProgress: (fileIndex: number, written: number, total: number) => {
const uncompressedWritten =
(written / total) * fileArray[fileIndex].data.length;
const newPct = Math.floor(
((totalWritten + uncompressedWritten) / totalSize) * 100,
);
// we're done with this file
if (written === total) {
totalWritten += uncompressedWritten;
return;
}
fireStateEvent({
state: FlashStateType.WRITING,
message: `Writing progress: ${newPct}%`,
details: {
bytesTotal: totalSize,
bytesWritten: totalWritten + written,
percentage: newPct,
},
});
},
});
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: err.message,
details: { error: FlashError.WRITE_FAILED, details: err },
});
await hardResetDevice(transport, esploader);
await transport.disconnect();
return;
}
fireStateEvent({
state: FlashStateType.WRITING,
message: "Writing complete",
details: {
bytesTotal: totalSize,
bytesWritten: totalWritten,
percentage: 100,
},
});
await hardResetDevice(transport, esploader);
console.log("DISCONNECT");
await transport.disconnect();
fireStateEvent({
state: FlashStateType.FINISHED,
message: "All done!",
});
};