mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-12 09:36:27 +00:00
add disconnect monitor to firmware flash sagas
This commit is contained in:
@@ -50,6 +50,8 @@ type Reason<T> = {
|
||||
export enum FailToStartReasonType {
|
||||
/** Connecting to the hub failed. */
|
||||
FailedToConnect = 'flashFirmware.failToStart.reason.failedToConnect',
|
||||
/** The hub was disconnected. */
|
||||
Disconnected = 'flashFirmware.failToStart.reason.disconnected',
|
||||
/** The is no firmware available that matches the connected hub. */
|
||||
NoFirmware = 'flashFirmware.failToStart.reason.noFirmware',
|
||||
/** The provided firmware.zip does not match the connected hub. */
|
||||
@@ -68,6 +70,8 @@ export enum FailToStartReasonType {
|
||||
|
||||
export type FailToStartReasonFailedToConnect = Reason<FailToStartReasonType.FailedToConnect>;
|
||||
|
||||
export type FailToStartReasonDisconnected = Reason<FailToStartReasonType.Disconnected>;
|
||||
|
||||
export type FailToStartReasonNoFirmware = Reason<FailToStartReasonType.NoFirmware>;
|
||||
|
||||
export type FailToStartReasonDeviceMismatch = Reason<FailToStartReasonType.DeviceMismatch>;
|
||||
@@ -91,6 +95,7 @@ export type FailToStartReasonUnknown = Reason<FailToStartReasonType.Unknown> & {
|
||||
|
||||
export type FailToStartReason =
|
||||
| FailToStartReasonFailedToConnect
|
||||
| FailToStartReasonDisconnected
|
||||
| FailToStartReasonNoFirmware
|
||||
| FailToStartReasonDeviceMismatch
|
||||
| FailToStartReasonZipError
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
checksumResponse,
|
||||
connect,
|
||||
didConnect,
|
||||
didDisconnect,
|
||||
didFailToConnect,
|
||||
didRequest,
|
||||
eraseRequest,
|
||||
@@ -238,6 +239,64 @@ describe('flashFirmware', () => {
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('untimely disconnect before start cancels saga', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
'max-firmware-size': 1024,
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('firmware-base.bin', new Uint8Array(64));
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('main.py', 'print("test")');
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
||||
new Response(await zip.generateAsync({ type: 'blob' })),
|
||||
);
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
});
|
||||
|
||||
saga.setState({ settings: { flashCurrentProgram: false } });
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(flashFirmwareAction());
|
||||
|
||||
// first step is to connect to the hub bootloader
|
||||
|
||||
let action = await saga.take();
|
||||
expect(action).toEqual(connect());
|
||||
|
||||
saga.put(didConnect());
|
||||
|
||||
// then find out what kind of hub it is
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(infoRequest(0));
|
||||
|
||||
saga.put(didRequest(0));
|
||||
|
||||
// hub disconnects before replying
|
||||
|
||||
saga.put(didDisconnect());
|
||||
|
||||
// should get a failure to start
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didFailToStart(FailToStartReasonType.Disconnected));
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
});
|
||||
|
||||
describe('user supplied firmware.zip', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
call,
|
||||
cancel,
|
||||
delay,
|
||||
fork,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
@@ -19,11 +20,14 @@ import {
|
||||
takeEvery,
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { Action } from '../actions';
|
||||
import { disconnect } from '../actions/ble';
|
||||
import {
|
||||
FailToFinishReasonType,
|
||||
FailToStartReasonType,
|
||||
FlashFirmwareActionType,
|
||||
FlashFirmwareFlashAction,
|
||||
MetadataProblem,
|
||||
didFailToFinish,
|
||||
didFailToStart,
|
||||
didFinish,
|
||||
didProgress,
|
||||
@@ -201,6 +205,50 @@ function* loadFirmware(
|
||||
return { firmware, deviceId: metadata['device-id'] };
|
||||
}
|
||||
|
||||
/**
|
||||
* The purpose of this function is two-fold. If the BLE device is disconnected,
|
||||
* then it will raise a failure action and cancel the task (including the
|
||||
* parent task). Or, if the parent task fails, it will disconnect the BLE device
|
||||
* and return.
|
||||
*/
|
||||
function* disconnectMonitor(): SagaGenerator<void> {
|
||||
const { disconnectedBeforeStart, failedToStart } = yield* race({
|
||||
disconnectedBeforeStart: take(BootloaderConnectionActionType.DidDisconnect),
|
||||
started: take(FlashFirmwareActionType.DidStart),
|
||||
failedToStart: take(FlashFirmwareActionType.DidFailToStart),
|
||||
});
|
||||
|
||||
if (disconnectedBeforeStart) {
|
||||
yield* put(didFailToStart(FailToStartReasonType.Disconnected));
|
||||
yield* cancel();
|
||||
}
|
||||
|
||||
if (failedToStart) {
|
||||
yield* put(disconnect());
|
||||
return;
|
||||
}
|
||||
|
||||
// if we get here, `started` won the race
|
||||
|
||||
const { disconnectedAfterStart, failedToFinish } = yield* race({
|
||||
disconnectedAfterStart: take(BootloaderConnectionActionType.DidDisconnect),
|
||||
finished: take(FlashFirmwareActionType.DidFinish),
|
||||
failedToFinish: take(FlashFirmwareActionType.DidFailToFinish),
|
||||
});
|
||||
|
||||
if (disconnectedAfterStart) {
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.Disconnected));
|
||||
yield* cancel();
|
||||
}
|
||||
|
||||
if (failedToFinish) {
|
||||
yield* put(disconnect());
|
||||
return;
|
||||
}
|
||||
|
||||
// if we get here, `finished` won the race.
|
||||
}
|
||||
|
||||
/**
|
||||
* Flashes firmware to a Powered Up device.
|
||||
* @param action The action that triggered this saga.
|
||||
@@ -242,6 +290,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
return;
|
||||
}
|
||||
|
||||
const disconnectMonitorTask = yield* fork(disconnectMonitor);
|
||||
|
||||
const nextMessageId = yield* getContext<() => number>('nextMessageId');
|
||||
|
||||
const infoAction = yield* put(infoRequest(nextMessageId()));
|
||||
@@ -378,6 +428,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator {
|
||||
|
||||
// this will cause the remote device to disconnect and reboot
|
||||
const rebootAction = yield* put(rebootRequest(nextMessageId()));
|
||||
disconnectMonitorTask.cancel();
|
||||
yield* waitForDidRequest(rebootAction.id);
|
||||
|
||||
yield* put(didFinish());
|
||||
|
||||
Reference in New Issue
Block a user