1 |
|
2 | import { Transport } from "esptool-js/webserial.js";
|
3 |
|
4 | import { ESPLoader } from "esptool-js/ESPLoader.js";
|
5 | import {
|
6 | Build,
|
7 | FlashError,
|
8 | FlashState,
|
9 | Manifest,
|
10 | FlashStateType,
|
11 | } from "./const";
|
12 | import { sleep } from "./util/sleep";
|
13 |
|
14 | const resetTransport = async (transport: Transport) => {
|
15 | await transport.device.setSignals({
|
16 | dataTerminalReady: false,
|
17 | requestToSend: true,
|
18 | });
|
19 | await transport.device.setSignals({
|
20 | dataTerminalReady: false,
|
21 | requestToSend: false,
|
22 | });
|
23 | };
|
24 |
|
25 | export const flash = async (
|
26 | onEvent: (state: FlashState) => void,
|
27 | port: SerialPort,
|
28 | manifestPath: string,
|
29 | manifest: Manifest,
|
30 | eraseFirst: boolean
|
31 | ) => {
|
32 | let build: Build | undefined;
|
33 | let chipFamily: Build["chipFamily"];
|
34 |
|
35 | const fireStateEvent = (stateUpdate: FlashState) =>
|
36 | onEvent({
|
37 | ...stateUpdate,
|
38 | manifest,
|
39 | build,
|
40 | chipFamily,
|
41 | });
|
42 |
|
43 | const transport = new Transport(port);
|
44 | const esploader = new ESPLoader(transport, 115200);
|
45 |
|
46 |
|
47 | (window as any).esploader = esploader;
|
48 |
|
49 | fireStateEvent({
|
50 | state: FlashStateType.INITIALIZING,
|
51 | message: "Initializing...",
|
52 | details: { done: false },
|
53 | });
|
54 |
|
55 | try {
|
56 | await esploader.main_fn();
|
57 | await esploader.flash_id();
|
58 | } catch (err: any) {
|
59 | console.error(err);
|
60 | fireStateEvent({
|
61 | state: FlashStateType.ERROR,
|
62 | message:
|
63 | "Failed to initialize. Try resetting your device or holding the BOOT button while clicking INSTALL.",
|
64 | details: { error: FlashError.FAILED_INITIALIZING, details: err },
|
65 | });
|
66 | await resetTransport(transport);
|
67 | await transport.disconnect();
|
68 | return;
|
69 | }
|
70 |
|
71 | chipFamily = await esploader.chip.CHIP_NAME;
|
72 |
|
73 | if (!esploader.chip.ROM_TEXT) {
|
74 | fireStateEvent({
|
75 | state: FlashStateType.ERROR,
|
76 | message: `Chip ${chipFamily} is not supported`,
|
77 | details: {
|
78 | error: FlashError.NOT_SUPPORTED,
|
79 | details: `Chip ${chipFamily} is not supported`,
|
80 | },
|
81 | });
|
82 | await resetTransport(transport);
|
83 | await transport.disconnect();
|
84 | return;
|
85 | }
|
86 |
|
87 | fireStateEvent({
|
88 | state: FlashStateType.INITIALIZING,
|
89 | message: `Initialized. Found ${chipFamily}`,
|
90 | details: { done: true },
|
91 | });
|
92 |
|
93 | build = manifest.builds.find((b) => b.chipFamily === chipFamily);
|
94 |
|
95 | if (!build) {
|
96 | fireStateEvent({
|
97 | state: FlashStateType.ERROR,
|
98 | message: `Your ${chipFamily} board is not supported.`,
|
99 | details: { error: FlashError.NOT_SUPPORTED, details: chipFamily },
|
100 | });
|
101 | await resetTransport(transport);
|
102 | await transport.disconnect();
|
103 | return;
|
104 | }
|
105 |
|
106 | fireStateEvent({
|
107 | state: FlashStateType.PREPARING,
|
108 | message: "Preparing installation...",
|
109 | details: { done: false },
|
110 | });
|
111 |
|
112 | const manifestURL = new URL(manifestPath, location.toString()).toString();
|
113 | const filePromises = build.parts.map(async (part) => {
|
114 | const url = new URL(part.path, manifestURL).toString();
|
115 | const resp = await fetch(url);
|
116 | if (!resp.ok) {
|
117 | throw new Error(
|
118 | `Downlading firmware ${part.path} failed: ${resp.status}`
|
119 | );
|
120 | }
|
121 |
|
122 | const reader = new FileReader();
|
123 | const blob = await resp.blob();
|
124 |
|
125 | return new Promise<string>((resolve) => {
|
126 | reader.addEventListener("load", () => resolve(reader.result as string));
|
127 | reader.readAsBinaryString(blob);
|
128 | });
|
129 | });
|
130 |
|
131 | const fileArray: Array<{ data: string; address: number }> = [];
|
132 | let totalSize = 0;
|
133 |
|
134 | for (let part = 0; part < filePromises.length; part++) {
|
135 | try {
|
136 | const data = await filePromises[part];
|
137 | fileArray.push({ data, address: build.parts[part].offset });
|
138 | totalSize += data.length;
|
139 | } catch (err: any) {
|
140 | fireStateEvent({
|
141 | state: FlashStateType.ERROR,
|
142 | message: err.message,
|
143 | details: {
|
144 | error: FlashError.FAILED_FIRMWARE_DOWNLOAD,
|
145 | details: err.message,
|
146 | },
|
147 | });
|
148 | await resetTransport(transport);
|
149 | await transport.disconnect();
|
150 | return;
|
151 | }
|
152 | }
|
153 |
|
154 | fireStateEvent({
|
155 | state: FlashStateType.PREPARING,
|
156 | message: "Installation prepared",
|
157 | details: { done: true },
|
158 | });
|
159 |
|
160 | if (eraseFirst) {
|
161 | fireStateEvent({
|
162 | state: FlashStateType.ERASING,
|
163 | message: "Erasing device...",
|
164 | details: { done: false },
|
165 | });
|
166 | await esploader.erase_flash();
|
167 | fireStateEvent({
|
168 | state: FlashStateType.ERASING,
|
169 | message: "Device erased",
|
170 | details: { done: true },
|
171 | });
|
172 | }
|
173 |
|
174 | fireStateEvent({
|
175 | state: FlashStateType.WRITING,
|
176 | message: `Writing progress: 0%`,
|
177 | details: {
|
178 | bytesTotal: totalSize,
|
179 | bytesWritten: 0,
|
180 | percentage: 0,
|
181 | },
|
182 | });
|
183 |
|
184 | let totalWritten = 0;
|
185 |
|
186 | try {
|
187 | await esploader.write_flash({
|
188 | fileArray,
|
189 | reportProgress(fileIndex: number, written: number, total: number) {
|
190 | const uncompressedWritten =
|
191 | (written / total) * fileArray[fileIndex].data.length;
|
192 |
|
193 | const newPct = Math.floor(
|
194 | ((totalWritten + uncompressedWritten) / totalSize) * 100
|
195 | );
|
196 |
|
197 |
|
198 | if (written === total) {
|
199 | totalWritten += uncompressedWritten;
|
200 | return;
|
201 | }
|
202 |
|
203 | fireStateEvent({
|
204 | state: FlashStateType.WRITING,
|
205 | message: `Writing progress: ${newPct}%`,
|
206 | details: {
|
207 | bytesTotal: totalSize,
|
208 | bytesWritten: totalWritten + written,
|
209 | percentage: newPct,
|
210 | },
|
211 | });
|
212 | },
|
213 | });
|
214 | } catch (err: any) {
|
215 | fireStateEvent({
|
216 | state: FlashStateType.ERROR,
|
217 | message: err.message,
|
218 | details: { error: FlashError.WRITE_FAILED, details: err },
|
219 | });
|
220 | await resetTransport(transport);
|
221 | await transport.disconnect();
|
222 | return;
|
223 | }
|
224 |
|
225 | fireStateEvent({
|
226 | state: FlashStateType.WRITING,
|
227 | message: "Writing complete",
|
228 | details: {
|
229 | bytesTotal: totalSize,
|
230 | bytesWritten: totalWritten,
|
231 | percentage: 100,
|
232 | },
|
233 | });
|
234 |
|
235 | await sleep(100);
|
236 | console.log("HARD RESET");
|
237 | await resetTransport(transport);
|
238 | console.log("DISCONNECT");
|
239 | await transport.disconnect();
|
240 |
|
241 | fireStateEvent({
|
242 | state: FlashStateType.FINISHED,
|
243 | message: "All done!",
|
244 | });
|
245 | };
|
246 |
|
\ | No newline at end of file |