firmware/sagas: show progress for BLE flashing

This commit is contained in:
David Lechner
2022-07-27 15:19:36 -05:00
parent 1148b169d5
commit 2b7f1b1cbf
5 changed files with 103 additions and 1 deletions
+21
View File
@@ -0,0 +1,21 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2022 The Pybricks Authors
import { Intent } from '@blueprintjs/core';
import React from 'react';
import { CreateToast } from '../../i18nToaster';
import { useI18n } from './i18n';
const ReleaseButton: React.VoidFunctionComponent = () => {
const i18n = useI18n();
return <p>{i18n.translate('releaseButton.message')}</p>;
};
export const releaseButton: CreateToast = (onAction) => {
return {
message: <ReleaseButton />,
icon: 'info-sign',
intent: Intent.PRIMARY,
onDismiss: () => onAction('dismiss'),
};
};
+2
View File
@@ -5,10 +5,12 @@ import { firmwareMismatch } from './FirmwareMismatch';
import { noDfuHub } from './NoDfuHub';
import { noDfuInterface } from './NoDfuInterface';
import { noWebUsb } from './NoWebUsb';
import { releaseButton } from './ReleaseButton';
export default {
firmwareMismatch,
noDfuHub,
noDfuInterface,
noWebUsb,
releaseButton,
};
+3
View File
@@ -21,5 +21,8 @@
"flashProgress": {
"erasing": "Erasing internal flash memory: {percent}",
"flashing": "Writing new firmware: {percent}"
},
"releaseButton": {
"message": "You may release the power button on the hub now."
}
}
+45 -1
View File
@@ -1,13 +1,16 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2021-2022 The Pybricks Authors
import { IToaster } from '@blueprintjs/core';
import {
FirmwareMetadata,
FirmwareReaderError,
FirmwareReaderErrorCode,
} from '@pybricks/firmware';
import { mock } from 'jest-mock-extended';
import JSZip from 'jszip';
import { AsyncSaga } from '../../test';
import { alertsShowAlert } from '../alerts/actions';
import {
BootloaderConnectionFailureReason,
checksumRequest,
@@ -78,6 +81,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -130,6 +134,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -224,6 +231,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -273,6 +281,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -340,6 +349,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -403,6 +413,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -469,6 +480,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -528,6 +540,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -593,6 +606,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -675,6 +689,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -740,6 +755,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -790,6 +806,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -835,6 +854,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -885,6 +905,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -939,6 +962,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -989,6 +1013,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -1083,6 +1110,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1133,6 +1161,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -1225,6 +1256,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1283,6 +1315,9 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
@@ -1374,6 +1409,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1421,6 +1457,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1467,6 +1504,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1527,6 +1565,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1588,6 +1627,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1652,6 +1692,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1741,6 +1782,7 @@ describe('flashFirmware', () => {
const saga = new AsyncSaga(flashFirmware, {
nextMessageId: createCountFunc(),
toaster: mock<IToaster>(),
});
// saga is triggered by this action
@@ -1791,12 +1833,14 @@ describe('flashFirmware', () => {
// erase first
action = await saga.take();
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
action = await saga.take();
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
saga.put(didRequest(1));
saga.put(eraseResponse(Result.OK));
// then write the new firmware
const totalFirmwareSize = metadata['user-mpy-offset'] + mpySize + 8;
+32
View File
@@ -94,6 +94,10 @@ const firmwareZipMap = new Map<HubType, string>([
* parent task).
*/
function* disconnectAndCancel(): SagaGenerator<void> {
const toaster = yield* getContext<IToaster>('toaster');
toaster.dismiss('firmware.ble.progress');
const connection = yield* select((s: RootState) => s.bootloader.connection);
if (connection === BootloaderConnectionState.Connected) {
@@ -328,6 +332,8 @@ function* loadFirmware(
* @param action The action that triggered this saga.
*/
function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generator {
const toaster = yield* getContext<IToaster>('toaster');
try {
let firmware: Uint8Array | undefined = undefined;
let deviceId: HubType | undefined = undefined;
@@ -418,6 +424,16 @@ function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generat
yield* put(didStart());
toaster.show(
flashProgress(() => undefined, {
action: 'erase',
progress: undefined,
}),
'firmware.ble.progress',
);
yield* put(alertsShowAlert('firmware', 'releaseButton'));
const eraseAction = yield* put(
eraseRequest(nextMessageId(), deviceId === HubType.CityHub),
);
@@ -469,6 +485,14 @@ function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generat
yield* put(didProgress(offset / firmware.length));
toaster.show(
flashProgress(() => undefined, {
action: 'flash',
progress: offset / firmware.length,
}),
'firmware.ble.progress',
);
// we don't want to request checksum if this is the last packet since
// the bootloader will send a response to the program request already.
offset += maxDataSize;
@@ -542,6 +566,14 @@ function* handleFlashFirmware(action: ReturnType<typeof flashFirmware>): Generat
yield* put(didProgress(1));
toaster.show(
flashProgress(() => undefined, {
action: 'flash',
progress: 1,
}),
'firmware.ble.progress',
);
// this will cause the remote device to disconnect and reboot
const rebootAction = yield* put(rebootRequest(nextMessageId()));
yield* waitForDidRequest(rebootAction.id);