notifications: add firmware version check

This adds a firmware version check that shows an error message if the
connected hub is running an older Pybricks firmware version.

Issue: https://github.com/pybricks/support/issues/482
This commit is contained in:
David Lechner
2021-09-15 16:49:51 -05:00
committed by David Lechner
parent 094807ccb5
commit 788b77fda0
13 changed files with 106 additions and 14 deletions
+5
View File
@@ -4,6 +4,11 @@
## [Unreleased]
### Added
- Show error message if connected hub is running old firmware ([support#482]).
[support#482]: https://github.com/pybricks/support/issues/482
## [1.1.0-beta.5] - 2021-08-30
### Changed
+2
View File
@@ -26,6 +26,7 @@
"@types/react-redux": "^7.1.18",
"@types/react-splitter-layout": "^3.0.2",
"@types/redux-logger": "^3.0.9",
"@types/semver": "^7.3.8",
"@types/web-bluetooth": "^0.0.11",
"@types/zen-push": "^0.1.1",
"babel-plugin-macros": "^3.0.1",
@@ -49,6 +50,7 @@
"redux": "^4.0.5",
"redux-logger": "^3.0.6",
"redux-saga": "^1.1.3",
"semver": "^7.3.5",
"spdx-satisfies": "^5.0.0",
"typed-redux-saga": "^1.3.1",
"typescript": "~4.4.3",
+1 -4
View File
@@ -206,9 +206,6 @@ function* connect(_action: BleDeviceConnectAction): Generator {
return;
}
// TODO: save firmware version for later use
console.log(`Hub firmware version: ${firmwareVersion}`);
let softwareVersionChar: BluetoothRemoteGATTCharacteristic;
try {
softwareVersionChar = yield* call(
@@ -422,7 +419,7 @@ function* connect(_action: BleDeviceConnectAction): Generator {
tasks.push(yield* takeEvery(BleUartActionType.Write, writeUart, uartRxChar));
yield* put(didConnect());
yield* put(didConnect(firmwareVersion));
// wait for disconnection
yield* takeMaybe(disconnectChannel);
+7 -4
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
//
// Actions for managing Bluetooth Low Energy connections.
@@ -44,13 +44,16 @@ export function connect(): BleDeviceConnectAction {
return { type: BleDeviceActionType.Connect };
}
export type BleDeviceDidConnectAction = Action<BleDeviceActionType.DidConnect>;
export type BleDeviceDidConnectAction = Action<BleDeviceActionType.DidConnect> & {
firmwareVersion: string;
};
/**
* Creates an action that indicates a device was connected.
* @param firmwareVersion The firmware version of the hub (e.g. 3.0.0a1)
*/
export function didConnect(): BleDeviceDidConnectAction {
return { type: BleDeviceActionType.DidConnect };
export function didConnect(firmwareVersion: string): BleDeviceDidConnectAction {
return { type: BleDeviceActionType.DidConnect, firmwareVersion };
}
export enum BleDeviceFailToConnectReasonType {
+5 -2
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { firmwareVersion } from '@pybricks/firmware';
import { Action } from '../actions';
import {
BleDeviceDidFailToConnectReason,
@@ -29,8 +30,10 @@ test('connection', () => {
.connection,
).toBe(BleConnectionState.Connecting);
expect(
reducers({ connection: BleConnectionState.Connecting } as State, didConnect())
.connection,
reducers(
{ connection: BleConnectionState.Connecting } as State,
didConnect(firmwareVersion),
).connection,
).toBe(BleConnectionState.Connected);
expect(
reducers(
+5 -2
View File
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { firmwareVersion } from '@pybricks/firmware';
import { Action } from '../actions';
import { statusReportEvent } from '../ble-pybricks-service/actions';
import { Status, statusToFlag } from '../ble-pybricks-service/protocol';
@@ -27,8 +28,10 @@ test('initial state', () => {
describe('runtime', () => {
test('', () => {
expect(
reducers({ runtime: HubRuntimeState.Disconnected } as State, didConnect())
.runtime,
reducers(
{ runtime: HubRuntimeState.Disconnected } as State,
didConnect(firmwareVersion),
).runtime,
).toBe(HubRuntimeState.Unknown);
});
+3
View File
@@ -39,5 +39,8 @@
"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."
}
}
+2 -1
View File
@@ -1,5 +1,5 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2020 The Pybricks Authors
// Copyright (c) 2020-2021 The Pybricks Authors
//
// Notification translation keys.
@@ -29,4 +29,5 @@ export enum MessageId {
ServiceWorkerUpdateMessage = 'serviceWorker.update.message',
ServiceWorkerUpdateAction = 'serviceWorker.update.action',
MpyError = 'mpy.error',
CheckFirmwareTooOld = 'check.firmwareTooOld',
}
+8 -1
View File
@@ -2,13 +2,18 @@
// Copyright (c) 2021 The Pybricks Authors
import { IToaster } from '@blueprintjs/core';
import { FirmwareReaderError, FirmwareReaderErrorCode } from '@pybricks/firmware';
import {
FirmwareReaderError,
FirmwareReaderErrorCode,
firmwareVersion,
} from '@pybricks/firmware';
import { AsyncSaga } from '../../test';
import { Action } from '../actions';
import { didCheckForUpdate } from '../app/actions';
import {
BleDeviceFailToConnectReasonType,
didFailToConnect as bleDidFailToConnect,
didConnect,
} from '../ble/actions';
import { storageChanged } from '../editor/actions';
import {
@@ -76,6 +81,7 @@ test.each([
didFailToFinish(FailToFinishReasonType.FirmwareSize),
didFailToFinish(FailToFinishReasonType.Unknown, new Error('test error')),
didCheckForUpdate(false),
didConnect('3.0.0'),
])('actions that should show notification: %o', async (action: Action) => {
const getToasts = jest.fn().mockReturnValue([]);
const show = jest.fn();
@@ -106,6 +112,7 @@ test.each([
didFailToFinish(FailToFinishReasonType.FailedToConnect),
didSucceed({} as ServiceWorkerRegistration),
didCheckForUpdate(true),
didConnect(firmwareVersion),
])('actions that should not show a notification: %o', async (action: Action) => {
const getToasts = jest.fn().mockReturnValue([]);
const show = jest.fn();
+18
View File
@@ -10,14 +10,17 @@ import {
IconName,
Intent,
} 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 { AppActionType, AppDidCheckForUpdateAction, reload } from '../app/actions';
import { appName } from '../app/constants';
import {
BleDeviceActionType,
BleDeviceDidConnectAction,
BleDeviceDidFailToConnectAction,
BleDeviceFailToConnectReasonType,
} from '../ble/actions';
@@ -37,6 +40,7 @@ import {
ServiceWorkerAction,
ServiceWorkerActionType,
} from '../service-worker/actions';
import { pythonVersionToSemver } from '../utils/version';
import NotificationAction from './NotificationAction';
import NotificationMessage from './NotificationMessage';
import UnexpectedErrorNotification from './UnexpectedErrorNotification';
@@ -382,6 +386,19 @@ function* showNoUpdateInfo(action: AppDidCheckForUpdateAction): Generator {
});
}
function* checkVersion(action: BleDeviceDidConnectAction): Generator {
// ensure the actual hub firmware version is the same as the shipped
// firmware version or newer
if (
!semver.satisfies(
pythonVersionToSemver(action.firmwareVersion),
`>=${pythonVersionToSemver(firmwareVersion)}`,
)
) {
yield* showSingleton(Level.Error, MessageId.CheckFirmwareTooOld);
}
}
export default function* (): Generator {
yield* takeEvery(
BleDeviceActionType.DidFailToConnect,
@@ -398,4 +415,5 @@ export default function* (): Generator {
yield* takeEvery(NotificationActionType.Add, addNotification);
yield* takeEvery(ServiceWorkerActionType.DidUpdate, showServiceWorkerUpdate);
yield* takeEvery(AppActionType.DidCheckForUpdate, showNoUpdateInfo);
yield* takeEvery(BleDeviceActionType.DidConnect, checkVersion);
}
+20
View File
@@ -0,0 +1,20 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import { pythonVersionToSemver } from './version';
describe('pythonVersionToSemver', () => {
test.each([
['v1.0.0', 'v1.0.0'],
['v1.0.0a1', 'v1.0.0-alpha.1'],
['v1.0.0b2', 'v1.0.0-beta.2'],
['v1.0.0c3', 'v1.0.0-candidate.3'],
['v1.0.0f4', 'v1.0.0-final.4'],
])('valid version %s', (version, expected) => {
expect(pythonVersionToSemver(version)).toBe(expected);
});
test('invalid version', () => {
expect(() => pythonVersionToSemver('not a version')).toThrow();
});
});
+25
View File
@@ -0,0 +1,25 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021 The Pybricks Authors
import * as semver from 'semver';
/**
* Converts a Python short version string (e.g. '1.0.0b1') to a valid semver
* string (e.g. 1.0.0-beta.1).
*
* @param version The Python version string.
* @returns A modified version string that is a valid semver.
*/
export function pythonVersionToSemver(version: string): string {
const newVersion = version
.replace('a', '-alpha.')
.replace('b', '-beta.')
.replace('c', '-candidate.')
.replace('f', '-final.');
if (!semver.valid(newVersion)) {
throw new Error('invalid version');
}
return newVersion;
}
+5
View File
@@ -2146,6 +2146,11 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==
"@types/semver@^7.3.8":
version "7.3.8"
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.8.tgz#508a27995498d7586dcecd77c25e289bfaf90c59"
integrity sha512-D/2EJvAlCEtYFEYmmlGwbGXuK886HzyCc3nZX/tkFTQdEU8jZDAgiv08P162yB17y4ZXZoq7yFAnW4GDBb9Now==
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"