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