11// SPDX-License-Identifier: MIT
2- // Copyright (c) 2020-2025 The Pybricks Authors
2+ // Copyright (c) 2020-2026 The Pybricks Authors
33
44import {
55 FirmwareReader ,
@@ -1138,6 +1138,7 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11381138 function * sendCommand (
11391139 command : number ,
11401140 payload ?: Uint8Array ,
1141+ options ?: { timeoutms ?: number } ,
11411142 ) : SagaGenerator < [ DataView | undefined , Error | undefined ] > {
11421143 // We need to start listing for reply before sending command in order
11431144 // to avoid race conditions.
@@ -1163,9 +1164,11 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11631164 return [ undefined , sendError ] ;
11641165 }
11651166
1167+ const timeoutms = options ?. timeoutms ?? 5000 ;
1168+
11661169 const { reply, timeout } = yield * race ( {
11671170 reply : take ( replyChannel ) ,
1168- timeout : delay ( 5000 ) ,
1171+ timeout : delay ( timeoutms ) ,
11691172 } ) ;
11701173
11711174 replyChannel . close ( ) ;
@@ -1238,63 +1241,89 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
12381241 // FIXME: should be called much earlier.
12391242 yield * put ( didStart ( ) ) ;
12401243
1241- const sectorSize = 64 * 1024 ; // flash memory sector size
12421244 const maxPayloadSize = 1018 ; // maximum payload size for EV3 commands
12431245
1244- for ( let i = 0 ; i < action . firmware . byteLength ; i += sectorSize ) {
1245- const sectorData = action . firmware . slice ( i , i + sectorSize ) ;
1246- assert ( sectorData . byteLength <= sectorSize , 'sector data too large' ) ;
1246+ const erasePayload = new DataView ( new ArrayBuffer ( 8 ) ) ;
1247+ erasePayload . setUint32 ( 0 , 0 , true ) ; // start address
1248+ erasePayload . setUint32 ( 4 , action . firmware . byteLength , true ) ; // size
1249+ console . debug ( `Erasing bytes [0x0, ${ hex ( action . firmware . byteLength , 0 ) } )` ) ;
1250+
1251+ yield * put (
1252+ alertsShowAlert (
1253+ 'firmware' ,
1254+ 'flashProgress' ,
1255+ {
1256+ action : 'erase' ,
1257+ progress : undefined ,
1258+ } ,
1259+ firmwareBleProgressToastId ,
1260+ true ,
1261+ ) ,
1262+ ) ;
1263+
1264+ // Measured erase rate is approximately .25 kB/ms. This was on a powerful
1265+ // computer so it may be that flashing from something like a raspberry pi
1266+ // would take longer. We'll set a timeout three times as long as would have
1267+ // taken at the measured rate.
1268+ const eraseTimeoutMs = ( action . firmware . byteLength / 256 ) * 1000 * 3 ;
1269+ const startTime = Date . now ( ) ;
1270+ const [ , eraseError ] = yield * sendCommand (
1271+ 0xf0 ,
1272+ new Uint8Array ( erasePayload . buffer ) ,
1273+ { timeoutms : eraseTimeoutMs } ,
1274+ ) ;
1275+ console . debug (
1276+ `EV3 erase took ${ Date . now ( ) - startTime } ms for ${
1277+ action . firmware . byteLength
1278+ } bytes, timeout was ${ eraseTimeoutMs } ms`,
1279+ ) ;
1280+
1281+ if ( eraseError ) {
1282+ yield * put (
1283+ alertsShowAlert ( 'alerts' , 'unexpectedError' , {
1284+ error : eraseError ,
1285+ } ) ,
1286+ ) ;
1287+ // FIXME: should have a better error reason
1288+ yield * put ( didFailToFinish ( FailToFinishReasonType . Unknown , eraseError ) ) ;
1289+ yield * put ( firmwareDidFailToFlashEV3 ( ) ) ;
1290+ yield * cleanup ( ) ;
1291+ return ;
1292+ }
12471293
1248- const erasePayload = new DataView ( new ArrayBuffer ( 8 ) ) ;
1249- erasePayload . setUint32 ( 0 , i , true ) ;
1250- erasePayload . setUint32 ( 4 , sectorData . byteLength , true ) ;
1251- const [ , eraseError ] = yield * sendCommand (
1252- 0xf0 ,
1253- new Uint8Array ( erasePayload . buffer ) ,
1294+ // If we don't write an exact multiple of the sector size, the flash process
1295+ // will hang on the last write we send.
1296+ const firmware = action . firmware ;
1297+ for ( let i = 0 ; i < firmware . byteLength ; i += maxPayloadSize ) {
1298+ const payload = firmware . slice ( i , i + maxPayloadSize ) ;
1299+ console . debug (
1300+ `Programming bytes [${ hex ( i , 0 ) } , ${ hex ( i + maxPayloadSize , 0 ) } )` ,
12541301 ) ;
12551302
1256- if ( eraseError ) {
1303+ const [ , sendError ] = yield * sendCommand ( 0xf2 , new Uint8Array ( payload ) ) ;
1304+ if ( sendError ) {
12571305 yield * put (
12581306 alertsShowAlert ( 'alerts' , 'unexpectedError' , {
1259- error : eraseError ,
1307+ error : sendError ,
12601308 } ) ,
12611309 ) ;
12621310 // FIXME: should have a better error reason
1263- yield * put ( didFailToFinish ( FailToFinishReasonType . Unknown , eraseError ) ) ;
1311+ yield * put ( didFailToFinish ( FailToFinishReasonType . Unknown , sendError ) ) ;
12641312 yield * put ( firmwareDidFailToFlashEV3 ( ) ) ;
12651313 yield * cleanup ( ) ;
12661314 return ;
12671315 }
12681316
1269- for ( let j = 0 ; j < sectorData . byteLength ; j += maxPayloadSize ) {
1270- const payload = sectorData . slice ( j , j + maxPayloadSize ) ;
1271-
1272- const [ , sendError ] = yield * sendCommand ( 0xf2 , new Uint8Array ( payload ) ) ;
1273- if ( sendError ) {
1274- yield * put (
1275- alertsShowAlert ( 'alerts' , 'unexpectedError' , {
1276- error : sendError ,
1277- } ) ,
1278- ) ;
1279- // FIXME: should have a better error reason
1280- yield * put ( didFailToFinish ( FailToFinishReasonType . Unknown , sendError ) ) ;
1281- yield * put ( firmwareDidFailToFlashEV3 ( ) ) ;
1282- yield * cleanup ( ) ;
1283- return ;
1284- }
1285- }
1286-
1287- yield * put (
1288- didProgress ( ( i + sectorData . byteLength ) / action . firmware . byteLength ) ,
1289- ) ;
1317+ const progress = ( i + payload . byteLength ) / firmware . byteLength ;
1318+ yield * put ( didProgress ( progress ) ) ;
12901319
12911320 yield * put (
12921321 alertsShowAlert (
12931322 'firmware' ,
12941323 'flashProgress' ,
12951324 {
12961325 action : 'flash' ,
1297- progress : ( i + sectorData . byteLength ) / action . firmware . byteLength ,
1326+ progress : progress ,
12981327 } ,
12991328 firmwareBleProgressToastId ,
13001329 true ,
0 commit comments