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
23 changes: 19 additions & 4 deletions lib/accelerometer-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,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 { BackgroundErrorEvent, DeviceError } from "./device.js";
import {
CharacteristicDataTarget,
TypedServiceEventDispatcher,
Expand All @@ -26,10 +27,24 @@ export class AccelerometerService {
dispatcher: TypedServiceEventDispatcher,
isNotifying: boolean,
queueGattOperation: (gattOperation: GattOperation) => void,
): Promise<AccelerometerService> {
const accelerometerService = await gattServer.getPrimaryService(
profile.accelerometer.id,
);
listenerInit: boolean,
): Promise<AccelerometerService | undefined> {
let accelerometerService: BluetoothRemoteGATTService;
try {
accelerometerService = await gattServer.getPrimaryService(
profile.accelerometer.id,
);
} catch (err) {
if (listenerInit) {
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
return;
} else {
throw new DeviceError({
code: "service-missing",
message: err as string,
});
}
}
const accelerometerDataCharacteristic =
await accelerometerService.getCharacteristic(
profile.accelerometer.characteristics.data.id,
Expand Down
37 changes: 30 additions & 7 deletions lib/bluetooth-device-wrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import { AccelerometerService } from "./accelerometer-service.js";
import { profile } from "./bluetooth-profile.js";
import { BoardVersion, DeviceError } from "./device.js";
import { Logging, NullLogging } from "./logging.js";
import { TypedServiceEventDispatcher } from "./service-events.js";
import {
ServiceConnectionEventMap,
TypedServiceEventDispatcher,
} from "./service-events.js";

export interface GattOperationCallback {
resolve: (result: DataView | void) => void;
Expand Down Expand Up @@ -63,7 +66,7 @@ export class BluetoothDeviceWrapper {
private accelerometerService: AccelerometerService | undefined;

boardVersion: BoardVersion | undefined;
serviceListeners = {
serviceListenerState = {
accelerometerdatachanged: {
notifying: false,
service: this.getAccelerometerService,
Expand All @@ -79,11 +82,20 @@ export class BluetoothDeviceWrapper {
public readonly device: BluetoothDevice,
private logging: Logging = new NullLogging(),
private dispatchTypedEvent: TypedServiceEventDispatcher,
private addedServiceListeners: Record<
keyof ServiceConnectionEventMap,
boolean
>,
) {
device.addEventListener(
"gattserverdisconnected",
this.handleDisconnectEvent,
);
for (const [key, value] of Object.entries(this.addedServiceListeners)) {
this.serviceListenerState[
key as keyof ServiceConnectionEventMap
].notifying = value;
}
}

async connect(): Promise<void> {
Expand Down Expand Up @@ -170,9 +182,9 @@ export class BluetoothDeviceWrapper {

// Restart notifications for services and characteristics
// the user has listened to.
for (const serviceListener of Object.values(this.serviceListeners)) {
for (const serviceListener of Object.values(this.serviceListenerState)) {
if (serviceListener.notifying) {
serviceListener.service.call(this);
serviceListener.service.call(this, { listenerInit: true });
}
}

Expand Down Expand Up @@ -338,14 +350,19 @@ export class BluetoothDeviceWrapper {
this.gattOperations = { busy: false, queue: [] };
}

async getAccelerometerService(): Promise<AccelerometerService> {
async getAccelerometerService(
options: {
listenerInit: boolean;
} = { listenerInit: false },
): Promise<AccelerometerService | undefined> {
if (!this.accelerometerService) {
const gattServer = this.assertGattServer();
this.accelerometerService = await AccelerometerService.createService(
gattServer,
this.dispatchTypedEvent,
this.serviceListeners.accelerometerdatachanged.notifying,
this.serviceListenerState.accelerometerdatachanged.notifying,
this.queueGattOperation.bind(this),
options?.listenerInit,
);
}
return this.accelerometerService;
Expand All @@ -361,13 +378,19 @@ export const createBluetoothDeviceWrapper = async (
device: BluetoothDevice,
logging: Logging,
dispatchTypedEvent: TypedServiceEventDispatcher,
addedServiceListeners: Record<keyof ServiceConnectionEventMap, boolean>,
): Promise<BluetoothDeviceWrapper | undefined> => {
try {
// Reuse our connection objects for the same device as they
// track the GATT connect promise that never resolves.
const bluetooth =
deviceIdToWrapper.get(device.id) ??
new BluetoothDeviceWrapper(device, logging, dispatchTypedEvent);
new BluetoothDeviceWrapper(
device,
logging,
dispatchTypedEvent,
addedServiceListeners,
);
deviceIdToWrapper.set(device.id, bluetooth);
await bluetooth.connect();
return bluetooth;
Expand Down
18 changes: 14 additions & 4 deletions lib/bluetooth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export class MicrobitWebBluetoothConnection
private _addEventListener = this.addEventListener;
private _removeEventListener = this.removeEventListener;

private addedServiceListeners = {
accelerometerdatachanged: false,
};

constructor(options: MicrobitWebBluetoothConnectionOptions = {}) {
super();
this.logging = options.logging || new NullLogging();
Expand Down Expand Up @@ -164,6 +168,7 @@ export class MicrobitWebBluetoothConnection
device,
this.logging,
this.dispatchTypedEvent.bind(this),
this.addedServiceListeners,
);
}
// TODO: timeout unification?
Expand Down Expand Up @@ -242,21 +247,26 @@ export class MicrobitWebBluetoothConnection
}

private async startAccelerometerNotifications() {
const accelerometerService =
await this.connection?.getAccelerometerService();
const accelerometerService = await this.connection?.getAccelerometerService(
{ listenerInit: true },
);
accelerometerService?.startNotifications();
if (this.connection) {
this.connection.serviceListeners.accelerometerdatachanged.notifying =
this.connection.serviceListenerState.accelerometerdatachanged.notifying =
true;
} else {
this.addedServiceListeners.accelerometerdatachanged = true;
}
}

private async stopAccelerometerNotifications() {
const accelerometerService =
await this.connection?.getAccelerometerService();
if (this.connection) {
this.connection.serviceListeners.accelerometerdatachanged.notifying =
this.connection.serviceListenerState.accelerometerdatachanged.notifying =
false;
} else {
this.addedServiceListeners.accelerometerdatachanged = false;
}
accelerometerService?.stopNotifications();
}
Expand Down
13 changes: 12 additions & 1 deletion lib/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,11 @@ export type DeviceErrorCode =
/**
* Error occured during serial or bluetooth communication.
*/
| "background-comms-error";
| "background-comms-error"
/**
* Bluetooth service is missing on device.
*/
| "service-missing";

/**
* Error type used for all interactions with this module.
Expand Down Expand Up @@ -161,6 +165,12 @@ export class AfterRequestDevice extends Event {
}
}

export class BackgroundErrorEvent extends Event {
constructor(public readonly errorMessage: string) {
super("backgrounderror");
}
}

export class DeviceConnectionEventMap {
"status": ConnectionStatusEvent;
"serialdata": SerialDataEvent;
Expand All @@ -169,6 +179,7 @@ export class DeviceConnectionEventMap {
"flash": Event;
"beforerequestdevice": Event;
"afterrequestdevice": Event;
"backgrounderror": BackgroundErrorEvent;
}

export interface DeviceConnection
Expand Down
7 changes: 5 additions & 2 deletions lib/service-events.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AccelerometerDataEvent } from "./accelerometer.js";
import { DeviceConnectionEventMap } from "./device.js";

export class ServiceConnectionEventMap {
"accelerometerdatachanged": AccelerometerDataEvent;
Expand All @@ -8,9 +9,11 @@ export type CharacteristicDataTarget = EventTarget & {
value: DataView;
};

export type TypedServiceEvent = keyof ServiceConnectionEventMap;
export type TypedServiceEvent = keyof (ServiceConnectionEventMap &
DeviceConnectionEventMap);

export type TypedServiceEventDispatcher = (
_type: TypedServiceEvent,
event: ServiceConnectionEventMap[TypedServiceEvent],
event: (ServiceConnectionEventMap &
DeviceConnectionEventMap)[TypedServiceEvent],
) => boolean;
71 changes: 46 additions & 25 deletions src/demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "./demo.css";
import { MicrobitWebUSBConnection } from "../lib/usb";
import { HexFlashDataSource } from "../lib/hex-flash-data-source";
import {
BackgroundErrorEvent,
ConnectionStatus,
ConnectionStatusEvent,
DeviceConnection,
Expand Down Expand Up @@ -79,29 +80,35 @@ const displayStatus = (status: ConnectionStatus) => {
const handleDisplayStatusChange = (event: ConnectionStatusEvent) => {
displayStatus(event.status);
};
const initConnectionStatusDisplay = () => {
const backgroundErrorListener = (event: BackgroundErrorEvent) => {
console.error("Handled error:", event.errorMessage);
};

const initConnectionListeners = () => {
displayStatus(connection.status);
connection.addEventListener("status", handleDisplayStatusChange);
connection.addEventListener("backgrounderror", backgroundErrorListener);
};

let connection: DeviceConnection = new MicrobitWebUSBConnection();

initConnectionStatusDisplay();
initConnectionListeners();

const switchTransport = async () => {
await connection.disconnect();
connection.dispose();
connection.removeEventListener("status", handleDisplayStatusChange);
connection.removeEventListener("backgrounderror", backgroundErrorListener);

switch (transport.value) {
case "bluetooth": {
connection = new MicrobitWebBluetoothConnection();
initConnectionStatusDisplay();
initConnectionListeners();
break;
}
case "usb": {
connection = new MicrobitWebUSBConnection();
initConnectionStatusDisplay();
initConnectionListeners();
break;
}
}
Expand All @@ -120,23 +127,14 @@ flash.addEventListener("click", async () => {
const file = fileInput.files?.item(0);
if (file) {
const text = await file.text();
await connection.flash(new HexFlashDataSource(text), {
partial: true,
progress: (percentage: number | undefined) => {
console.log(percentage);
},
});
}
});

accDataGet.addEventListener("click", async () => {
if (connection instanceof MicrobitWebBluetoothConnection) {
const data = await connection.getAccelerometerData();
console.log("Get accelerometer data", data);
} else {
throw new Error(
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
);
if (connection.flash) {
await connection.flash(new HexFlashDataSource(text), {
partial: true,
progress: (percentage: number | undefined) => {
console.log(percentage);
},
});
}
}
});

Expand Down Expand Up @@ -170,10 +168,29 @@ accDataStop.addEventListener("click", async () => {
}
});

accDataGet.addEventListener("click", async () => {
if (connection instanceof MicrobitWebBluetoothConnection) {
try {
const data = await connection.getAccelerometerData();
console.log("Get accelerometer data", data);
} catch (err) {
console.error("Handled error:", err);
}
} else {
throw new Error(
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
);
}
});

accPeriodGet.addEventListener("click", async () => {
if (connection instanceof MicrobitWebBluetoothConnection) {
const period = await connection.getAccelerometerPeriod();
console.log("Get accelerometer period", period);
try {
const period = await connection.getAccelerometerPeriod();
console.log("Get accelerometer period", period);
} catch (err) {
console.error("Handled error:", err);
}
} else {
throw new Error(
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
Expand All @@ -183,8 +200,12 @@ accPeriodGet.addEventListener("click", async () => {

accPeriodSet.addEventListener("click", async () => {
if (connection instanceof MicrobitWebBluetoothConnection) {
const period = parseInt(accPeriodInput.value);
await connection.setAccelerometerPeriod(period);
try {
const period = parseInt(accPeriodInput.value);
await connection.setAccelerometerPeriod(period);
} catch (err) {
console.error("Handled error:", err);
}
} else {
throw new Error(
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
Expand Down