Skip to content

Commit 55b09c4

Browse files
Tweak flash interface (#7)
This also fixes a mistake where we padded with 0 not 0xff. On the implementation but not the types I've left open the possibility of passing a MemoryMap so I can experiment with it as an option. MemoryMap imports remain a bit of a disaster but one to investigate further another day. For the Python Editor this will mean that we always return a hex from the file system and then parse it for the partial flashing case. But that seems a much more reasonable API and the conversion should be quick enough. If we care for perf then we can work towards using the MemoryMap built by microbit-fs directly. There's no reason to be going via padded bytes as that drops the UICR data which it would be better to flash.
1 parent 470cd81 commit 55b09c4

File tree

7 files changed

+113
-95
lines changed

7 files changed

+113
-95
lines changed

lib/board-id.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,13 @@ export class BoardId {
6161
static parse(value: string): BoardId {
6262
return new BoardId(parseInt(value, 16));
6363
}
64+
65+
static forVersion(boardVersion: BoardVersion): BoardId {
66+
switch (boardVersion) {
67+
case "V1":
68+
return this.v1Normalized;
69+
case "V2":
70+
return this.v2Normalized;
71+
}
72+
}
6473
}

lib/device.ts

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
* SPDX-License-Identifier: MIT
55
*/
66
import { TypedEventTarget } from "./events.js";
7-
import { BoardId } from "./board-id.js";
87

98
/**
109
* Specific identified error types.
@@ -94,26 +93,9 @@ export enum ConnectionStatus {
9493

9594
export class FlashDataError extends Error {}
9695

97-
export interface FlashDataSource {
98-
/**
99-
* For now we only support partially flashing contiguous data.
100-
* This can be generated from microbit-fs directly (via getIntelHexBytes())
101-
* or from an existing Intel Hex via slicePad.
102-
*
103-
* This interface is quite confusing and worth revisiting.
104-
*
105-
* @param boardId the id of the board.
106-
* @throws FlashDataError if we cannot generate hex data.
107-
*/
108-
partialFlashData(boardId: BoardId): Promise<Uint8Array>;
109-
110-
/**
111-
* @param boardId the id of the board.
112-
* @returns A board-specific (non-universal) Intel Hex file for the given board id.
113-
* @throws FlashDataError if we cannot generate hex data.
114-
*/
115-
fullFlashData(boardId: BoardId): Promise<string>;
116-
}
96+
export type FlashDataSource = (
97+
boardVersion: BoardVersion,
98+
) => Promise<string | Uint8Array>;
11799

