Skip to content

Commit 359f11a

Browse files
WIP flashing integration - use the existing wrapper not Device
Remains to fix the conflicting ideas about connection/disconnection, status updates etc.
1 parent 90026ec commit 359f11a

File tree

7 files changed

+353
-337
lines changed

7 files changed

+353
-337
lines changed

lib/bluetooth-device-wrapper.ts

Lines changed: 219 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
* SPDX-License-Identifier: MIT
55
*/
66

7-
import { BleClient, BleDevice } from "@capacitor-community/bluetooth-le";
7+
import {
8+
BleClient,
9+
BleDevice,
10+
TimeoutOptions,
11+
} from "@capacitor-community/bluetooth-le";
812
import { AccelerometerService } from "./accelerometer-service.js";
913
import { ButtonService } from "./button-service.js";
1014
import { BoardVersion } from "./device.js";
@@ -19,6 +23,23 @@ import {
1923
import { UARTService } from "./uart-service.js";
2024
import { DeviceInformationService } from "./device-information-service.js";
2125

26+
const disconnectSymbol: unique symbol = Symbol("disconnected");
27+
const timeoutSymbol: unique symbol = Symbol("timeout");
28+
29+
export class BluetoothError extends Error {}
30+
31+
export class TimeoutError extends BluetoothError {
32+
constructor() {
33+
super("Timeout");
34+
}
35+
}
36+
37+
export class DisconnectError extends BluetoothError {
38+
constructor() {
39+
super("Disconnect");
40+
}
41+
}
42+
2243
const deviceIdToWrapper: Map<string, BluetoothDeviceWrapper> = new Map();
2344

2445
const connectTimeoutDuration: number = 10000;
@@ -113,6 +134,25 @@ export class BluetoothDeviceWrapper {
113134
];
114135
}
115136

