mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-07-28 04:08:05 +00:00
ble/sagas: show alert when Pybricks Profile is newer
If the user sideloads a firmware that has communication features that are newer than what is supported by the app, then show a warning message.
This commit is contained in:
committed by
David Lechner
parent
618f7840da
commit
1219a377d4
@@ -4,6 +4,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Added warning message when hub has newer Pybricks Profile version than supported version.
|
||||
|
||||
## [2.2.0-beta.3] - 2023-04-24
|
||||
|
||||
### Added
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022-2023 The Pybricks Authors
|
||||
|
||||
import { Toast } from '@blueprintjs/core';
|
||||
import { act } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { testRender } from '../../../test';
|
||||
import { newPybricksProfile } from './NewPybricksProfile';
|
||||
|
||||
it('should dismiss when close is clicked', async () => {
|
||||
const callback = jest.fn();
|
||||
const toast = newPybricksProfile(callback, undefined as never);
|
||||
|
||||
const [user, message] = testRender(<Toast {...toast} />);
|
||||
|
||||
await act(() => user.click(message.getByRole('button', { name: /close/i })));
|
||||
|
||||
expect(callback).toHaveBeenCalledWith('dismiss');
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2023 The Pybricks Authors
|
||||
|
||||
import { Intent } from '@blueprintjs/core';
|
||||
import React from 'react';
|
||||
import { appName } from '../../app/constants';
|
||||
import type { CreateToast } from '../../toasterTypes';
|
||||
import { useI18n } from './i18n';
|
||||
|
||||
type NewPybricksProfileProps = {
|
||||
/** The Pybricks Profile version reported by the hub. */
|
||||
hubVersion: string;
|
||||
/** The supported Pybricks Profile version. */
|
||||
supportedVersion: string;
|
||||
};
|
||||
|
||||
const NewPybricksProfile: React.VoidFunctionComponent<NewPybricksProfileProps> = ({
|
||||
hubVersion,
|
||||
supportedVersion,
|
||||
}) => {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<>
|
||||
<p>{i18n.translate('newPybricksProfile.message')}</p>
|
||||
<p>
|
||||
{i18n.translate('newPybricksProfile.versions', {
|
||||
hubVersion: `v${hubVersion}`,
|
||||
app: appName,
|
||||
appVersion: `v${supportedVersion}`,
|
||||
})}
|
||||
</p>
|
||||
<p>
|
||||
{i18n.translate('newPybricksProfile.suggestion', {
|
||||
app: appName,
|
||||
})}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const newPybricksProfile: CreateToast<NewPybricksProfileProps> = (
|
||||
onAction,
|
||||
props,
|
||||
) => ({
|
||||
message: <NewPybricksProfile {...props} />,
|
||||
icon: 'warning-sign',
|
||||
intent: Intent.WARNING,
|
||||
timeout: 15000, // long message, need more time to read
|
||||
onDismiss: () => onAction('dismiss'),
|
||||
});
|
||||
@@ -4,6 +4,7 @@
|
||||
import { bluetoothNotAvailable } from './BluetoothNotAvailable';
|
||||
import { disconnected } from './Disconnected';
|
||||
import { missingService } from './MissingService';
|
||||
import { newPybricksProfile } from './NewPybricksProfile';
|
||||
import { noGatt } from './NoGatt';
|
||||
import { noHub } from './NoHub';
|
||||
import { noWebBluetooth } from './NoWebBluetooth';
|
||||
@@ -14,6 +15,7 @@ export default {
|
||||
bluetoothNotAvailable,
|
||||
disconnected,
|
||||
missingService,
|
||||
newPybricksProfile,
|
||||
noGatt,
|
||||
noHub,
|
||||
noWebBluetooth,
|
||||
|
||||
@@ -30,6 +30,11 @@
|
||||
"label": "Update Pybricks firmware"
|
||||
}
|
||||
},
|
||||
"newPybricksProfile": {
|
||||
"message": "The connected hub uses a newer Pybricks communication profile version. Some features may not work correctly.",
|
||||
"versions": "The hub has {hubVersion} while {app} supports {appVersion}.",
|
||||
"suggestion": "Downgrade the hub firmware or upgrade {app} to avoid problems."
|
||||
},
|
||||
"disconnected": {
|
||||
"message": "The hub is no longer connected. Please reconnect and try again."
|
||||
}
|
||||
|
||||
+68
-12
@@ -1,5 +1,5 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
// Copyright (c) 2022-2023 The Pybricks Authors
|
||||
|
||||
import { MockProxy, mock } from 'jest-mock-extended';
|
||||
import { AsyncSaga } from '../../test';
|
||||
@@ -23,9 +23,13 @@ import {
|
||||
nordicUartServiceUUID,
|
||||
nordicUartTxCharUUID,
|
||||
} from '../ble-nordic-uart-service/protocol';
|
||||
import { blePybricksServiceDidNotReceiveHubCapabilities } from '../ble-pybricks-service/actions';
|
||||
import {
|
||||
blePybricksServiceDidNotReceiveHubCapabilities,
|
||||
blePybricksServiceDidReceiveHubCapabilities,
|
||||
} from '../ble-pybricks-service/actions';
|
||||
import {
|
||||
pybricksControlEventCharacteristicUUID,
|
||||
pybricksHubCapabilitiesCharacteristicUUID,
|
||||
pybricksServiceUUID,
|
||||
} from '../ble-pybricks-service/protocol';
|
||||
import { firmwareInstallPybricks } from '../firmware/actions';
|
||||
@@ -38,7 +42,7 @@ import {
|
||||
toggleBluetooth,
|
||||
} from './actions';
|
||||
import { BleConnectionState } from './reducers';
|
||||
import ble from './sagas';
|
||||
import ble, { supportedPybricksProfileVersion } from './sagas';
|
||||
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
@@ -106,10 +110,22 @@ function createMocks(): Mocks {
|
||||
pybricksChar.startNotifications.mockResolvedValue(pybricksChar);
|
||||
pybricksChar.stopNotifications.mockResolvedValue(pybricksChar);
|
||||
|
||||
const hubCapabilitiesChar = mock<BluetoothRemoteGATTCharacteristic>();
|
||||
hubCapabilitiesChar.readValue.mockResolvedValue(
|
||||
new DataView(
|
||||
new Uint8Array([
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
]).buffer,
|
||||
),
|
||||
);
|
||||
|
||||
const pybricksService = mock<BluetoothRemoteGATTService>();
|
||||
pybricksService.getCharacteristic
|
||||
.calledWith(pybricksControlEventCharacteristicUUID)
|
||||
.mockResolvedValue(pybricksChar);
|
||||
pybricksService.getCharacteristic
|
||||
.calledWith(pybricksHubCapabilitiesCharacteristicUUID)
|
||||
.mockResolvedValue(hubCapabilitiesChar);
|
||||
|
||||
const uartRxChar = mock<BluetoothRemoteGATTCharacteristic>();
|
||||
|
||||
@@ -188,6 +204,13 @@ enum ConnectRunPoint {
|
||||
DidConnect,
|
||||
}
|
||||
|
||||
const defaultPnpId: PnpId = {
|
||||
productId: 0x80,
|
||||
productVersion: 0,
|
||||
vendorId: 919,
|
||||
vendorIdSource: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Run the "success" path of the connect saga until a given point.
|
||||
*
|
||||
@@ -221,21 +244,16 @@ async function runConnectUntil(saga: AsyncSaga, point: ConnectRunPoint): Promise
|
||||
return;
|
||||
}
|
||||
|
||||
const pnpId: PnpId = {
|
||||
productId: 0x80,
|
||||
productVersion: 0,
|
||||
vendorId: 919,
|
||||
vendorIdSource: 1,
|
||||
};
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(bleDIServiceDidReceivePnPId(pnpId));
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
bleDIServiceDidReceivePnPId(defaultPnpId),
|
||||
);
|
||||
|
||||
if (point === ConnectRunPoint.DidReceivePnpId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
blePybricksServiceDidNotReceiveHubCapabilities(pnpId, '3.2.0b2'),
|
||||
blePybricksServiceDidNotReceiveHubCapabilities(defaultPnpId, '3.2.0b2'),
|
||||
);
|
||||
|
||||
if (point === ConnectRunPoint.DidNotReceiveHubCapabilities) {
|
||||
@@ -411,6 +429,44 @@ describe('connect action is dispatched', () => {
|
||||
expect(mocks.gatt.disconnect).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should alert if pybricks profile is newer than supported', async () => {
|
||||
// increase supported minor version by 1 to get hub version
|
||||
const hubVersionParts = supportedPybricksProfileVersion.split('.');
|
||||
hubVersionParts[1] = String(Number(hubVersionParts[1]) + 1);
|
||||
const hubVersion = hubVersionParts.join('.');
|
||||
|
||||
mocks.softwareRevisionChar.readValue.mockResolvedValue(
|
||||
new DataView(encoder.encode(hubVersion).buffer),
|
||||
);
|
||||
|
||||
await runConnectUntil(saga, ConnectRunPoint.DidReceiveFirmwareRevision);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
bleDIServiceDidReceiveSoftwareRevision(hubVersion),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
alertsShowAlert('ble', 'newPybricksProfile', {
|
||||
hubVersion: hubVersion,
|
||||
supportedVersion: supportedPybricksProfileVersion,
|
||||
}),
|
||||
);
|
||||
|
||||
// then continue as normal
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
bleDIServiceDidReceivePnPId(defaultPnpId),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
blePybricksServiceDidReceiveHubCapabilities(0, 0, 0),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
bleDidConnectPybricks('test-id', 'test name'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should fail if reading pnp id characteristic fails', async () => {
|
||||
const testError = new Error('test error');
|
||||
mocks.pnpIdChar.readValue.mockRejectedValue(testError);
|
||||
|
||||
+18
-1
@@ -72,6 +72,9 @@ import {
|
||||
} from './actions';
|
||||
import { BleConnectionState } from './reducers';
|
||||
|
||||
/** The version of the Pybricks Profile version currently implemented by this file. */
|
||||
export const supportedPybricksProfileVersion = '1.3.0';
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
function* handlePybricksControlValueChanged(data: DataView): Generator {
|
||||
@@ -265,6 +268,21 @@ function* handleBleConnectPybricks(): Generator {
|
||||
);
|
||||
yield* put(bleDIServiceDidReceiveSoftwareRevision(softwareRevision));
|
||||
|
||||
// notify user if newer Pybricks Profile on hub
|
||||
if (
|
||||
semver.gte(
|
||||
softwareRevision,
|
||||
new semver.SemVer(supportedPybricksProfileVersion).inc('minor'),
|
||||
)
|
||||
) {
|
||||
yield* put(
|
||||
alertsShowAlert('ble', 'newPybricksProfile', {
|
||||
hubVersion: softwareRevision,
|
||||
supportedVersion: supportedPybricksProfileVersion,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const pnpIdChar = yield* call(() =>
|
||||
deviceInfoService.getCharacteristic(pnpIdUUID).catch((err) => {
|
||||
if (err instanceof DOMException && err.name === 'NotFoundError') {
|
||||
@@ -274,7 +292,6 @@ function* handleBleConnectPybricks(): Generator {
|
||||
throw err;
|
||||
}),
|
||||
);
|
||||
|
||||
if (!pnpIdChar) {
|
||||
// possible with firmware < v3.1.0
|
||||
throw new Error('missing PnP ID characteristic');
|
||||
|
||||
Reference in New Issue
Block a user