mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
firmware: add support for firmware.metadata.json v2.0.0
This adds support for the new firmware.metadata.json v2.0.0 which drops support for appending a main.py file to the firmware.
This commit is contained in:
+1
-1
@@ -13,7 +13,7 @@
|
||||
"@blueprintjs/popover2": "^1.6.4",
|
||||
"@blueprintjs/select": "^4.6.4",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
|
||||
"@pybricks/firmware": "5.0.0",
|
||||
"@pybricks/firmware": "6.0.1",
|
||||
"@pybricks/ide-docs": "2.2.0",
|
||||
"@pybricks/jedi": "^1.0.1",
|
||||
"@pybricks/mpy-cross-v5": "^2.0.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2020-2022 The Pybricks Authors
|
||||
|
||||
import { FirmwareMetadata, FirmwareReaderError } from '@pybricks/firmware';
|
||||
import { FirmwareReaderError } from '@pybricks/firmware';
|
||||
import { createAction } from '../actions';
|
||||
|
||||
export enum MetadataProblem {
|
||||
@@ -86,7 +86,7 @@ export type FailToFinishReasonZipError = Reason<FailToFinishReasonType.ZipError>
|
||||
|
||||
export type FailToFinishReasonBadMetadata =
|
||||
Reason<FailToFinishReasonType.BadMetadata> & {
|
||||
property: keyof FirmwareMetadata;
|
||||
property: string;
|
||||
problem: MetadataProblem;
|
||||
};
|
||||
|
||||
@@ -228,7 +228,7 @@ function didFailToFinishCreator(
|
||||
|
||||
function didFailToFinishCreator(
|
||||
reason: FailToFinishReasonType.BadMetadata,
|
||||
property: keyof FirmwareMetadata,
|
||||
property: string,
|
||||
problem: MetadataProblem,
|
||||
): {
|
||||
type: typeof didFailToFinishType;
|
||||
@@ -324,7 +324,9 @@ function didFailToFinishCreator(
|
||||
arg1 !== 'mpy-abi-version' &&
|
||||
arg1 !== 'mpy-cross-options' &&
|
||||
arg1 !== 'user-mpy-offset' &&
|
||||
arg1 !== 'max-firmware-size'
|
||||
arg1 !== 'max-firmware-size' &&
|
||||
arg1 !== 'checksum-size' &&
|
||||
arg1 !== 'hub-name-size'
|
||||
) {
|
||||
throw new Error('missing or invalid property');
|
||||
}
|
||||
|
||||
+134
-4
@@ -4,6 +4,8 @@
|
||||
import { ToasterInstance } from '@blueprintjs/core';
|
||||
import {
|
||||
FirmwareMetadata,
|
||||
FirmwareMetadataV110,
|
||||
FirmwareMetadataV200,
|
||||
FirmwareReaderError,
|
||||
FirmwareReaderErrorCode,
|
||||
} from '@pybricks/firmware';
|
||||
@@ -55,9 +57,9 @@ afterEach(() => {
|
||||
|
||||
describe('flashFirmware', () => {
|
||||
describe('normal flow using app supplied firmware', () => {
|
||||
test('success', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
test('metadata v1.x works', async () => {
|
||||
const metadata: FirmwareMetadataV110 = {
|
||||
'metadata-version': '1.1.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'checksum-type': 'sum',
|
||||
'firmware-version': '1.2.3',
|
||||
@@ -65,7 +67,7 @@ describe('flashFirmware', () => {
|
||||
'mpy-abi-version': 5,
|
||||
'mpy-cross-options': ['-mno-unicode'],
|
||||
'user-mpy-offset': 100,
|
||||
'hub-name-offset': 90,
|
||||
'hub-name-offset': 54,
|
||||
'max-hub-name-size': 10,
|
||||
};
|
||||
|
||||
@@ -207,6 +209,134 @@ describe('flashFirmware', () => {
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('metadata v2.x works', async () => {
|
||||
const metadata: FirmwareMetadataV200 = {
|
||||
'metadata-version': '2.0.0',
|
||||
'device-id': HubType.MoveHub,
|
||||
'firmware-version': '1.2.3',
|
||||
'checksum-type': 'sum',
|
||||
'checksum-size': 1024,
|
||||
'hub-name-offset': 54,
|
||||
'hub-name-size': 10,
|
||||
};
|
||||
|
||||
const zip = new JSZip();
|
||||
zip.file('firmware-base.bin', new Uint8Array(64));
|
||||
zip.file('firmware.metadata.json', JSON.stringify(metadata));
|
||||
zip.file('ReadMe_OSS.txt', 'test');
|
||||
|
||||
jest.spyOn(window, 'fetch').mockResolvedValueOnce(
|
||||
new Response(await zip.generateAsync({ type: 'blob' })),
|
||||
);
|
||||
|
||||
const saga = new AsyncSaga(flashFirmware, {
|
||||
nextMessageId: createCountFunc(),
|
||||
toaster: mock<ToasterInstance>(),
|
||||
});
|
||||
|
||||
// saga is triggered by this action
|
||||
|
||||
saga.put(flashFirmwareAction(null, undefined, 'test name'));
|
||||
|
||||
// first step is to connect to the hub bootloader
|
||||
|
||||
let action = await saga.take();
|
||||
expect(action).toEqual(connect());
|
||||
|
||||
saga.updateState({
|
||||
bootloader: { connection: BootloaderConnectionState.Connected },
|
||||
});
|
||||
saga.put(didConnect());
|
||||
|
||||
// then find out what kind of hub it is
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(infoRequest(0));
|
||||
|
||||
saga.put(didRequest(0));
|
||||
saga.put(infoResponse(0x01000000, 0x08005000, 0x081f800, HubType.MoveHub));
|
||||
|
||||
// then start flashing the firmware
|
||||
|
||||
// should get didStart action just before starting to erase
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didStart());
|
||||
|
||||
// erase first
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(alertsShowAlert('firmware', 'releaseButton'));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(eraseRequest(1, /* isCityHub */ false));
|
||||
|
||||
saga.put(didRequest(1));
|
||||
saga.put(eraseResponse(Result.OK));
|
||||
|
||||
// then write the new firmware
|
||||
|
||||
const totalFirmwareSize = 68;
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(initRequest(2, totalFirmwareSize));
|
||||
|
||||
saga.put(didRequest(2));
|
||||
saga.put(initResponse(Result.OK));
|
||||
|
||||
const dummyPayload = new ArrayBuffer(0);
|
||||
let id = 2;
|
||||
for (let count = 1, offset = 0; ; count++, offset += 14) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(
|
||||
programRequest(++id, 0x08005000 + offset, dummyPayload),
|
||||
);
|
||||
expect(
|
||||
(action as ReturnType<typeof programRequest>).payload.byteLength,
|
||||
).toBe(Math.min(14, totalFirmwareSize - offset));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(offset / totalFirmwareSize));
|
||||
|
||||
// Have to be careful that a checksum request is not sent after
|
||||
// last payload is sent, otherwise the hub gets confused.
|
||||
|
||||
if (offset + 14 >= totalFirmwareSize) {
|
||||
expect(count).toBe(5);
|
||||
break;
|
||||
}
|
||||
|
||||
if (count % 10 === 0) {
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(checksumRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
saga.put(checksumResponse(0));
|
||||
}
|
||||
}
|
||||
|
||||
// hub indicates success
|
||||
|
||||
saga.put(programResponse(0xe0, totalFirmwareSize));
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didProgress(1));
|
||||
|
||||
// and finally reboot the hub
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(rebootRequest(++id));
|
||||
|
||||
saga.put(didRequest(id));
|
||||
|
||||
// then we are done
|
||||
|
||||
action = await saga.take();
|
||||
expect(action).toEqual(didFinish());
|
||||
|
||||
await saga.end();
|
||||
});
|
||||
|
||||
test('fail to connect', async () => {
|
||||
const metadata: FirmwareMetadata = {
|
||||
'metadata-version': '1.0.0',
|
||||
|
||||
+121
-64
@@ -7,6 +7,8 @@ import {
|
||||
FirmwareReaderError,
|
||||
HubType,
|
||||
encodeHubName,
|
||||
metadataIsV100,
|
||||
metadataIsV110,
|
||||
} from '@pybricks/firmware';
|
||||
import cityHubZip from '@pybricks/firmware/build/cityhub.zip';
|
||||
import moveHubZip from '@pybricks/firmware/build/movehub.zip';
|
||||
@@ -219,90 +221,145 @@ function* loadFirmware(
|
||||
const firmwareBase = yield* call(() => reader.readFirmwareBase());
|
||||
const metadata = yield* call(() => reader.readMetadata());
|
||||
|
||||
// if a user program was not given, then use main.py from the firmware.zip
|
||||
if (program === undefined) {
|
||||
program = yield* call(() => reader.readMainPy());
|
||||
}
|
||||
// v1.x allows appending main.py to firmware, later versions do not
|
||||
if (metadataIsV100(metadata) || metadataIsV110(metadata)) {
|
||||
// if a user program was not given, then use main.py from the firmware.zip
|
||||
if (program === undefined) {
|
||||
program = yield* call(() => reader.readMainPy());
|
||||
}
|
||||
|
||||
// REVISIT: the firmware may eventually be changed to allow no main.py
|
||||
// for now, ensure there is a program even if it does nothing
|
||||
if (!program) {
|
||||
program = '';
|
||||
}
|
||||
// REVISIT: the firmware may eventually be changed to allow no main.py
|
||||
// for now, ensure there is a program even if it does nothing
|
||||
if (!program) {
|
||||
program = '';
|
||||
}
|
||||
|
||||
if (![5, 6].includes(metadata['mpy-abi-version'])) {
|
||||
yield* put(
|
||||
didFailToFinish(
|
||||
FailToFinishReasonType.BadMetadata,
|
||||
'mpy-abi-version',
|
||||
MetadataProblem.NotSupported,
|
||||
),
|
||||
);
|
||||
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
if (![5, 6].includes(metadata['mpy-abi-version'])) {
|
||||
yield* put(
|
||||
didFailToFinish(
|
||||
FailToFinishReasonType.BadMetadata,
|
||||
'mpy-abi-version',
|
||||
MetadataProblem.NotSupported,
|
||||
compile(
|
||||
program,
|
||||
metadata['mpy-abi-version'],
|
||||
metadata['mpy-cross-options'],
|
||||
),
|
||||
);
|
||||
const { mpy, mpyFail } = yield* race({
|
||||
mpy: take(didCompile),
|
||||
mpyFail: take(didFailToCompile),
|
||||
});
|
||||
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* disconnectAndCancel();
|
||||
if (mpyFail) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FailedToCompile));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
defined(mpy);
|
||||
|
||||
// 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']) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FirmwareSize));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmware.set(firmwareBase);
|
||||
firmwareView.setUint32(metadata['user-mpy-offset'], mpy.data.length, true);
|
||||
firmware.set(mpy.data, metadata['user-mpy-offset'] + 4);
|
||||
|
||||
// if the firmware supports it, we can set a custom hub name
|
||||
if (!metadataIsV100(metadata)) {
|
||||
// empty string means use default name (don't write over firmware)
|
||||
if (hubName) {
|
||||
firmware.set(
|
||||
encodeHubName(hubName, metadata),
|
||||
metadata['hub-name-offset'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const checksum = (function () {
|
||||
switch (metadata['checksum-type']) {
|
||||
case 'sum':
|
||||
return sumComplement32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
case 'crc32':
|
||||
return crc32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!checksum) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(
|
||||
didFailToFinish(
|
||||
FailToFinishReasonType.BadMetadata,
|
||||
'checksum-type',
|
||||
MetadataProblem.NotSupported,
|
||||
),
|
||||
);
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmwareView.setUint32(checksumOffset, checksum, true);
|
||||
|
||||
return { firmware, deviceId: metadata['device-id'] };
|
||||
}
|
||||
|
||||
yield* put(
|
||||
compile(program, metadata['mpy-abi-version'], metadata['mpy-cross-options']),
|
||||
);
|
||||
const { mpy, mpyFail } = yield* race({
|
||||
mpy: take(didCompile),
|
||||
mpyFail: take(didFailToCompile),
|
||||
});
|
||||
|
||||
if (mpyFail) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FailedToCompile));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
defined(mpy);
|
||||
|
||||
// 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 firmware = new Uint8Array(firmwareBase.length + 4);
|
||||
const firmwareView = new DataView(firmware.buffer);
|
||||
|
||||
if (firmware.length > metadata['max-firmware-size']) {
|
||||
// FIXME: we should return error/throw instead
|
||||
yield* put(didFailToFinish(FailToFinishReasonType.FirmwareSize));
|
||||
yield* disconnectAndCancel();
|
||||
|
||||
// istanbul ignore next: needed for typescript flow
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmware.set(firmwareBase);
|
||||
firmwareView.setUint32(metadata['user-mpy-offset'], mpy.data.length, true);
|
||||
firmware.set(mpy.data, metadata['user-mpy-offset'] + 4);
|
||||
|
||||
// if the firmware supports it, we can set a custom hub name
|
||||
if (metadata['max-hub-name-size']) {
|
||||
// empty string means use default name (don't write over firmware)
|
||||
if (hubName) {
|
||||
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
|
||||
}
|
||||
// empty string means use default name (don't write over firmware)
|
||||
if (hubName) {
|
||||
firmware.set(encodeHubName(hubName, metadata), metadata['hub-name-offset']);
|
||||
}
|
||||
|
||||
const checksum = (function () {
|
||||
switch (metadata['checksum-type']) {
|
||||
case 'sum':
|
||||
return sumComplement32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
firmwareIterator(firmwareView, metadata['checksum-size']),
|
||||
);
|
||||
case 'crc32':
|
||||
return crc32(
|
||||
firmwareIterator(firmwareView, metadata['max-firmware-size']),
|
||||
);
|
||||
return crc32(firmwareIterator(firmwareView, metadata['checksum-size']));
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
@@ -323,7 +380,7 @@ function* loadFirmware(
|
||||
throw new Error('unreachable');
|
||||
}
|
||||
|
||||
firmwareView.setUint32(checksumOffset, checksum, true);
|
||||
firmwareView.setUint32(firmwareBase.length, checksum, true);
|
||||
|
||||
return { firmware, deviceId: metadata['device-id'] };
|
||||
}
|
||||
|
||||
@@ -2369,12 +2369,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pybricks/firmware@npm:5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "@pybricks/firmware@npm:5.0.0"
|
||||
"@pybricks/firmware@npm:6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "@pybricks/firmware@npm:6.0.1"
|
||||
dependencies:
|
||||
jszip: ^3.7.1
|
||||
checksum: 049dd90e988aa574cfa0ead1e62bcb74e6fdfc9b709bc1c40874ddf3abb63cd35555d22806c91184bc2e982912a001e0ef94ef72ef66217eb0319bcaf45a7cb3
|
||||
checksum: c0d6e9bef7ac8b1009f90f64cedb8872bae02c6ef6c02fcaf73238407b5164c9c27312dc7afaf051663f7f086bca7b8ccbeec641ef3105ec03308e3d9221175a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -2415,7 +2415,7 @@ __metadata:
|
||||
"@blueprintjs/popover2": ^1.6.4
|
||||
"@blueprintjs/select": ^4.6.4
|
||||
"@pmmmwh/react-refresh-webpack-plugin": ^0.5.7
|
||||
"@pybricks/firmware": 5.0.0
|
||||
"@pybricks/firmware": 6.0.1
|
||||
"@pybricks/ide-docs": 2.2.0
|
||||
"@pybricks/jedi": ^1.0.1
|
||||
"@pybricks/mpy-cross-v5": ^2.0.0
|
||||
|
||||
Reference in New Issue
Block a user