implement flashing firmware from new zip format

This commit is contained in:
David Lechner
2020-05-02 14:40:18 -05:00
parent 9d545f8f5c
commit c7b7162693
7 changed files with 149 additions and 15 deletions
+2
View File
@@ -8,6 +8,7 @@
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/jszip": "^3.1.7",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
@@ -18,6 +19,7 @@
"ace-builds": "^1.4.9",
"bootstrap": "^4.4.1",
"bootswatch": "^4.4.1",
"jszip": "^3.4.0",
"node-sass": "^4.13.1",
"react": "^16.13.1",
"react-ace": "^8.1.0",
+2 -2
View File
@@ -4,11 +4,11 @@ import { Action } from 'redux';
// this starts the mpy-cross wasm runtime and leaves it running in the background
const mpy = MpyCross({ arguments: ['-mno-unicode'] });
enum MpyActionType {
export enum MpyActionType {
Compiled = 'mpy.action.compile',
}
interface MpyCompiledAction extends Action<MpyActionType.Compiled> {
export interface MpyCompiledAction extends Action<MpyActionType.Compiled> {
/**
* The compiled .mpy data.
*/
+1 -1
View File
@@ -33,7 +33,7 @@ const mergeProps = (
dispatchProps: DispatchProps,
ownProps: OwnProps,
): OpenFileButtonProps => ({
fileExtension: '.bin',
fileExtension: '.zip',
tooltip: 'Flash hub firmware',
icon: 'firmware.svg',
...ownProps,
+80 -10
View File
@@ -1,8 +1,10 @@
import JSZip from 'jszip';
import { Action } from 'redux';
import { Channel, buffers } from 'redux-saga';
import {
Effect,
actionChannel,
call,
delay,
fork,
put,
@@ -42,9 +44,11 @@ import {
send,
stateResponse,
} from '../actions/bootloader';
import { MpyCompiledAction, compile } from '../actions/mpy';
import {
Command,
ErrorBytecode,
HubType,
MaxProgramFlashSize,
createDisconnectRequest,
createEraseFlashRequest,
@@ -62,6 +66,7 @@ import {
parseInitLoaderResponse,
parseProgramFlashResponse,
} from '../protocols/bootloader';
import { fmod, sumComplement32 } from '../utils/math';
/**
* Converts a request action into bytecodes and creates a new action to send
@@ -166,11 +171,76 @@ 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': Array<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;
}
}
/**
* Flashes firmware to a Powered Up device.
* @param action The action that triggered this saga.
*/
function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
const zip = (yield call(() => JSZip().loadAsync(action.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'] !== 4) {
throw Error(
`Firmware requires mpy-cross ABI version ${metadata['mpy-abi-version']} we have v4`,
);
}
// TODO: pass metadata["mpy-cross-options"] to compiler
const mpy = (yield put(compile(main))) as MpyCompiledAction;
// 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,
);
yield put(connect());
const didConnect = (yield take([
BootloaderConnectionActionType.DidConnect,
@@ -198,7 +268,11 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
throw Error(`failed to get info: ${info}`);
}
// TODO: verify hubType === info.response.hubType
if (info[0].hubType !== metadata['device-id']) {
throw Error(
`Connected to ${info[0].hubType} but firmware is for ${metadata['device-id']}`,
);
}
yield put(eraseRequest());
const erase = (yield wait(
@@ -210,7 +284,7 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
throw Error(`Failed to erase: ${erase}`);
}
yield put(initRequest(action.data.byteLength));
yield put(initRequest(firmware.length));
const init = (yield wait(BootloaderResponseActionType.Init)) as WaitResponse<
BootloaderInitResponseAction
>;
@@ -221,13 +295,9 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
let count = 0;
for (
let offset = 0;
offset < action.data.byteLength;
offset += MaxProgramFlashSize
) {
const payload = action.data.slice(offset, offset + MaxProgramFlashSize);
yield put(programRequest(info[0].startAddress + offset, payload));
for (let offset = 0; offset < firmware.length; offset += MaxProgramFlashSize) {
const payload = firmware.slice(offset, offset + MaxProgramFlashSize);
yield put(programRequest(info[0].startAddress + offset, payload.buffer));
// TODO: dispatch progress action
@@ -253,7 +323,7 @@ function* flashFirmware(action: BootloaderFlashFirmwareAction): Generator {
if (!flash[0]) {
throw Error(`failed to get final response: ${flash}`);
}
if (flash[0].count !== action.data.byteLength) {
if (flash[0].count !== firmware.length) {
// TODO: proper error handling
throw Error("Didn't flash all bytes");
}
+27
View File
@@ -0,0 +1,27 @@
/**
* Compute modulo using floored division
* @param a first operand
* @param b second operand
*/
export function fmod(a: number, b: number): number {
let c = a % b;
if (c / b < 0) {
c += b;
}
return c;
}
/**
* Calculates the 32-bit "sum complement" checksum
* @data an iterable of 32-bit integers
* @returns the value that needs to be added to get 0
*/
export function sumComplement32(data: Iterable<number>): number {
let total = 0;
for (const n of data) {
total += n;
total &= ~0;
}
// checksum is two's complement of total
return ~total + 1;
}
+2 -1
View File
@@ -13,7 +13,8 @@
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react"
"jsx": "react",
"downlevelIteration": true
},
"include": ["src"]
}
+35 -1
View File
@@ -1570,6 +1570,13 @@
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.4.tgz#38fd73ddfd9b55abb1e1b2ed578cb55bd7b7d339"
integrity sha512-8+KAKzEvSUdeo+kmqnKrqgeE+LcA0tjYWFY7RPProVYwnqDjukzO+3b6dLD56rYX5TdWejnEOLJYOIeh4CXKuA==
"@types/jszip@^3.1.7":
version "3.1.7"
resolved "https://registry.yarnpkg.com/@types/jszip/-/jszip-3.1.7.tgz#c45bd72b448b3fb002125282c57c36190247cb34"
integrity sha512-+XQKNI5zpxutK05hO67huUTw/2imXCuJWjnFdU63tRES/xXSX1yVR9cv/QAdO6Rii2y2tTHbzjQ4i2apLfuK0Q==
dependencies:
"@types/node" "*"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
@@ -5525,6 +5532,11 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=
immer@1.10.0:
version "1.10.0"
resolved "https://registry.yarnpkg.com/immer/-/immer-1.10.0.tgz#bad67605ba9c810275d91e1c2a47d4582e98286d"
@@ -6668,6 +6680,16 @@ jsx-ast-utils@^2.2.1, jsx-ast-utils@^2.2.3:
array-includes "^3.0.3"
object.assign "^4.1.0"
jszip@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.4.0.tgz#1a69421fa5f0bb9bc222a46bca88182fba075350"
integrity sha512-gZAOYuPl4EhPTXT0GjhI3o+ZAz3su6EhLrKUoAivcKqyqC7laS5JEv4XWZND9BgcDcF83vI85yGbDmDR6UhrIg==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
set-immediate-shim "~1.0.1"
killable@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/killable/-/killable-1.0.1.tgz#4c8ce441187a061c7474fb87ca08e2a638194892"
@@ -6766,6 +6788,13 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lines-and-columns@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
@@ -7862,7 +7891,7 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
pako@~1.0.5:
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@@ -9979,6 +10008,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=
set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"