From 07d0b5ef3918b959c73fe4eea360340fd72a0dbd Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 9 Jun 2020 16:37:19 -0500 Subject: [PATCH] split firmware flashing from lwp3-bootloader BLE won't always be the only way to flash firmware --- src/actions/flash-firmware.ts | 58 +++++++ src/actions/index.ts | 10 +- src/actions/lwp3-bootloader.ts | 57 ------ src/components/FlashButton.tsx | 2 +- src/reducers/status.ts | 7 +- src/sagas/flash-firmare.ts | 305 +++++++++++++++++++++++++++++++++ src/sagas/index.ts | 3 +- src/sagas/lwp3-bootloader.ts | 305 +-------------------------------- 8 files changed, 376 insertions(+), 371 deletions(-) create mode 100644 src/actions/flash-firmware.ts create mode 100644 src/sagas/flash-firmare.ts diff --git a/src/actions/flash-firmware.ts b/src/actions/flash-firmware.ts new file mode 100644 index 00000000..2262681c --- /dev/null +++ b/src/actions/flash-firmware.ts @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020 The Pybricks Authors + +import { Action } from 'redux'; + +/** + * High-level bootloader actions. + */ +export enum FlashFirmwareActionType { + /** + * Flash new firmware to the device. + */ + FlashFirmware = 'flashFirmware.action.flashFirmware', + /** + * Firmware flash progress. + */ + Progress = 'flashFirmware.action.progress', +} + +/** + * Action that flashes firmware to a hub. + */ +export interface FlashFirmwareFlashAction + extends Action { + /** The firmware zip file data or undefined to get firmware later. */ + data?: ArrayBuffer; +} + +/** + * Creates a new action to flash firmware to a hub. + * @param data The firmware zip file data or undefined to get firmware later. + */ +export function flashFirmware(data?: ArrayBuffer): FlashFirmwareFlashAction { + return { type: FlashFirmwareActionType.FlashFirmware, data }; +} + +export interface FlashFirmwareProgressAction + extends Action { + /** + * The number of bytes that have been flashed so far. + */ + complete: number; + /** + * The total number of bytes to be flashed. + */ + total: number; +} + +export function progress(complete: number, total: number): FlashFirmwareProgressAction { + return { type: FlashFirmwareActionType.Progress, complete, total }; +} + +/** + * Common type for all high-level bootloader actions. + */ +export type FlashFirmwareAction = + | FlashFirmwareFlashAction + | FlashFirmwareProgressAction; diff --git a/src/actions/index.ts b/src/actions/index.ts index 24a42d6b..ab9c917a 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -4,9 +4,9 @@ import { Dispatch as ReduxDispatch } from 'redux'; import { BLEAction, BLEConnectAction, BLEDataAction } from './ble'; import { EditorAction } from './editor'; +import { FlashFirmwareAction } from './flash-firmware'; import { HubAction, HubMessageAction } from './hub'; import { - BootloaderAction, BootloaderConnectionAction, BootloaderDidRequestAction, BootloaderRequestAction, @@ -21,17 +21,17 @@ import { TerminalDataAction } from './terminal'; * Common type for all actions. */ export type Action = + | BLEAction | BLEConnectAction | BLEDataAction - | BLEAction | BootloaderConnectionAction - | BootloaderRequestAction | BootloaderDidRequestAction + | BootloaderRequestAction | BootloaderResponseAction - | BootloaderAction | EditorAction - | HubMessageAction + | FlashFirmwareAction | HubAction + | HubMessageAction | MpyAction | NotificationAction | ServiceWorkerAction diff --git a/src/actions/lwp3-bootloader.ts b/src/actions/lwp3-bootloader.ts index 100a5ea8..cb0342b6 100644 --- a/src/actions/lwp3-bootloader.ts +++ b/src/actions/lwp3-bootloader.ts @@ -463,60 +463,3 @@ export type BootloaderResponseAction = | BootloaderChecksumResponseAction | BootloaderStateResponseAction | BootloaderErrorResponseAction; - -/** - * High-level bootloader actions. - */ -export enum BootloaderActionType { - /** - * Flash new firmware to the device. - */ - FlashFirmware = 'bootloader.action.flash', - /** - * Firmware flash progress. - */ - FlashProgress = 'bootloader.action.flash.progress', -} - -/** - * Action that flashes firmware to a hub. - */ -export interface BootloaderFlashFirmwareAction - extends Action { - /** The firmware zip file data or undefined to get firmware later. */ - data?: ArrayBuffer; -} - -/** - * Creates a new action to flash firmware to a hub. - * @param data The firmware zip file data or undefined to get firmware later. - */ -export function flashFirmware(data?: ArrayBuffer): BootloaderFlashFirmwareAction { - return { type: BootloaderActionType.FlashFirmware, data }; -} - -export interface BootloaderFlashProgressAction - extends Action { - /** - * The number of bytes that have been flashed so far. - */ - complete: number; - /** - * The total number of bytes to be flashed. - */ - total: number; -} - -export function progress( - complete: number, - total: number, -): BootloaderFlashProgressAction { - return { type: BootloaderActionType.FlashProgress, complete, total }; -} - -/** - * Common type for all high-level bootloader actions. - */ -export type BootloaderAction = - | BootloaderFlashFirmwareAction - | BootloaderFlashProgressAction; diff --git a/src/components/FlashButton.tsx b/src/components/FlashButton.tsx index 382c46db..b1c38bec 100644 --- a/src/components/FlashButton.tsx +++ b/src/components/FlashButton.tsx @@ -3,7 +3,7 @@ import { connect } from 'react-redux'; import { Dispatch } from '../actions'; -import { flashFirmware } from '../actions/lwp3-bootloader'; +import { flashFirmware } from '../actions/flash-firmware'; import * as notification from '../actions/notification'; import { RootState } from '../reducers'; import { BootloaderConnectionState } from '../reducers/bootloader'; diff --git a/src/reducers/status.ts b/src/reducers/status.ts index 70adbc7d..d882116c 100644 --- a/src/reducers/status.ts +++ b/src/reducers/status.ts @@ -3,11 +3,12 @@ import { Reducer } from 'react'; import { combineReducers } from 'redux'; -import { BootloaderAction, BootloaderActionType } from '../actions/lwp3-bootloader'; +import { Action } from '../actions'; +import { FlashFirmwareActionType } from '../actions/flash-firmware'; -const progress: Reducer = (state = -1, action) => { +const progress: Reducer = (state = -1, action) => { switch (action.type) { - case BootloaderActionType.FlashProgress: + case FlashFirmwareActionType.Progress: return action.complete / action.total; default: return state; diff --git a/src/sagas/flash-firmare.ts b/src/sagas/flash-firmare.ts new file mode 100644 index 00000000..eb164168 --- /dev/null +++ b/src/sagas/flash-firmare.ts @@ -0,0 +1,305 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2020 The Pybricks Authors + +import cPlusHubZip from '@pybricks/firmware/build/cplushub.zip'; +import moveHubZip from '@pybricks/firmware/build/movehub.zip'; +import JSZip from 'jszip'; +import { Effect, call, delay, put, race, take, takeEvery } from 'redux-saga/effects'; +import { Action } from '../actions'; +import { + FlashFirmwareActionType, + FlashFirmwareFlashAction, + progress, +} from '../actions/flash-firmware'; +import { + BootloaderChecksumResponseAction, + BootloaderConnectionActionType, + BootloaderConnectionDidConnectAction, + BootloaderConnectionDidFailToConnectAction, + BootloaderDidRequestAction, + BootloaderDidRequestType, + BootloaderEraseResponseAction, + BootloaderErrorResponseAction, + BootloaderInfoResponseAction, + BootloaderInitResponseAction, + BootloaderProgramRequestAction, + BootloaderProgramResponseAction, + BootloaderResponseAction, + BootloaderResponseActionType, + checksumRequest, + connect, + disconnectRequest, + eraseRequest, + infoRequest, + initRequest, + programRequest, + rebootRequest, +} from '../actions/lwp3-bootloader'; +import { + MpyActionType, + MpyDidCompileAction, + MpyDidFailToCompileAction, + compile, +} from '../actions/mpy'; +import * as notification from '../actions/notification'; +import { HubType, MaxProgramFlashSize } from '../protocols/lwp3-bootloader'; +import { fmod, sumComplement32 } from '../utils/math'; + +const firmwareZipMap = new Map([ + [HubType.CPlusHub, cPlusHubZip], + [HubType.MoveHub, moveHubZip], +]); + +/** + * Helper type for return value of wait() function. + */ +type WaitResponse = [ + T, + BootloaderErrorResponseAction, + boolean, +]; + +/** + * Waits for a response action, an error response or timeout, whichever comes + * first. + * @param type The action type to wait for. + * @param timeout The timeout in milliseconds. + */ +function wait(type: BootloaderResponseActionType, timeout = 500): Effect { + return race([take(type), take(BootloaderResponseActionType.Error), delay(timeout)]); +} + +interface FirmwareMetadata { + 'metadata-version': string; + 'firmware-version': string; + 'device-id': HubType; + 'checksum-type': 'sum' | 'crc32'; + 'mpy-abi-version': number; + 'mpy-cross-options': string[]; + 'user-mpy-offset': number; + 'max-firmware-size': number; +} + +function* firmwareIterator(data: DataView, maxSize: number): Generator { + // read each 32-bit word of the firmware + for (let i = 0; i < data.byteLength; i += 4) { + yield data.getUint32(i, true); + } + // remaining free space in flash will be 0xff after erase + for (let i = data.byteLength; i < maxSize; i += 4) { + yield ~0; + } +} + +/** + * Loads Pybricks firmware from a .zip file + * @param data The zip file raw data + */ +function* loadFirmware( + data: ArrayBuffer, +): Generator { + const zip = (yield call(() => JSZip.loadAsync(data))) as JSZip; + const firmwareBase = (yield call(() => + zip.file('firmware-base.bin').async('uint8array'), + )) as Uint8Array; + const metadata = JSON.parse( + (yield call(() => zip.file('firmware.metadata.json').async('text'))) as string, + ) as FirmwareMetadata; + const main = (yield call(() => zip.file('main.py').async('text'))) as string; + + if (metadata['mpy-abi-version'] !== 5) { + throw Error( + `Firmware requires mpy-cross ABI version ${metadata['mpy-abi-version']} we have v5`, + ); + } + + yield put(compile(main, metadata['mpy-cross-options'])); + const [mpy, mpyFail] = (yield race([ + take(MpyActionType.DidCompile), + take(MpyActionType.DidFailToCompile), + ])) as [MpyDidCompileAction, MpyDidFailToCompileAction]; + + if (mpyFail) { + throw Error(mpyFail.err); + } + + // compute offset for checksum - must be aligned to 4-byte boundary + const checksumOffset = + metadata['user-mpy-offset'] + 4 + mpy.data.length + fmod(-mpy.data.length, 4); + + const firmware = new Uint8Array(checksumOffset + 4); + const firmwareView = new DataView(firmware.buffer); + + if (firmware.length > metadata['max-firmware-size']) { + throw Error('firmware + main.mpy is too large'); + } + + firmware.set(firmwareBase); + firmwareView.setUint32(metadata['user-mpy-offset'], mpy.data.length, true); + firmware.set(mpy.data, metadata['user-mpy-offset'] + 4); + + if (metadata['checksum-type'] !== 'sum') { + throw Error(`Unknown checksum type "${metadata['checksum-type']}"`); + } + + firmwareView.setUint32( + checksumOffset, + sumComplement32(firmwareIterator(firmwareView, metadata['max-firmware-size'])), + true, + ); + + return { firmware, deviceId: metadata['device-id'] }; +} + +/** + * Flashes firmware to a Powered Up device. + * @param action The action that triggered this saga. + */ +function* flashFirmware(action: FlashFirmwareFlashAction): Generator { + let firmware: Uint8Array | undefined = undefined; + let deviceId: HubType | undefined = undefined; + + if (action.data !== undefined) { + ({ firmware, deviceId } = yield* loadFirmware(action.data)); + } + + yield put(connect()); + const connectResult = (yield take([ + BootloaderConnectionActionType.DidConnect, + BootloaderConnectionActionType.DidFailToConnect, + ])) as + | BootloaderConnectionDidConnectAction + | BootloaderConnectionDidFailToConnectAction; + + if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) { + return; + } + + yield put(infoRequest()); + const info = (yield wait(BootloaderResponseActionType.Info)) as WaitResponse< + BootloaderInfoResponseAction + >; + if (!info[0]) { + throw Error(`failed to get info: ${info}`); + } + + if (deviceId !== undefined && info[0].hubType !== deviceId) { + throw Error(`Connected to ${info[0].hubType} but firmware is for ${deviceId}`); + } + + if (firmware === undefined) { + const firmwarePath = firmwareZipMap.get(info[0].hubType); + if (firmwarePath === undefined) { + yield put( + notification.add( + 'error', + "Sorry, we don't have firmware for this hub yet.", + ), + ); + yield put(disconnectRequest()); + return; + } + + const response = (yield call(() => fetch(firmwarePath))) as Response; + if (!response.ok) { + yield put(notification.add('error', 'Failed to fetch firmware.')); + yield put(disconnectRequest()); + return; + } + + const data = (yield call(() => response.arrayBuffer())) as ArrayBuffer; + ({ firmware, deviceId } = yield* loadFirmware(data)); + + if (deviceId !== undefined && info[0].hubType !== deviceId) { + throw Error( + `Connected to ${info[0].hubType} but firmware is for ${deviceId}`, + ); + } + } + + // City hub bootloader is buggy. See note in encodeRequest(). + if (info[0].hubType === HubType.CityHub && !connectResult.canWriteWithoutResponse) { + yield put( + notification.add( + 'error', + 'City Hub is not compatible with this web browser.', + ), + ); + yield put(disconnectRequest()); + return; + } + + yield put(eraseRequest()); + const erase = (yield wait( + BootloaderResponseActionType.Erase, + 5000, + )) as WaitResponse; + if (!erase[0] || erase[0].result) { + // TODO: proper error handling + throw Error(`Failed to erase: ${erase}`); + } + + yield put(initRequest(firmware.length)); + const init = (yield wait(BootloaderResponseActionType.Init)) as WaitResponse< + BootloaderInitResponseAction + >; + if (!init[0] || init[0].result) { + // TODO: proper error handling + throw Error(`Failed to init: ${init}`); + } + + let count = 0; + + for (let offset = 0; offset < firmware.length; offset += MaxProgramFlashSize) { + const payload = firmware.slice(offset, offset + MaxProgramFlashSize); + const req = (yield put( + programRequest(info[0].startAddress + offset, payload.buffer), + )) as BootloaderProgramRequestAction; + + // TODO: check for error + yield take( + (a: Action) => + a.type === BootloaderDidRequestType && + (a as BootloaderDidRequestAction).id === req.id, + ); + + yield put(progress(offset, firmware.length)); + + if (connectResult.canWriteWithoutResponse) { + // request checksum every 8K to prevent buffer overrun on the hub + // because of sending too much data at once + if (++count % 585 === 0) { + yield put(checksumRequest()); + const checksum = (yield wait( + BootloaderResponseActionType.Checksum, + 5000, + )) as WaitResponse; + if (!checksum[0]) { + // TODO: proper error handling + throw Error(`Failed to get checksum: ${checksum}`); + } + } + } + } + + const flash = (yield wait( + BootloaderResponseActionType.Program, + 5000, + )) as WaitResponse; + if (!flash[0]) { + throw Error(`failed to get final response: ${flash}`); + } + if (flash[0].count !== firmware.length) { + // TODO: proper error handling + throw Error("Didn't flash all bytes"); + } + + yield put(progress(firmware.length, firmware.length)); + + // this will cause the remote device to disconnect and reboot + yield put(rebootRequest()); +} + +export default function* (): Generator { + yield takeEvery(FlashFirmwareActionType.FlashFirmware, flashFirmware); +} diff --git a/src/sagas/index.ts b/src/sagas/index.ts index 1154e1b7..1ae8206e 100644 --- a/src/sagas/index.ts +++ b/src/sagas/index.ts @@ -3,10 +3,11 @@ import { all } from 'redux-saga/effects'; import editor from './editor'; +import flashFirmware from './flash-firmare'; import bootloader from './lwp3-bootloader'; import mpy from './mpy'; /* istanbul ignore next */ export default function* (): Generator { - yield all([bootloader(), editor(), mpy()]); + yield all([bootloader(), editor(), flashFirmware(), mpy()]); } diff --git a/src/sagas/lwp3-bootloader.ts b/src/sagas/lwp3-bootloader.ts index 820e7324..f359628d 100644 --- a/src/sagas/lwp3-bootloader.ts +++ b/src/sagas/lwp3-bootloader.ts @@ -1,75 +1,29 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2020 The Pybricks Authors -import cPlusHubZip from '@pybricks/firmware/build/cplushub.zip'; -import moveHubZip from '@pybricks/firmware/build/movehub.zip'; -import JSZip from 'jszip'; import { Channel, buffers } from 'redux-saga'; -import { - Effect, - actionChannel, - call, - delay, - fork, - put, - race, - take, - takeEvery, -} from 'redux-saga/effects'; +import { actionChannel, fork, put, take, takeEvery } from 'redux-saga/effects'; import { Action } from '../actions'; import { - BootloaderActionType, - BootloaderChecksumResponseAction, BootloaderConnectionActionType, - BootloaderConnectionDidConnectAction, - BootloaderConnectionDidFailToConnectAction, BootloaderConnectionDidReceiveAction, BootloaderConnectionDidSendAction, - BootloaderDidRequestAction, - BootloaderDidRequestType, - BootloaderEraseResponseAction, - BootloaderErrorResponseAction, - BootloaderFlashFirmwareAction, - BootloaderInfoResponseAction, - BootloaderInitResponseAction, - BootloaderProgramRequestAction, - BootloaderProgramResponseAction, BootloaderRequestAction, BootloaderRequestActionType, - BootloaderResponseAction, - BootloaderResponseActionType, - checksumRequest, checksumResponse, - connect, didError, didRequest, - disconnectRequest, - eraseRequest, eraseResponse, errorResponse, - infoRequest, infoResponse, - initRequest, initResponse, - programRequest, programResponse, - progress, - rebootRequest, send, stateResponse, } from '../actions/lwp3-bootloader'; -import { - MpyActionType, - MpyDidCompileAction, - MpyDidFailToCompileAction, - compile, -} from '../actions/mpy'; -import * as notification from '../actions/notification'; import { Command, ErrorBytecode, - HubType, - MaxProgramFlashSize, ProtocolError, createDisconnectRequest, createEraseFlashRequest, @@ -89,12 +43,6 @@ import { parseProgramFlashResponse, } from '../protocols/lwp3-bootloader'; import { hex } from '../utils'; -import { fmod, sumComplement32 } from '../utils/math'; - -const firmwareZipMap = new Map([ - [HubType.CPlusHub, cPlusHubZip], - [HubType.MoveHub, moveHubZip], -]); /** * Converts a request action into bytecodes and creates a new action to send @@ -203,258 +151,7 @@ function* decodeResponse(action: BootloaderConnectionDidReceiveAction): Generato } } -/** - * Helper type for return value of wait() function. - */ -type WaitResponse = [ - T, - BootloaderErrorResponseAction, - boolean, -]; - -/** - * Waits for a response action, an error response or timeout, whichever comes - * first. - * @param type The action type to wait for. - * @param timeout The timeout in milliseconds. - */ -function wait(type: BootloaderResponseActionType, timeout = 500): Effect { - return race([take(type), take(BootloaderResponseActionType.Error), delay(timeout)]); -} - -interface FirmwareMetadata { - 'metadata-version': string; - 'firmware-version': string; - 'device-id': HubType; - 'checksum-type': 'sum' | 'crc32'; - 'mpy-abi-version': number; - 'mpy-cross-options': string[]; - 'user-mpy-offset': number; - 'max-firmware-size': number; -} - -function* firmwareIterator(data: DataView, maxSize: number): Generator { - // read each 32-bit word of the firmware - for (let i = 0; i < data.byteLength; i += 4) { - yield data.getUint32(i, true); - } - // remaining free space in flash will be 0xff after erase - for (let i = data.byteLength; i < maxSize; i += 4) { - yield ~0; - } -} - -/** - * Loads Pybricks firmware from a .zip file - * @param data The zip file raw data - */ -function* loadFirmware( - data: ArrayBuffer, -): Generator { - const zip = (yield call(() => JSZip.loadAsync(data))) as JSZip; - const firmwareBase = (yield call(() => - zip.file('firmware-base.bin').async('uint8array'), - )) as Uint8Array; - const metadata = JSON.parse( - (yield call(() => zip.file('firmware.metadata.json').async('text'))) as string, - ) as FirmwareMetadata; - const main = (yield call(() => zip.file('main.py').async('text'))) as string; - - if (metadata['mpy-abi-version'] !== 5) { - throw Error( - `Firmware requires mpy-cross ABI version ${metadata['mpy-abi-version']} we have v5`, - ); - } - - yield put(compile(main, metadata['mpy-cross-options'])); - const [mpy, mpyFail] = (yield race([ - take(MpyActionType.DidCompile), - take(MpyActionType.DidFailToCompile), - ])) as [MpyDidCompileAction, MpyDidFailToCompileAction]; - - if (mpyFail) { - throw Error(mpyFail.err); - } - - // compute offset for checksum - must be aligned to 4-byte boundary - const checksumOffset = - metadata['user-mpy-offset'] + 4 + mpy.data.length + fmod(-mpy.data.length, 4); - - const firmware = new Uint8Array(checksumOffset + 4); - const firmwareView = new DataView(firmware.buffer); - - if (firmware.length > metadata['max-firmware-size']) { - throw Error('firmware + main.mpy is too large'); - } - - firmware.set(firmwareBase); - firmwareView.setUint32(metadata['user-mpy-offset'], mpy.data.length, true); - firmware.set(mpy.data, metadata['user-mpy-offset'] + 4); - - if (metadata['checksum-type'] !== 'sum') { - throw Error(`Unknown checksum type "${metadata['checksum-type']}"`); - } - - firmwareView.setUint32( - checksumOffset, - sumComplement32(firmwareIterator(firmwareView, metadata['max-firmware-size'])), - true, - ); - - return { firmware, deviceId: metadata['device-id'] }; -} - -/** - * Flashes firmware to a Powered Up device. - * @param action The action that triggered this saga. - */ -function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator { - let firmware: Uint8Array | undefined = undefined; - let deviceId: HubType | undefined = undefined; - - if (action.data !== undefined) { - ({ firmware, deviceId } = yield* loadFirmware(action.data)); - } - - yield put(connect()); - const connectResult = (yield take([ - BootloaderConnectionActionType.DidConnect, - BootloaderConnectionActionType.DidFailToConnect, - ])) as - | BootloaderConnectionDidConnectAction - | BootloaderConnectionDidFailToConnectAction; - - if (connectResult.type === BootloaderConnectionActionType.DidFailToConnect) { - return; - } - - yield put(infoRequest()); - const info = (yield wait(BootloaderResponseActionType.Info)) as WaitResponse< - BootloaderInfoResponseAction - >; - if (!info[0]) { - throw Error(`failed to get info: ${info}`); - } - - if (deviceId !== undefined && info[0].hubType !== deviceId) { - throw Error(`Connected to ${info[0].hubType} but firmware is for ${deviceId}`); - } - - if (firmware === undefined) { - const firmwarePath = firmwareZipMap.get(info[0].hubType); - if (firmwarePath === undefined) { - yield put( - notification.add( - 'error', - "Sorry, we don't have firmware for this hub yet.", - ), - ); - yield put(disconnectRequest()); - return; - } - - const response = (yield call(() => fetch(firmwarePath))) as Response; - if (!response.ok) { - yield put(notification.add('error', 'Failed to fetch firmware.')); - yield put(disconnectRequest()); - return; - } - - const data = (yield call(() => response.arrayBuffer())) as ArrayBuffer; - ({ firmware, deviceId } = yield* loadFirmware(data)); - - if (deviceId !== undefined && info[0].hubType !== deviceId) { - throw Error( - `Connected to ${info[0].hubType} but firmware is for ${deviceId}`, - ); - } - } - - // City hub bootloader is buggy. See note in encodeRequest(). - if (info[0].hubType === HubType.CityHub && !connectResult.canWriteWithoutResponse) { - yield put( - notification.add( - 'error', - 'City Hub is not compatible with this web browser.', - ), - ); - yield put(disconnectRequest()); - return; - } - - yield put(eraseRequest()); - const erase = (yield wait( - BootloaderResponseActionType.Erase, - 5000, - )) as WaitResponse; - if (!erase[0] || erase[0].result) { - // TODO: proper error handling - throw Error(`Failed to erase: ${erase}`); - } - - yield put(initRequest(firmware.length)); - const init = (yield wait(BootloaderResponseActionType.Init)) as WaitResponse< - BootloaderInitResponseAction - >; - if (!init[0] || init[0].result) { - // TODO: proper error handling - throw Error(`Failed to init: ${init}`); - } - - let count = 0; - - for (let offset = 0; offset < firmware.length; offset += MaxProgramFlashSize) { - const payload = firmware.slice(offset, offset + MaxProgramFlashSize); - const req = (yield put( - programRequest(info[0].startAddress + offset, payload.buffer), - )) as BootloaderProgramRequestAction; - - // TODO: check for error - yield take( - (a: Action) => - a.type === BootloaderDidRequestType && - (a as BootloaderDidRequestAction).id === req.id, - ); - - yield put(progress(offset, firmware.length)); - - if (connectResult.canWriteWithoutResponse) { - // request checksum every 8K to prevent buffer overrun on the hub - // because of sending too much data at once - if (++count % 585 === 0) { - yield put(checksumRequest()); - const checksum = (yield wait( - BootloaderResponseActionType.Checksum, - 5000, - )) as WaitResponse; - if (!checksum[0]) { - // TODO: proper error handling - throw Error(`Failed to get checksum: ${checksum}`); - } - } - } - } - - const flash = (yield wait( - BootloaderResponseActionType.Program, - 5000, - )) as WaitResponse; - if (!flash[0]) { - throw Error(`failed to get final response: ${flash}`); - } - if (flash[0].count !== firmware.length) { - // TODO: proper error handling - throw Error("Didn't flash all bytes"); - } - - yield put(progress(firmware.length, firmware.length)); - - // this will cause the remote device to disconnect and reboot - yield put(rebootRequest()); -} - export default function* (): Generator { yield fork(encodeRequest); yield takeEvery(BootloaderConnectionActionType.DidReceive, decodeResponse); - yield takeEvery(BootloaderActionType.FlashFirmware, flashFirmware); }