Use esptool-js for installation (#269)

This commit is contained in:
Paulus Schoutsen 2022-07-19 22:55:22 -07:00 committed by GitHub
parent 4e19973bb1
commit 8c17d20aea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 338 additions and 654 deletions

729
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,7 +7,8 @@
"author": "ESPHome maintainers", "author": "ESPHome maintainers",
"license": "Apache-2.0", "license": "Apache-2.0",
"scripts": { "scripts": {
"prepublishOnly": "script/build" "prepublishOnly": "script/build",
"postinstall": "patch -Ntu node_modules/esptool-js/ESPLoader.js -i patches/esploader.patch || true"
}, },
"devDependencies": { "devDependencies": {
"@rollup/plugin-json": "^4.1.0", "@rollup/plugin-json": "^4.1.0",
@ -28,9 +29,10 @@
"@material/mwc-formfield": "^0.26.1", "@material/mwc-formfield": "^0.26.1",
"@material/mwc-icon-button": "^0.26.1", "@material/mwc-icon-button": "^0.26.1",
"@material/mwc-textfield": "^0.26.1", "@material/mwc-textfield": "^0.26.1",
"esp-web-flasher": "^5.1.4", "esptool-js": "github:espressif/esptool-js#0c1b972a05d691c85da23fcc937d91dcf7e283eb",
"improv-wifi-serial-sdk": "^2.2.2", "improv-wifi-serial-sdk": "^2.2.2",
"lit": "^2.0.0", "lit": "^2.0.0",
"pako": "^2.0.4",
"tslib": "^2.3.1" "tslib": "^2.3.1"
} }
} }

16
patches/esploader.patch Normal file
View File

@ -0,0 +1,16 @@
--- node_modules/esptool-js/ESPLoader.js 2022-07-19 09:17:05.000000000 -0700
+++ node_modules/esptool-js/ESPLoader.fixed.js 2022-07-19 09:19:04.000000000 -0700
@@ -1,3 +1,4 @@
+import pako from 'pako';
import {ESPError, TimeoutError} from "./error.js";
const MAGIC_TO_CHIP = {
@@ -680,7 +681,7 @@
await this.run_stub();
- await this.change_baud();
+ // await this.change_baud();
return chip;
}

View File

