Skip to content

Commit dbdf6e7

Browse files
committed
npm/firmware: add v1.1 metadata support
- Add new metadata fields. - Add new `encodeHubName()` function. - Auto-format code with Prettier.
1 parent b292993 commit dbdf6e7

File tree

6 files changed

+126
-26
lines changed

6 files changed

+126
-26
lines changed

.github/workflows/npm-firmware.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
- run: python -m pip install PyGithub
2222
- uses: actions/setup-node@v1
2323
with:
24-
node-version: '10.x'
24+
node-version: '16.x'
2525
registry-url: 'https://registry.npmjs.org'
2626
- run: yarn install
2727
- run: yarn build

npm/firmware/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11

2+
### Added
3+
- Added support for v1.1.0 metadata.
4+
- Added `encodeHubName()` function.
5+
26
## 4.13.0-beta.1 - 2021-09-21
37
### Changed
48
- Updated dependencies.

npm/firmware/index.test.ts

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,22 @@
11
// SPDX-License-Identifier: MIT
2-
// Copyright (c) 2020 The Pybricks Authors
2+
// Copyright (c) 2020-2021 The Pybricks Authors
33

44
import * as fs from 'fs';
55
import * as path from 'path';
66
import * as util from 'util';
7-
import { firmwareVersion, FirmwareReader, FirmwareReaderErrorCode } from '.';
7+
import {
8+
firmwareVersion,
9+
FirmwareReader,
10+
FirmwareReaderErrorCode,
11+
encodeHubName,
12+
FirmwareMetadata,
13+
} from './index';
14+
15+
let TextEncoder;
16+
17+
beforeAll(() => {
18+
TextEncoder = util.TextEncoder;
19+
});
820

921
const readFile = util.promisify(fs.readFile);
1022

@@ -84,3 +96,45 @@ test('reading data works', async () => {
8496
expect(await reader.readMainPy()).toMatchSnapshot();
8597
expect(await reader.readReadMeOss()).toMatchSnapshot();
8698
});
99+
100+
describe('firmware name encoder', () => {
101+
const metadata = { 'max-hub-name-size': 16 } as FirmwareMetadata;
102+
103+
test('default works', () => {
104+
expect(encodeHubName('', metadata)).toEqual(
105+
new Uint8Array([
106+
80, 121, 98, 114, 105, 99, 107, 115, 32, 72, 117, 98, 0, 0, 0,
107+
0,
108+
])
109+
);
110+
});
111+
112+
test('max length works', () => {
113+
expect(encodeHubName('123456789012345', metadata)).toEqual(
114+
new Uint8Array([
115+
49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0,
116+
])
117+
);
118+
});
119+
120+
test('exceeding max length truncates and preserves zero termination', () => {
121+
expect(encodeHubName('1234567890123456', metadata)).toEqual(
122+
new Uint8Array([
123+
49, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 0,
124+
])
125+
);
126+
});
127+
128+
test('truncating on multi-byte characters results in valid UTF-8', () => {
129+
expect(encodeHubName('Pybricks Hub 😀', metadata)).toEqual(
130+
new Uint8Array([
131+
80, 121, 98, 114, 105, 99, 107, 115, 32, 72, 117, 98, 32, 0, 0,
132+
0,
133+
])
134+
);
135+
});
136+
137+
test('old firmware throws', () => {
138+
expect(() => encodeHubName('', {} as FirmwareMetadata)).toThrowError();
139+
});
140+
});

npm/firmware/index.ts

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
// Copyright (c) 2020-2021 The Pybricks Authors
33

44
import JSZip, { JSZipObject } from 'jszip';
5-
import {PACKAGE_VERSION} from './version';
5+
import { PACKAGE_VERSION } from './version';
6+
7+
const encoder = new TextEncoder();
68

79
/**
810
* String containing the firmware version.
911
*/
10-
export const firmwareVersion = PACKAGE_VERSION.substring(PACKAGE_VERSION.lastIndexOf('v') + 1);
12+
export const firmwareVersion = PACKAGE_VERSION.substring(
13+
PACKAGE_VERSION.lastIndexOf('v') + 1
14+
);
1115

1216
/**
1317
* LEGO Powered Up Hub IDs
@@ -58,6 +62,12 @@ export interface FirmwareMetadata {
5862
'user-mpy-offset': number;
5963
/** The maximum firmware size allowed on the hub. */
6064
'max-firmware-size': number;
65+
/** The offset to where the hub name is stored in the firmware. (since v1.1.0) */
66+
'hub-name-offset': number | undefined;
67+
/** The maximum size of the firmware name in bytes, including the zero-termination. (since v1.1.0) */
68+
'max-hub-name-size': number | undefined;
69+
/** The SHA256 hash of the firmware. (since v1.1.0) */
70+
'firmware-sha256': string | undefined;
6171
}
6272

