Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions lib/accelerometer-service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js";
import { GattOperation } from "./bluetooth-device-wrapper.js";
import { profile } from "./bluetooth-profile.js";
import { createGattOperationPromise } from "./bluetooth-utils.js";
import {
CharacteristicDataTarget,
TypedServiceEventDispatcher,
Expand All @@ -11,6 +13,7 @@ export class AccelerometerService {
private accelerometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic,
private dispatchTypedEvent: TypedServiceEventDispatcher,
private isNotifying: boolean,
private queueGattOperation: (gattOperation: GattOperation) => void,
) {
this.addDataEventListener();
if (this.isNotifying) {
Expand All @@ -22,6 +25,7 @@ export class AccelerometerService {
gattServer: BluetoothRemoteGATTServer,
dispatcher: TypedServiceEventDispatcher,
isNotifying: boolean,
queueGattOperation: (gattOperation: GattOperation) => void,
): Promise<AccelerometerService> {
const accelerometerService = await gattServer.getPrimaryService(
profile.accelerometer.id,
Expand All @@ -39,6 +43,7 @@ export class AccelerometerService {
accelerometerPeriodCharacteristic,
dispatcher,
isNotifying,
queueGattOperation,
);
}

Expand All @@ -51,12 +56,22 @@ export class AccelerometerService {
}

async getData(): Promise<AccelerometerData> {
const dataView = await this.accelerometerDataCharacteristic.readValue();
const { callback, gattOperationPromise } = createGattOperationPromise();
this.queueGattOperation({
callback,
operation: () => this.accelerometerDataCharacteristic.readValue(),
});
const dataView = (await gattOperationPromise) as DataView;
return this.dataViewToData(dataView);
}

async getPeriod(): Promise<number> {
const dataView = await this.accelerometerPeriodCharacteristic.readValue();
const { callback, gattOperationPromise } = createGattOperationPromise();
this.queueGattOperation({
callback,
operation: () => this.accelerometerPeriodCharacteristic.readValue(),
});
const dataView = (await gattOperationPromise) as DataView;
return dataView.getUint16(0, true);
}

Expand All @@ -69,11 +84,16 @@ export class AccelerometerService {
// Values passed are rounded up to the allowed values on device.
// Documentation for allowed values looks wrong.
// https://lancaster-university.github.io/microbit-docs/resources/bluetooth/bluetooth_profile.html
const { callback } = createGattOperationPromise();
const dataView = new DataView(new ArrayBuffer(2));
dataView.setUint16(0, value, true);
await this.accelerometerPeriodCharacteristic.writeValueWithoutResponse(
dataView,
);
this.queueGattOperation({
callback,
operation: () =>
this.accelerometerPeriodCharacteristic.writeValueWithoutResponse(
dataView,
),
});
}

private addDataEventListener(): void {
Expand Down
77 changes: 74 additions & 3 deletions lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,25 @@

import { AccelerometerService } from "./accelerometer-service.js";
import { profile } from "./bluetooth-profile.js";
import { BoardVersion } from "./device.js";
import { BoardVersion, DeviceError } from "./device.js";
import { Logging, NullLogging } from "./logging.js";
import { TypedServiceEventDispatcher } from "./service-events.js";

export interface GattOperationCallback {
resolve: (result: DataView | void) => void;
reject: (error: DeviceError) => void;
}

export interface GattOperation {
operation: () => Promise<DataView | void>;
callback: GattOperationCallback;
}

interface GattOperations {
busy: boolean;
queue: GattOperation[];
}

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

const connectTimeoutDuration: number = 10000;
Expand Down Expand Up @@ -55,6 +70,11 @@ export class BluetoothDeviceWrapper {
},
};

private gattOperations: GattOperations = {
busy: false,
queue: [],
};

constructor(
public readonly device: BluetoothDevice,
private logging: Logging = new NullLogging(),
Expand Down Expand Up @@ -212,8 +232,6 @@ export class BluetoothDeviceWrapper {
}

handleDisconnectEvent = async (): Promise<void> => {
// this.outputWriteQueue = { busy: false, queue: [] };

try {
if (!this.duringExplicitConnectDisconnect) {
this.logging.log(
Expand Down Expand Up @@ -269,20 +287,73 @@ export class BluetoothDeviceWrapper {
}
}

private queueGattOperation(gattOperation: GattOperation) {
this.gattOperations.queue.push(gattOperation);
this.processGattOperationQueue();
}

private processGattOperationQueue = (): void => {
if (!this.device.gatt?.connected) {
// No longer connected. Drop queue.
this.clearGattQueueOnDisconnect();
return;
}
if (this.gattOperations.busy) {
// We will finish processing the current operation, then
// pick up processing the queue in the finally block.
return;
}
const gattOperation = this.gattOperations.queue.shift();
if (!gattOperation) {
return;
}
this.gattOperations.busy = true;
gattOperation
.operation()
.then((result) => {
gattOperation.callback.resolve(result);
})
.catch((err) => {
gattOperation.callback.reject(
new DeviceError({ code: "background-comms-error", message: err }),
);
this.logging.error("Error processing gatt operations queue", err);
})
.finally(() => {
this.gattOperations.busy = false;
this.processGattOperationQueue();
});
};

private clearGattQueueOnDisconnect() {
this.gattOperations.queue.forEach((op) => {
op.callback.reject(
new DeviceError({
code: "device-disconnected",
message:
"Error processing gatt operations queue - device disconnected",
}),
);
});
this.gattOperations = { busy: false, queue: [] };
}

async getAccelerometerService(): Promise<AccelerometerService> {
if (!this.accelerometerService) {
const gattServer = this.assertGattServer();
this.accelerometerService = await AccelerometerService.createService(
gattServer,
this.dispatchTypedEvent,
this.serviceListeners.accelerometerdatachanged.notifying,
this.queueGattOperation.bind(this),
);
}
return this.accelerometerService;
}

private disposeServices() {
this.accelerometerService = undefined;
this.clearGattQueueOnDisconnect();
}
}

Expand Down
18 changes: 18 additions & 0 deletions lib/bluetooth-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { GattOperationCallback } from "./bluetooth-device-wrapper.js";

export const createGattOperationPromise = (): {
callback: GattOperationCallback;
gattOperationPromise: Promise<DataView | void>;
} => {
let resolve: (result: DataView | void) => void;
let reject: () => void;
const gattOperationPromise = new Promise<DataView | void>((res, rej) => {
resolve = res;
reject = rej;
});
const callback = {
resolve: resolve!,
reject: reject!,
};
return { callback, gattOperationPromise };
};
2 changes: 1 addition & 1 deletion lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class MicrobitWebBluetoothConnection
this.connection = await createBluetoothDeviceWrapper(
device,
this.logging,
(type, event) => this.dispatchTypedEvent(type, event),
this.dispatchTypedEvent.bind(this),
);
}
// TODO: timeout unification?
Expand Down
6 changes: 5 additions & 1 deletion lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ export type DeviceErrorCode =
/**
* This is the fallback error case suggesting that the user reconnects their device.
*/
| "reconnect-microbit";
| "reconnect-microbit"
/**
* Error occured during serial or bluetooth communication.
*/
| "background-comms-error";

/**
* Error type used for all interactions with this module.
Expand Down