diff --git a/src/actions/flash-firmware.ts b/src/actions/flash-firmware.ts index d4837573..cb0dbe87 100644 --- a/src/actions/flash-firmware.ts +++ b/src/actions/flash-firmware.ts @@ -50,8 +50,14 @@ type Reason = { export enum FailToStartReasonType { /** Connecting to the hub failed. */ FailedToConnect = 'flashFirmware.failToStart.reason.failedToConnect', + /** The hub connection timed out. */ + TimedOut = 'flashFirmware.failToStart.reason.timedOut', + /** Something went wrong with the BLE connection. */ + BleError = 'flashFirmware.failToStart.reason.bleError', /** The hub was disconnected. */ Disconnected = 'flashFirmware.failToStart.reason.disconnected', + /** The hub sent a response indicating a problem. */ + HubError = 'flashFirmware.failToStart.reason.hubError', /** 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. */ @@ -70,8 +76,18 @@ export enum FailToStartReasonType { export type FailToStartReasonFailedToConnect = Reason; +export type FailToStartReasonTimedOut = Reason; + +export type FailToStartReasonBleError = Reason & { + err: Error; +}; + export type FailToStartReasonDisconnected = Reason; +export type FailToStartReasonHubError = Reason & { + hubError: HubError; +}; + export type FailToStartReasonNoFirmware = Reason; export type FailToStartReasonDeviceMismatch = Reason; @@ -95,7 +111,10 @@ export type FailToStartReasonUnknown = Reason & { export type FailToStartReason = | FailToStartReasonFailedToConnect + | FailToStartReasonTimedOut + | FailToStartReasonBleError | FailToStartReasonDisconnected + | FailToStartReasonHubError | FailToStartReasonNoFirmware | FailToStartReasonDeviceMismatch | FailToStartReasonZipError @@ -119,7 +138,9 @@ export enum FailToFinishReasonType { export type FailToFinishReasonTimedOut = Reason; -export type FailToFinishReasonBleError = Reason; +export type FailToFinishReasonBleError = Reason & { + err: Error; +}; export type FailToFinishReasonDisconnected = Reason; @@ -170,6 +191,16 @@ export type FlashFirmwareDidFailToStartAction = Action, ): FlashFirmwareDidFailToFinishAction; @@ -303,6 +365,17 @@ export function didFailToFinish( reason: FailToFinishReasonType, arg1?: HubError | Error, ): FlashFirmwareDidFailToFinishAction { + if (reason === FailToFinishReasonType.BleError) { + // istanbul ignore if: programmer error give wrong arg + if (!(arg1 instanceof Error)) { + throw new Error('missing or invalid err'); + } + return { + type: FlashFirmwareActionType.DidFailToFinish, + reason: { reason, err: arg1 }, + }; + } + if (reason === FailToFinishReasonType.HubError) { // istanbul ignore if: programmer error give wrong arg if (!isHubError(arg1)) { diff --git a/src/actions/lwp3-bootloader.ts b/src/actions/lwp3-bootloader.ts index 6bcfbe6a..c0066c1b 100644 --- a/src/actions/lwp3-bootloader.ts +++ b/src/actions/lwp3-bootloader.ts @@ -41,6 +41,10 @@ export enum BootloaderConnectionActionType { * The connection received a message. */ DidReceive = 'bootloader.action.connection.did.receive', + /** + * Initiate disconnection/ + */ + Disconnect = 'bootloader.action.connection.disconnect', /** * The connection has been closed. */ @@ -59,6 +63,12 @@ export function didConnect(): BootloaderConnectionDidConnectAction { return { type: BootloaderConnectionActionType.DidConnect }; } +export type BootloaderConnectionDisconnectAction = Action; + +export function disconnect(): BootloaderConnectionDisconnectAction { + return { type: BootloaderConnectionActionType.Disconnect }; +} + /** * Possible reasons a device could fail to connect. */ diff --git a/src/sagas/flash-firmware.test.ts b/src/sagas/flash-firmware.test.ts index f7d6a23f..6fcbfd01 100644 --- a/src/sagas/flash-firmware.test.ts +++ b/src/sagas/flash-firmware.test.ts @@ -27,6 +27,7 @@ import { didDisconnect, didFailToConnect, didRequest, + disconnect, eraseRequest, eraseResponse, infoRequest, @@ -297,6 +298,68 @@ describe('flashFirmware', () => { await saga.end(); }); + + test('fail to send info request', 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)); + + const testError = new Error('test'); + saga.put(didRequest(0, testError)); + + // should get a failure to start + + action = await saga.take(); + expect(action).toEqual( + didFailToStart(FailToStartReasonType.BleError, testError), + ); + + // should request to disconnect after failure + + action = await saga.take(); + expect(action).toEqual(disconnect()); + + await saga.end(); + }); }); describe('user supplied firmware.zip', () => { diff --git a/src/sagas/flash-firmware.ts b/src/sagas/flash-firmware.ts index e3016800..b0c1f95d 100644 --- a/src/sagas/flash-firmware.ts +++ b/src/sagas/flash-firmware.ts @@ -20,12 +20,12 @@ import { takeEvery, } from 'typed-redux-saga/macro'; import { Action } from '../actions'; -import { disconnect } from '../actions/ble'; import { FailToFinishReasonType, FailToStartReasonType, FlashFirmwareActionType, FlashFirmwareFlashAction, + HubError, MetadataProblem, didFailToFinish, didFailToStart, @@ -48,6 +48,7 @@ import { BootloaderResponseActionType, checksumRequest, connect, + disconnect, disconnectRequest, eraseRequest, infoRequest, @@ -74,9 +75,16 @@ const firmwareZipMap = new Map([ ]); function* waitForDidRequest(id: number): SagaGenerator { - return yield* take( + const request = yield* take( (a: Action) => a.type === BootloaderDidRequestType && a.id === id, ); + if (request.err) { + yield* put(didFailToStart(FailToStartReasonType.BleError, request.err)); + yield* put(disconnect()); + yield* cancel(); + } + + return request; } /** @@ -88,16 +96,30 @@ function* waitForDidRequest(id: number): SagaGenerator( type: BootloaderResponseActionType, timeout = 500, -): SagaGenerator<{ - response?: T; - error?: BootloaderErrorResponseAction; - timeout?: boolean; -}> { - return yield* race({ +): SagaGenerator { + const { response, error, timedOut } = yield* race({ response: take(type), error: take(BootloaderResponseActionType.Error), - timeout: delay(timeout), + timedOut: delay(timeout), }); + + if (timedOut) { + yield* put(didFailToStart(FailToStartReasonType.TimedOut)); + yield* put(disconnect()); + yield* cancel(); + } + + if (error) { + yield* put( + didFailToStart(FailToStartReasonType.HubError, HubError.UnknownCommand), + ); + yield* put(disconnect()); + cancel(); + } + + defined(response); + + return response; } function* firmwareIterator(data: DataView, maxSize: number): Generator { @@ -206,16 +228,13 @@ function* loadFirmware( } /** - * 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. + * Monitors for BLE disconnection event. If disconnection occurs, then a failure + * action is raised and the task (including the parent task) is canceled. */ function* disconnectMonitor(): SagaGenerator { - const { disconnectedBeforeStart, failedToStart } = yield* race({ + const { disconnectedBeforeStart } = yield* race({ disconnectedBeforeStart: take(BootloaderConnectionActionType.DidDisconnect), started: take(FlashFirmwareActionType.DidStart), - failedToStart: take(FlashFirmwareActionType.DidFailToStart), }); if (disconnectedBeforeStart) { @@ -223,17 +242,11 @@ function* disconnectMonitor(): SagaGenerator { yield* cancel(); } - if (failedToStart) { - yield* put(disconnect()); - return; - } - // if we get here, `started` won the race - const { disconnectedAfterStart, failedToFinish } = yield* race({ + const { disconnectedAfterStart } = yield* race({ disconnectedAfterStart: take(BootloaderConnectionActionType.DidDisconnect), finished: take(FlashFirmwareActionType.DidFinish), - failedToFinish: take(FlashFirmwareActionType.DidFailToFinish), }); if (disconnectedAfterStart) { @@ -241,11 +254,6 @@ function* disconnectMonitor(): SagaGenerator { yield* cancel(); } - if (failedToFinish) { - yield* put(disconnect()); - return; - } - // if we get here, `finished` won the race. } @@ -301,18 +309,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { BootloaderResponseActionType.Info, ), }); - if (!info.response) { - throw Error(`failed to get info: ${info}`); - } - if (deviceId !== undefined && info.response.hubType !== deviceId) { - throw Error( - `Connected to ${info.response.hubType} but firmware is for ${deviceId}`, - ); + if (deviceId !== undefined && info.hubType !== deviceId) { + throw Error(`Connected to ${info.hubType} but firmware is for ${deviceId}`); } if (firmware === undefined) { - const firmwarePath = firmwareZipMap.get(info.response.hubType); + const firmwarePath = firmwareZipMap.get(info.hubType); if (firmwarePath === undefined) { yield* put( notification.add( @@ -335,10 +338,8 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { const data = yield* call(() => response.arrayBuffer()); ({ firmware, deviceId } = yield* loadFirmware(data, program)); - if (deviceId !== undefined && info.response.hubType !== deviceId) { - throw Error( - `Connected to ${info.response.hubType} but firmware is for ${deviceId}`, - ); + if (deviceId !== undefined && info.hubType !== deviceId) { + throw Error(`Connected to ${info.hubType} but firmware is for ${deviceId}`); } } @@ -352,7 +353,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { 5000, ), }); - if (!erase.response || erase.response.result) { + if (erase.result) { // TODO: proper error handling throw Error(`Failed to erase: ${erase}`); } @@ -364,22 +365,18 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { BootloaderResponseActionType.Init, ), }); - if (!init.response || init.response.result) { + if (init.result) { // TODO: proper error handling throw Error(`Failed to init: ${init}`); } // 14 is "safe" size for all hubs - const maxDataSize = MaxProgramFlashSize.get(info.response.hubType) || 14; + const maxDataSize = MaxProgramFlashSize.get(info.hubType) || 14; for (let count = 1, offset = 0; ; count++) { const payload = firmware.slice(offset, offset + maxDataSize); const programAction = yield* put( - programRequest( - nextMessageId(), - info.response.startAddress + offset, - payload.buffer, - ), + programRequest(nextMessageId(), info.startAddress + offset, payload.buffer), ); yield* waitForDidRequest(programAction.id); @@ -398,17 +395,13 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { // the hub is not known and could vary by device. if (count % 10 === 0) { const checksumAction = yield* put(checksumRequest(nextMessageId())); - const { checksum } = yield* all({ + yield* all({ sent: waitForDidRequest(checksumAction.id), checksum: waitForResponse( BootloaderResponseActionType.Checksum, 5000, ), }); - if (!checksum.response) { - // TODO: proper error handling - throw Error(`Failed to get checksum: ${checksum}`); - } } } @@ -416,10 +409,7 @@ function* flashFirmware(action: FlashFirmwareFlashAction): Generator { BootloaderResponseActionType.Program, 5000, ); - if (!flash.response) { - throw Error(`failed to get final response: ${flash}`); - } - if (flash.response.count !== firmware.length) { + if (flash.count !== firmware.length) { // TODO: proper error handling throw Error("Didn't flash all bytes"); } diff --git a/src/sagas/lwp3-bootloader-ble.ts b/src/sagas/lwp3-bootloader-ble.ts index 946b45d3..c5235355 100644 --- a/src/sagas/lwp3-bootloader-ble.ts +++ b/src/sagas/lwp3-bootloader-ble.ts @@ -143,6 +143,7 @@ function* connect(_action: BootloaderConnectionAction): Generator { yield takeEvery(notificationChannel, handleNotify); yield takeEvery(BootloaderConnectionActionType.Send, write, characteristic); + yield takeEvery(BootloaderConnectionActionType.Disconnect, server.disconnect); yield put(didConnect());