6373
/** Types of errors that can be raised by FirmwareReader. */
@@ -75,22 +85,20 @@ export enum FirmwareReaderErrorCode {
7585
}
7686

7787
/** Maps error codes to error messages. */
78-
const firmwareReaderErrorMessage: ReadonlyMap<
79-
FirmwareReaderErrorCode,
80-
string
81-
> = new Map([
82-
[FirmwareReaderErrorCode.ZipError, 'bad zip data'],
83-
[
84-
FirmwareReaderErrorCode.MissingFirmwareBaseBin,
85-
'missing firmware-base.bin',
86-
],
87-
[
88-
FirmwareReaderErrorCode.MissingMetadataJson,
89-
'missing firmware.metadata.json',
90-
],
91-
[FirmwareReaderErrorCode.MissingMainPy, 'missing main.py'],
92-
[FirmwareReaderErrorCode.MissingReadmeOssTxt, 'ReadMe_OSS.txt'],
93-
]);
88+
const firmwareReaderErrorMessage: ReadonlyMap<FirmwareReaderErrorCode, string> =
89+
new Map([
90+
[FirmwareReaderErrorCode.ZipError, 'bad zip data'],
91+
[
92+
FirmwareReaderErrorCode.MissingFirmwareBaseBin,
93+
'missing firmware-base.bin',
94+
],
95+
[
96+
FirmwareReaderErrorCode.MissingMetadataJson,
97+
'missing firmware.metadata.json',
98+
],
99+
[FirmwareReaderErrorCode.MissingMainPy, 'missing main.py'],
100+
[FirmwareReaderErrorCode.MissingReadmeOssTxt, 'ReadMe_OSS.txt'],
101+
]);
94102

95103
/** Errors throw by FirmwareReader */
96104
export class FirmwareReaderError extends Error {
@@ -159,7 +167,9 @@ export class FirmwareReader {
159167
* Loads data from a firmware.zip file and does a few sanity checks.
160168
* @param zipData The firmware.zip file binary data.
161169
*/
162-
public static async load(zipData: Uint8Array | ArrayBuffer | Blob): Promise<FirmwareReader> {
170+
public static async load(
171+
zipData: Uint8Array | ArrayBuffer | Blob
172+
): Promise<FirmwareReader> {
163173
const reader = new FirmwareReader();
164174
const zip = await wrapError(
165175
() => JSZip.loadAsync(zipData),
@@ -217,3 +227,34 @@ export class FirmwareReader {
217227
return this.readMeOss!.async('text');
218228
}
219229
}
230+
231+
/**
232+
* Encodes a firmware name as UTF-8 bytes with zero-termination.
233+
*
234+
* If the name is too long to fit in the size specified by the metadata, the
235+
* name will be truncated. The resulting value can be written to the firmware
236+
* image at 'hub-name-offset'.
237+
*
238+
* @param name The hub name.
239+
* @param metadata The firmware metadata.
240+
*/
241+
export function encodeHubName(
242+
name: string,
243+
metadata: FirmwareMetadata
244+
): Uint8Array {
245+
if (metadata['max-hub-name-size'] === undefined) {
246+
throw new Error('firmware image does not support firmware name');
247+
}
248+
249+
// fall back to default on empty name
250+
if (!name) {
251+
name = 'Pybricks Hub';
252+
}
253+
254+
const bytes = new Uint8Array(metadata['max-hub-name-size']);
255+
256+
// subarray ensures zero termination if encoded length is >= 'max-hub-name-size'.
257+
encoder.encodeInto(name, bytes.subarray(0, bytes.length - 1));
258+
259+
return bytes;
260+
}

npm/firmware/jest.config.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
module.exports = {
2-
preset: 'ts-jest',
3-
testEnvironment: 'node',
4-
};
2+
preset: 'ts-jest',
3+
testEnvironment: 'node',
4+
testPathIgnorePatterns: ['<rootDir>/build/', '<rootDir>/node_modules/'],
5+
};

npm/firmware/tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,5 +63,5 @@
6363
/* Advanced Options */
6464
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
6565
},
66-
"files": ["index.ts"]
66+
"files": ["index.ts", "index.test.ts"]
6767
}

0 commit comments

Comments
 (0)