@@ -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