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/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/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 7ac4fea7..be910740 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,
@@ -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',
@@ -754,6 +749,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
@@ -1033,6 +1037,7 @@ function* handleRestoreOfficialDfu(
}
}
+const firmwareEv3ProgressToastId = 'firmware.ev3.progress';
const getNextEV3MessageId = createCountFunc();
function* handleFlashEV3(action: ReturnType): Generator {
@@ -1252,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());
@@ -1269,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());
@@ -1277,10 +1284,6 @@ function* handleFlashEV3(action: ReturnType): Generator
}
}
- yield* put(
- didProgress((i + sectorData.byteLength) / action.firmware.byteLength),
- );
-
yield* put(
alertsShowAlert(
'firmware',
@@ -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,
),
);