@@ -477,83 +477,91 @@ onboard_logging.initialize = function (callback) {
477477 }
478478
479479 function flash_save_begin ( ) {
480- if ( GUI . connected_to ) {
481- self . blockSize = self . BLOCK_SIZE ;
482-
483- // Begin by refreshing the occupied size in case it changed while the tab was open
484- flash_update_summary ( function ( ) {
485- const maxBytes = FC . DATAFLASH . usedSize ;
486-
487- let openedFile ;
488- prepare_file ( function ( fileWriter ) {
489- let nextAddress = 0 ;
490- let totalBytesCompressed = 0 ;
491-
492- show_saving_dialog ( ) ;
480+ if ( ! GUI . connected_to ) return ;
481+
482+ self . blockSize = self . BLOCK_SIZE ;
483+
484+ flash_update_summary ( async ( ) => {
485+ const maxBytes = FC . DATAFLASH . usedSize ;
486+ let openedFile ;
487+ let totalBytesCompressed = 0 ;
488+ show_saving_dialog ( ) ;
489+
490+ const MAX_SIMPLE_RETRIES = 5 ;
491+ const BASE_RETRY_BACKOFF_MS = 30 ; // starting backoff
492+ const INTER_BLOCK_DELAY_MS = 2 ; // small delay between successful blocks
493+ const startTime = new Date ( ) . getTime ( ) ;
494+
495+ prepare_file ( async ( fileWriter ) => {
496+ openedFile = await FileSystem . openFile ( fileWriter ) ;
497+ let nextAddress = 0 ;
498+
499+ async function readNextBlock ( ) {
500+ if ( saveCancelled || nextAddress >= maxBytes ) {
501+ mark_saving_dialog_done ( startTime , nextAddress , totalBytesCompressed ) ;
502+ await FileSystem . closeFile ( openedFile ) ;
503+ return ;
504+ }
493505
494- // START PATCH: minimal retry for null/missing blocks
495- const MAX_SIMPLE_RETRIES = 5 ;
496506 let simpleRetryCount = 0 ;
497507
498- const startTime = new Date ( ) . getTime ( ) ; // Start timestamp
499-
500- function onChunkRead ( chunkAddress , chunkDataView , bytesCompressed ) {
501- if ( chunkDataView && chunkDataView . byteLength > 0 ) {
502- // Reset retry counter after a good block
503- simpleRetryCount = 0 ;
504-
505- // --- ORIGINAL BLOCK WRITE LOGIC ---
506- const blob = new Blob ( [ chunkDataView ] ) ;
507- FileSystem . writeChunk ( openedFile , blob ) ;
508-
509- nextAddress += chunkDataView . byteLength ;
510-
511- if ( typeof bytesCompressed === "number" ) {
512- if ( totalBytesCompressed == null ) totalBytesCompressed = 0 ;
513- totalBytesCompressed += bytesCompressed ;
514- }
515-
516- $ ( ".dataflash-saving progress" ) . attr ( "value" , ( nextAddress / maxBytes ) * 100 ) ;
517-
518- if ( saveCancelled || nextAddress >= maxBytes ) {
519- mark_saving_dialog_done ( startTime , nextAddress , totalBytesCompressed ) ;
520- FileSystem . closeFile ( openedFile ) ;
521- } else {
522- mspHelper . dataflashRead ( nextAddress , self . blockSize , onChunkRead ) ;
523- }
524- // --- END ORIGINAL LOGIC ---
525- } else if ( chunkDataView && chunkDataView . byteLength === 0 ) {
526- // Zero-length block → EOF
527- mark_saving_dialog_done ( startTime , nextAddress , totalBytesCompressed ) ;
528- FileSystem . closeFile ( openedFile ) ;
529- } else {
530- // Null/missing block
531- if ( simpleRetryCount < MAX_SIMPLE_RETRIES ) {
532- simpleRetryCount ++ ;
533- if ( simpleRetryCount % 2 === 1 ) {
534- console . warn ( `Null/missing block at ${ nextAddress } , retry ${ simpleRetryCount } ` ) ;
508+ async function attemptRead ( ) {
509+ mspHelper . dataflashRead (
510+ nextAddress ,
511+ self . blockSize ,
512+ async ( chunkAddress , chunkDataView , bytesCompressed ) => {
513+ if ( chunkDataView && chunkDataView . byteLength > 0 ) {
514+ // Reset retry counter
515+ simpleRetryCount = 0 ;
516+
517+ // Write and await completion to prevent Mac buffer stalls
518+ const blob = new Blob ( [ chunkDataView ] ) ;
519+ await FileSystem . writeChunk ( openedFile , blob ) ;
520+
521+ nextAddress += chunkDataView . byteLength ;
522+ if ( typeof bytesCompressed === "number" ) {
523+ totalBytesCompressed = ( totalBytesCompressed || 0 ) + bytesCompressed ;
524+ }
525+
526+ $ ( ".dataflash-saving progress" ) . attr ( "value" , ( nextAddress / maxBytes ) * 100 ) ;
527+
528+ // Small delay between blocks to reduce Mac Chrome hangs
529+ setTimeout ( readNextBlock , INTER_BLOCK_DELAY_MS ) ;
530+ } else if ( chunkDataView && chunkDataView . byteLength === 0 ) {
531+ // EOF
532+ mark_saving_dialog_done ( startTime , nextAddress , totalBytesCompressed ) ;
533+ await FileSystem . closeFile ( openedFile ) ;
534+ } else {
535+ // Null/missing block
536+ if ( simpleRetryCount < MAX_SIMPLE_RETRIES ) {
537+ simpleRetryCount ++ ;
538+ const backoff = BASE_RETRY_BACKOFF_MS * simpleRetryCount ;
539+ if ( simpleRetryCount % 2 === 1 ) {
540+ console . warn (
541+ `Null/missing block at ${ nextAddress } , retry ${ simpleRetryCount } , backoff ${ backoff } ms` ,
542+ ) ;
543+ }
544+ setTimeout ( attemptRead , backoff ) ;
545+ } else {
546+ console . error (
547+ `Skipping null block at ${ nextAddress } after ${ MAX_SIMPLE_RETRIES } retries` ,
548+ ) ;
549+ nextAddress += self . blockSize ;
550+ readNextBlock ( ) ;
551+ }
535552 }
536- mspHelper . dataflashRead ( nextAddress , self . blockSize , onChunkRead ) ;
537- } else {
538- console . error (
539- `Skipping null block at ${ nextAddress } after ${ MAX_SIMPLE_RETRIES } retries` ,
540- ) ;
541- nextAddress += self . blockSize ; // Move to next block only after retries exhausted
542- simpleRetryCount = 0 ;
543- mspHelper . dataflashRead ( nextAddress , self . blockSize , onChunkRead ) ;
544- }
545- }
553+ } ,
554+ ) ;
546555 }
547556
548- // Fetch the initial block
549- FileSystem . openFile ( fileWriter ) . then ( ( file ) => {
550- openedFile = file ;
551- mspHelper . dataflashRead ( nextAddress , self . blockSize , onChunkRead ) ;
552- } ) ;
553- } ) ;
557+ attemptRead ( ) ;
558+ }
559+
560+ // Start reading the first block
561+ readNextBlock ( ) ;
554562 } ) ;
555- }
556- }
563+ } ) ;
564+ } // end of flash_save_begin
557565
558566 function prepare_file ( onComplete ) {
559567 const prefix = "BLACKBOX_LOG" ;
0 commit comments