mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 01:23:52 +00:00
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:
committed by
David Lechner
parent
094807ccb5
commit
788b77fda0
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user