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