diff --git a/lib/accelerometer-service.ts b/lib/accelerometer-service.ts index be11b71..4566375 100644 --- a/lib/accelerometer-service.ts +++ b/lib/accelerometer-service.ts @@ -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, @@ -26,10 +27,24 @@ export class AccelerometerService { dispatcher: TypedServiceEventDispatcher, isNotifying: boolean, queueGattOperation: (gattOperation: GattOperation) => void, - ): Promise { - const accelerometerService = await gattServer.getPrimaryService( - profile.accelerometer.id, - ); + listenerInit: boolean, + ): Promise { + 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, diff --git a/lib/bluetooth-device-wrapper.ts b/lib/bluetooth-device-wrapper.ts index 73ae7b7..17f965c 100644 --- a/lib/bluetooth-device-wrapper.ts +++ b/lib/bluetooth-device-wrapper.ts @@ -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; @@ -63,7 +66,7 @@ export class BluetoothDeviceWrapper { private accelerometerService: AccelerometerService | undefined; boardVersion: BoardVersion | undefined; - serviceListeners = { + serviceListenerState = { accelerometerdatachanged: { notifying: false, service: this.getAccelerometerService, @@ -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 { @@ -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 }); } } @@ -338,14 +350,19 @@ export class BluetoothDeviceWrapper { this.gattOperations = { busy: false, queue: [] }; } - async getAccelerometerService(): Promise { + async getAccelerometerService( + options: { + listenerInit: boolean; + } = { listenerInit: false }, + ): Promise { 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; @@ -361,13 +378,19 @@ export const createBluetoothDeviceWrapper = async ( device: BluetoothDevice, logging: Logging, dispatchTypedEvent: TypedServiceEventDispatcher, + addedServiceListeners: Record, ): Promise => { 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; diff --git a/lib/bluetooth.ts b/lib/bluetooth.ts index 36f4214..d438f55 100644 --- a/lib/bluetooth.ts +++ b/lib/bluetooth.ts @@ -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(); @@ -164,6 +168,7 @@ export class MicrobitWebBluetoothConnection device, this.logging, this.dispatchTypedEvent.bind(this), + this.addedServiceListeners, ); } // TODO: timeout unification? @@ -242,12 +247,15 @@ 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; } } @@ -255,8 +263,10 @@ export class MicrobitWebBluetoothConnection 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(); } diff --git a/lib/device.ts b/lib/device.ts index e91f9fe..0714cf3 100644 --- a/lib/device.ts +++ b/lib/device.ts @@ -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. @@ -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; @@ -169,6 +179,7 @@ export class DeviceConnectionEventMap { "flash": Event; "beforerequestdevice": Event; "afterrequestdevice": Event; + "backgrounderror": BackgroundErrorEvent; } export interface DeviceConnection diff --git a/lib/service-events.ts b/lib/service-events.ts index 0d47203..82ba2b4 100644 --- a/lib/service-events.ts +++ b/lib/service-events.ts @@ -1,4 +1,5 @@ import { AccelerometerDataEvent } from "./accelerometer.js"; +import { DeviceConnectionEventMap } from "./device.js"; export class ServiceConnectionEventMap { "accelerometerdatachanged": AccelerometerDataEvent; @@ -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; diff --git a/src/demo.ts b/src/demo.ts index 386d49b..98257d0 100644 --- a/src/demo.ts +++ b/src/demo.ts @@ -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, @@ -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; } } @@ -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); + }, + }); + } } }); @@ -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`", @@ -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`",