Skip to content

Commit

Permalink
First pass at Web Bluetooth accelerometer data
Browse files Browse the repository at this point in the history
  • Loading branch information
microbit-robert committed Jul 11, 2024
1 parent 27cf783 commit 8f330a0
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 25 deletions.
84 changes: 84 additions & 0 deletions lib/accelerometer-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Accelerometer,
AccelerometerData,
AccelerometerDataEvent,
AccelerometerEventMap,
} from "./accelerometer";
import { profile } from "./bluetooth-profile";
import { TypedEventTarget } from "./events";

export type CharacteristicDataTarget = EventTarget & {
value: DataView;
};

export class AccelerometerService
extends TypedEventTarget<AccelerometerEventMap>
implements Accelerometer
{
private static accelerometerInstance: AccelerometerService | undefined;

constructor(
private accelerometerDataCharacteristic: BluetoothRemoteGATTCharacteristic,
// @ts-ignore temporarily unused characteristic
private accelerometerPeriodCharacteristic: BluetoothRemoteGATTCharacteristic
) {
super();
this.accelerometerDataCharacteristic.addEventListener(
"characteristicvaluechanged",
this.dataListener
);
}

static async init(gattServer: BluetoothRemoteGATTServer) {
if (this.accelerometerInstance) {
return this.accelerometerInstance;
}
const accelerometerService = await gattServer.getPrimaryService(
profile.accelerometer.id
);
const accelerometerDataCharacteristic =
await accelerometerService.getCharacteristic(
profile.accelerometer.characteristics.data.id
);
const accelerometerPeriodCharacteristic =
await accelerometerService.getCharacteristic(
profile.accelerometer.characteristics.period.id
);
this.accelerometerInstance = new AccelerometerService(
accelerometerDataCharacteristic,
accelerometerPeriodCharacteristic
);
return this.accelerometerInstance;
}

async getData(): Promise<AccelerometerData> {
const dataView = await this.accelerometerDataCharacteristic.readValue();
const data = this.dataViewToData(dataView);
return data;
}

private dataViewToData(dataView: DataView): AccelerometerData {
return {
x: dataView.getInt16(0, true),
y: dataView.getInt16(2, true),
z: dataView.getInt16(4, true),
};
}

private dataListener = (event: Event) => {
const target = event.target as CharacteristicDataTarget;
const data = this.dataViewToData(target.value);
this.dispatchTypedEvent(
"accelerometerdatachanged",
new AccelerometerDataEvent(data)
);
};

startNotifications() {
this.accelerometerDataCharacteristic.startNotifications();
}

stopNotifications() {
this.accelerometerDataCharacteristic.stopNotifications();
}
}
23 changes: 23 additions & 0 deletions lib/accelerometer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { TypedEventTarget } from "./events";

export interface Accelerometer extends TypedEventTarget<AccelerometerEventMap> {
getData: () => Promise<AccelerometerData>;
startNotifications: () => void;
stopNotifications: () => void;
}

export class AccelerometerDataEvent extends Event {
constructor(public readonly data: AccelerometerData) {
super("accelerometerdatachanged");
}
}

export class AccelerometerEventMap {
"accelerometerdatachanged": AccelerometerDataEvent;
}

export interface AccelerometerData {
x: number;
y: number;
z: number;
}
40 changes: 24 additions & 16 deletions lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: MIT
*/

