From 463b7e6b664284a784a7015d72dbc57aed50b5a3 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 2 Jan 2026 23:05:12 +0000 Subject: [PATCH 1/3] firmware/sagas: add check for new SPIKE Prime bootloader version --- src/firmware/alerts/UnsupportedDfuHub.tsx | 24 +++++++++++++++++++++++ src/firmware/alerts/index.ts | 4 +++- src/firmware/alerts/translations/en.json | 3 +++ src/firmware/sagas.ts | 11 ++++++++++- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 src/firmware/alerts/UnsupportedDfuHub.tsx diff --git a/src/firmware/alerts/UnsupportedDfuHub.tsx b/src/firmware/alerts/UnsupportedDfuHub.tsx new file mode 100644 index 00000000..23e4a910 --- /dev/null +++ b/src/firmware/alerts/UnsupportedDfuHub.tsx @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2025-2026 The Pybricks Authors + +import { Intent } from '@blueprintjs/core'; +import { Error } from '@blueprintjs/icons'; +import React from 'react'; +import type { CreateToast } from '../../toasterTypes'; +import { useI18n } from './i18n'; + +const UnsupportedDfuHub: React.FunctionComponent = () => { + const i18n = useI18n(); + return ( + <> +

{i18n.translate('unsupportedDfuHub.message')}

+ + ); +}; + +export const unsupportedDfuHub: CreateToast = (onAction) => ({ + message: , + icon: , + intent: Intent.DANGER, + onDismiss: () => onAction('dismiss'), +}); diff --git a/src/firmware/alerts/index.ts b/src/firmware/alerts/index.ts index a6187418..1d922f91 100644 --- a/src/firmware/alerts/index.ts +++ b/src/firmware/alerts/index.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2022-2025 The Pybricks Authors +// Copyright (c) 2022-2026 The Pybricks Authors import { dfuError } from './DfuError'; import { flashProgress } from './FlashProgress'; @@ -8,6 +8,7 @@ import { noDfuInterface } from './NoDfuInterface'; import { noWebHid } from './NoWebHid'; import { noWebUsb } from './NoWebUsb'; import { releaseButton } from './ReleaseButton'; +import { unsupportedDfuHub } from './UnsupportedDfuHub'; export default { dfuError, @@ -17,4 +18,5 @@ export default { noWebHid, noWebUsb, releaseButton, + unsupportedDfuHub, }; diff --git a/src/firmware/alerts/translations/en.json b/src/firmware/alerts/translations/en.json index d43aa55d..d701b2b8 100644 --- a/src/firmware/alerts/translations/en.json +++ b/src/firmware/alerts/translations/en.json @@ -22,6 +22,9 @@ "installUsbDriverButton": "Install USB Driver", "configureUdevRulesButton": "Configure udev rules" }, + "unsupportedDfuHub": { + "message": "This hub has different internal electronics and requires a different firmware. Pybricks does not support this yet. We are working on it!" + }, "noDfuInterface": { "message": "This is very unusual. The USB device did not contain the expected interface." }, diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index 7ac4fea7..553166bf 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2025 The Pybricks Authors +// Copyright (c) 2020-2026 The Pybricks Authors import { FirmwareReader, @@ -754,6 +754,15 @@ function* handleFlashUsbDfu(action: ReturnType): Gen return; } + if ( + device.productId === LegoUsbProductId.SpikePrimeBootloader && + device.deviceVersionMajor !== 1 + ) { + yield* put(alertsShowAlert('firmware', 'unsupportedDfuHub')); + yield* put(firmwareDidFailToFlashUsbDfu()); + return; + } + const dfu = new WebDFU( device, // forceInterfacesName is needed to get the flash layout map From 176288c3035013465830b24189348326095d9c5a Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 4 Jan 2026 02:33:59 +0000 Subject: [PATCH 2/3] firmware: remove progress/didProgress Remove the didProgress action and progress state, as they were not being used. --- src/firmware/actions.ts | 11 +---------- src/firmware/reducers.test.ts | 9 +-------- src/firmware/reducers.ts | 16 +--------------- src/firmware/sagas.test.ts | 33 +-------------------------------- src/firmware/sagas.ts | 9 --------- 5 files changed, 4 insertions(+), 74 deletions(-) diff --git a/src/firmware/actions.ts b/src/firmware/actions.ts index af64e80f..8a4bc484 100644 --- a/src/firmware/actions.ts +++ b/src/firmware/actions.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2020-2025 The Pybricks Authors +// Copyright (c) 2020-2026 The Pybricks Authors import { FirmwareReaderError, HubType } from '@pybricks/firmware'; import { createAction } from '../actions'; @@ -139,15 +139,6 @@ export const didStart = createAction(() => ({ type: 'flashFirmware.action.didStart', })); -/** - * Action that indicates current firmware flashing progress. - * @param value The current progress (0 to 1). - */ -export const didProgress = createAction((value: number) => { - // assert(value >= 0 && value <= 1, 'value out of range'); - return { type: 'flashFirmware.action.didProgress', value }; -}); - /** Action that indicates that flashing firmware completed successfully. */ export const didFinish = createAction(() => ({ type: 'flashFirmware.action.didFinish', diff --git a/src/firmware/reducers.test.ts b/src/firmware/reducers.test.ts index 2a3ae568..1bbd527a 100644 --- a/src/firmware/reducers.test.ts +++ b/src/firmware/reducers.test.ts @@ -1,12 +1,11 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2025 The Pybricks Authors +// Copyright (c) 2021-2026 The Pybricks Authors import { AnyAction } from 'redux'; import { FailToFinishReasonType, didFailToFinish, didFinish, - didProgress, didStart, } from './actions'; import reducers from './reducers'; @@ -26,7 +25,6 @@ test('initial state', () => { "isFirmwareFlashEV3InProgress": false, "isFirmwareFlashUsbDfuInProgress": false, "isFirmwareRestoreOfficialDfuInProgress": false, - "progress": null, "restoreOfficialDialog": { "isOpen": false, }, @@ -44,8 +42,3 @@ test('flashing', () => { ).flashing, ).toBe(false); }); - -test('progress', () => { - expect(reducers({ progress: 1 } as State, didStart()).progress).toBe(null); - expect(reducers({ progress: null } as State, didProgress(1)).progress).toBe(1); -}); diff --git a/src/firmware/reducers.ts b/src/firmware/reducers.ts index 552a6c4e..acd3e622 100644 --- a/src/firmware/reducers.ts +++ b/src/firmware/reducers.ts @@ -1,11 +1,10 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2025 The Pybricks Authors +// Copyright (c) 2021-2026 The Pybricks Authors import { Reducer, combineReducers } from 'redux'; import { didFailToFinish, didFinish, - didProgress, didStart, firmwareDidFailToFlashUsbDfu, firmwareDidFailToRestoreOfficialDfu, @@ -33,18 +32,6 @@ const flashing: Reducer = (state = false, action) => { return state; }; -const progress: Reducer = (state = null, action) => { - if (didStart.matches(action)) { - return null; - } - - if (didProgress.matches(action)) { - return action.value; - } - - return state; -}; - const isFirmwareFlashUsbDfuInProgress: Reducer = (state = false, action) => { if (firmwareFlashUsbDfu.matches(action)) { return true; @@ -100,7 +87,6 @@ export default combineReducers({ installPybricksDialog, restoreOfficialDialog, flashing, - progress, isFirmwareFlashUsbDfuInProgress, isFirmwareRestoreOfficialDfuInProgress, isFirmwareFlashEV3InProgress, diff --git a/src/firmware/sagas.test.ts b/src/firmware/sagas.test.ts index 303c61c5..97d446a4 100644 --- a/src/firmware/sagas.test.ts +++ b/src/firmware/sagas.test.ts @@ -1,5 +1,5 @@ // SPDX-License-Identifier: MIT -// Copyright (c) 2021-2024 The Pybricks Authors +// Copyright (c) 2021-2026 The Pybricks Authors import { FirmwareMetadata, @@ -43,7 +43,6 @@ import { MetadataProblem, didFailToFinish, didFinish, - didProgress, didStart, flashFirmware as flashFirmwareAction, } from './actions'; @@ -175,9 +174,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -213,9 +209,6 @@ describe('flashFirmware', () => { saga.put(programResponse(0x33, totalFirmwareSize)); - action = await saga.take(); - expect(action).toEqual(didProgress(1)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -341,9 +334,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -379,9 +369,6 @@ describe('flashFirmware', () => { saga.put(programResponse(0xe0, totalFirmwareSize)); - action = await saga.take(); - expect(action).toEqual(didProgress(1)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -1306,9 +1293,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -1482,9 +1466,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -1664,9 +1645,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -1702,9 +1680,6 @@ describe('flashFirmware', () => { saga.put(programResponse(0xf3, totalFirmwareSize)); - action = await saga.take(); - expect(action).toEqual(didProgress(1)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -2230,9 +2205,6 @@ describe('flashFirmware', () => { saga.put(didRequest(id)); - action = await saga.take(); - expect(action).toEqual(didProgress(offset / totalFirmwareSize)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( @@ -2267,9 +2239,6 @@ describe('flashFirmware', () => { saga.put(programResponse(0x27, totalFirmwareSize)); - action = await saga.take(); - expect(action).toEqual(didProgress(1)); - action = await saga.take(); expect(action).toEqual( alertsShowAlert( diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index 553166bf..f62d17e0 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -75,7 +75,6 @@ import { MetadataProblem, didFailToFinish, didFinish, - didProgress, didStart, firmwareDidFailToFlashEV3, firmwareDidFailToFlashUsbDfu, @@ -531,8 +530,6 @@ function* handleFlashFirmware(action: ReturnType): Generat ); yield* waitForDidRequest(programAction.id); - yield* put(didProgress(offset / firmware.length)); - yield* put( alertsShowAlert( 'firmware', @@ -617,8 +614,6 @@ function* handleFlashFirmware(action: ReturnType): Generat yield* disconnectAndCancel(); } - yield* put(didProgress(1)); - yield* put( alertsShowAlert( 'firmware', @@ -1286,10 +1281,6 @@ function* handleFlashEV3(action: ReturnType): Generator } } - yield* put( - didProgress((i + sectorData.byteLength) / action.firmware.byteLength), - ); - yield* put( alertsShowAlert( 'firmware', From b88b96efc1a7c8e467b69d683e80035dc6101912 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Sun, 4 Jan 2026 02:51:21 +0000 Subject: [PATCH 3/3] firmware/sagas: fix progress toasts not being removed on error Add some calls to hide the progress toasts when an error occurs during firmware flashing. Without this, the toasts would remain visible even after the error alert was shown. --- src/firmware/sagas.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/firmware/sagas.ts b/src/firmware/sagas.ts index f62d17e0..be910740 100644 --- a/src/firmware/sagas.ts +++ b/src/firmware/sagas.ts @@ -1037,6 +1037,7 @@ function* handleRestoreOfficialDfu( } } +const firmwareEv3ProgressToastId = 'firmware.ev3.progress'; const getNextEV3MessageId = createCountFunc(); function* handleFlashEV3(action: ReturnType): Generator { @@ -1256,6 +1257,7 @@ function* handleFlashEV3(action: ReturnType): Generator error: eraseError, }), ); + yield* put(alertsHideAlert(firmwareEv3ProgressToastId)); // FIXME: should have a better error reason yield* put(didFailToFinish(FailToFinishReasonType.Unknown, eraseError)); yield* put(firmwareDidFailToFlashEV3()); @@ -1273,6 +1275,7 @@ function* handleFlashEV3(action: ReturnType): Generator error: sendError, }), ); + yield* put(alertsHideAlert(firmwareEv3ProgressToastId)); // FIXME: should have a better error reason yield* put(didFailToFinish(FailToFinishReasonType.Unknown, sendError)); yield* put(firmwareDidFailToFlashEV3()); @@ -1289,7 +1292,7 @@ function* handleFlashEV3(action: ReturnType): Generator action: 'flash', progress: (i + sectorData.byteLength) / action.firmware.byteLength, }, - firmwareBleProgressToastId, + firmwareEv3ProgressToastId, true, ), ); @@ -1303,7 +1306,7 @@ function* handleFlashEV3(action: ReturnType): Generator action: 'flash', progress: 1, }, - firmwareBleProgressToastId, + firmwareEv3ProgressToastId, true, ), );