Skip to content

Commit 470cd81

Browse files
Add error handling for missing services (#5)
Dispatch background errors where errors cannot be handled directly. Respect event listeners added before connection initialization.
1 parent d393bee commit 470cd81

File tree

6 files changed

+126
-43
lines changed

6 files changed

+126
-43
lines changed

lib/accelerometer-service.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { AccelerometerData, AccelerometerDataEvent } from "./accelerometer.js";
22
import { GattOperation } from "./bluetooth-device-wrapper.js";
33
import { profile } from "./bluetooth-profile.js";
44
import { createGattOperationPromise } from "./bluetooth-utils.js";
5+
import { BackgroundErrorEvent, DeviceError } from "./device.js";
56
import {
67
CharacteristicDataTarget,
78
TypedServiceEventDispatcher,
@@ -26,10 +27,24 @@ export class AccelerometerService {
2627
dispatcher: TypedServiceEventDispatcher,
2728
isNotifying: boolean,
2829
queueGattOperation: (gattOperation: GattOperation) => void,
29-
): Promise<AccelerometerService> {
30-
const accelerometerService = await gattServer.getPrimaryService(
31-
profile.accelerometer.id,
32-
);
30+
listenerInit: boolean,
31+
): Promise<AccelerometerService | undefined> {
32+
let accelerometerService: BluetoothRemoteGATTService;
33+
try {
34+
accelerometerService = await gattServer.getPrimaryService(
35+
profile.accelerometer.id,
36+
);
37+
} catch (err) {
38+
if (listenerInit) {
39+
dispatcher("backgrounderror", new BackgroundErrorEvent(err as string));
40+
return;
41+
} else {
42+
throw new DeviceError({
43+
code: "service-missing",
44+
message: err as string,
45+
});
46+
}
47+
}
3348
const accelerometerDataCharacteristic =
3449
await accelerometerService.getCharacteristic(
3550
profile.accelerometer.characteristics.data.id,

lib/bluetooth-device-wrapper.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ import { AccelerometerService } from "./accelerometer-service.js";
88
import { profile } from "./bluetooth-profile.js";
99
import { BoardVersion, DeviceError } from "./device.js";
1010
import { Logging, NullLogging } from "./logging.js";
11-
import { TypedServiceEventDispatcher } from "./service-events.js";
11+
import {
12+
ServiceConnectionEventMap,
13+
TypedServiceEventDispatcher,
14+
} from "./service-events.js";
1215

1316
export interface GattOperationCallback {
1417
resolve: (result: DataView | void) => void;
@@ -67,7 +70,7 @@ export class BluetoothDeviceWrapper {
6770
private accelerometerService: AccelerometerService | undefined;
6871

6972
boardVersion: BoardVersion | undefined;
70-
serviceListeners = {
73+
serviceListenerState = {
7174
accelerometerdatachanged: {
7275
notifying: false,
7376
service: this.getAccelerometerService,
@@ -83,11 +86,20 @@ export class BluetoothDeviceWrapper {
8386
public readonly device: BluetoothDevice,
8487
private logging: Logging = new NullLogging(),
8588
private dispatchTypedEvent: TypedServiceEventDispatcher,
89+
private addedServiceListeners: Record<
90+
keyof ServiceConnectionEventMap,
91+
boolean
92+
>,
8693
) {
8794
device.addEventListener(
8895
"gattserverdisconnected",
8996
this.handleDisconnectEvent,
9097
);
98+
for (const [key, value] of Object.entries(this.addedServiceListeners)) {
99+
this.serviceListenerState[
100+
key as keyof ServiceConnectionEventMap
101+
].notifying = value;
102+
}
91103
}
92104

93105
async connect(): Promise<void> {
@@ -174,9 +186,9 @@ export class BluetoothDeviceWrapper {
174186

175187
// Restart notifications for services and characteristics
176188
// the user has listened to.
177-
for (const serviceListener of Object.values(this.serviceListeners)) {
189+
for (const serviceListener of Object.values(this.serviceListenerState)) {
178190
if (serviceListener.notifying) {
179-
serviceListener.service.call(this);
191+
serviceListener.service.call(this, { listenerInit: true });
180192
}
181193
}
182194

@@ -342,14 +354,19 @@ export class BluetoothDeviceWrapper {
342354
this.gattOperations = { busy: false, queue: [] };
343355
}
344356

345-
async getAccelerometerService(): Promise<AccelerometerService> {
357+
async getAccelerometerService(
358+
options: {
359+
listenerInit: boolean;
360+
} = { listenerInit: false },
361+
): Promise<AccelerometerService | undefined> {
346362
if (!this.accelerometerService) {
347363
const gattServer = this.assertGattServer();
348364
this.accelerometerService = await AccelerometerService.createService(
349365
gattServer,
350366
this.dispatchTypedEvent,
351-
this.serviceListeners.accelerometerdatachanged.notifying,
367+
this.serviceListenerState.accelerometerdatachanged.notifying,
352368
this.queueGattOperation.bind(this),
369+
options?.listenerInit,
353370
);
354371
}
355372
return this.accelerometerService;
@@ -365,13 +382,19 @@ export const createBluetoothDeviceWrapper = async (
365382
device: BluetoothDevice,
366383
logging: Logging,
367384
dispatchTypedEvent: TypedServiceEventDispatcher,
385+
addedServiceListeners: Record<keyof ServiceConnectionEventMap, boolean>,
368386
): Promise<BluetoothDeviceWrapper | undefined> => {
369387
try {
370388
// Reuse our connection objects for the same device as they
371389
// track the GATT connect promise that never resolves.
372390
const bluetooth =
373391
deviceIdToWrapper.get(device.id) ??
374-
new BluetoothDeviceWrapper(device, logging, dispatchTypedEvent);
392+
new BluetoothDeviceWrapper(
393+
device,
394+
logging,
395+
dispatchTypedEvent,
396+
addedServiceListeners,
397+
);
375398
deviceIdToWrapper.set(device.id, bluetooth);
376399
await bluetooth.connect();
377400
return bluetooth;

lib/bluetooth.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ export class MicrobitWebBluetoothConnection
5757
private _addEventListener = this.addEventListener;
5858
private _removeEventListener = this.removeEventListener;
5959

60+
private addedServiceListeners = {
61+
accelerometerdatachanged: false,
62+
};
63+
6064
constructor(options: MicrobitWebBluetoothConnectionOptions = {}) {
6165
super();
6266
this.logging = options.logging || new NullLogging();
@@ -164,6 +168,7 @@ export class MicrobitWebBluetoothConnection
164168
device,
165169
this.logging,
166170
this.dispatchTypedEvent.bind(this),
171+
this.addedServiceListeners,
167172
);
168173
}
169174
// TODO: timeout unification?
@@ -242,21 +247,26 @@ export class MicrobitWebBluetoothConnection
242247
}
243248

244249
private async startAccelerometerNotifications() {
245-
const accelerometerService =
246-
await this.connection?.getAccelerometerService();
250+
const accelerometerService = await this.connection?.getAccelerometerService(
251+
{ listenerInit: true },
252+
);
247253
accelerometerService?.startNotifications();
248254
if (this.connection) {
249-
this.connection.serviceListeners.accelerometerdatachanged.notifying =
255+
this.connection.serviceListenerState.accelerometerdatachanged.notifying =
250256
true;
257+
} else {
258+
this.addedServiceListeners.accelerometerdatachanged = true;
251259
}
252260
}
253261

254262
private async stopAccelerometerNotifications() {
255263
const accelerometerService =
256264
await this.connection?.getAccelerometerService();
257265
if (this.connection) {
258-
this.connection.serviceListeners.accelerometerdatachanged.notifying =
266+
this.connection.serviceListenerState.accelerometerdatachanged.notifying =
259267
false;
268+
} else {
269+
this.addedServiceListeners.accelerometerdatachanged = false;
260270
}
261271
accelerometerService?.stopNotifications();
262272
}

lib/device.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export type DeviceErrorCode =
3939
/**
4040
* Error occured during serial or bluetooth communication.
4141
*/
42-
| "background-comms-error";
42+
| "background-comms-error"
43+
/**
44+
* Bluetooth service is missing on device.
45+
*/
46+
| "service-missing";
4347

4448
/**
4549
* Error type used for all interactions with this module.
@@ -161,6 +165,12 @@ export class AfterRequestDevice extends Event {
161165
}
162166
}
163167

168+
export class BackgroundErrorEvent extends Event {
169+
constructor(public readonly errorMessage: string) {
170+
super("backgrounderror");
171+
}
172+
}
173+
164174
export class DeviceConnectionEventMap {
165175
"status": ConnectionStatusEvent;
166176
"serialdata": SerialDataEvent;
@@ -169,6 +179,7 @@ export class DeviceConnectionEventMap {
169179
"flash": Event;
170180
"beforerequestdevice": Event;
171181
"afterrequestdevice": Event;
182+
"backgrounderror": BackgroundErrorEvent;
172183
}
173184

174185
export interface DeviceConnection

lib/service-events.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AccelerometerDataEvent } from "./accelerometer.js";
2+
import { DeviceConnectionEventMap } from "./device.js";
23

34
export class ServiceConnectionEventMap {
45
"accelerometerdatachanged": AccelerometerDataEvent;
@@ -8,9 +9,11 @@ export type CharacteristicDataTarget = EventTarget & {
89
value: DataView;
910
};
1011

11-
export type TypedServiceEvent = keyof ServiceConnectionEventMap;
12+
export type TypedServiceEvent = keyof (ServiceConnectionEventMap &
13+
DeviceConnectionEventMap);
1214

1315
export type TypedServiceEventDispatcher = (
1416
_type: TypedServiceEvent,
15-
event: ServiceConnectionEventMap[TypedServiceEvent],
17+
event: (ServiceConnectionEventMap &
18+
DeviceConnectionEventMap)[TypedServiceEvent],
1619
) => boolean;

src/demo.ts

Lines changed: 46 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import "./demo.css";
77
import { MicrobitWebUSBConnection } from "../lib/usb";
88
import { HexFlashDataSource } from "../lib/hex-flash-data-source";
99
import {
10+
BackgroundErrorEvent,
1011
ConnectionStatus,
1112
ConnectionStatusEvent,
1213
DeviceConnection,
@@ -79,29 +80,35 @@ const displayStatus = (status: ConnectionStatus) => {
7980
const handleDisplayStatusChange = (event: ConnectionStatusEvent) => {
8081
displayStatus(event.status);
8182
};
82-
const initConnectionStatusDisplay = () => {
83+
const backgroundErrorListener = (event: BackgroundErrorEvent) => {
84+
console.error("Handled error:", event.errorMessage);
85+
};
86+
87+
const initConnectionListeners = () => {
8388
displayStatus(connection.status);
8489
connection.addEventListener("status", handleDisplayStatusChange);
90+
connection.addEventListener("backgrounderror", backgroundErrorListener);
8591
};
8692

8793
let connection: DeviceConnection = new MicrobitWebUSBConnection();
8894

89-
initConnectionStatusDisplay();
95+
initConnectionListeners();
9096

9197
const switchTransport = async () => {
9298
await connection.disconnect();
9399
connection.dispose();
94100
connection.removeEventListener("status", handleDisplayStatusChange);
101+
connection.removeEventListener("backgrounderror", backgroundErrorListener);
95102

96103
switch (transport.value) {
97104
case "bluetooth": {
98105
connection = new MicrobitWebBluetoothConnection();
99-
initConnectionStatusDisplay();
106+
initConnectionListeners();
100107
break;
101108
}
102109
case "usb": {
103110
connection = new MicrobitWebUSBConnection();
104-
initConnectionStatusDisplay();
111+
initConnectionListeners();
105112
break;
106113
}
107114
}
@@ -120,23 +127,14 @@ flash.addEventListener("click", async () => {
120127
const file = fileInput.files?.item(0);
121128
if (file) {
122129
const text = await file.text();
123-
await connection.flash(new HexFlashDataSource(text), {
124-
partial: true,
125-
progress: (percentage: number | undefined) => {
126-
console.log(percentage);
127-
},
128-
});
129-
}
130-
});
131-
132-
accDataGet.addEventListener("click", async () => {
133-
if (connection instanceof MicrobitWebBluetoothConnection) {
134-
const data = await connection.getAccelerometerData();
135-
console.log("Get accelerometer data", data);
136-
} else {
137-
throw new Error(
138-
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
139-
);
130+
if (connection.flash) {
131+
await connection.flash(new HexFlashDataSource(text), {
132+
partial: true,
133+
progress: (percentage: number | undefined) => {
134+
console.log(percentage);
135+
},
136+
});
137+
}
140138
}
141139
});
142140

@@ -170,10 +168,29 @@ accDataStop.addEventListener("click", async () => {
170168
}
171169
});
172170

171+
accDataGet.addEventListener("click", async () => {
172+
if (connection instanceof MicrobitWebBluetoothConnection) {
173+
try {
174+
const data = await connection.getAccelerometerData();
175+
console.log("Get accelerometer data", data);
176+
} catch (err) {
177+
console.error("Handled error:", err);
178+
}
179+
} else {
180+
throw new Error(
181+
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
182+
);
183+
}
184+
});
185+
173186
accPeriodGet.addEventListener("click", async () => {
174187
if (connection instanceof MicrobitWebBluetoothConnection) {
175-
const period = await connection.getAccelerometerPeriod();
176-
console.log("Get accelerometer period", period);
188+
try {
189+
const period = await connection.getAccelerometerPeriod();
190+
console.log("Get accelerometer period", period);
191+
} catch (err) {
192+
console.error("Handled error:", err);
193+
}
177194
} else {
178195
throw new Error(
179196
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",
@@ -183,8 +200,12 @@ accPeriodGet.addEventListener("click", async () => {
183200

184201
accPeriodSet.addEventListener("click", async () => {
185202
if (connection instanceof MicrobitWebBluetoothConnection) {
186-
const period = parseInt(accPeriodInput.value);
187-
await connection.setAccelerometerPeriod(period);
203+
try {
204+
const period = parseInt(accPeriodInput.value);
205+
await connection.setAccelerometerPeriod(period);
206+
} catch (err) {
207+
console.error("Handled error:", err);
208+
}
188209
} else {
189210
throw new Error(
190211
"`getAccelerometerData` is not supported on `MicrobitWebUSBConnection`",

0 commit comments

Comments
 (0)