From 33861d970b5bbd1532c5f7d22e25fadcc6ef13ad Mon Sep 17 00:00:00 2001 From: David Lechner Date: Mon, 25 May 2020 21:24:25 -0500 Subject: [PATCH] Package firmware in app - includes firmware .zip for movehub and cplushub - firmware flash button no longer opens file dialog - now connects first, then choses firmware based on connected device - drag and drop of custom firmware still works --- package.json | 1 + src/actions/bootloader.ts | 12 +++++- src/components/FlashButton.tsx | 5 ++- src/components/OpenFileButton.tsx | 10 +++++ src/react-app-env.d.ts | 5 +++ src/sagas/bootloader.ts | 68 +++++++++++++++++++++++++++---- yarn.lock | 5 +++ 7 files changed, 95 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index f2170da9..81d12e9c 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@pybricks/firmware": "^1.0.0", "@pybricks/mpy-cross-v4": "^1.0.0", "@testing-library/jest-dom": "^5.8.0", "@testing-library/react": "^10.0.4", diff --git a/src/actions/bootloader.ts b/src/actions/bootloader.ts index 4b222d3b..d003b4c4 100644 --- a/src/actions/bootloader.ts +++ b/src/actions/bootloader.ts @@ -428,12 +428,20 @@ export enum BootloaderActionType { FlashProgress = 'bootloader.action.flash.progress', } +/** + * Action that flashes firmware to a hub. + */ export interface BootloaderFlashFirmwareAction extends Action { - data: ArrayBuffer; + /** The firmware zip file data or undefined to get firmware later. */ + data?: ArrayBuffer; } -export function flashFirmware(data: ArrayBuffer): BootloaderFlashFirmwareAction { +/** + * 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 }; } diff --git a/src/components/FlashButton.tsx b/src/components/FlashButton.tsx index d983000a..001face8 100644 --- a/src/components/FlashButton.tsx +++ b/src/components/FlashButton.tsx @@ -10,7 +10,7 @@ import { BootloaderConnectionState } from '../reducers/bootloader'; import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton'; type StateProps = Pick; -type DispatchProps = Pick; +type DispatchProps = Pick; type OwnProps = Pick; const mapStateToProps = (state: RootState): StateProps => ({ @@ -26,6 +26,9 @@ const mapDispatchToProps = (dispatch: Dispatch): DispatchProps => ({ notification.add('error', `'${file.name}' is not a valid firmware file.`), ); }, + onClick: (): void => { + dispatch(flashFirmware()); + }, }); const mergeProps = ( diff --git a/src/components/OpenFileButton.tsx b/src/components/OpenFileButton.tsx index 04dc966f..e6b68116 100644 --- a/src/components/OpenFileButton.tsx +++ b/src/components/OpenFileButton.tsx @@ -23,6 +23,8 @@ export interface OpenFileButtonProps { readonly onFile: (data: ArrayBuffer) => void; /** Callback that is called when a file has been rejected (e.g. bad file extension). */ readonly onReject: (file: File) => void; + /** If defined, will call custom function instead of opening file browser. */ + readonly onClick?: () => void; } /** @@ -70,6 +72,8 @@ class OpenFileButton extends React.Component { onDropRejected={this.onDropRejected} accept={this.props.fileExtension} multiple={false} + noClick={this.props.onClick !== undefined} + noKeyboard={this.props.onClick !== undefined} > {({ getRootProps, getInputProps }): JSX.Element => ( { ? { pointerEvents: 'none' } : undefined } + // onClick={this.props.onClick} + // breaks Dropzone when this.props.onClick is undefined + // so we have to do it the long way + {...(this.props.onClick + ? { onClick: this.props.onClick } + : {})} > + +declare module '*.zip' { + const src: string; + export default src; +} diff --git a/src/sagas/bootloader.ts b/src/sagas/bootloader.ts index 12b615a1..3541b35a 100644 --- a/src/sagas/bootloader.ts +++ b/src/sagas/bootloader.ts @@ -1,6 +1,8 @@ // 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 { Action } from 'redux'; import { Channel, buffers } from 'redux-saga'; @@ -82,6 +84,11 @@ import { } from '../protocols/bootloader'; 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 * the bytecodes to to the device. @@ -227,11 +234,13 @@ function* firmwareIterator(data: DataView, maxSize: number): Generator { } /** - * Flashes firmware to a Powered Up device. - * @param action The action that triggered this saga. + * Loads Pybricks firmware from a .zip file + * @param data The zip file raw data */ -function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator { - const zip = (yield call(() => JSZip().loadAsync(action.data))) as JSZip; +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; @@ -279,6 +288,21 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator { 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 didConnect = (yield take([ BootloaderConnectionActionType.DidConnect, @@ -306,10 +330,38 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator { throw Error(`failed to get info: ${info}`); } - if (info[0].hubType !== metadata['device-id']) { - throw Error( - `Connected to ${info[0].hubType} but firmware is for ${metadata['device-id']}`, - ); + 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(). diff --git a/yarn.lock b/yarn.lock index 7d71c82c..a5866572 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1254,6 +1254,11 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.4.0.tgz#0e1bdf8d021e7ea58affade33d9d607e11365915" integrity sha512-NMrDy6EWh9TPdSRiHmHH2ye1v5U0gBD7pRYwSwJvomx7Bm4GG04vu63dYiVzebLOx2obPpJugew06xVP0Nk7hA== +"@pybricks/firmware@^1.0.0": + version "1.0.0" + resolved "https://npm.pkg.github.com/download/@pybricks/firmware/1.0.0/dfc52164caa622a8c5e0c59d110319441b7966f25e4d2764d592ce7b56672465#3f763dff5751d8e4b8ff94f73678ee30ac52233b" + integrity sha512-eMGuzLRKVMvnZc093/7aW5/fGtRfz39aSLy+Drhc6j4tJqySBdRhNk709mH7ZYMj4RJ2D6mWNbuzi3LVGsVkAA== + "@pybricks/mpy-cross-v4@^1.0.0": version "1.0.0" resolved "https://npm.pkg.github.com/download/@pybricks/mpy-cross-v4/1.0.0/1961a986e77b302cb05c3e6dc03feefe06292488a05d23ea32d27e2f625e782a#82633377519ba904dbd3e0f700a4b38f8212a80f"