From 18edc7870e855ce18580b528182cc032d7e59e65 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Fri, 22 Jan 2021 12:57:37 -0600 Subject: [PATCH] add disconnect monitor to firmware flash sagas --- src/actions/flash-firmware.ts | 5 +++ src/sagas/flash-firmware.test.ts | 59 ++++++++++++++++++++++++++++++++ src/sagas/flash-firmware.ts | 51 +++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/src/actions/flash-firmware.ts b/src/actions/flash-firmware.ts index 0187bd6c..d4837573 100644 --- a/src/actions/flash-firmware.ts +++ b/src/actions/flash-firmware.ts @@ -50,6 +50,8 @@ type Reason = { 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; +export type FailToStartReasonDisconnected = Reason; + export type FailToStartReasonNoFirmware = Reason; export type FailToStartReasonDeviceMismatch = Reason; @@ -91,6 +95,7 @@ export type FailToStartReasonUnknown = Reason & { export type FailToStartReason = | FailToStartReasonFailedToConnect + | FailToStartReasonDisconnected | FailToStartReasonNoFirmware | FailToStartReasonDeviceMismatch | FailToStartReasonZipError diff --git a/src/sagas/flash-firmware.test.ts b/src/sagas/flash-firmware.test.ts index 6e39d2c8..f7d6a23f 100644 --- a/src/sagas/flash-firmware.test.ts +++ b/src/sagas/flash-firmware.test.ts @@ -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', () => { diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index 231cf814..e3016800 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -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 { + 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());