@ -38,11 +38,6 @@ export interface InitializingState extends BaseFlashState {
details: { done: boolean }; details: { done: boolean };
} }
export interface ManifestState extends BaseFlashState {
state: FlashStateType.MANIFEST;
details: { done: boolean };
}
export interface PreparingState extends BaseFlashState { export interface PreparingState extends BaseFlashState {
state: FlashStateType.PREPARING; state: FlashStateType.PREPARING;
details: { done: boolean }; details: { done: boolean };
@ -69,7 +64,6 @@ export interface ErrorState extends BaseFlashState {
export type FlashState = export type FlashState =
| InitializingState | InitializingState
| ManifestState
| PreparingState | PreparingState
| ErasingState | ErasingState
| WritingState | WritingState
@ -78,7 +72,6 @@ export type FlashState =
export const enum FlashStateType { export const enum FlashStateType {
INITIALIZING = "initializing", INITIALIZING = "initializing",
MANIFEST = "manifest",
PREPARING = "preparing", PREPARING = "preparing",
ERASING = "erasing", ERASING = "erasing",
WRITING = "writing", WRITING = "writing",

View File

@ -1,4 +1,7 @@
import { ESPLoader, Logger } from "esp-web-flasher"; // @ts-ignore-next-line
import { Transport } from "esptool-js/webserial.js";
// @ts-ignore-next-line
import { ESPLoader } from "esptool-js/esploader.js";
import { import {
Build, Build,
FlashError, FlashError,
@ -6,19 +9,28 @@ import {
Manifest, Manifest,
FlashStateType, FlashStateType,
} from "./const"; } from "./const";
import { getChipFamilyName } from "./util/chip-family-name";
import { sleep } from "./util/sleep"; import { sleep } from "./util/sleep";
const resetTransport = async (transport: Transport) => {
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: true,
});
await transport.device.setSignals({
dataTerminalReady: false,
requestToSend: false,
});
};
export const flash = async ( export const flash = async (
onEvent: (state: FlashState) => void, onEvent: (state: FlashState) => void,
port: SerialPort, port: SerialPort,
logger: Logger,
manifestPath: string, manifestPath: string,
manifest: Manifest,
eraseFirst: boolean eraseFirst: boolean
) => { ) => {
let manifest: Manifest;
let build: Build | undefined; let build: Build | undefined;
let chipFamily: ReturnType<typeof getChipFamilyName>; let chipFamily: Build["chipFamily"];
const fireStateEvent = (stateUpdate: FlashState) => const fireStateEvent = (stateUpdate: FlashState) =>
onEvent({ onEvent({
@ -28,12 +40,8 @@ export const flash = async (
chipFamily, chipFamily,
}); });
const manifestURL = new URL(manifestPath, location.toString()).toString(); const transport = new Transport(port);
const manifestProm = fetch(manifestURL).then( const esploader = new ESPLoader(transport, 115200);
(resp): Promise<Manifest> => resp.json()
);
const esploader = new ESPLoader(port, logger);
// For debugging // For debugging
(window as any).esploader = esploader; (window as any).esploader = esploader;
@ -45,61 +53,53 @@ export const flash = async (
}); });
try { try {
await esploader.initialize(); await esploader.main_fn();
await esploader.flash_id();
} catch (err: any) { } catch (err: any) {
logger.error(err); console.error(err);
fireStateEvent({ fireStateEvent({
state: FlashStateType.ERROR, state: FlashStateType.ERROR,
message: message:
"Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.", "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
details: { error: FlashError.FAILED_INITIALIZING, details: err }, details: { error: FlashError.FAILED_INITIALIZING, details: err },
}); });
if (esploader.connected) { await resetTransport(transport);
await esploader.disconnect(); await transport.disconnect();
}
return; return;
} }
chipFamily = getChipFamilyName(esploader); chipFamily = await esploader.chip.CHIP_NAME;
if (!esploader.chip.ROM_TEXT) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Chip ${chipFamily} is not supported`,
details: {
error: FlashError.NOT_SUPPORTED,
details: `Chip ${chipFamily} is not supported`,
},
});
await resetTransport(transport);
await transport.disconnect();
return;
}
fireStateEvent({ fireStateEvent({
state: FlashStateType.INITIALIZING, state: FlashStateType.INITIALIZING,
message: `Initialized. Found ${chipFamily}`, message: `Initialized. Found ${chipFamily}`,
details: { done: true }, details: { done: true },
}); });
fireStateEvent({
state: FlashStateType.MANIFEST,
message: "Fetching manifest...",
details: { done: false },
});
try {
manifest = await manifestProm;
} catch (err: any) {
fireStateEvent({
state: FlashStateType.ERROR,
message: `Unable to fetch manifest: ${err}`,
details: { error: FlashError.FAILED_MANIFEST_FETCH, details: err },
});
await esploader.disconnect();
return;
}
build = manifest.builds.find((b) => b.chipFamily === chipFamily); build = manifest.builds.find((b) => b.chipFamily === chipFamily);
fireStateEvent({
state: FlashStateType.MANIFEST,
message: `Found manifest for ${manifest.name}`,
details: { done: true },
});
if (!build) { if (!build) {
fireStateEvent({ fireStateEvent({
state: FlashStateType.ERROR, state: FlashStateType.ERROR,
message: `Your ${chipFamily} board is not supported.`, message: `Your ${chipFamily} board is not supported.`,
details: { error: FlashError.NOT_SUPPORTED, details: chipFamily }, details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
}); });
await esploader.disconnect(); await resetTransport(transport);
await transport.disconnect();
return; return;
} }
@ -109,6 +109,7 @@ export const flash = async (
details: { done: false }, details: { done: false },
}); });
const manifestURL = new URL(manifestPath, location.toString()).toString();
const filePromises = build.parts.map(async (part) => { const filePromises = build.parts.map(async (part) => {
const url = new URL(part.path, manifestURL).toString(); const url = new URL(part.path, manifestURL).toString();
const resp = await fetch(url); const resp = await fetch(url);
@ -117,20 +118,24 @@ export const flash = async (
`Downlading firmware ${part.path} failed: ${resp.status}` `Downlading firmware ${part.path} failed: ${resp.status}`
); );
} }
return resp.arrayBuffer();
const reader = new FileReader();
const blob = await resp.blob();
return new Promise<string>((resolve) => {
reader.addEventListener("load", () => resolve(reader.result as string));
reader.readAsBinaryString(blob);
});
}); });
// Run the stub while we wait for files to download const fileArray: Array<{ data: string; address: number }> = [];
const espStub = await esploader.runStub();
const files: ArrayBuffer[] = [];
let totalSize = 0; let totalSize = 0;
for (const prom of filePromises) { for (let part = 0; part < filePromises.length; part++) {
try { try {
const data = await prom; const data = await filePromises[part];
files.push(data); fileArray.push({ data, address: build.parts[part].offset });
totalSize += data.byteLength; totalSize += data.length;
} catch (err: any) { } catch (err: any) {
fireStateEvent({ fireStateEvent({
state: FlashStateType.ERROR, state: FlashStateType.ERROR,
@ -140,7 +145,8 @@ export const flash = async (
details: err.message, details: err.message,
}, },
}); });
await esploader.disconnect(); await resetTransport(transport);
await transport.disconnect();
return; return;
} }
} }
@ -157,7 +163,7 @@ export const flash = async (
message: "Erasing device...", message: "Erasing device...",
details: { done: false }, details: { done: false },
}); });
await espStub.eraseFlash(); await esploader.erase_flash();
fireStateEvent({ fireStateEvent({
state: FlashStateType.ERASING, state: FlashStateType.ERASING,
message: "Device erased", message: "Device erased",
@ -165,57 +171,56 @@ export const flash = async (
}); });
} }
let lastPct = 0;
fireStateEvent({ fireStateEvent({
state: FlashStateType.WRITING, state: FlashStateType.WRITING,
message: `Writing progress: ${lastPct}%`, message: `Writing progress: 0%`,
details: { details: {
bytesTotal: totalSize, bytesTotal: totalSize,
bytesWritten: 0, bytesWritten: 0,
percentage: lastPct, percentage: 0,
}, },
}); });
let totalWritten = 0; let totalWritten = 0;
for (const part of build.parts) {
const file = files.shift()!;
try { try {
await espStub.flashData( await esploader.write_flash({
file, fileArray,
(bytesWritten: number) => { reportProgress(fileIndex: number, written: number, total: number) {
const uncompressedWritten =
(written / total) * fileArray[fileIndex].data.length;
const newPct = Math.floor( const newPct = Math.floor(
((totalWritten + bytesWritten) / totalSize) * 100 ((totalWritten + uncompressedWritten) / totalSize) * 100
); );
if (newPct === lastPct) {
// we're done with this file
if (written === total) {
totalWritten += uncompressedWritten;
return; return;
} }
lastPct = newPct;
fireStateEvent({ fireStateEvent({
state: FlashStateType.WRITING, state: FlashStateType.WRITING,
message: `Writing progress: ${newPct}%`, message: `Writing progress: ${newPct}%`,
details: { details: {
bytesTotal: totalSize, bytesTotal: totalSize,
bytesWritten: totalWritten + bytesWritten, bytesWritten: totalWritten + written,
percentage: newPct, percentage: newPct,
}, },
}); });
}, },
part.offset, });
true
);
} catch (err: any) { } catch (err: any) {
fireStateEvent({ fireStateEvent({
state: FlashStateType.ERROR, state: FlashStateType.ERROR,
message: err.message, message: err.message,
details: { error: FlashError.WRITE_FAILED, details: err }, details: { error: FlashError.WRITE_FAILED, details: err },
}); });
await esploader.disconnect(); await resetTransport(transport);
await transport.disconnect();
return; return;
} }
totalWritten += file.byteLength;
}
fireStateEvent({ fireStateEvent({
state: FlashStateType.WRITING, state: FlashStateType.WRITING,
@ -228,10 +233,10 @@ export const flash = async (
}); });
await sleep(100); await sleep(100);
console.log("DISCONNECT");
await esploader.disconnect();
console.log("HARD RESET"); console.log("HARD RESET");
await esploader.hardReset(); await resetTransport(transport);
console.log("DISCONNECT");
await transport.disconnect();
fireStateEvent({ fireStateEvent({
state: FlashStateType.FINISHED, state: FlashStateType.FINISHED,

View File

@ -571,7 +571,6 @@ export class EwtInstallDialog extends LitElement {
} else if ( } else if (
!this._installState || !this._installState ||
this._installState.state === FlashStateType.INITIALIZING || this._installState.state === FlashStateType.INITIALIZING ||
this._installState.state === FlashStateType.MANIFEST ||
this._installState.state === FlashStateType.PREPARING this._installState.state === FlashStateType.PREPARING
) { ) {
heading = "Installing"; heading = "Installing";
@ -826,19 +825,27 @@ export class EwtInstallDialog extends LitElement {
} }
this._client = undefined; this._client = undefined;
// Close port. ESPLoader likes opening it.
await this.port.close();
flash( flash(
(state) => { (state) => {
this._installState = state; this._installState = state;
if (state.state === FlashStateType.FINISHED) { if (state.state === FlashStateType.FINISHED) {
sleep(100) sleep(100)
// Flashing closes the port
.then(() => this.port.open({ baudRate: 115200 }))
.then(() => this._initialize(true)) .then(() => this._initialize(true))
.then(() => this.requestUpdate()); .then(() => this.requestUpdate());
} else if (state.state === FlashStateType.ERROR) {
sleep(100)
// Flashing closes the port
.then(() => this.port.open({ baudRate: 115200 }));
} }
}, },
this.port, this.port,
this.logger,
this.manifestPath, this.manifestPath,
this._manifest,
this._installErase this._installErase
); );
} }

View File

@ -1,28 +0,0 @@
import {
CHIP_FAMILY_ESP32,
CHIP_FAMILY_ESP32S2,
CHIP_FAMILY_ESP32S3,
CHIP_FAMILY_ESP8266,
CHIP_FAMILY_ESP32C3,
ESPLoader,
} from "esp-web-flasher";
import type { BaseFlashState } from "../const";
export const getChipFamilyName = (
esploader: ESPLoader
): NonNullable<BaseFlashState["chipFamily"]> => {
switch (esploader.chipFamily) {
case CHIP_FAMILY_ESP32:
return "ESP32";
case CHIP_FAMILY_ESP8266:
return "ESP8266";
case CHIP_FAMILY_ESP32S2:
return "ESP32-S2";
case CHIP_FAMILY_ESP32S3:
return "ESP32-S3";
case CHIP_FAMILY_ESP32C3:
return "ESP32-C3";
default:
return "Unknown Chip";
}
};