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
This commit is contained in:
David Lechner
2020-05-25 21:31:35 -05:00
committed by David Lechner
parent 5160d29c7e
commit 33861d970b
7 changed files with 95 additions and 11 deletions
+1
View File
@@ -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",
+10 -2
View File
@@ -428,12 +428,20 @@ export enum BootloaderActionType {
FlashProgress = 'bootloader.action.flash.progress',
}
/**
* Action that flashes firmware to a hub.
*/
export interface BootloaderFlashFirmwareAction
extends Action<BootloaderActionType.FlashFirmware> {
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 };
}
+4 -1
View File
@@ -10,7 +10,7 @@ import { BootloaderConnectionState } from '../reducers/bootloader';
import OpenFileButton, { OpenFileButtonProps } from './OpenFileButton';
type StateProps = Pick<OpenFileButtonProps, 'enabled'>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject'>;
type DispatchProps = Pick<OpenFileButtonProps, 'onFile' | 'onReject' | 'onClick'>;
type OwnProps = Pick<OpenFileButtonProps, 'id'>;
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 = (
+10
View File
@@ -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<OpenFileButtonProps> {
onDropRejected={this.onDropRejected}
accept={this.props.fileExtension}
multiple={false}
noClick={this.props.onClick !== undefined}
noKeyboard={this.props.onClick !== undefined}
>
{({ getRootProps, getInputProps }): JSX.Element => (
<OverlayTrigger
@@ -89,6 +93,12 @@ class OpenFileButton extends React.Component<OpenFileButtonProps> {
? { 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 }
: {})}
>
<input {...getInputProps()} />
<Image
+5
View File
@@ -1 +1,6 @@
/// <reference types="react-scripts" />
declare module '*.zip' {
const src: string;
export default src;
}
+60 -8
View File
@@ -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, string>([
[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<number> {
}
/**
* 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<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;
@@ -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().
+5
View File
@@ -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"