Skip to content

Commit 33f1733

Browse files
Add gatt operation queue (#4)
1 parent 52eda7d commit 33f1733

File tree

5 files changed

+123
-10
lines changed

5 files changed

+123
-10
lines changed

lib/accelerometer-service.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js";
2+
import { GattOperation } from "./bluetooth-device-wrapper.js";
23
import { profile } from "./bluetooth-profile.js";
4+
import { createGattOperationPromise } from "./bluetooth-utils.js";
35
import {
46
CharacteristicDataTarget,
57
TypedServiceEventDispatcher,
@@ -11,6 +13,7 @@ export class AccelerometerService {
1113
private accelerometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic,
1214
private dispatchTypedEvent: TypedServiceEventDispatcher,
1315
private isNotifying: boolean,
16+
private queueGattOperation: (gattOperation: GattOperation) => void,
1417
) {
1518
this.addDataEventListener();
1619
if (this.isNotifying) {
@@ -22,6 +25,7 @@ export class AccelerometerService {
2225
gattServer: BluetoothRemoteGATTServer,
2326
dispatcher: TypedServiceEventDispatcher,
2427
isNotifying: boolean,
28+
queueGattOperation: (gattOperation: GattOperation) => void,
2529
): Promise<AccelerometerService> {
2630
const accelerometerService = await gattServer.getPrimaryService(
2731
profile.accelerometer.id,
@@ -39,6 +43,7 @@ export class AccelerometerService {
3943
accelerometerPeriodCharacteristic,
4044
dispatcher,
4145
isNotifying,
46+
queueGattOperation,
4247
);
4348
}
4449

@@ -51,12 +56,22 @@ export class AccelerometerService {
5156
}
5257

5358
async getData(): Promise<AccelerometerData> {
54-
const dataView = await this.accelerometerDataCharacteristic.readValue();
59+
const { callback, gattOperationPromise } = createGattOperationPromise();
60+
this.queueGattOperation({
61+
callback,
62+
operation: () => this.accelerometerDataCharacteristic.readValue(),
63+
});
64+
const dataView = (await gattOperationPromise) as DataView;
5565
return this.dataViewToData(dataView);
5666
}
5767

5868
async getPeriod(): Promise<number> {
59-
const dataView = await this.accelerometerPeriodCharacteristic.readValue();
69+
const { callback, gattOperationPromise } = createGattOperationPromise();
70+
this.queueGattOperation({
71+
callback,
72+
operation: () => this.accelerometerPeriodCharacteristic.readValue(),
73+
});
74+
const dataView = (await gattOperationPromise) as DataView;
6075
return dataView.getUint16(0, true);
6176
}
6277

@@ -69,11 +84,16 @@ export class AccelerometerService {
6984
// Values passed are rounded up to the allowed values on device.
7085
// Documentation for allowed values looks wrong.
7186
// https://lancaster-university.github.io/microbit-docs/resources/bluetooth/bluetooth_profile.html
87+
const { callback } = createGattOperationPromise();
7288
const dataView = new DataView(new ArrayBuffer(2));
7389
dataView.setUint16(0, value, true);
74-
await this.accelerometerPeriodCharacteristic.writeValueWithoutResponse(
75-
dataView,
76-
);
90+
this.queueGattOperation({
91+
callback,
92+
operation: () =>
93+
this.accelerometerPeriodCharacteristic.writeValueWithoutResponse(
94+
dataView,
95+
),
96+
});
7797
}
7898

7999
private addDataEventListener(): void {

lib/bluetooth-device-wrapper.ts

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,25 @@
66

77
import { AccelerometerService } from "./accelerometer-service.js";
88
import { profile } from "./bluetooth-profile.js";
9-
import { BoardVersion } from "./device.js";
9+
import { BoardVersion, DeviceError } from "./device.js";
1010
import { Logging, NullLogging } from "./logging.js";
1111
import { TypedServiceEventDispatcher } from "./service-events.js";
1212

13+
export interface GattOperationCallback {
14+
resolve: (result: DataView | void) => void;
15+
reject: (error: DeviceError) => void;
16+
}
17+
18+
export interface GattOperation {
19+
operation: () => Promise<DataView | void>;
20+
callback: GattOperationCallback;
21+
}
22+
23+
interface GattOperations {
24+
busy: boolean;
25+
queue: GattOperation[];
26+
}
27+
1328
const deviceIdToWrapper: Map<string, BluetoothDeviceWrapper> = new Map();
1429

1530
const connectTimeoutDuration: number = 10000;
@@ -55,6 +70,11 @@ export class BluetoothDeviceWrapper {
5570
},
5671
};
5772

73+
private gattOperations: GattOperations = {
74+
busy: false,
75+
queue: [],
76+
};
77+
5878
constructor(
5979
public readonly device: BluetoothDevice,
6080
private logging: Logging = new NullLogging(),
@@ -212,8 +232,6 @@ export class BluetoothDeviceWrapper {
212232
}
213233

214234
handleDisconnectEvent = async (): Promise<void> => {
215-
// this.outputWriteQueue = { busy: false, queue: [] };
216-
217235
try {
218236
if (!this.duringExplicitConnectDisconnect) {
219237
this.logging.log(
@@ -269,20 +287,73 @@ export class BluetoothDeviceWrapper {
269287
}
270288
}
271289

290+
private queueGattOperation(gattOperation: GattOperation) {
291+
this.gattOperations.queue.push(gattOperation);
292+
this.processGattOperationQueue();
293+
}
294+
295+
private processGattOperationQueue = (): void => {
296+
if (!this.device.gatt?.connected) {
297+
// No longer connected. Drop queue.
298+
this.clearGattQueueOnDisconnect();
299+
return;
300+
}
301+
if (this.gattOperations.busy) {
302+
// We will finish processing the current operation, then
303+
// pick up processing the queue in the finally block.
304+
return;
305+
}
306+
const gattOperation = this.gattOperations.queue.shift();
307+
if (!gattOperation) {
308+
return;
309+
}
310+
this.gattOperations.busy = true;
311+
gattOperation
312+
.operation()
313+
.then((result) => {
314+
gattOperation.callback.resolve(result);
315+
})
316+
.catch((err) => {
317+
gattOperation.callback.reject(
318+
new DeviceError({ code: "background-comms-error", message: err }),
319+
);
320+
this.logging.error("Error processing gatt operations queue", err);
321+
})
322+
.finally(() => {
323+
this.gattOperations.busy = false;
324+
this.processGattOperationQueue();
325+
});
326+
};
327+
328+
private clearGattQueueOnDisconnect() {
329+
this.gattOperations.queue.forEach((op) => {
330+
op.callback.reject(
331+
new DeviceError({
332+
code: "device-disconnected",
333+
message:
334+
"Error processing gatt operations queue - device disconnected",
335+
}),
336+
);
337+
});
338+
this.gattOperations = { busy: false, queue: [] };
339+
}
340+
272341
async getAccelerometerService(): Promise<AccelerometerService> {
273342
if (!this.accelerometerService) {
274343
const gattServer = this.assertGattServer();
275344
this.accelerometerService = await AccelerometerService.createService(
276345
gattServer,
277346
this.dispatchTypedEvent,
278347
this.serviceListeners.accelerometerdatachanged.notifying,
348+
this.queueGattOperation.bind(this),
279349
);
280350
}
281351
return this.accelerometerService;
282352
}
283353

284354
private disposeServices() {
285355
this.accelerometerService = undefined;
356+
this.clearGattQueueOnDisconnect();
286357
}
287358
}
288359

lib/bluetooth-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { GattOperationCallback } from "./bluetooth-device-wrapper.js";
2+
3+
export const createGattOperationPromise = (): {
4+
callback: GattOperationCallback;
5+
gattOperationPromise: Promise<DataView | void>;
6+
} => {
7+
let resolve: (result: DataView | void) => void;
8+
let reject: () => void;
9+
const gattOperationPromise = new Promise<DataView | void>((res, rej) => {
10+
resolve = res;
11+
reject = rej;
12+
});
13+
const callback = {
14+
resolve: resolve!,
15+
reject: reject!,
16+
};
17+
return { callback, gattOperationPromise };
18+
};

lib/bluetooth.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export class MicrobitWebBluetoothConnection
163163
this.connection = await createBluetoothDeviceWrapper(
164164
device,
165165
this.logging,
166-
(type, event) => this.dispatchTypedEvent(type, event),
166+
this.dispatchTypedEvent.bind(this),
167167
);
168168
}
169169
// TODO: timeout unification?

lib/device.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,11 @@ export type DeviceErrorCode =
3535
/**
3636
* This is the fallback error case suggesting that the user reconnects their device.
3737
*/
38-
| "reconnect-microbit";
38+
| "reconnect-microbit"
39+
/**
40+
* Error occured during serial or bluetooth communication.
41+
*/
42+
| "background-comms-error";
3943

4044
/**
4145
* Error type used for all interactions with this module.

0 commit comments

Comments
 (0)