Skip to content

Commit 3fe94a9

Browse files
authored
feat: Improved serial flasher (#183)
1 parent 72e31a7 commit 3fe94a9

8 files changed

Lines changed: 1144 additions & 190 deletions

File tree

src/routes/flashtool/+page.svelte

Lines changed: 278 additions & 132 deletions
Large diffs are not rendered by default.

src/routes/flashtool/FlashManager.ts

Lines changed: 98 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -130,28 +130,44 @@ export default class FlashManager {
130130
*/
131131
private terminal: IEspLoaderTerminal;
132132
/**
133-
* Chip: During connect, the chip is read from the ESPLoader.
133+
* Chip: Detected during bootloader setup. null until first ensureBootloader() call.
134134
*/
135-
private chip: string;
135+
private chip: string | null;
136136

137-
private constructor(loader: ESPLoader, terminal: IEspLoaderTerminal) {
138-
this.serialPort = loader.transport.device;
137+
private constructor(serialPort: SerialPort, terminal: IEspLoaderTerminal, loader?: ESPLoader) {
138+
this.serialPort = serialPort;
139139
this.serialPortReader = null;
140140
this.serialPortWriter = null;
141-
this.loader = loader;
141+
this.loader = loader ?? null;
142142
this.terminal = terminal;
143-
this.chip = loader.chip.CHIP_NAME;
143+
this.chip = loader?.chip.CHIP_NAME ?? null;
144144
}
145145

146-
static async Connect(serialPort: SerialPort, terminal: IEspLoaderTerminal) {
146+
/**
147+
* Connect in bootloader mode (legacy). Enters ESPLoader immediately.
148+
*/
149+
static async ConnectBootloader(serialPort: SerialPort, terminal: IEspLoaderTerminal) {
147150
const espLoader = await setupESPLoader(serialPort, terminal);
148151
if (espLoader != null) {
149-
return new FlashManager(espLoader, terminal);
152+
return new FlashManager(espLoader.transport.device, terminal, espLoader);
150153
} else {
151154
return null;
152155
}
153156
}
154157

158+
/**
159+
* Connect in application mode. Opens the port, resets the device into app mode,
160+
* and starts reading serial output immediately. Bootloader is entered lazily when flash is triggered.
161+
*/
162+
static async ConnectApplication(serialPort: SerialPort, terminal: IEspLoaderTerminal) {
163+
const port = await setupApplication(serialPort);
164+
if (!port) return null;
165+
166+
const manager = new FlashManager(port, terminal);
167+
manager._startApplicationReadLoop();
168+
return manager;
169+
}
170+
155171
get SerialPort() {
156172
return this.serialPort;
157173
}
@@ -160,6 +176,12 @@ export default class FlashManager {
160176
return this.chip;
161177
}
162178

179+
get mode(): 'application' | 'bootloader' | 'disconnected' {
180+
if (!this.serialPort) return 'disconnected';
181+
if (this.loader) return 'bootloader';
182+
return 'application';
183+
}
184+
163185
/**
164186
* Assumes the FlashManager is connected.
165187
* To work around esptool.js issues (namely, any timeout whatsoever corrupts newRead and probably everything else too), some operations have to 'reboot' the transport.
@@ -216,69 +238,88 @@ export default class FlashManager {
216238
this.loader = loader;
217239
this.chip = loader.chip.CHIP_NAME;
218240
}
241+
return !!this.loader;
219242
}
220243

221-
async ensureApplication(forceReset?: boolean) {
222-
if (!this.serialPort) return false;
223-
if (!this.loader && !forceReset) return true;
224-
225-
const serialPort = await setupApplication(await this._cycleTransport());
226-
this.serialPort = serialPort;
227-
228-
if (serialPort) {
229-
const serialPortReader = serialPort!.readable!.getReader();
230-
const serialPortWriter = serialPort!.writable!.getWriter();
231-
this.serialPortReader = serialPortReader;
232-
this.serialPortWriter = serialPortWriter;
233-
// connect application to terminal
234-
(async () => {
235-
try {
236-
let lineBuffer: Uint8Array | null = null; // Buffer to hold data between chunks
237-
238-
while (true) {
239-
// since we're using Transport APIs, and since they have no "no timeout" option, get as close as possible
240-
const { done, value } = await serialPortReader.read();
241-
if (done) break; // Stream ended - exit the loop
242-
if (!value) {
243-
await sleep(1); // No data received, wait a bit
244-
continue; // Skip to the next iteration
245-
}
244+
/**
245+
* Starts an async read loop that reads from the serial port and writes to the terminal.
246+
* Assumes the port is already open with reader/writer available.
247+
*/
248+
private _startApplicationReadLoop() {
249+
if (!this.serialPort) return;
246250

247-
let start = 0; // Where to start reading from the value
251+
const serialPortReader = this.serialPort.readable!.getReader();
252+
const serialPortWriter = this.serialPort.writable!.getWriter();
253+
this.serialPortReader = serialPortReader;
254+
this.serialPortWriter = serialPortWriter;
248255

249-
// Process each byte in the received chunk
250-
for (let i = 0; i < value.length; i++) {
251-
const byte = value[i];
256+
(async () => {
257+
try {
258+
let lineBuffer: Uint8Array | null = null;
259+
260+
while (true) {
261+
const { done, value } = await serialPortReader.read();
262+
if (done) break;
263+
if (!value) {
264+
await sleep(1);
265+
continue;
266+
}
252267

253-
// Skip until we encounter a line terminator (LF or CR)
254-
if (byte !== 10 && byte !== 13) continue;
268+
let start = 0;
255269

256-
// Copy all data from rstart to current index (i) into the buffer
257-
if (i > start) {
258-
lineBuffer = appendBuffer(lineBuffer, value.subarray(start, i));
259-
}
270+
for (let i = 0; i < value.length; i++) {
271+
const byte = value[i];
260272

261-
// Line Feed (\n): flush buffer as a complete line
262-
if (byte === 10) {
263-
this.terminal.writeLine(lineBuffer?.length ? DecodeString(lineBuffer) : '');
264-
lineBuffer = null; // Reset buffer after flushing
265-
}
273+
if (byte !== 10 && byte !== 13) continue;
266274

267-
// Set start to the next byte after the line terminator
268-
start = i + 1;
275+
if (i > start) {
276+
lineBuffer = appendBuffer(lineBuffer, value.subarray(start, i));
269277
}
270278

271-
// Push any remaining data in the buffer
272-
if (start < value.length) {
273-
lineBuffer = appendBuffer(lineBuffer, value.subarray(start));
279+
if (byte === 10) {
280+
this.terminal.writeLine(lineBuffer?.length ? DecodeString(lineBuffer) : '');
281+
lineBuffer = null;
274282
}
283+
284+
start = i + 1;
285+
}
286+
287+
if (start < value.length) {
288+
lineBuffer = appendBuffer(lineBuffer, value.subarray(start));
275289
}
276-
} catch (e) {
277-
console.log(e);
278-
this.terminal.writeLine(`firmware disconnected: ${e}`);
279290
}
280-
})();
291+
} catch (e) {
292+
console.log(e);
293+
this.terminal.writeLine(`firmware disconnected: ${e}`);
294+
} finally {
295+
try {
296+
serialPortReader.releaseLock();
297+
} catch {
298+
/* ignore */
299+
}
300+
try {
301+
serialPortWriter.releaseLock();
302+
} catch {
303+
/* ignore */
304+
}
305+
if (this.serialPortReader === serialPortReader) this.serialPortReader = null;
306+
if (this.serialPortWriter === serialPortWriter) this.serialPortWriter = null;
307+
}
308+
})();
309+
}
310+
311+
async ensureApplication(forceReset?: boolean): Promise<boolean> {
312+
if (!this.serialPort) return false;
313+
if (!this.loader && !forceReset) return true;
314+
315+
const serialPort = await setupApplication(await this._cycleTransport());
316+
this.serialPort = serialPort;
317+
318+
if (serialPort) {
319+
this._startApplicationReadLoop();
320+
return true;
281321
}
322+
return false;
282323
}
283324

284325
async disconnect() {

0 commit comments

Comments
 (0)