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." } }