split firmware flashing from lwp3-bootloader

BLE won't always be the only way to flash firmware
This commit is contained in:
David Lechner
2020-06-09 23:51:32 -05:00
committed by David Lechner
parent 6d81da0df0
commit 07d0b5ef39
8 changed files with 376 additions and 371 deletions
+58
View File
@@ -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<FlashFirmwareActionType.FlashFirmware> {
/** 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<FlashFirmwareActionType.Progress> {
/**
* 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;
+5 -5
View File
@@ -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
-57
View File
@@ -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<BootloaderActionType.FlashFirmware> {
/** 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<BootloaderActionType.FlashProgress> {
/**
* 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;
+1 -1
View File
@@ -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';
+4 -3
View File
@@ -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<number, BootloaderAction> = (state = -1, action) => {
const progress: Reducer<number, Action> = (state = -1, action) => {
switch (action.type) {
case BootloaderActionType.FlashProgress:
case FlashFirmwareActionType.Progress:
return action.complete / action.total;
default:
return state;
+305
View File
@@ -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, string>([
[HubType.CPlusHub, cPlusHubZip],
[HubType.MoveHub, moveHubZip],
]);
/**
* Helper type for return value of wait() function.
*/
type WaitResponse<T extends BootloaderResponseAction> = [
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<number> {
// 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<unknown, { firmware: Uint8Array; deviceId: HubType }> {
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<BootloaderEraseResponseAction>;
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<BootloaderChecksumResponseAction>;
if (!checksum[0]) {
// TODO: proper error handling
throw Error(`Failed to get checksum: ${checksum}`);
}
}
}
}
const flash = (yield wait(
BootloaderResponseActionType.Program,
5000,
)) as WaitResponse<BootloaderProgramResponseAction>;
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);
}
+2 -1
View File
@@ -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()]);
}
+1 -304
View File
@@ -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, string>([
[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 extends BootloaderResponseAction> = [
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<number> {
// 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<unknown, { firmware: Uint8Array; deviceId: HubType }> {
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<BootloaderEraseResponseAction>;
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<BootloaderChecksumResponseAction>;
if (!checksum[0]) {
// TODO: proper error handling
throw Error(`Failed to get checksum: ${checksum}`);
}
}
}
}
const flash = (yield wait(
BootloaderResponseActionType.Program,
5000,
)) as WaitResponse<BootloaderProgramResponseAction>;
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);
}