118100
export interface ConnectOptions {
119101
serial?: boolean;

lib/hex-flash-data-source.ts

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,33 @@
1-
import MemoryMap from "nrf-intel-hex";
21
import { BoardId } from "./board-id.js";
3-
import { FlashDataSource, FlashDataError as FlashDataError } from "./device.js";
2+
import {
3+
FlashDataError as FlashDataError,
4+
BoardVersion,
5+
FlashDataSource,
6+
} from "./device.js";
47
import {
58
isUniversalHex,
69
separateUniversalHex,
710
} from "@microbit/microbit-universal-hex";
811

9-
export class HexFlashDataSource implements FlashDataSource {
10-
constructor(private hex: string) {}
11-
12-
partialFlashData(boardId: BoardId): Promise<Uint8Array> {
13-
// Perhaps this would make more sense if we returned a MemoryMap?
14-
// Then the partial flashing code could be given everything including UICR without
15-
// passing a very large Uint8Array.
16-
17-
// Or use MM inside PF and return a (partial) hex string in the microbit-fs case?
18-
19-
const part = this.matchingPart(boardId);
20-
21-
// Cludge for a packaging issue
22-
const fromHex: (
23-
hexText: string,
24-
maxBlockSize?: number,
25-
) => MemoryMap.default =
26-
(MemoryMap as any).fromHex ?? MemoryMap.default.fromHex;
27-
28-
const hex = fromHex(part);
29-
const keys = Array.from(hex.keys()).filter((k) => k < 0x10000000);
30-
const lastKey = keys[keys.length - 1];
31-
if (lastKey === undefined) {
32-
throw new FlashDataError("Empty hex");
33-
}
34-
const lastPart = hex.get(lastKey);
35-
if (!lastPart) {
36-
throw new FlashDataError("Empty hex");
37-
}
38-
const length = lastKey + lastPart.length;
39-
const data = hex.slicePad(0, length, 0);
40-
return Promise.resolve(data);
41-
}
42-
43-
fullFlashData(boardId: BoardId): Promise<string> {
44-
const part = this.matchingPart(boardId);
45-
return Promise.resolve(part);
46-
}
47-
48-
private matchingPart(boardId: BoardId): string {
49-
if (isUniversalHex(this.hex)) {
50-
const parts = separateUniversalHex(this.hex);
51-
const matching = parts.find((p) => p.boardId == boardId.normalize().id);
12+
/**
13+
* A flash data source that converts universal hex files as needed.
14+
*
15+
* @param universalHex A hex file, potentially universal.
16+
*/
17+
export const createUniversalHexFlashDataSource = (
18+
universalHex: string,
19+
): FlashDataSource => {
20+
return (boardVersion: BoardVersion) => {
21+
if (isUniversalHex(universalHex)) {
22+
const parts = separateUniversalHex(universalHex);
23+
const matching = parts.find(
24+
(p) => p.boardId == BoardId.forVersion(boardVersion).id,
25+
);
5226
if (!matching) {
5327
throw new FlashDataError("No matching part");
5428
}
55-
return matching.hex;
29+
return Promise.resolve(matching.hex);
5630
}
57-
return this.hex;
58-
}
59-
}
31+
return Promise.resolve(universalHex);
32+
};
33+
};

lib/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import {
1919
SerialErrorEvent,
2020
SerialResetEvent,
2121
} from "./device.js";
22-
import { HexFlashDataSource } from "./hex-flash-data-source.js";
22+
import { createUniversalHexFlashDataSource } from "./hex-flash-data-source.js";
2323

2424
export {
2525
MicrobitWebUSBConnection,
2626
MicrobitWebBluetoothConnection,
2727
BoardId,
28-
HexFlashDataSource,
28+
createUniversalHexFlashDataSource,
2929
AfterRequestDevice,
3030
BeforeRequestDevice,
3131
ConnectionStatus,

lib/usb-partial-flashing.ts

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,16 @@
4747
import { DAPLink } from "dapjs";
4848
import { Logging } from "./logging.js";
4949
import { withTimeout, TimeoutError } from "./async-util.js";
50-
import { BoardId } from "./board-id.js";
5150
import { DAPWrapper } from "./usb-device-wrapper.js";
52-
import { FlashDataSource } from "./device.js";
5351
import {
5452
CoreRegister,
5553
onlyChanged,
5654
Page,
5755
pageAlignBlocks,
5856
read32FromUInt8Array,
5957
} from "./usb-partial-flashing-utils.js";
58+
import MemoryMap from "nrf-intel-hex";
59+
import { BoardVersion } from "./device.js";
6060

6161
type ProgressCallback = (n: number, partial: boolean) => void;
6262

@@ -102,6 +102,7 @@ export class PartialFlashing {
102102
constructor(
103103
private dapwrapper: DAPWrapper,
104104
private logging: Logging,
105+
private boardVersion: BoardVersion,
105106
) {}
106107

107108
private log(v: any): void {
@@ -204,11 +205,11 @@ export class PartialFlashing {
204205
// Falls back to a full flash if partial flashing fails.
205206
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L335
206207
private async partialFlashAsync(
207-
boardId: BoardId,
208-
dataSource: FlashDataSource,
208+
data: string | Uint8Array | MemoryMap.default,
209209
updateProgress: ProgressCallback,
210210
): Promise<boolean> {
211-
const flashBytes = await dataSource.partialFlashData(boardId);
211+
const flashBytes = this.convertDataToPaddedBytes(data);
212+
212213
const checksums = await this.getFlashChecksumsAsync();
213214
await this.dapwrapper.writeBlockAsync(loadAddr, flashPageBIN);
214215
let aligned = pageAlignBlocks(flashBytes, 0, this.dapwrapper.pageSize);
@@ -219,7 +220,7 @@ export class PartialFlashing {
219220
let partial: boolean | undefined;
220221
if (aligned.length > totalPages / 2) {
221222
try {
222-
await this.fullFlashAsync(boardId, dataSource, updateProgress);
223+
await this.fullFlashAsync(data, updateProgress);
223224
partial = false;
224225
} catch (e) {
225226
this.log(e);
@@ -234,7 +235,7 @@ export class PartialFlashing {
234235
} catch (e) {
235236
this.log(e);
236237
this.log("Partial flash failed, attempting full flash.");
237-
await this.fullFlashAsync(boardId, dataSource, updateProgress);
238+
await this.fullFlashAsync(data, updateProgress);
238239
partial = false;
239240
}
240241
}
@@ -250,8 +251,7 @@ export class PartialFlashing {
250251

251252
// Perform full flash of micro:bit's ROM using daplink.
252253
async fullFlashAsync(
253-
boardId: BoardId,
254-
dataSource: FlashDataSource,
254+
data: string | Uint8Array | MemoryMap.default,
255255
updateProgress: ProgressCallback,
256256
) {
257257
this.log("Full flash");
@@ -261,7 +261,7 @@ export class PartialFlashing {
261261
};
262262
this.dapwrapper.daplink.on(DAPLink.EVENT_PROGRESS, fullFlashProgress);
263263
try {
264-
const data = await dataSource.fullFlashData(boardId);
264+
data = this.convertDataToHexString(data);
265265
await this.dapwrapper.transport.open();
266266
await this.dapwrapper.daplink.flash(new TextEncoder().encode(data));
267267
this.logging.event({
@@ -279,8 +279,7 @@ export class PartialFlashing {
279279
// Flash the micro:bit's ROM with the provided image, resetting the micro:bit first.
280280
// Drawn from https://github.com/microsoft/pxt-microbit/blob/dec5b8ce72d5c2b4b0b20aafefce7474a6f0c7b2/editor/extension.tsx#L439
281281
async flashAsync(
282-
boardId: BoardId,
283-
dataSource: FlashDataSource,
282+
data: string | Uint8Array | MemoryMap.default,
284283
updateProgress: ProgressCallback,
285284
): Promise<boolean> {
286285
let resetPromise = (async () => {
@@ -300,11 +299,7 @@ export class PartialFlashing {
300299
await withTimeout(resetPromise, 1000);
301300

302301
this.log("Begin flashing");
303-
return await this.partialFlashAsync(
304-
boardId,
305-
dataSource,
306-
updateProgress,
307-
);
302+
return await this.partialFlashAsync(data, updateProgress);
308303
} catch (e) {
309304
if (e instanceof TimeoutError) {
310305
this.log("Resetting micro:bit timed out");
@@ -313,7 +308,7 @@ export class PartialFlashing {
313308
type: "WebUSB-info",
314309
message: "flash-failed/attempting-full-flash",
315310
});
316-
await this.fullFlashAsync(boardId, dataSource, updateProgress);
311+
await this.fullFlashAsync(data, updateProgress);
317312
return false;
318313
} else {
319314
throw e;
@@ -324,4 +319,56 @@ export class PartialFlashing {
324319
await this.dapwrapper.disconnectAsync();
325320
}
326321
}
322+
323+
private convertDataToHexString(
324+
data: string | Uint8Array | MemoryMap.default,
325+
): string {
326+
if (typeof data === "string") {
327+
return data;
328+
}
329+
if (data instanceof Uint8Array) {
330+
return this.paddedBytesToHexString(data);
331+
}
332+
return data.asHexString();
333+
}
334+
335+
private convertDataToPaddedBytes(
336+
data: string | Uint8Array | MemoryMap.default,
337+
): Uint8Array {
338+
if (data instanceof Uint8Array) {
339+
return data;
340+
}
341+
if (typeof data === "string") {
342+
return this.hexStringToPaddedBytes(data);
343+
}
344+
return this.memoryMapToPaddedBytes(data);
345+
}
346+
347+
private hexStringToPaddedBytes(hex: string): Uint8Array {
348+
// Cludge for a packaging issue
349+
const fromHex: (
350+
hexText: string,
351+
maxBlockSize?: number,
352+
) => MemoryMap.default =
353+
(MemoryMap as any).fromHex ?? MemoryMap.default.fromHex;
354+
355+
return this.memoryMapToPaddedBytes(fromHex(hex));
356+
}
357+
358+
private paddedBytesToHexString(data: Uint8Array): string {
359+
// Cludge for a packaging issue
360+
const fromPaddedUint8Array: (data: Uint8Array) => MemoryMap.default =
361+
(MemoryMap as any).fromPaddedUint8Array ??
362+
MemoryMap.default.fromPaddedUint8Array;
363+
364+
return fromPaddedUint8Array(data).asHexString();
365+
}
366+
367+
private memoryMapToPaddedBytes(memoryMap: MemoryMap.default): Uint8Array {
368+
const flashSize = {
369+
V1: 256 * 1024,
370+
V2: 512 * 1024,
371+
}[this.boardVersion];
372+
return memoryMap.slicePad(0, flashSize);
373+
}
327374
}

lib/usb.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,19 @@ export class MicrobitWebUSBConnection
231231
const progress = options.progress || (() => {});
232232

233233
const boardId = this.connection.boardSerialInfo.id;
234-
const flashing = new PartialFlashing(this.connection, this.logging);
234+
const boardVersion = boardId.toBoardVersion();
235+
const data = await dataSource(boardVersion);
236+
const flashing = new PartialFlashing(
237+
this.connection,
238+
this.logging,
239+
boardVersion,
240+
);
235241
let wasPartial: boolean = false;
236242
try {
237243
if (partial) {
238-
wasPartial = await flashing.flashAsync(boardId, dataSource, progress);
244+
wasPartial = await flashing.flashAsync(data, progress);
239245
} else {
240-
await flashing.fullFlashAsync(boardId, dataSource, progress);
246+
await flashing.fullFlashAsync(data, progress);
241247
}
242248
} finally {
243249
progress(undefined, wasPartial);

src/demo.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66
import "./demo.css";
77
import { MicrobitWebUSBConnection } from "../lib/usb";
8-
import { HexFlashDataSource } from "../lib/hex-flash-data-source";
8+
import { createUniversalHexFlashDataSource } from "../lib/hex-flash-data-source";
99
import {
1010
BackgroundErrorEvent,
1111
ConnectionStatus,
@@ -128,7 +128,7 @@ flash.addEventListener("click", async () => {
128128
if (file) {
129129
const text = await file.text();
130130
if (connection.flash) {
131-
await connection.flash(new HexFlashDataSource(text), {
131+
await connection.flash(createUniversalHexFlashDataSource(text), {
132132
partial: true,
133133
progress: (percentage: number | undefined) => {
134134
console.log(percentage);

0 commit comments

Comments
 (0)