mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-11 17:14:15 +00:00
implement flashing firmware from new zip format
This commit is contained in:
@@ -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
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -13,7 +13,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react",
|
||||
"downlevelIteration": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user