mirror of
https://github.com/esphome/esp-web-tools.git
synced 2025-07-28 06:06:36 +00:00
Use esptool-js for installation (#269)
This commit is contained in:
parent
4e19973bb1
commit
8c17d20aea
729
package-lock.json
generated
729
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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
16
patches/esploader.patch
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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",
|
||||||
|
195
src/flash.ts
195
src/flash.ts
@ -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,56 +171,55 @@ 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) {
|
try {
|
||||||
const file = files.shift()!;
|
await esploader.write_flash({
|
||||||
try {
|
fileArray,
|
||||||
await espStub.flashData(
|
reportProgress(fileIndex: number, written: number, total: number) {
|
||||||
file,
|
const uncompressedWritten =
|
||||||
(bytesWritten: number) => {
|
(written / total) * fileArray[fileIndex].data.length;
|
||||||
const newPct = Math.floor(
|
|
||||||
((totalWritten + bytesWritten) / totalSize) * 100
|
const newPct = Math.floor(
|
||||||
);
|
((totalWritten + uncompressedWritten) / totalSize) * 100
|
||||||
if (newPct === lastPct) {
|
);
|
||||||
return;
|
|
||||||
}
|
// we're done with this file
|
||||||
lastPct = newPct;
|
if (written === total) {
|
||||||
fireStateEvent({
|
totalWritten += uncompressedWritten;
|
||||||
state: FlashStateType.WRITING,
|
return;
|
||||||
message: `Writing progress: ${newPct}%`,
|
}
|
||||||
details: {
|
|
||||||
bytesTotal: totalSize,
|
fireStateEvent({
|
||||||
bytesWritten: totalWritten + bytesWritten,
|
state: FlashStateType.WRITING,
|
||||||
percentage: newPct,
|
message: `Writing progress: ${newPct}%`,
|
||||||
},
|
details: {
|
||||||
});
|
bytesTotal: totalSize,
|
||||||
},
|
bytesWritten: totalWritten + written,
|
||||||
part.offset,
|
percentage: newPct,
|
||||||
true
|
},
|
||||||
);
|
});
|
||||||
} catch (err: any) {
|
},
|
||||||
fireStateEvent({
|
});
|
||||||
state: FlashStateType.ERROR,
|
} catch (err: any) {
|
||||||
message: err.message,
|
fireStateEvent({
|
||||||
details: { error: FlashError.WRITE_FAILED, details: err },
|
state: FlashStateType.ERROR,
|
||||||
});
|
message: err.message,
|
||||||
await esploader.disconnect();
|
details: { error: FlashError.WRITE_FAILED, details: err },
|
||||||
return;
|
});
|
||||||
}
|
await resetTransport(transport);
|
||||||
totalWritten += file.byteLength;
|
await transport.disconnect();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fireStateEvent({
|
fireStateEvent({
|
||||||
@ -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,
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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";
|
|
||||||
}
|
|
||||||
};
|
|
Loading…
x
Reference in New Issue
Block a user