Skip to content

Commit 90026ec

Browse files
WIP integration of flashing
Remaining errors are Device vs BluetoothConnectionWrapper.
1 parent 1075bee commit 90026ec

15 files changed

+1558
-26
lines changed

lib/bluetooth-device-wrapper.ts

Lines changed: 20 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
import { BleClient, BleDevice } from "@capacitor-community/bluetooth-le";
88
import { AccelerometerService } from "./accelerometer-service.js";
9-
import { profile } from "./bluetooth-profile.js";
109
import { ButtonService } from "./button-service.js";
1110
import { BoardVersion } from "./device.js";
1211
import { LedService } from "./led-service.js";
@@ -18,6 +17,7 @@ import {
1817
TypedServiceEventDispatcher,
1918
} from "./service-events.js";
2019
import { UARTService } from "./uart-service.js";
20+
import { DeviceInformationService } from "./device-information-service.js";
2121

2222
const deviceIdToWrapper: Map<string, BluetoothDeviceWrapper> = new Map();
2323

@@ -75,12 +75,15 @@ export class BluetoothDeviceWrapper {
7575

7676
private accelerometer: AccelerometerService;
7777
private buttons: ButtonService;
78+
private deviceInformation: DeviceInformationService;
7879
private led: LedService;
7980
private magnetometer: MagnetometerService;
8081
private uart: UARTService;
81-
82+
/**
83+
* Only defined after connection.
84+
*/
8285
boardVersion: BoardVersion | undefined;
83-
services: Service[];
86+
private services: Service[];
8487

8588
constructor(
8689
public readonly device: BleDevice,
@@ -94,6 +97,7 @@ export class BluetoothDeviceWrapper {
9497
dispatchTypedEvent,
9598
);
9699
this.buttons = new ButtonService(device.deviceId, dispatchTypedEvent);
100+
this.deviceInformation = new DeviceInformationService(device.deviceId);
97101
this.led = new LedService(device.deviceId);
98102
this.magnetometer = new MagnetometerService(
99103
device.deviceId,
@@ -150,8 +154,8 @@ export class BluetoothDeviceWrapper {
150154

151155
// We always do this even if we might immediately disconnect as disconnecting
152156
// without using services causes getPrimaryService calls to hang on subsequent
153-
// reconnect - probably a device-side issue.
154-
this.boardVersion = await this.getBoardVersion();
157+
// reconnect - probably a device-side issue. The result is then cached.
158+
await this.getBoardVersion();
155159
// This connection could be arbitrarily later when our manual timeout may have passed.
156160
// Do we still want to be connected?
157161
if (!this.connecting) {
@@ -277,34 +281,24 @@ export class BluetoothDeviceWrapper {
277281
}
278282
};
279283

280-
private async getBoardVersion(): Promise<BoardVersion> {
281-
const serviceMeta = profile.deviceInformation;
282-
try {
283-
const modelNumberBytes = await BleClient.read(
284-
this.device.deviceId,
285-
serviceMeta.id,
286-
serviceMeta.characteristics.modelNumber.id,
287-
);
288-
const modelNumber = new TextDecoder().decode(modelNumberBytes);
289-
if (modelNumber.toLowerCase() === "BBC micro:bit".toLowerCase()) {
290-
return "V1";
291-
}
292-
if (
293-
modelNumber.toLowerCase().includes("BBC micro:bit v2".toLowerCase())
294-
) {
295-
return "V2";
296-
}
297-
throw new Error(`Unexpected model number ${modelNumber}`);
298-
} catch (e) {
299-
this.logging.error("Could not read model number", e);
300-
throw new Error("Could not read model number");
284+
async getBoardVersion(): Promise<BoardVersion> {
285+
// We read this when we connect and it won't change.
286+
if (this.boardVersion) {
287+
return this.boardVersion;
301288
}
289+
return this.deviceInformation.getBoardVersion();
302290
}
303291

304292
async getAccelerometerService(): Promise<AccelerometerService | undefined> {
305293
return this.accelerometer;
306294
}
307295

296+
async getDeviceInformationService(): Promise<
297+
DeviceInformationService | undefined
298+
> {
299+
return this.deviceInformation;
300+
}
301+
308302
async getLedService(): Promise<LedService | undefined> {
309303
return this.led;
310304
}

lib/bluetooth.ts

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
ConnectionStatusEvent,
1919
DeviceConnection,
2020
DeviceConnectionEventMap,
21+
FlashDataError,
22+
FlashDataSource,
2123
} from "./device.js";
2224
import { TypedEventTarget } from "./events.js";
2325
import { LedMatrix } from "./led.js";
@@ -27,6 +29,18 @@ import {
2729
ServiceConnectionEventMap,
2830
TypedServiceEvent,
2931
} from "./service-events.js";
32+
import { Capacitor } from "@capacitor/core";
33+
import { Device, requestDeviceNative } from "./capacitor-ble/bluetooth.js";
34+
import {
35+
FlashProgressStage,
36+
FlashResult,
37+
Progress,
38+
} from "./capacitor-ble/model.js";
39+
import MemoryMap from "nrf-intel-hex";
40+
import partialFlash, {
41+
PartialFlashResult,
42+
} from "./capacitor-ble/flashing-partial.js";
43+
import { fullFlash } from "./capacitor-ble/flashing-full.js";
3044

3145
const requestDeviceTimeoutDuration: number = 30000;
3246

@@ -191,6 +205,10 @@ class MicrobitWebBluetoothConnectionImpl
191205
this.logging.log(v);
192206
}
193207

208+
private error(message: string, e?: unknown) {
209+
this.logging.error(message, e);
210+
}
211+
194212
async initialize(): Promise<void> {
195213
navigator.bluetooth?.addEventListener(
196214
"availabilitychanged",
@@ -296,8 +314,16 @@ class MicrobitWebBluetoothConnectionImpl
296314
if (this.device) {
297315
return this.device;
298316
}
317+
299318
this.dispatchTypedEvent("beforerequestdevice", new BeforeRequestDevice());
300319
try {
320+
const namePrefix = this.nameFilter
321+
? `BBC micro:bit [${this.nameFilter}]`
322+
: "BBC micro:bit";
323+
if (Capacitor.isNativePlatform()) {
324+
return requestDeviceNative(namePrefix);
325+
}
326+
301327
// In some situations the Chrome device prompt simply doesn't appear so we time this out after 30 seconds and reload the page
302328
// TODO: give control over this to the caller
303329
const result = await Promise.race([
@@ -413,4 +439,105 @@ class MicrobitWebBluetoothConnectionImpl
413439
const uartService = await this.connection?.getUARTService();
414440
uartService?.writeData(data);
415441
}
442+
443+
// Extra API, matching USB case
444+
445+
/**
446+
* Flash the micro:bit.
447+
*
448+
* Note that this will always leave the connection disconnected.
449+
*
450+
* @param dataSource The data to use.
451+
* @param options Flash options and progress callback.
452+
*/
453+
async flash(
454+
dataSource: FlashDataSource,
455+
options: { progress?: (v: number | undefined) => void },
456+
): Promise<void> {
457+
// TODO: deal with the need for richer progress for BLE
458+
const externalProgress = options.progress ?? (() => {});
459+
const progress: Progress = (stage, v) => {
460+
if (
461+
(stage === FlashProgressStage.Partial ||
462+
stage === FlashProgressStage.Full) &&
463+
v !== undefined
464+
) {
465+
externalProgress(v);
466+
}
467+
};
468+
469+
// We'll disconnect/reconnect multiple times due to device resets, but reporting this is unhelpful.
470+
this.deferStatusUpdates = true;
471+
try {
472+
if (this.status !== ConnectionStatus.CONNECTED) {
473+
const status = await this.connect();
474+
if (status !== ConnectionStatus.CONNECTED) {
475+
throw new Error(`Failed to connect ${status}`);
476+
}
477+
}
478+
try {
479+
const memoryMap = convertDataToMemoryMap(
480+
await dataSource(this.getBoardVersion()!),
481+
);
482+
if (!memoryMap) {
483+
throw new FlashDataError();
484+
}
485+
486+
const boardVersion = this.connection?.boardVersion;
487+
if (!this.device || !boardVersion) {
488+
throw new Error();
489+
}
490+
const partialFlashResult = await partialFlash(
491+
this.device,
492+
memoryMap,
493+
progress,
494+
);
495+
496+
switch (partialFlashResult) {
497+
case PartialFlashResult.Success: {
498+
return;
499+
}
500+
case PartialFlashResult.Failed: {
501+
throw new Error("Partial flash failed");
502+
}
503+
case PartialFlashResult.AttemptFullFlash: {
504+
const fullFlashResult = await fullFlash(
505+
this.device,
506+
boardVersion,
507+
memoryMap,
508+
progress,
509+
);
510+
// TODO: get a grip on this return value
511+
if (fullFlashResult !== FlashResult.Success) {
512+
throw new Error();
513+
}
514+
return;
515+
}
516+
default: {
517+
throw new Error("Unexpected");
518+
}
519+
}
520+
} catch (e) {
521+
this.error("Failed to flash", e);
522+
throw e;
523+
} finally {
524+
await this.disconnect();
525+
}
526+
} finally {
527+
this.deferStatusUpdates = false;
528+
this.setStatus(this.status);
529+
}
530+
}
416531
}
532+
533+
const convertDataToMemoryMap = (
534+
data: string | Uint8Array | MemoryMap,
535+
): MemoryMap => {
536+
if (data instanceof MemoryMap) {
537+
return data;
538+
}
539+
if (data instanceof Uint8Array) {
540+
return MemoryMap.fromPaddedUint8Array(data);
541+
}
542+
return MemoryMap.fromHex(data);
543+
};

0 commit comments

Comments
 (0)