diff --git a/src/ble/alerts/NoHub.tsx b/src/ble/alerts/NoHub.tsx
index ce80b181..61fc0728 100644
--- a/src/ble/alerts/NoHub.tsx
+++ b/src/ble/alerts/NoHub.tsx
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
-import './noHub.scss';
+import './index.scss';
import { AnchorButton, Button, Intent } from '@blueprintjs/core';
import React from 'react';
import { appName, pybricksBluetoothTroubleshootingUrl } from '../../app/constants';
@@ -30,7 +30,7 @@ const NoHub: React.VoidFunctionComponent = ({ onFlashFirmware }) =>
})}
{i18n.translate(I18nId.NoHubSuggestion2)}
-
+
diff --git a/src/ble/alerts/OldFirmware.tsx b/src/ble/alerts/OldFirmware.tsx
new file mode 100644
index 00000000..a396bbc0
--- /dev/null
+++ b/src/ble/alerts/OldFirmware.tsx
@@ -0,0 +1,40 @@
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2022 The Pybricks Authors
+
+import './index.scss';
+import { Button, Intent } from '@blueprintjs/core';
+import React from 'react';
+import { CreateToast } from '../../i18nToaster';
+import { I18nId, useI18n } from './i18n';
+
+type OldFirmwareProps = {
+ onFlashFirmware: () => void;
+};
+
+const OldFirmware: React.VoidFunctionComponent
= ({
+ onFlashFirmware,
+}) => {
+ const i18n = useI18n();
+
+ return (
+ <>
+ {i18n.translate(I18nId.OldFirmwareMessage)}
+
+
+
+ >
+ );
+};
+
+export const oldFirmware: CreateToast = (
+ onAction,
+) => {
+ return {
+ message: onAction('flashFirmware')} />,
+ icon: 'info-sign',
+ intent: Intent.PRIMARY,
+ onDismiss: () => onAction('dismiss'),
+ };
+};
diff --git a/src/ble/alerts/i18n.ts b/src/ble/alerts/i18n.ts
index 81045edd..20fca0a1 100644
--- a/src/ble/alerts/i18n.ts
+++ b/src/ble/alerts/i18n.ts
@@ -24,4 +24,6 @@ export enum I18nId {
NoHubSuggestion2 = 'noHub.suggestion2',
NoHubFlashFirmwareButton = 'noHub.flashFirmwareButton',
NoHubTroubleshootButton = 'noHub.troubleshootButton',
+ OldFirmwareMessage = 'oldFirmware.message',
+ OldFirmwareFlashFirmwareLabel = 'oldFirmware.flashFirmware.label',
}
diff --git a/src/ble/alerts/noHub.scss b/src/ble/alerts/index.scss
similarity index 86%
rename from src/ble/alerts/noHub.scss
rename to src/ble/alerts/index.scss
index c011e9a3..7771c733 100644
--- a/src/ble/alerts/noHub.scss
+++ b/src/ble/alerts/index.scss
@@ -1,7 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
-.pb-ble-alerts-noHub {
+.pb-ble-alerts {
&-buttons {
display: flex;
gap: 10px;
diff --git a/src/ble/alerts/index.ts b/src/ble/alerts/index.ts
index 2e5ec4dd..22b7ffc0 100644
--- a/src/ble/alerts/index.ts
+++ b/src/ble/alerts/index.ts
@@ -6,6 +6,14 @@ import { missingService } from './MissingService';
import { noGatt } from './NoGatt';
import { noHub } from './NoHub';
import { noWebBluetooth } from './NoWebBluetooth';
+import { oldFirmware } from './OldFirmware';
// gathers all of the alert creation functions for passing up to the top level
-export default { bluetoothNotAvailable, missingService, noGatt, noHub, noWebBluetooth };
+export default {
+ bluetoothNotAvailable,
+ missingService,
+ noGatt,
+ noHub,
+ noWebBluetooth,
+ oldFirmware,
+};
diff --git a/src/ble/alerts/translations/en.json b/src/ble/alerts/translations/en.json
index 94c9f34d..fe9bfd18 100644
--- a/src/ble/alerts/translations/en.json
+++ b/src/ble/alerts/translations/en.json
@@ -23,5 +23,11 @@
"suggestion2": "If you have flashed the Pybricks firmware to the hub already and you are still having problems connecting, please visit the troubleshooting guide.",
"flashFirmwareButton": "Flash Firmware",
"troubleshootButton": "Troubleshooting Tips"
+ },
+ "oldFirmware": {
+ "message": "A new firmware version is available for this hub. Please install the latest version to use all new features.",
+ "flashFirmware": {
+ "label": "Flash firmware now"
+ }
}
}
diff --git a/src/ble/sagas.test.ts b/src/ble/sagas.test.ts
index f58f2086..5b973691 100644
--- a/src/ble/sagas.test.ts
+++ b/src/ble/sagas.test.ts
@@ -1,7 +1,6 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
-import { HubType } from '@pybricks/firmware';
import { MockProxy, mock } from 'jest-mock-extended';
import { AsyncSaga } from '../../test';
import { alertsDidShowAlert, alertsShowAlert } from '../alerts/actions';
@@ -17,6 +16,7 @@ import {
softwareRevisionStringUUID,
} from '../ble-device-info-service/protocol';
import { encodeInfo } from '../ble-device-info-service/protocol.test';
+import { HubType } from '../ble-lwp3-service/protocol';
import {
nordicUartRxCharUUID,
nordicUartServiceUUID,
@@ -204,6 +204,8 @@ async function runConnectUntil(saga: AsyncSaga, point: ConnectRunPoint): Promise
bleDIServiceDidReceiveFirmwareRevision('3.2.0b2'),
);
+ await expect(saga.take()).resolves.toEqual(alertsShowAlert('ble', 'oldFirmware'));
+
if (point === ConnectRunPoint.DidReceiveFirmwareRevision) {
return;
}
diff --git a/src/ble/sagas.ts b/src/ble/sagas.ts
index 87d43e1f..f291af2a 100644
--- a/src/ble/sagas.ts
+++ b/src/ble/sagas.ts
@@ -6,7 +6,9 @@
// TODO: this file needs to be combined with the firmware BLE connection management
// to reduce duplicated code
+import { firmwareVersion } from '@pybricks/firmware';
import { Task, buffers, eventChannel } from 'redux-saga';
+import { satisfies } from 'semver';
import {
call,
cancel,
@@ -14,6 +16,7 @@ import {
fork,
put,
select,
+ spawn,
take,
takeEvery,
} from 'typed-redux-saga/macro';
@@ -54,6 +57,7 @@ import {
import { firmwareInstallPybricks } from '../firmware/actions';
import { RootState } from '../reducers';
import { ensureError } from '../utils';
+import { pythonVersionToSemver } from '../utils/version';
import {
bleConnectPybricks as bleConnectPybricks,
bleDidConnectPybricks,
@@ -219,6 +223,35 @@ function* handleBleConnectPybricks(): Generator {
);
yield* put(bleDIServiceDidReceiveFirmwareRevision(firmwareRevision));
+ // notify user if old firmware
+ if (
+ satisfies(
+ pythonVersionToSemver(firmwareRevision),
+ `<${pythonVersionToSemver(firmwareVersion)}`,
+ )
+ ) {
+ yield* put(alertsShowAlert('ble', 'oldFirmware'));
+
+ // initiate flashing firmware if user requested
+ const flashIfRequested = function* () {
+ const { action } = yield* take<
+ ReturnType>
+ >(
+ alertsDidShowAlert.when(
+ (a) => a.domain === 'ble' && a.specific === 'oldFirmware',
+ ),
+ );
+
+ if (action === 'flashFirmware') {
+ yield* put(firmwareInstallPybricks());
+ }
+ };
+
+ // have to spawn so that we don't block the task and it still works
+ // if parent task ends
+ yield* spawn(flashIfRequested);
+ }
+
const softwareVersionChar = yield* call(() =>
deviceInfoService.getCharacteristic(softwareRevisionStringUUID),
);
diff --git a/src/notifications/i18n.ts b/src/notifications/i18n.ts
index e8fd902d..eceeb4b2 100644
--- a/src/notifications/i18n.ts
+++ b/src/notifications/i18n.ts
@@ -39,5 +39,4 @@ export enum I18nId {
ServiceWorkerUpdateMessage = 'serviceWorker.update.message',
ServiceWorkerUpdateAction = 'serviceWorker.update.action',
MpyError = 'mpy.error',
- CheckFirmwareTooOld = 'check.firmwareTooOld',
}
diff --git a/src/notifications/sagas.test.ts b/src/notifications/sagas.test.ts
index ed48626c..4ad88de6 100644
--- a/src/notifications/sagas.test.ts
+++ b/src/notifications/sagas.test.ts
@@ -2,16 +2,11 @@
// Copyright (c) 2021-2022 The Pybricks Authors
import { IToaster } from '@blueprintjs/core';
-import {
- FirmwareReaderError,
- FirmwareReaderErrorCode,
- firmwareVersion,
-} from '@pybricks/firmware';
+import { FirmwareReaderError, FirmwareReaderErrorCode } from '@pybricks/firmware';
import { I18nManager } from '@shopify/react-i18n';
import { AnyAction } from 'redux';
import { AsyncSaga, uuid } from '../../test';
import { appDidCheckForUpdate } from '../app/actions';
-import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions';
import { editorDidFailToOpenFile } from '../editor/actions';
import { EditorError } from '../editor/error';
import {
@@ -91,7 +86,6 @@ test.each([
didFailToFinish(FailToFinishReasonType.FirmwareSize),
didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')),
appDidCheckForUpdate(false),
- bleDIServiceDidReceiveFirmwareRevision('3.0.0'),
fileStorageDidFailToInitialize(new Error('test error')),
explorerDidFailToImportFiles(new Error('test error')),
explorerDidFailToCreateNewFile(new Error('test error')),
@@ -118,7 +112,6 @@ test.each([
didFailToFinish(FailToFinishReasonType.FailedToConnect),
serviceWorkerDidSucceed(),
appDidCheckForUpdate(true),
- bleDIServiceDidReceiveFirmwareRevision(firmwareVersion),
explorerDidFailToImportFiles(new DOMException('test message', 'AbortError')),
explorerDidFailToCreateNewFile(new DOMException('test message', 'AbortError')),
explorerDidFailToDuplicateFile(
diff --git a/src/notifications/sagas.ts b/src/notifications/sagas.ts
index 33636225..89a84665 100644
--- a/src/notifications/sagas.ts
+++ b/src/notifications/sagas.ts
@@ -4,16 +4,13 @@
// Saga for managing notifications (toasts)
import { ActionProps, IToaster, IconName, Intent, LinkProps } from '@blueprintjs/core';
-import { firmwareVersion } from '@pybricks/firmware';
import { Replacements } from '@shopify/react-i18n';
import React from 'react';
import { channel } from 'redux-saga';
-import * as semver from 'semver';
import { delay, getContext, put, take, takeEvery } from 'typed-redux-saga/macro';
import { getAlertProps } from '../alerts';
import { appDidCheckForUpdate, appReload } from '../app/actions';
import { appName } from '../app/constants';
-import { bleDIServiceDidReceiveFirmwareRevision } from '../ble-device-info-service/actions';
import { editorDidFailToOpenFile } from '../editor/actions';
import { EditorError } from '../editor/error';
import {
@@ -31,7 +28,6 @@ import {
} from '../lwp3-bootloader/actions';
import { didCompile, didFailToCompile } from '../mpy/actions';
import { serviceWorkerDidUpdate } from '../service-worker/actions';
-import { pythonVersionToSemver } from '../utils/version';
import NotificationAction from './NotificationAction';
import NotificationMessage from './NotificationMessage';
import { add as addNotification } from './actions';
@@ -309,21 +305,6 @@ function* showNoUpdateInfo(action: ReturnType): Gen
});
}
-function* checkVersion(
- action: ReturnType,
-): Generator {
- // ensure the actual hub firmware version is the same as the shipped
- // firmware version or newer
- if (
- !semver.satisfies(
- pythonVersionToSemver(action.version),
- `>=${pythonVersionToSemver(firmwareVersion)}`,
- )
- ) {
- yield* showSingleton(Level.Error, I18nId.CheckFirmwareTooOld);
- }
-}
-
function* showFileStorageFailToInitialize(
action: ReturnType,
): Generator {
@@ -405,7 +386,6 @@ export default function* (): Generator {
yield* takeEvery(addNotification, handleAddNotification);
yield* takeEvery(serviceWorkerDidUpdate, showServiceWorkerUpdate);
yield* takeEvery(appDidCheckForUpdate, showNoUpdateInfo);
- yield* takeEvery(bleDIServiceDidReceiveFirmwareRevision, checkVersion);
yield* takeEvery(fileStorageDidFailToInitialize, showFileStorageFailToInitialize);
yield* takeEvery(explorerDidFailToImportFiles, showExplorerFailToImportFiles);
yield* takeEvery(explorerDidFailToCreateNewFile, showExplorerFailToCreateFile);
diff --git a/src/notifications/translations/en.json b/src/notifications/translations/en.json
index f2d956aa..5e5de875 100644
--- a/src/notifications/translations/en.json
+++ b/src/notifications/translations/en.json
@@ -43,8 +43,5 @@
"message": "A new version of {appName} is available. Click {action} to start using the new version.",
"action": "Restart"
}
- },
- "check": {
- "firmwareTooOld": "A new firmware version is available for this hub. Please install the latest version to use all new features."
}
}