33 */
44
55import { FileTransferClient } from '../common/ble-file-transfer.js' ;
6- import { CONNTYPE } from '../constants.js' ;
6+ import { CONNTYPE , CONNSTATE } from '../constants.js' ;
77import { Workflow } from './workflow.js' ;
88import { GenericModal , DeviceInfoModal } from '../common/dialogs.js' ;
99import { sleep } from '../common/utilities.js' ;
@@ -14,6 +14,17 @@ const bleNusCharTXUUID = 'adaf0003-4369-7263-7569-74507974686e';
1414
1515const BYTES_PER_WRITE = 20 ;
1616
17+ // Tunables for silent auto-reconnect after firmware autoreload.
18+ // CircuitPython's BLE file transfer triggers an autoreload after every
19+ // mutating op (write/move/delete/mkdir), which tears down the GATT
20+ // Silent reconnect after firmware autoreload. See #377.
21+ const RECONNECT_DELAYS_MS = [ 1500 , 2500 , 4000 ] ;
22+ const POST_OP_RECONNECT_WINDOW_MS = 8000 ;
23+ // How long to wait for the post-op disconnect to fire (~2s observed).
24+ const POST_OP_DISCONNECT_GRACE_MS = 4000 ;
25+ // Wait after GATT reconnects so the VM finishes booting before the next op.
26+ const POST_RECONNECT_SETTLE_MS = 2000 ;
27+
1728let btnRequestBluetoothDevice , btnReconnect ;
1829
1930class BLEWorkflow extends Workflow {
@@ -34,6 +45,48 @@ class BLEWorkflow extends Workflow {
3445 { reconnect : false , request : true } ,
3546 { reconnect : true , request : true } ,
3647 ] ;
48+ // Mutating-op disconnects within this window trigger silent reconnect.
49+ this . _lastMutatingOpAt = 0 ;
50+ this . _silentReconnectInFlight = false ;
51+ this . _silentReconnectPromise = null ;
52+ }
53+
54+ // Called by the FileTransferClient wrapper right before any mutating
55+ // BLE-FT op (write/move/delete/mkdir). Marks the moment so that the
56+ // disconnect handler can recognize the next disconnect as an expected
57+ // autoreload and recover silently.
58+ markMutatingOp ( ) {
59+ this . _lastMutatingOpAt = Date . now ( ) ;
60+ }
61+
62+ _wasMutatingOpRecent ( ) {
63+ return ( Date . now ( ) - this . _lastMutatingOpAt ) < POST_OP_RECONNECT_WINDOW_MS ;
64+ }
65+
66+ // Awaited by mutating-op wrappers so callers see a live GATT before proceeding.
67+ async awaitPostOpReconnect ( ) {
68+ const startedAt = Date . now ( ) ;
69+ while ( Date . now ( ) - startedAt < POST_OP_DISCONNECT_GRACE_MS ) {
70+ // gatt.connected flips false before gattserverdisconnected fires.
71+ if ( this . bleDevice && this . bleDevice . gatt && ! this . bleDevice . gatt . connected ) {
72+ const waitForPromise = Date . now ( ) ;
73+ while ( ! this . _silentReconnectPromise && Date . now ( ) - waitForPromise < POST_OP_DISCONNECT_GRACE_MS ) {
74+ await sleep ( 25 ) ;
75+ }
76+ break ;
77+ }
78+ if ( this . _silentReconnectPromise ) {
79+ break ;
80+ }
81+ await sleep ( 25 ) ;
82+ }
83+ if ( this . _silentReconnectPromise ) {
84+ try {
85+ await this . _silentReconnectPromise ;
86+ } catch ( e ) {
87+ console . log ( 'awaitPostOpReconnect: silent reconnect rejected:' , e ) ;
88+ }
89+ }
3790 }
3891
3992 // This is called when a user clicks the main disconnect button
@@ -213,7 +266,7 @@ class BLEWorkflow extends Workflow {
213266 }
214267
215268 console . log ( 'Initializing File Transfer Client...' ) ;
216- this . initFileClient ( new FileTransferClient ( this . bleDevice , 65536 ) ) ;
269+ this . initFileClient ( new FileTransferClient ( this . bleDevice , 65536 , this ) ) ;
217270 await this . fileHelper . bond ( ) ;
218271 await this . connectToSerial ( ) ;
219272
@@ -248,10 +301,26 @@ class BLEWorkflow extends Workflow {
248301 }
249302
250303 async connect ( ) {
251- let result ;
252- if ( result = await super . connect ( ) instanceof Error ) {
304+ const result = await super . connect ( ) ;
305+ if ( result instanceof Error ) {
253306 return result ;
254307 }
308+
309+ // Disconnect right after a mutating op = firmware autoreload. Reconnect silently.
310+ if ( this . bleDevice && this . _wasMutatingOpRecent ( ) ) {
311+ this . _silentReconnectPromise = this . _attemptSilentReconnect ( ) ;
312+ let ok = false ;
313+ try {
314+ ok = await this . _silentReconnectPromise ;
315+ } finally {
316+ this . _silentReconnectPromise = null ;
317+ }
318+ if ( ok ) {
319+ return ;
320+ }
321+ // Silent reconnect failed; fall through to normal reconnect.
322+ }
323+
255324 // Is this a new connection?
256325 if ( ! this . bleDevice ) {
257326 try {
@@ -266,6 +335,49 @@ class BLEWorkflow extends Workflow {
266335 }
267336 }
268337
338+ // Reconnect to the same paired device after firmware autoreload.
339+ // Reuses the existing FileTransferClient so FileDialog bindings stay live;
340+ // upstream checkConnection() re-fetches characteristics on next op.
341+ async _attemptSilentReconnect ( ) {
342+ if ( this . _silentReconnectInFlight ) {
343+ return false ;
344+ }
345+ this . _silentReconnectInFlight = true ;
346+ try {
347+ for ( const delay of RECONNECT_DELAYS_MS ) {
348+ await sleep ( delay ) ;
349+ try {
350+ console . log ( `Silent reconnect: attempting after ${ delay } ms…` ) ;
351+ this . bleServer = await this . bleDevice . gatt . connect ( ) ;
352+ if ( this . bleServer && this . bleServer . connected ) {
353+ console . log ( 'Silent reconnect: GATT reconnected, rebinding characteristics…' ) ;
354+ await this . _rebindAfterSilentReconnect ( ) ;
355+ console . log ( 'Silent reconnect succeeded.' ) ;
356+ return true ;
357+ }
358+ } catch ( error ) {
359+ console . log ( `Silent reconnect attempt failed: ${ error } . Retrying…` ) ;
360+ }
361+ }
362+ console . log ( 'Silent reconnect exhausted; falling back to manual reconnect UI.' ) ;
363+ return false ;
364+ } finally {
365+ this . _silentReconnectInFlight = false ;
366+ }
367+ }
368+
369+ // Rebind characteristics after silent reconnect without rebuilding fileHelper.
370+ async _rebindAfterSilentReconnect ( ) {
371+ // Re-attach disconnect listener (idempotent).
372+ this . bleDevice . removeEventListener ( 'gattserverdisconnected' , this . onDisconnected . bind ( this ) ) ;
373+ this . bleDevice . addEventListener ( 'gattserverdisconnected' , this . onDisconnected . bind ( this ) ) ;
374+
375+ // NUS serial chars need re-fetch; BLE-FT chars re-fetched lazily by checkConnection().
376+ await this . connectToSerial ( ) ;
377+
378+ this . updateConnected ( CONNSTATE . connected ) ;
379+ }
380+
269381 updateConnected ( connectionState ) {
270382 super . updateConnected ( connectionState ) ;
271383 this . connectionStep ( 2 ) ;
0 commit comments