import { Accelerometer } from "./accelerometer";
import { AccelerometerService } from "./accelerometer-service";
import { profile } from "./bluetooth-profile";
import { BoardVersion } from "./device";
import { Logging, NullLogging } from "./logging";
Expand Down Expand Up @@ -48,11 +50,11 @@ export class BluetoothDeviceWrapper {

constructor(
public readonly device: BluetoothDevice,
private logging: Logging = new NullLogging(),
private logging: Logging = new NullLogging()
) {
device.addEventListener(
"gattserverdisconnected",
this.handleDisconnectEvent,
this.handleDisconnectEvent
);
}

Expand All @@ -63,7 +65,7 @@ export class BluetoothDeviceWrapper {
});
if (this.duringExplicitConnectDisconnect) {
this.logging.log(
"Skipping connect attempt when one is already in progress",
"Skipping connect attempt when one is already in progress"
);
// Wait for the gattConnectPromise while showing a "connecting" dialog.
// If the user clicks disconnect while the automatic reconnect is in progress,
Expand All @@ -74,7 +76,7 @@ export class BluetoothDeviceWrapper {
this.duringExplicitConnectDisconnect++;
if (this.device.gatt === undefined) {
throw new Error(
"BluetoothRemoteGATTServer for micro:bit device is undefined",
"BluetoothRemoteGATTServer for micro:bit device is undefined"
);
}
try {
Expand All @@ -93,15 +95,15 @@ export class BluetoothDeviceWrapper {
// Do we still want to be connected?
if (!this.connecting) {
this.logging.log(
"Bluetooth GATT server connect after timeout, triggering disconnect",
"Bluetooth GATT server connect after timeout, triggering disconnect"
);
this.disconnectPromise = (async () => {
await this.disconnectInternal(false);
this.disconnectPromise = undefined;
})();
} else {
this.logging.log(
"Bluetooth GATT server connected when connecting",
"Bluetooth GATT server connected when connecting"
);
}
})
Expand All @@ -112,7 +114,7 @@ export class BluetoothDeviceWrapper {
} else {
this.logging.error(
"Bluetooth GATT server connect error after our timeout",
e,
e
);
return undefined;
}
Expand All @@ -127,7 +129,7 @@ export class BluetoothDeviceWrapper {
const gattConnectResult = await Promise.race([
this.gattConnectPromise,
new Promise<"timeout">((resolve) =>
setTimeout(() => resolve("timeout"), connectTimeoutDuration),
setTimeout(() => resolve("timeout"), connectTimeoutDuration)
),
]);
if (gattConnectResult === "timeout") {
Expand Down Expand Up @@ -161,7 +163,7 @@ export class BluetoothDeviceWrapper {

private async disconnectInternal(userTriggered: boolean): Promise<void> {
this.logging.log(
`Bluetooth disconnect ${userTriggered ? "(user triggered)" : "(programmatic)"}`,
`Bluetooth disconnect ${userTriggered ? "(user triggered)" : "(programmatic)"}`
);
this.duringExplicitConnectDisconnect++;
try {
Expand All @@ -175,7 +177,7 @@ export class BluetoothDeviceWrapper {
this.duringExplicitConnectDisconnect--;
}
this.reconnectReadyPromise = new Promise((resolve) =>
setTimeout(resolve, 3_500),
setTimeout(resolve, 3_500)
);
}

Expand All @@ -198,19 +200,19 @@ export class BluetoothDeviceWrapper {
try {
if (!this.duringExplicitConnectDisconnect) {
this.logging.log(
"Bluetooth GATT disconnected... automatically trying reconnect",
"Bluetooth GATT disconnected... automatically trying reconnect"
);
// stateOnReconnectionAttempt();
await this.reconnect();
} else {
this.logging.log(
"Bluetooth GATT disconnect ignored during explicit disconnect",
"Bluetooth GATT disconnect ignored during explicit disconnect"
);
}
} catch (e) {
this.logging.error(
"Bluetooth connect triggered by disconnect listener failed",
e,
e
);
}
};
Expand All @@ -227,10 +229,10 @@ export class BluetoothDeviceWrapper {
const serviceMeta = profile.deviceInformation;
try {
const deviceInfo = await this.assertGattServer().getPrimaryService(
serviceMeta.id,
serviceMeta.id
);
const characteristic = await deviceInfo.getCharacteristic(
serviceMeta.characteristics.modelNumber.id,
serviceMeta.characteristics.modelNumber.id
);
const modelNumberBytes = await characteristic.readValue();
const modelNumber = new TextDecoder().decode(modelNumberBytes);
Expand All @@ -248,11 +250,17 @@ export class BluetoothDeviceWrapper {
throw new Error("Could not read model number");
}
}

async getAccelerometerService(): Promise<Accelerometer> {
const gattServer = this.assertGattServer();
const accelerometer = await AccelerometerService.init(gattServer);
return accelerometer;
}
}

export const createBluetoothDeviceWrapper = async (
device: BluetoothDevice,
logging: Logging,
logging: Logging
): Promise<BluetoothDeviceWrapper | undefined> => {
try {
// Reuse our connection objects for the same device as they
Expand Down
5 changes: 5 additions & 0 deletions lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*
* SPDX-License-Identifier: MIT
*/
import { Accelerometer } from "./accelerometer";
import {
BluetoothDeviceWrapper,
createBluetoothDeviceWrapper,
Expand Down Expand Up @@ -136,6 +137,10 @@ export class MicrobitWebBluetoothConnection
}
}

async getAccelerometer(): Promise<Accelerometer | undefined> {
return this.connection?.getAccelerometerService();
}

private setStatus(newStatus: ConnectionStatus) {
this.status = newStatus;
this.log("Device status " + newStatus);
Expand Down
3 changes: 3 additions & 0 deletions lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import { TypedEventTarget } from "./events";
import { BoardId } from "./board-id";
import { Accelerometer } from "./accelerometer";

/**
* Specific identified error types.
Expand Down Expand Up @@ -242,4 +243,6 @@ export interface DeviceConnection
* Clear device to enable chooseDevice.
*/
clearDevice(): void;

getAccelerometer(): Promise<Accelerometer | undefined>;
}
7 changes: 6 additions & 1 deletion lib/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
WebUSBError,
WebUSBErrorCode,
} from "./device";
import { Accelerometer } from "./accelerometer";

/**
* A mock device used during end-to-end testing.
Expand Down Expand Up @@ -83,7 +84,7 @@ export class MockDeviceConnection
* A progress callback. Called with undefined when the process is complete or has failed.
*/
progress: (percentage: number | undefined) => void;
},
}
): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 100));
options.progress(0.5);
Expand Down Expand Up @@ -112,4 +113,8 @@ export class MockDeviceConnection
mockWebUsbNotSupported(): void {
this.setStatus(ConnectionStatus.NOT_SUPPORTED);
}

getAccelerometer(): Promise<Accelerometer | undefined> {
return Promise.resolve(undefined);
}
}
Loading

0 comments on commit 8f330a0

Please sign in to comment.