137+
// TODO: this is the Device connect method
138+
async connect(tag: string) {
139+
this.tag = tag;
140+
let onDisconnect: (() => void) | undefined;
141+
const promise = new Promise<typeof disconnectSymbol>((resolve) => {
142+
onDisconnect = () => {
143+
this.log("Disconnected");
144+
this.internalNotificationListeners = new Map();
145+
resolve(disconnectSymbol);
146+
};
147+
});
148+
this.disconnectTracker = { promise, onDisconnect: onDisconnect! };
149+
this.log("Connecting");
150+
await BleClient.connect(this.device.deviceId, onDisconnect, {
151+
timeout: connectTimeoutInMs,
152+
});
153+
this.log("Connected");
154+
}
155+
116156
async connect(): Promise<void> {
117157
this.logging.event({
118158
type: this.isReconnect ? "Reconnect" : "Connect",
@@ -322,6 +362,184 @@ export class BluetoothDeviceWrapper {
322362
.find((s) => s.getRelevantEvents().includes(type))
323363
?.stopNotifications(type);
324364
}
365+
366+
// Added for flashing
367+
368+
private tag: string | undefined;
369+
disconnectTracker:
370+
| { promise: Promise<typeof disconnectSymbol>; onDisconnect: () => void }
371+
| undefined;
372+
private internalNotificationListeners = new Map<
373+
string,
374+
Set<(data: Uint8Array) => void>
375+
>();
376+
377+
async startInternalNotifications(
378+
serviceId: string,
379+
characteristicId: string,
380+
options?: TimeoutOptions,
381+
): Promise<void> {
382+
const key = this.getNotificationKey(serviceId, characteristicId);
383+
await this.raceDisconnectAndTimeout(
384+
BleClient.startNotifications(
385+
this.device.deviceId,
386+
serviceId,
387+
characteristicId,
388+
(value: DataView) => {
389+
const bytes = new Uint8Array(value.buffer);
390+
// Notify all registered callbacks.
391+
this.internalNotificationListeners
392+
.get(key)
393+
?.forEach((cb) => cb(bytes));
394+
},
395+
options,
396+
),
397+
{ actionName: "start notifications" },
398+
);
399+
}
400+
401+
subscribe(
402+
serviceId: string,
403+
characteristicId: string,
404+
callback: (data: Uint8Array) => void,
405+
): void {
406+
const key = this.getNotificationKey(serviceId, characteristicId);
407+
if (!this.internalNotificationListeners.has(key)) {
408+
this.internalNotificationListeners.set(key, new Set());
409+
}
410+
this.internalNotificationListeners.get(key)!.add(callback);
411+
}
412+
413+
unsubscribe(
414+
serviceId: string,
415+
characteristicId: string,
416+
callback: (data: Uint8Array) => void,
417+
): void {
418+
const key = this.getNotificationKey(serviceId, characteristicId);
419+
this.internalNotificationListeners.get(key)?.delete(callback);
420+
}
421+
422+
async stopInternalNotifications(
423+
serviceId: string,
424+
characteristicId: string,
425+
): Promise<void> {
426+
await BleClient.stopNotifications(
427+
this.device.deviceId,
428+
serviceId,
429+
characteristicId,
430+
);
431+
const key = this.getNotificationKey(serviceId, characteristicId);
432+
this.internalNotificationListeners.delete(key);
433+
}
434+
435+
/**
436+
* Write to characteristic and wait for a notification response.
437+
*
438+
* It is the responsibility of the caller to have started notifications
439+
* for the characteristic.
440+
*/
441+
async writeForNotification(
442+
serviceId: string,
443+
characteristicId: string,
444+
value: DataView,
445+
notificationId: number,
446+
isFinalNotification: (p: Uint8Array) => boolean = () => true,
447+
): Promise<Uint8Array> {
448+
let notificationListener: ((bytes: Uint8Array) => void) | undefined;
449+
const notificationPromise = new Promise<Uint8Array>((resolve) => {
450+
notificationListener = (bytes: Uint8Array) => {
451+
if (bytes[0] === notificationId && isFinalNotification(bytes)) {
452+
resolve(bytes);
453+
}
454+
};
455+
this.subscribe(serviceId, characteristicId, notificationListener);
456+
});
457+
458+
try {
459+
await BleClient.writeWithoutResponse(
460+
this.device.deviceId,
461+
serviceId,
462+
characteristicId,
463+
value,
464+
);
465+
return await this.raceDisconnectAndTimeout(notificationPromise, {
466+
timeout: 3_000,
467+
actionName: "flash notification wait",
468+
});
469+
} finally {
470+
if (notificationListener) {
471+
this.unsubscribe(serviceId, characteristicId, notificationListener);
472+
}
473+
}
474+
}
475+
476+
async waitForDisconnect(timeout: number): Promise<void> {
477+
if (!this.disconnectTracker) {
478+
this.log("Waiting for disconnect but not connected");
479+
return;
480+
}
481+
this.log(`Waiting for disconnect (timeout ${timeout})`);
482+
const result = await Promise.race([
483+
this.disconnectTracker.promise,
484+
this.timeoutPromise(timeout),
485+
]);
486+
if (result === timeoutSymbol) {
487+
this.log("Timeout waiting for disconnect");
488+
throw new TimeoutError();
489+
}
490+
}
491+
492+
/**
493+
* Suitable for running a series of BLE interactions with an overall timeout
494+
* and general disconnection
495+
*/
496+
async raceDisconnectAndTimeout<T>(
497+
promise: Promise<T>,
498+
options: {
499+
actionName?: string;
500+
timeout?: number;
501+
} = {},
502+
): Promise<T> {
503+
if (!this.disconnectTracker) {
504+
throw new DisconnectError();
505+
}
506+
const actionName = options.actionName ?? "action";
507+
const result = await Promise.race([
508+
promise,
509+
this.disconnectTracker.promise,
510+
...(options.timeout ? [this.timeoutPromise(options.timeout)] : []),
511+
]);
512+
if (result === timeoutSymbol) {
513+
this.log(`Timeout during ${actionName}`);
514+
throw new TimeoutError();
515+
}
516+
if (result === disconnectSymbol) {
517+
this.log(`Disconnected during ${actionName}`);
518+
throw new DisconnectError();
519+
}
520+
return result;
521+
}
522+
523+
private timeoutPromise(timeout: number): Promise<typeof timeoutSymbol> {
524+
return new Promise((resolve) =>
525+
setTimeout(() => resolve(timeoutSymbol), timeout),
526+
);
527+
}
528+
529+
log(message: string) {
530+
console.log(`[${this.tag}] ${message}`);
531+
}
532+
533+
error(e: unknown) {
534+
console.error(e);
535+
}
536+
537+
private getNotificationKey(
538+
serviceId: string,
539+
characteristicId: string,
540+
): string {
541+
return `${serviceId}:${characteristicId}`;
542+
}
325543
}
326544

327545
export const createBluetoothDeviceWrapper = async (

lib/bluetooth-profile.ts

Lines changed: 36 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
1-
// Very incomplete BT profile
21
export const profile = {
3-
uart: {
4-
id: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
5-
characteristics: {
6-
tx: { id: "6e400002-b5a3-f393-e0a9-e50e24dcca9e" },
7-
rx: { id: "6e400003-b5a3-f393-e0a9-e50e24dcca9e" },
8-
},
9-
},
2+
// Alphabetised
103
accelerometer: {
114
id: "e95d0753-251d-470a-a062-fa1922dfa9a8",
125
characteristics: {
136
data: { id: "e95dca4b-251d-470a-a062-fa1922dfa9a8" },
147
period: { id: "e95dfb24-251d-470a-a062-fa1922dfa9a8" },
158
},
169
},
10+
button: {
11+
id: "e95d9882-251d-470a-a062-fa1922dfa9a8",
12+
characteristics: {
13+
a: { id: "e95dda90-251d-470a-a062-fa1922dfa9a8" },
14+
b: { id: "e95dda91-251d-470a-a062-fa1922dfa9a8" },
15+
},
16+
},
17+
1718
deviceInformation: {
1819
id: "0000180a-0000-1000-8000-00805f9b34fb",
1920
characteristics: {
20-
modelNumber: { id: "00002a24-0000-1000-8000-00805f9b34fb" },
21-
serialNumber: { id: "00002a25-0000-1000-8000-00805f9b34fb" },
2221
firmwareRevision: { id: "00002a26-0000-1000-8000-00805f9b34fb" },
2322
hardwareRevision: { id: "00002a27-0000-1000-8000-00805f9b34fb" },
2423
manufacturer: { id: "00002a29-0000-1000-8000-00805f9b34fb" },
24+
modelNumber: { id: "00002a24-0000-1000-8000-00805f9b34fb" },
25+
serialNumber: { id: "00002a25-0000-1000-8000-00805f9b34fb" },
2526
},
2627
},
2728
dfuControl: {
@@ -30,37 +31,30 @@ export const profile = {
3031
control: { id: "e95d93b1-251d-470a-a062-fa1922dfa9a8" },
3132
},
3233
},
33-
led: {
34-
id: "e95dd91d-251d-470a-a062-fa1922dfa9a8",
34+
event: {
35+
id: "e95d93af-251d-470a-a062-fa1922dfa9a8",
3536
characteristics: {
36-
matrixState: { id: "e95d7b77-251d-470a-a062-fa1922dfa9a8" },
37-
text: { id: "e95d93ee-251d-470a-a062-fa1922dfa9a8" },
38-
scrollingDelay: { id: "e95d0d2d-251d-470a-a062-fa1922dfa9a8" },
37+
clientEvent: { id: "e95d5404-251d-470a-a062-fa1922dfa9a8" },
38+
clientRequirements: { id: "e95d23c4-251d-470a-a062-fa1922dfa9a8" },
39+
microBitEvent: { id: "e95d9775-251d-470a-a062-fa1922dfa9a8" },
40+
microBitRequirements: { id: "e95db84c-251d-470a-a062-fa1922dfa9a8" },
3941
},
4042
},
4143
ioPin: {
4244
id: "e95d127b-251d-470a-a062-fa1922dfa9a8",
4345
characteristics: {
44-
pinData: { id: "e95d8d00-251d-470a-a062-fa1922dfa9a8" },
4546
pinAdConfiguration: { id: "e95d5899-251d-470a-a062-fa1922dfa9a8" },
47+
pinData: { id: "e95d8d00-251d-470a-a062-fa1922dfa9a8" },
4648
pinIoConfiguration: { id: "e95db9fe-251d-470a-a062-fa1922dfa9a8" },
4749
pwmControl: { id: "e95dd822-251d-470a-a062-fa1922dfa9a8" },
4850
},
4951
},
50-
button: {
51-
id: "e95d9882-251d-470a-a062-fa1922dfa9a8",
52-
characteristics: {
53-
a: { id: "e95dda90-251d-470a-a062-fa1922dfa9a8" },
54-
b: { id: "e95dda91-251d-470a-a062-fa1922dfa9a8" },
55-
},
56-
},
57-
event: {
58-
id: "e95d93af-251d-470a-a062-fa1922dfa9a8",
52+
led: {
53+
id: "e95dd91d-251d-470a-a062-fa1922dfa9a8",
5954
characteristics: {
60-
microBitRequirements: { id: "e95db84c-251d-470a-a062-fa1922dfa9a8" },
61-
microBitEvent: { id: "e95d9775-251d-470a-a062-fa1922dfa9a8" },
62-
clientRequirements: { id: "e95d23c4-251d-470a-a062-fa1922dfa9a8" },
63-
clientEvent: { id: "e95d5404-251d-470a-a062-fa1922dfa9a8" },
55+
matrixState: { id: "e95d7b77-251d-470a-a062-fa1922dfa9a8" },
56+
scrollingDelay: { id: "e95d0d2d-251d-470a-a062-fa1922dfa9a8" },
57+
text: { id: "e95d93ee-251d-470a-a062-fa1922dfa9a8" },
6458
},
6559
},
6660
magnetometer: {
@@ -72,11 +66,24 @@ export const profile = {
7266
calibration: { id: "e95db358-251d-470a-a062-fa1922dfa9a8" },
7367
},
7468
},
69+
partialFlashing: {
70+
id: "e97dd91d-251d-470a-a062-fa1922dfa9a8",
71+
characteristics: {
72+
partialFlash: { id: "e97d3b10-251d-470a-a062-fa1922dfa9a8" },
73+
},
74+
},
7575
temperature: {
7676
id: "e95d6100-251d-470a-a062-fa1922dfa9a8",
7777
characteristics: {
7878
data: { id: "e95d9250-251d-470a-a062-fa1922dfa9a8" },
7979
period: { id: "e95d1b25-251d-470a-a062-fa1922dfa9a8" },
8080
},
8181
},
82+
uart: {
83+
id: "6e400001-b5a3-f393-e0a9-e50e24dcca9e",
84+
characteristics: {
85+
rx: { id: "6e400003-b5a3-f393-e0a9-e50e24dcca9e" },
86+
tx: { id: "6e400002-b5a3-f393-e0a9-e50e24dcca9e" },
87+
},
88+
},
8289
};

0 commit comments

Comments
 (0)