22// Copyright (c) 2020-2025 The Pybricks Authors
33
44import {
5+ FirmwareMetadataV200 ,
56 FirmwareReader ,
67 FirmwareReaderError ,
78 HubType ,
@@ -14,7 +15,7 @@ import moveHubZip from '@pybricks/firmware/build/movehub.zip';
1415import technicHubZip from '@pybricks/firmware/build/technichub.zip' ;
1516import { WebDFU } from 'dfu' ;
1617import { AnyAction } from 'redux' ;
17- import { eventChannel } from 'redux-saga' ;
18+ import { channel , eventChannel } from 'redux-saga' ;
1819import { ActionPattern } from 'redux-saga/effects' ;
1920import {
2021 SagaGenerator ,
@@ -229,6 +230,10 @@ function* loadFirmware(
229230
230231 const firmwareBase = yield * call ( ( ) => reader . readFirmwareBase ( ) ) ;
231232 const metadata = yield * call ( ( ) => reader . readMetadata ( ) ) ;
233+ let checksum : number | undefined = undefined ;
234+ let checksum_size = 4 ;
235+ let firmware : Uint8Array | undefined = undefined ;
236+ let firmwareView : DataView | undefined = undefined ;
232237
233238 // v1.x allows appending main.py to firmware, later versions do not
234239 if ( metadataIsV100 ( metadata ) || metadataIsV110 ( metadata ) ) {
@@ -280,8 +285,8 @@ function* loadFirmware(
280285 mpy . data . length +
281286 fmod ( - mpy . data . length , 4 ) ;
282287
283- const firmware = new Uint8Array ( checksumOffset + 4 ) ;
284- const firmwareView = new DataView ( firmware . buffer ) ;
288+ firmware = new Uint8Array ( checksumOffset + 4 ) ;
289+ firmwareView = new DataView ( firmware . buffer ) ;
285290
286291 if ( firmware . length > metadata [ 'max-firmware-size' ] ) {
287292 // FIXME: we should return error/throw instead
@@ -307,7 +312,7 @@ function* loadFirmware(
307312 }
308313 }
309314
310- const checksum = ( function ( ) {
315+ checksum = ( function ( ) {
311316 switch ( metadata [ 'checksum-type' ] ) {
312317 case 'sum' :
313318 return sumComplement32 (
@@ -342,28 +347,41 @@ function* loadFirmware(
342347 return { firmware, deviceId : metadata [ 'device-id' ] } ;
343348 }
344349
345- const firmware = new Uint8Array ( firmwareBase . length + 4 ) ;
346- const firmwareView = new DataView ( firmware . buffer ) ;
350+ // v2.x supports setting the checksum size to 0 to indicate no checksum
351+ else {
352+ const metadataV2 = metadata as FirmwareMetadataV200 ;
353+ checksum_size = metadataV2 [ 'checksum-size' ] ;
347354
348- firmware . set ( firmwareBase ) ;
355+ // TODO: v2.x supports setting checksum size, prior it was a fixed 4 bytes
356+ firmware = new Uint8Array ( firmwareBase . length + checksum_size ) ;
357+ firmwareView = new DataView ( firmware . buffer ) ;
358+ checksum = ( function ( ) {
359+ switch ( metadata [ 'checksum-type' ] ) {
360+ case 'sum' :
361+ console . log ( 'computing sum' ) ;
362+ return sumComplement32 (
363+ firmwareIterator ( firmwareView , metadataV2 [ 'checksum-size' ] ) ,
364+ ) ;
365+ case 'crc32' :
366+ console . log ( 'computing crc32' ) ;
367+ return crc32 (
368+ firmwareIterator ( firmwareView , metadataV2 [ 'checksum-size' ] ) ,
369+ ) ;
370+ default :
371+ return 0 ;
372+ }
373+ } ) ( ) ;
349374
350- // empty string means use default name (don't write over firmware)
351- if ( hubName ) {
352- firmware . set ( encodeHubName ( hubName , metadata ) , metadata [ 'hub-name-offset' ] ) ;
353- }
375+ firmware . set ( firmwareBase ) ;
354376
355- const checksum = ( function ( ) {
356- switch ( metadata [ 'checksum-type' ] ) {
357- case 'sum' :
358- return sumComplement32 (
359- firmwareIterator ( firmwareView , metadata [ 'checksum-size' ] ) ,
360- ) ;
361- case 'crc32' :
362- return crc32 ( firmwareIterator ( firmwareView , metadata [ 'checksum-size' ] ) ) ;
363- default :
364- return undefined ;
377+ // empty string means use default name (don't write over firmware)
378+ if ( hubName ) {
379+ firmware . set (
380+ encodeHubName ( hubName , metadata ) ,
381+ metadataV2 [ 'hub-name-offset' ] ,
382+ ) ;
365383 }
366- } ) ( ) ;
384+ }
367385
368386 if ( checksum === undefined ) {
369387 // FIXME: we should return error/throw instead
@@ -380,7 +398,9 @@ function* loadFirmware(
380398 throw new Error ( 'unreachable' ) ;
381399 }
382400
383- firmwareView . setUint32 ( firmwareBase . length , checksum , true ) ;
401+ if ( checksum_size ) {
402+ firmwareView . setUint32 ( firmwareBase . length , checksum , true ) ;
403+ }
384404
385405 return { firmware, deviceId : metadata [ 'device-id' ] } ;
386406}
@@ -922,8 +942,14 @@ function* handleInstallPybricks(): Generator {
922942 }
923943 break ;
924944 case 'usb-ev3' :
925- // TODO: implement flashing via EV3 USB
926- console . error ( 'Flashing via EV3 USB is not implemented yet' ) ;
945+ {
946+ const { firmware } = yield * loadFirmware (
947+ accepted . firmwareZip ,
948+ accepted . hubName ,
949+ ) ;
950+
951+ yield * put ( firmwareFlashEV3 ( firmware . buffer as ArrayBuffer ) ) ;
952+ }
927953 break ;
928954 }
929955}
@@ -1105,8 +1131,23 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11051131 command : number ,
11061132 payload ?: Uint8Array ,
11071133 ) : SagaGenerator < [ DataView | undefined , Error | undefined ] > {
1108- console . debug ( `EV3 send: command=${ command } , payload=${ payload } ` ) ;
1134+ // Create a channel that buffers the actions
1135+ const replyChannel = yield * call (
1136+ channel < ReturnType < typeof firmwareDidReceiveEV3Reply > > ,
1137+ ) ;
11091138
1139+ // Create a task that forwards matching actions to our channel
1140+ const forwardTask = yield * fork ( function * ( ) {
1141+ while ( true ) {
1142+ const action = yield * take ( firmwareDidReceiveEV3Reply ) ;
1143+ if ( action . replyCommand === command ) {
1144+ yield * put ( replyChannel , action ) ;
1145+ break ; // Stop after finding the matching reply
1146+ }
1147+ }
1148+ } ) ;
1149+
1150+ // Send the command
11101151 const dataBuffer = new Uint8Array ( ( payload ?. byteLength ?? 0 ) + 6 ) ;
11111152 const data = new DataView ( dataBuffer . buffer ) ;
11121153
@@ -1121,13 +1162,19 @@ function* handleFlashEV3(action: ReturnType<typeof firmwareFlashEV3>): Generator
11211162 const [ , sendError ] = yield * call ( ( ) => maybe ( hidDevice . sendReport ( 0 , data ) ) ) ;
11221163
11231164 if ( sendError ) {
1165+ yield * cancel ( forwardTask ) ;
11241166 return [ undefined , sendError ] ;
11251167 }
11261168
11271169 const { reply, timeout } = yield * race ( {
1128- reply : take ( firmwareDidReceiveEV3Reply ) ,
1129- timeout : delay ( 5000 ) ,
1170+ reply : take ( replyChannel ) ,
1171+ timeout : delay ( 15000 ) ,
11301172 } ) ;
1173+
1174+ // Always clean up
1175+ yield * cancel ( forwardTask ) ;
1176+ yield * call ( ( ) => replyChannel . close ( ) ) ;
1177+
11311178 if ( timeout ) {
11321179 return [ undefined , new Error ( 'Timeout waiting for EV3 reply' ) ] ;
11331180 }
0 commit comments