From 5812d7f9e199b2767ec25b15c6b8d5744b93fe66 Mon Sep 17 00:00:00 2001 From: Alex Shaw Date: Sun, 4 Jun 2023 10:50:57 -0700 Subject: [PATCH 1/4] refactor: remove Node-specific APIs. --- lib/bindings.cpp | 16 +- .../adapter.ts} | 121 +++--- src/adapter/adapters.ts | 46 ++ src/{adapters => adapter}/simpleble.ts | 69 ++- src/adapters/adapter.ts | 51 --- src/adapters/index.ts | 29 -- src/adapters/noble-adapter.ts | 399 ------------------ src/bluetooth.ts | 88 ++-- src/characteristic.ts | 74 ++-- src/common.ts | 32 ++ src/descriptor.ts | 10 +- src/device.ts | 111 +++-- src/events.ts | 49 +-- src/index.ts | 12 +- src/server.ts | 50 +-- src/service.ts | 91 ++-- 16 files changed, 462 insertions(+), 786 deletions(-) rename src/{adapters/simpleble-adapter.ts => adapter/adapter.ts} (78%) create mode 100644 src/adapter/adapters.ts rename src/{adapters => adapter}/simpleble.ts (58%) delete mode 100644 src/adapters/adapter.ts delete mode 100644 src/adapters/index.ts delete mode 100644 src/adapters/noble-adapter.ts create mode 100644 src/common.ts diff --git a/lib/bindings.cpp b/lib/bindings.cpp index b6992d34..06f06977 100644 --- a/lib/bindings.cpp +++ b/lib/bindings.cpp @@ -7,9 +7,23 @@ Napi::Value GetAdapters(const Napi::CallbackInfo &info) { Napi::Env env = info.Env(); - const size_t count = simpleble_adapter_get_count(); + // Respect `SIMPLEBLE_ADAPTER` if it exists. + const char* adapterIndexEnv = std::getenv("SIMPLEBLE_ADAPTER"); + if (adapterIndexEnv) { + const int adapterIndex = std::atoi(adapterIndexEnv); + if (adapterIndex < 0 || adapterIndex >= count) { + Napi::RangeError::New(env, "SIMPLEBLE_ADAPTER is out of range") + .ThrowAsJavaScriptException(); + return env.Null(); + } + Napi::Value adapterInstance = Adapter::constructor.New({Napi::Number::New(env, adapterIndex)}); + Napi::Array adapters = Napi::Array::New(env, 1); + adapters.Set(0u, adapterInstance); + return adapters; + } + Napi::Array adapters = Napi::Array::New(env, count); for (size_t i = 0; i < count; i++) { diff --git a/src/adapters/simpleble-adapter.ts b/src/adapter/adapter.ts similarity index 78% rename from src/adapters/simpleble-adapter.ts rename to src/adapter/adapter.ts index cc34b47c..fe244435 100644 --- a/src/adapters/simpleble-adapter.ts +++ b/src/adapter/adapter.ts @@ -1,48 +1,72 @@ /* -* Node Web Bluetooth -* Copyright (c) 2023 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ - -import { EventEmitter } from 'events'; -import { Adapter as BluetoothAdapter } from './adapter'; + * Node Web Bluetooth + * Copyright (c) 2017-2023 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import { adapters, isEnabled } from "./adapters"; import { BluetoothUUID } from '../uuid'; -import { BluetoothDeviceImpl } from '../device'; -import { BluetoothRemoteGATTCharacteristicImpl } from '../characteristic'; -import { BluetoothRemoteGATTServiceImpl } from '../service'; -import { BluetoothRemoteGATTDescriptorImpl } from '../descriptor'; -import { - isEnabled, - getAdapters, +import type { BluetoothDevice } from '../device'; +import type { BluetoothRemoteGATTService } from '../service'; +import type { BluetoothRemoteGATTCharacteristic } from '../characteristic'; +import type { BluetoothRemoteGATTDescriptor } from '../descriptor'; +import type { CustomEventListener } from "../common"; +import type { Adapter, Peripheral, Service, Characteristic } from './simpleble'; -/** - * @hidden - */ -export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { +/** @hidden BluetoothAdapter event map. */ +export interface BluetoothAdapterEventMap { + enabledchanged: Event; +} + +/** @hidden Type-safe BluetoothAdapter events. */ +export interface BluetoothAdapter extends EventTarget { + addEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; +} + +/** @hidden Wrapper around SimpleBLE. */ +export class BluetoothAdapter extends EventTarget { private adapter: Adapter; private peripherals = new Map(); private servicesByPeripheral = new Map(); @@ -53,7 +77,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { private descriptors = new Map(); private charEvents = new Map void>(); - private validDevice(device: Partial, serviceUUIDs: Array): boolean { + private validDevice(device: Partial, serviceUUIDs: Array): boolean { if (serviceUUIDs.length === 0) { // Match any device return true; @@ -70,7 +94,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { return serviceUUIDs.some(serviceUUID => advertisedUUIDs.indexOf(serviceUUID) >= 0); } - private buildBluetoothDevice(device: Peripheral): Partial { + private buildBluetoothDevice(device: Peripheral): Partial { const name = device.identifier; const address = device.address; const rssi = device.rssi; @@ -147,11 +171,12 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { public async startScan(serviceUUIDs: Array, foundFn: (device: Partial) => void): Promise { if (this.state === false) { - throw new Error('adapter not enabled'); + // TODO: DOMException("Adapter not enabled", "NotFoundError") was added in Node 17. + throw new Error('Adapter not enabled'); } if (!this.adapter) { - this.adapter = getAdapters()[0]; + this.adapter = adapters[0]; } this.adapter.setCallbackOnScanFound(peripheral => { @@ -168,7 +193,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { this.peripherals.clear(); const success = this.adapter.scanStart(); if (!success) { - throw new Error('scan start failed'); + throw new Error('Scan start failed'); } } @@ -176,7 +201,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { if (this.adapter) { const success = this.adapter.scanStop(); if (!success) { - throw new Error('scan stop failed'); + throw new Error('Scan stop failed'); } } } @@ -215,7 +240,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { } } - public async discoverServices(id: string, serviceUUIDs?: Array): Promise>> { + public async discoverServices(id: string, serviceUUIDs?: Array): Promise>> { const peripheral = this.peripherals.get(id); if (!peripheral) { throw new Error('Peripheral not found'); @@ -234,12 +259,12 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { return discovered; } - public async discoverIncludedServices(_handle: string, _serviceUUIDs?: Array): Promise>> { + public async discoverIncludedServices(_handle: string, _serviceUUIDs?: Array): Promise>> { // Currently not implemented return []; } - public async discoverCharacteristics(serviceUuid: string, characteristicUUIDs?: Array): Promise>> { + public async discoverCharacteristics(serviceUuid: string, characteristicUUIDs?: Array): Promise>> { const peripheral = this.peripheralByService.get(serviceUuid); const characteristics = this.characteristicsByService.get(serviceUuid); const discovered = []; @@ -288,7 +313,7 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { return discovered; } - public async discoverDescriptors(charUuid: string, descriptorUUIDs?: Array): Promise>> { + public async discoverDescriptors(charUuid: string, descriptorUUIDs?: Array): Promise>> { const descriptors = this.descriptors.get(charUuid); const discovered = []; @@ -360,3 +385,5 @@ export class SimplebleAdapter extends EventEmitter implements BluetoothAdapter { } } } + +export const adapter = new BluetoothAdapter(); diff --git a/src/adapter/adapters.ts b/src/adapter/adapters.ts new file mode 100644 index 00000000..0d2ebd60 --- /dev/null +++ b/src/adapter/adapters.ts @@ -0,0 +1,46 @@ +/* + * Node Web Bluetooth + * Copyright (c) 2017-2023 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import type { Adapter, SimpleBLE } from './simpleble'; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const simpleble: SimpleBLE = require('bindings')('simpleble.node'); + +/** @hidden Array of Node-specific SimpleBLE implementation. */ +export const adapters: Adapter[] = simpleble.getAdapters(); + +/** @hidden Is Bluetooth available? */ +export const isEnabled = simpleble.isEnabled; + +// Prevent memory leaks. +// Might not be necessary since all bindings do is delete `this`. +function unload() { + if (adapters) { + for (const adapter of adapters) { + adapter.release(); + } + } +} +process.on('exit', unload); +process.on('SIGINT', unload); diff --git a/src/adapters/simpleble.ts b/src/adapter/simpleble.ts similarity index 58% rename from src/adapters/simpleble.ts rename to src/adapter/simpleble.ts index 0ee81b6b..3588b8ca 100644 --- a/src/adapters/simpleble.ts +++ b/src/adapter/simpleble.ts @@ -1,43 +1,39 @@ /* -* Node Web Bluetooth -* Copyright (c) 2022 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ + * Node Web Bluetooth + * Copyright (c) 2017-2023 Rob Moran + * + * The MIT License (MIT) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ -// eslint-disable-next-line @typescript-eslint/no-var-requires -const simpleble = require('bindings')('simpleble.node'); -module.exports = simpleble; - -/** SimpleBLE address type. */ +/** @hidden SimpleBLE address type. */ export enum AddressType { PUBLIC = 0, RANDOM = 1, UNSPECIFIED = 2, } -/** SimpleBLE descriptor. */ +/** @hidden SimpleBLE descriptor. */ export type Descriptor = string; -/** SimpleBLE characteristic. */ +/** @hidden SimpleBLE characteristic. */ export interface Characteristic { canRead: boolean; canWriteRequest: boolean; @@ -48,14 +44,14 @@ export interface Characteristic { uuid: string; } -/** SimpleBLE Service. */ +/** @hidden SimpleBLE Service. */ export interface Service { uuid: string; data: Uint8Array; characteristics: Characteristic[]; } -/** SimpleBLE Peripheral. */ +/** @hidden SimpleBLE Peripheral. */ export interface Peripheral { identifier: string; address: string; @@ -84,7 +80,7 @@ export interface Peripheral { setCallbackOnDisconnected(cb: () => void): boolean; } -/** SimpleBLE Adapter. */ +/** @hidden SimpleBLE Adapter. */ export interface Adapter { identifier: string; address: string; @@ -101,5 +97,8 @@ export interface Adapter { release(): void; } -export declare function getAdapters(): Adapter[]; -export declare function isEnabled(): boolean; +/** @hidden SimpleBLE module. */ +export interface SimpleBLE { + getAdapters(): Adapter[]; + isEnabled(): boolean; +} diff --git a/src/adapters/adapter.ts b/src/adapters/adapter.ts deleted file mode 100644 index cb43550b..00000000 --- a/src/adapters/adapter.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* -* Node Web Bluetooth -* Copyright (c) 2022 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ - -import { EventEmitter } from 'events'; -import { BluetoothDeviceImpl } from '../device'; -import { BluetoothRemoteGATTServiceImpl } from '../service'; -import { BluetoothRemoteGATTCharacteristicImpl } from '../characteristic'; -import { BluetoothRemoteGATTDescriptorImpl } from '../descriptor'; - -/** - * @hidden - */ -export interface Adapter extends EventEmitter { - getEnabled: () => Promise; - startScan: (serviceUUIDs: Array, foundFn: (device: Partial) => void) => Promise; - stopScan: () => void; - connect: (handle: string, disconnectFn?: () => void) => Promise; - disconnect: (handle: string) => Promise; - discoverServices: (handle: string, serviceUUIDs?: Array) => Promise>>; - discoverIncludedServices: (handle: string, serviceUUIDs?: Array) => Promise>>; - discoverCharacteristics: (handle: string, characteristicUUIDs?: Array) => Promise>>; - discoverDescriptors: (handle: string, descriptorUUIDs?: Array) => Promise>>; - readCharacteristic: (handle: string) => Promise; - writeCharacteristic: (handle: string, value: DataView, withoutResponse?: boolean) => Promise; - enableNotify: (handle: string, notifyFn: () => void) => Promise; - disableNotify: (handle: string) => Promise; - readDescriptor: (handle: string) => Promise; - writeDescriptor: (handle: string, value: DataView) => Promise; -} diff --git a/src/adapters/index.ts b/src/adapters/index.ts deleted file mode 100644 index 0c41e5b4..00000000 --- a/src/adapters/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* -* Node Web Bluetooth -* Copyright (c) 2022 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ - -import { SimplebleAdapter } from './simpleble-adapter'; - -export const EVENT_ENABLED = 'enabledchanged'; -export const adapter = new SimplebleAdapter(); diff --git a/src/adapters/noble-adapter.ts b/src/adapters/noble-adapter.ts deleted file mode 100644 index f1e080de..00000000 --- a/src/adapters/noble-adapter.ts +++ /dev/null @@ -1,399 +0,0 @@ -/* -* Node Web Bluetooth -* Copyright (c) 2017 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ - -/* -import { platform } from 'os'; -import { EventEmitter } from 'events'; -import { EVENT_ENABLED } from './'; -import { Adapter } from './adapter'; -import { BluetoothUUID } from '../helpers'; -import { BluetoothDevice } from '../device'; -import { BluetoothRemoteGATTService } from '../service'; -import { BluetoothRemoteGATTCharacteristic } from '../characteristic'; -import * as noble from '@abandonware/noble'; - -export class NobleAdapter extends EventEmitter implements Adapter { - - private deviceHandles = new Map(); - private serviceHandles = new Map(); - private characteristicHandles = new Map(); - private descriptorHandles = new Map(); - private charNotifies = new Map void>(); - private discoverFn: ((device: noble.Peripheral) => void | undefined) | undefined; - private initialised = false; - private enabled = false; - private os: string = platform(); - - constructor() { - super(); - this.enabled = this.state; - noble.on('stateChange', () => { - if (this.enabled !== this.state) { - this.enabled = this.state; - this.emit(EVENT_ENABLED, this.enabled); - } - }); - } - - private get state(): boolean { - return (noble.state === 'poweredOn'); - } - - private init(): void { - if (this.initialised) { - return; - } - noble.on('discover', (deviceInfo: noble.Peripheral) => { - if (this.discoverFn) this.discoverFn(deviceInfo); - }); - this.initialised = true; - } - - private bufferToDataView(buffer: Buffer): DataView { - // Buffer to ArrayBuffer - const arrayBuffer = new Uint8Array(buffer).buffer; - return new DataView(arrayBuffer); - } - - private dataViewToBuffer(dataView: DataView): Buffer { - // DataView to TypedArray - const typedArray = new Uint8Array(dataView.buffer); - return new Buffer(typedArray); - } - - private validDevice(deviceInfo: noble.Peripheral, serviceUUIDs: Array): boolean { - if (serviceUUIDs.length === 0) { - // Match any device - return true; - } - - if (!deviceInfo.advertisement.serviceUuids) { - // No advertised services, no match - return false; - } - - const advertisedUUIDs = deviceInfo.advertisement.serviceUuids.map((serviceUUID: string) => { - return BluetoothUUID.canonicalUUID(serviceUUID); - }); - - return serviceUUIDs.some(serviceUUID => { - // An advertised UUID matches our search UUIDs - return (advertisedUUIDs.indexOf(serviceUUID) >= 0); - }); - } - - private deviceToBluetoothDevice(deviceInfo: noble.Peripheral): Partial { - const deviceID = (deviceInfo.address && deviceInfo.address !== 'unknown') ? deviceInfo.address : deviceInfo.id; - const serviceUUIDs = deviceInfo.advertisement.serviceUuids ? deviceInfo.advertisement.serviceUuids.map((serviceUUID: string) => BluetoothUUID.canonicalUUID(serviceUUID)) : []; - - const manufacturerData = new Map(); - if (deviceInfo.advertisement.manufacturerData) { - // First 2 bytes are 16-bit company identifier - const company = deviceInfo.advertisement.manufacturerData.readUInt16LE(0); - - // Remove company ID - const buffer = deviceInfo.advertisement.manufacturerData.slice(2); - manufacturerData.set(('0000' + company.toString(16)).slice(-4), this.bufferToDataView(buffer)); - } - - const serviceData = new Map(); - if (deviceInfo.advertisement.serviceData) { - for (const serviceAdvert of deviceInfo.advertisement.serviceData) { - serviceData.set(BluetoothUUID.canonicalUUID(serviceAdvert.uuid), this.bufferToDataView(serviceAdvert.data)); - } - } - - return { - id: deviceID, - name: deviceInfo.advertisement.localName, - _serviceUUIDs: serviceUUIDs, - _adData: { - rssi: deviceInfo.rssi, - txPower: deviceInfo.advertisement.txPowerLevel, - serviceData: serviceData, - manufacturerData: manufacturerData - } - }; - } - - public async getEnabled(): Promise { - if (noble.state === 'unknown' || noble.state === 'poweredOff') { - return new Promise(resolve => noble.once('stateChange', () => resolve(this.state))); - } - - return this.state; - } - - public async startScan(serviceUUIDs: Array, foundFn: (device: Partial) => void): Promise { - - this.discoverFn = deviceInfo => { - if (this.validDevice(deviceInfo, serviceUUIDs)) { - const device = this.deviceToBluetoothDevice(deviceInfo); - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - if (!this.deviceHandles.has(device.id!)) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.deviceHandles.set(device.id!, deviceInfo); - // Only call the found function the first time we find a valid device - foundFn(device); - } - } - }; - - this.init(); - this.deviceHandles.clear(); - if (noble.state === 'unknown' || noble.state === 'poweredOff') { - await new Promise(resolve => noble.once('stateChange', () => resolve(undefined))); - } - - if (this.state === false) { - throw new Error('adapter not enabled'); - } - // Noble doesn't correctly match short and canonical UUIDs on Linux, so we need to check ourselves - // Continually scan to pick up all advertised UUIDs - await noble.startScanningAsync([], true); - } - - public stopScan(_errorFn?: (errorMsg: string) => void): void { - this.discoverFn = undefined; - noble.stopScanning(); - } - - public connect(handle: string, disconnectFn?: () => void): Promise { - const baseDevice = this.deviceHandles.get(handle); - baseDevice.removeAllListeners('connect'); - baseDevice.removeAllListeners('disconnect'); - - if (disconnectFn) { - baseDevice.once('disconnect', () => { - this.serviceHandles.clear(); - this.characteristicHandles.clear(); - this.descriptorHandles.clear(); - this.charNotifies.clear(); - disconnectFn(); - }); - } - - return baseDevice.connectAsync(); - } - - public disconnect(handle: string): Promise { - const baseDevice = this.deviceHandles.get(handle); - return baseDevice.disconnectAsync(); - } - - public async discoverServices(handle: string, serviceUUIDs?: Array): Promise>> { - const baseDevice = this.deviceHandles.get(handle); - const services = await baseDevice.discoverServicesAsync(); - const discovered = []; - - for (const serviceInfo of services) { - const serviceUUID = BluetoothUUID.canonicalUUID(serviceInfo.uuid); - - if (!serviceUUIDs || serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { - if (!this.serviceHandles.has(serviceUUID)) { - this.serviceHandles.set(serviceUUID, serviceInfo); - } - - discovered.push({ - uuid: serviceUUID, - primary: true - }); - } - } - - return discovered; - } - - public async discoverIncludedServices(handle: string, serviceUUIDs?: Array): Promise>> { - const serviceInfo = this.serviceHandles.get(handle); - const services = await serviceInfo.discoverIncludedServicesAsync(); - const discovered = []; - - // TODO: check retiurn here! - for (const service of services) { - const serviceUUID = BluetoothUUID.canonicalUUID(service); - - if (!serviceUUIDs || serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { - discovered.push({ - uuid: serviceUUID, - primary: false - }); - } - } - - return discovered; - } - - public async discoverCharacteristics(handle: string, characteristicUUIDs?: Array): Promise>> { - const serviceInfo = this.serviceHandles.get(handle); - const characteristics = await serviceInfo.discoverCharacteristicsAsync(); - const discovered = []; - - for (const characteristicInfo of characteristics) { - const charUUID = BluetoothUUID.canonicalUUID(characteristicInfo.uuid); - - if (!characteristicUUIDs || characteristicUUIDs.length === 0 || characteristicUUIDs.indexOf(charUUID) >= 0) { - if (!this.characteristicHandles.has(charUUID)) { - this.characteristicHandles.set(charUUID, characteristicInfo); - } - - discovered.push({ - uuid: charUUID, - properties: { - broadcast: (characteristicInfo.properties.indexOf('broadcast') >= 0), - read: (characteristicInfo.properties.indexOf('read') >= 0), - writeWithoutResponse: (characteristicInfo.properties.indexOf('writeWithoutResponse') >= 0), - write: (characteristicInfo.properties.indexOf('write') >= 0), - notify: (characteristicInfo.properties.indexOf('notify') >= 0), - indicate: (characteristicInfo.properties.indexOf('indicate') >= 0), - authenticatedSignedWrites: (characteristicInfo.properties.indexOf('authenticatedSignedWrites') >= 0), - reliableWrite: (characteristicInfo.properties.indexOf('reliableWrite') >= 0), - writableAuxiliaries: (characteristicInfo.properties.indexOf('writableAuxiliaries') >= 0) - } - }); - - characteristicInfo.on('data', (data: Buffer, isNotification: boolean) => { - if (isNotification === true && this.charNotifies.has(charUUID)) { - const dataView = this.bufferToDataView(data); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - this.charNotifies.get(charUUID)!(dataView); - } - }); - } - } - - return discovered; - } - - public async discoverDescriptors(handle: string, descriptorUUIDs?: Array): Promise>> { - const characteristicInfo = this.characteristicHandles.get(handle); - const descriptors = await characteristicInfo.discoverDescriptorsAsync(); - const discovered = []; - - for (const descriptorInfo of descriptors) { - const descUUID = BluetoothUUID.canonicalUUID(descriptorInfo.uuid); - - if (!descriptorUUIDs || descriptorUUIDs.length === 0 || descriptorUUIDs.indexOf(descUUID) >= 0) { - const descHandle = characteristicInfo.uuid + '-' + descriptorInfo.uuid; - if (!this.descriptorHandles.has(descHandle)) { - this.descriptorHandles.set(descHandle, descriptorInfo); - } - - discovered.push({ - uuid: descUUID - }); - } - } - - return discovered; - } - - public async readCharacteristic(handle: string): Promise { - const characteristic = this.characteristicHandles.get(handle); - const data = await characteristic.readAsync(); - const dataView = this.bufferToDataView(data); - return dataView; - } - - public async writeCharacteristic(handle: string, value: DataView, withoutResponse = false): Promise { - const buffer = this.dataViewToBuffer(value); - const characteristic = this.characteristicHandles.get(handle); - - if (withoutResponse === undefined) { - // writeWithoutResponse and authenticatedSignedWrites don't require a response - withoutResponse = characteristic.properties.indexOf('writeWithoutResponse') >= 0 - || characteristic.properties.indexOf('authenticatedSignedWrites') >= 0; - } - - await characteristic.writeAsync(buffer, withoutResponse); - - // TODO: check still needed - // Add a small delay for writing without response when not on MacOS - if (this.os !== 'darwin' && withoutResponse) { - await new Promise(resolve => setTimeout(resolve, 25)); - } - } - - public enableNotify(handle: string, notifyFn: (value: DataView) => void): Promise { - if (this.charNotifies.has(handle)) { - this.charNotifies.set(handle, notifyFn); - return Promise.resolve(); - } - - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const characteristic = this.characteristicHandles.get(handle); - - // TODO: check type emitted - characteristic.once('notify', (state: string) => { - if (state !== 'true') { - reject('notify failed to enable'); - } - this.charNotifies.set(handle, notifyFn); - resolve(undefined); - }); - - await characteristic.notifyAsync(true); - }); - } - - public disableNotify(handle: string): Promise { - if (!this.charNotifies.has(handle)) { - return Promise.resolve(); - } - - // eslint-disable-next-line no-async-promise-executor - return new Promise(async (resolve, reject) => { - const characteristic = this.characteristicHandles.get(handle); - - // TODO: check type emitted - characteristic.once('notify', (state: string) => { - if (state !== 'false') { - reject('notify failed to disable'); - } - - if (this.charNotifies.has(handle)) { - this.charNotifies.delete(handle); - } - resolve(undefined); - }); - - await characteristic.notifyAsync(false); - }); - } - - public async readDescriptor(handle: string): Promise { - const data = await this.descriptorHandles.get(handle).readValueAsync(); - const dataView = this.bufferToDataView(data); - return dataView; - } - - public writeDescriptor(handle: string, value: DataView): Promise { - const buffer = this.dataViewToBuffer(value); - return this.descriptorHandles.get(handle).writeValueAsync(buffer); - } -} -*/ diff --git a/src/bluetooth.ts b/src/bluetooth.ts index 368241f0..d495d056 100644 --- a/src/bluetooth.ts +++ b/src/bluetooth.ts @@ -23,63 +23,69 @@ * SOFTWARE. */ -import { adapter, EVENT_ENABLED } from './adapters'; -import { BluetoothDeviceImpl, BluetoothDeviceEvents } from './device'; +import { adapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; -import { EventDispatcher, DOMEvent } from './events'; +import { DOMEvent } from './events'; +import { + BluetoothDeviceEventMap, + BluetoothDevice, +} from './device'; /** - * Bluetooth Options interface + * Bluetooth options. */ export interface BluetoothOptions { /** - * A `device found` callback function to allow the user to select a device + * A `device found` callback function to allow the user to select a device. */ deviceFound?: (device: BluetoothDevice, selectFn: () => void) => boolean; /** - * The amount of seconds to scan for the device (default is 10) + * The amount of seconds to scan for the device (default is 10). */ scanTime?: number; /** - * Optional flag to automatically allow all devices + * Optional flag to automatically allow all devices. */ allowAllDevices?: boolean; /** - * An optional referring device + * An optional referring device. */ referringDevice?: BluetoothDevice; } -/** - * @hidden - */ -export interface BluetoothEvents extends BluetoothDeviceEvents { - /** - * Bluetooth Availability Changed event - */ +/** @hidden Events for {@link BluetoothDevice} */ +export interface BluetoothEventMap extends BluetoothDeviceEventMap { + /** Bluetooth Availability Changed event. */ availabilitychanged: Event; } /** - * Bluetooth class + * Bluetooth class. */ -export class BluetoothImpl extends EventDispatcher implements Bluetooth { - /** - * Referring device for the bluetooth instance - */ - public readonly referringDevice?: BluetoothDevice; - +export class Bluetooth extends EventTarget { private deviceFound: (device: BluetoothDevice, selectFn: () => void) => boolean = undefined; private scanTime: number = 10.24 * 1000; private scanner = undefined; private allowedDevices = new Set(); + private _oncharacteristicvaluechanged: EventListenerOrEventListenerObject; + private _onserviceadded?: EventListenerOrEventListenerObject; + private _onservicechanged?: EventListenerOrEventListenerObject; + private _onserviceremoved?: EventListenerOrEventListenerObject; + private _ongattserverdisconnected?: EventListenerOrEventListenerObject; + private _onadvertisementreceived?: EventListenerOrEventListenerObject; + private _onavailabilitychanged?: EventListenerOrEventListenerObject; + + /** + * Referring device for the bluetooth instance. + */ + public readonly referringDevice?: BluetoothDevice; /** - * Bluetooth constructor - * @param options Bluetooth initialisation options + * Bluetooth constructor. + * @param options Bluetooth initialisation options. */ constructor(private options: BluetoothOptions = {}) { super(); @@ -89,13 +95,13 @@ export class BluetoothImpl extends EventDispatcher implements B this.scanTime = options.scanTime * 1000; } - adapter.on(EVENT_ENABLED, _value => { + adapter.addEventListener("enabledchanged", (_) => { + // TODO: WebBluetooth says e.value should be a boolean. this.dispatchEvent(new DOMEvent(this, 'availabilitychanged')); }); } - private _oncharacteristicvaluechanged: (ev: Event) => void; - public set oncharacteristicvaluechanged(fn: (ev: Event) => void) { + public set oncharacteristicvaluechanged(fn: EventListenerOrEventListenerObject) { if (this._oncharacteristicvaluechanged) { this.removeEventListener('characteristicvaluechanged', this._oncharacteristicvaluechanged); this._oncharacteristicvaluechanged = undefined; @@ -106,8 +112,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _onserviceadded: (ev: Event) => void; - public set onserviceadded(fn: (ev: Event) => void) { + public set onserviceadded(fn: EventListenerOrEventListenerObject) { if (this._onserviceadded) { this.removeEventListener('serviceadded', this._onserviceadded); this._onserviceadded = undefined; @@ -118,8 +123,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _onservicechanged: (ev: Event) => void; - public set onservicechanged(fn: (ev: Event) => void) { + public set onservicechanged(fn: EventListenerOrEventListenerObject) { if (this._onservicechanged) { this.removeEventListener('servicechanged', this._onservicechanged); this._onservicechanged = undefined; @@ -130,8 +134,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _onserviceremoved: (ev: Event) => void; - public set onserviceremoved(fn: (ev: Event) => void) { + public set onserviceremoved(fn: EventListenerOrEventListenerObject) { if (this._onserviceremoved) { this.removeEventListener('serviceremoved', this._onserviceremoved); this._onserviceremoved = undefined; @@ -142,8 +145,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _ongattserverdisconnected: (ev: Event) => void; - public set ongattserverdisconnected(fn: (ev: Event) => void) { + public set ongattserverdisconnected(fn: EventListenerOrEventListenerObject) { if (this._ongattserverdisconnected) { this.removeEventListener('gattserverdisconnected', this._ongattserverdisconnected); this._ongattserverdisconnected = undefined; @@ -154,8 +156,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _onadvertisementreceived: (ev: Event) => void; - public set onadvertisementreceived(fn: (ev: Event) => void) { + public set onadvertisementreceived(fn: EventListenerOrEventListenerObject) { if (this._onadvertisementreceived) { this.removeEventListener('advertisementreceived', this._onadvertisementreceived); this._onadvertisementreceived = undefined; @@ -166,8 +167,7 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private _onavailabilitychanged: (ev: Event) => void; - public set onavailabilitychanged(fn: (ev: Event) => void) { + public set onavailabilitychanged(fn: EventListenerOrEventListenerObject) { if (this._onavailabilitychanged) { this.removeEventListener('availabilitychanged', this._onavailabilitychanged); this._onavailabilitychanged = undefined; @@ -178,7 +178,11 @@ export class BluetoothImpl extends EventDispatcher implements B } } - private filterDevice(filters: Array, deviceInfo: Partial, validServices): Partial | undefined { + private filterDevice( + filters: Array, + deviceInfo: Partial, + validServices: BluetoothServiceUUID[] + ): Partial | undefined { let valid = false; filters.forEach(filter => { @@ -341,7 +345,7 @@ export class BluetoothImpl extends EventDispatcher implements B _allowedServices: allowedServices }); - const bluetoothDevice = new BluetoothDeviceImpl(deviceInfo, () => this.forgetDevice(deviceInfo.id)); + const bluetoothDevice = new BluetoothDevice(deviceInfo, () => this.forgetDevice(deviceInfo.id)); const selectFn = () => { complete.call(this, bluetoothDevice); @@ -379,7 +383,7 @@ export class BluetoothImpl extends EventDispatcher implements B _allowedServices: [] }); - const bluetoothDevice = new BluetoothDeviceImpl(deviceInfo, () => this.forgetDevice(deviceInfo.id)); + const bluetoothDevice = new BluetoothDevice(deviceInfo, () => this.forgetDevice(deviceInfo.id)); devices.push(bluetoothDevice); } }); diff --git a/src/characteristic.ts b/src/characteristic.ts index 8f49ebbd..96f76e45 100644 --- a/src/characteristic.ts +++ b/src/characteristic.ts @@ -23,56 +23,78 @@ * SOFTWARE. */ -import { adapter } from './adapters'; -import { BluetoothRemoteGATTDescriptorImpl } from './descriptor'; +import { adapter } from './adapter/adapter'; +import { BluetoothRemoteGATTDescriptor } from './descriptor'; import { BluetoothUUID } from './uuid'; -import { BluetoothRemoteGATTServiceImpl } from './service'; -import { EventDispatcher, DOMEvent } from './events'; +import { DOMEvent } from './events'; +import { CustomEventListener, isView } from "./common"; +import type { BluetoothRemoteGATTService } from './service'; -const isView = (source: ArrayBuffer | ArrayBufferView): source is ArrayBufferView => (source as ArrayBufferView).buffer !== undefined; - -/** - * @hidden - */ -export interface CharacteristicEvents { - /** - * Characteristic value changed event - */ +/** @hidden Events for {@link BluetoothRemoteGATTCharacteristic} */ +export interface BluetoothRemoteGATTCharacteristicEventMap { characteristicvaluechanged: Event; } +/** @hidden Type-safe events for {@link BluetoothRemoteGATTCharacteristic}. */ +export interface BluetoothRemoteGATTCharacteristic extends EventTarget { + /** @hidden */ + addEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + /** @hidden */ + addEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + /** @hidden */ + removeEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; + /** @hidden */ + removeEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; +} + /** * Bluetooth Remote GATT Characteristic class */ -export class BluetoothRemoteGATTCharacteristicImpl extends EventDispatcher implements BluetoothRemoteGATTCharacteristic { +export class BluetoothRemoteGATTCharacteristic extends EventTarget { + private _value: DataView = undefined; + private handle: string = undefined; + private descriptors: Array = undefined; + private _oncharacteristicvaluechanged: (ev: Event) => void; /** - * The service the characteristic is related to + * The service the characteristic is related to. */ - public readonly service: BluetoothRemoteGATTServiceImpl = undefined; + public readonly service: BluetoothRemoteGATTService = undefined; /** - * The unique identifier of the characteristic + * The unique identifier of the characteristic. */ public readonly uuid: string | undefined = undefined; /** - * The properties of the characteristic + * The properties of the characteristic. */ public readonly properties: BluetoothCharacteristicProperties; - private _value: DataView = undefined; /** - * The value of the characteristic + * The value of the characteristic. */ public get value(): DataView { return this._value; } - private handle: string = undefined; - private descriptors: Array = undefined; - - private _oncharacteristicvaluechanged: (ev: Event) => void; + /** A listener for the `characteristicvaluechanged` event. */ public set oncharacteristicvaluechanged(fn: (ev: Event) => void) { if (this._oncharacteristicvaluechanged) { this.removeEventListener('characteristicvaluechanged', this._oncharacteristicvaluechanged); @@ -88,7 +110,7 @@ export class BluetoothRemoteGATTCharacteristicImpl extends EventDispatcher) { + constructor(init: Partial) { super(); this.service = init.service; @@ -147,7 +169,7 @@ export class BluetoothRemoteGATTCharacteristicImpl extends EventDispatcher = + | ((this: T, event: E) => void | Promise) + | { handleEvent(event: E): void | Promise }; + +/** @hidden Determine if something is an ArrayBuffer or a ArrayBufferView. */ +export const isView = (source: ArrayBuffer | ArrayBufferView): source is ArrayBufferView => (source as ArrayBufferView).buffer !== undefined; diff --git a/src/descriptor.ts b/src/descriptor.ts index 19099855..46e14593 100644 --- a/src/descriptor.ts +++ b/src/descriptor.ts @@ -23,12 +23,14 @@ * SOFTWARE. */ -import { adapter } from './adapters'; +import { adapter } from './adapter/adapter'; /** * Bluetooth Remote GATT Descriptor class */ -export class BluetoothRemoteGATTDescriptorImpl implements BluetoothRemoteGATTDescriptor { +export class BluetoothRemoteGATTDescriptor extends EventTarget { + private _value: DataView = undefined; + private handle: string = undefined; /** * The characteristic the descriptor is related to @@ -40,7 +42,6 @@ export class BluetoothRemoteGATTDescriptorImpl implements BluetoothRemoteGATTDes */ public readonly uuid: string = undefined; - private _value: DataView = undefined; /** * The value of the descriptor */ @@ -48,13 +49,12 @@ export class BluetoothRemoteGATTDescriptorImpl implements BluetoothRemoteGATTDes return this._value; } - private handle: string = undefined; - /** * Descriptor constructor * @param init A partial class to initialise values */ constructor(init: Partial) { + super(); this.characteristic = init.characteristic; this.uuid = init.uuid; this._value = init.value; diff --git a/src/device.ts b/src/device.ts index 11fb93e7..45fb2ca9 100644 --- a/src/device.ts +++ b/src/device.ts @@ -23,52 +23,83 @@ * SOFTWARE. */ -import { BluetoothRemoteGATTServerImpl } from './server'; -import { ServiceEvents } from './service'; -import { EventDispatcher } from './events'; +import { BluetoothRemoteGATTServer } from './server'; +import type { BluetoothRemoteGATTServiceEventMap } from './service'; + +/** @hidden Interface for creating an Advertisement event. */ +export interface BluetoothAdvertisingEventInit extends EventInit { + device: BluetoothDevice; + uuids: BluetoothServiceUUID[]; + name?: string; + appearance?: number; + txPower?: number; + rssi?: number; + manufacturerData: BluetoothManufacturerData; + serviceData: BluetoothServiceData; +} -/** - * @hidden - */ -export interface BluetoothDeviceEvents extends ServiceEvents { - /** - * GATT server disconnected event - */ +/** Bluetooth Advertisement event. */ +export class BluetoothAdvertisingEvent extends Event { + readonly device: BluetoothDevice; + readonly uuids: BluetoothServiceUUID[]; + readonly name?: string | undefined; + readonly appearance?: number | undefined; + readonly txPower?: number | undefined; + readonly rssi?: number | undefined; + readonly manufacturerData: BluetoothManufacturerData; + readonly serviceData: BluetoothServiceData; + + constructor(dict: BluetoothAdvertisingEventInit) { + super("advertisementreceived", dict); + this.device = dict.device; + this.uuids = dict.uuids; + this.name = dict.name; + this.appearance = dict.appearance; + this.txPower = dict.txPower; + this.rssi = dict.rssi; + this.manufacturerData = dict.manufacturerData; + this.serviceData = dict.serviceData; + } +} + +/** @hidden Events for {@link BluetoothDevice} */ +export interface BluetoothDeviceEventMap extends BluetoothRemoteGATTServiceEventMap { + advertisementreceived: BluetoothAdvertisingEvent; gattserverdisconnected: Event; - /** - * Advertisement received event - */ - advertisementreceived: Event; } /** - * Bluetooth Device class + * Bluetooth Device class. */ -export class BluetoothDeviceImpl extends EventDispatcher implements BluetoothDevice { +export class BluetoothDevice extends EventTarget { + private _oncharacteristicvaluechanged: (ev: Event) => void; + private _onserviceadded: (ev: Event) => void; + private _onservicechanged: (ev: Event) => void; + private _onserviceremoved: (ev: Event) => void; + private _ongattserverdisconnected: (ev: Event) => void; + private _onadvertisementreceived: (ev: Event) => void; /** - * The unique identifier of the device + * The unique identifier of the device. */ public readonly id: string = undefined; /** - * The name of the device + * The name of the device. */ public readonly name: string = undefined; /** - * The gatt server of the device + * The gatt server of the device. */ public readonly gatt: BluetoothRemoteGATTServer = undefined; /** - * Whether adverts are being watched (not implemented) + * Whether advertisements are being watched (not implemented) */ public readonly watchingAdvertisements: boolean = false; - /** - * @hidden - */ + /** @hidden Advertisement data. */ public readonly _adData: { rssi?: number; txPower?: number; @@ -76,22 +107,16 @@ export class BluetoothDeviceImpl extends EventDispatcher manufacturerData?: BluetoothManufacturerData; }; - /** - * @hidden - */ + /** @hidden Root Bluetooth instance. */ public readonly _bluetooth: Bluetooth = undefined; - /** - * @hidden - */ + /** @hidden Allowed services */ public readonly _allowedServices: Array = []; - /** - * @hidden - */ + /** @hidden List of Service UUIDs. */ public readonly _serviceUUIDs: Array = []; - private _oncharacteristicvaluechanged: (ev: Event) => void; + /** A listener for the `characteristicvaluechanged` event. */ public set oncharacteristicvaluechanged(fn: (ev: Event) => void) { if (this._oncharacteristicvaluechanged) { this.removeEventListener('characteristicvaluechanged', this._oncharacteristicvaluechanged); @@ -103,7 +128,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } } - private _onserviceadded: (ev: Event) => void; + /** A listener for the `serviceadded` event. */ public set onserviceadded(fn: (ev: Event) => void) { if (this._onserviceadded) { this.removeEventListener('serviceadded', this._onserviceadded); @@ -115,7 +140,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } } - private _onservicechanged: (ev: Event) => void; + /** A listener for the `servicechanged` event. */ public set onservicechanged(fn: (ev: Event) => void) { if (this._onservicechanged) { this.removeEventListener('servicechanged', this._onservicechanged); @@ -127,7 +152,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } } - private _onserviceremoved: (ev: Event) => void; + /** A listener for the `serviceremoved` event. */ public set onserviceremoved(fn: (ev: Event) => void) { if (this._onserviceremoved) { this.removeEventListener('serviceremoved', this._onserviceremoved); @@ -139,7 +164,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } } - private _ongattserverdisconnected: (ev: Event) => void; + /** A listener for the `gattserverdisconnected` event. */ public set ongattserverdisconnected(fn: (ev: Event) => void) { if (this._ongattserverdisconnected) { this.removeEventListener('gattserverdisconnected', this._ongattserverdisconnected); @@ -151,7 +176,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } } - private _onadvertisementreceived: (ev: Event) => void; + /** A listener for the `advertisementreceived` event. */ public set onadvertisementreceived(fn: (ev: Event) => void) { if (this._onadvertisementreceived) { this.removeEventListener('advertisementreceived', this._onadvertisementreceived); @@ -164,10 +189,10 @@ export class BluetoothDeviceImpl extends EventDispatcher } /** - * Device constructor - * @param init A partial class to initialise values + * Device constructor. + * @param init A partial class to initialise values. */ - constructor(init: Partial, private forgetFn: () => void) { + constructor(init: Partial, private forgetFn: () => void) { super(); this.id = init.id; @@ -180,7 +205,7 @@ export class BluetoothDeviceImpl extends EventDispatcher this._serviceUUIDs = init._serviceUUIDs; if (!this.name) this.name = `Unknown or Unsupported Device (${this.id})`; - if (!this.gatt) this.gatt = new BluetoothRemoteGATTServerImpl(this); + if (!this.gatt) this.gatt = new BluetoothRemoteGATTServer(this); } /** @@ -198,7 +223,7 @@ export class BluetoothDeviceImpl extends EventDispatcher } /** - * Forget this device + * Forget this device. */ public async forget(): Promise { this.forgetFn(); diff --git a/src/events.ts b/src/events.ts index 8f9bda84..0d03d307 100644 --- a/src/events.ts +++ b/src/events.ts @@ -23,47 +23,10 @@ * SOFTWARE. */ -import { EventEmitter } from 'events'; - -/** - * @hidden - */ -export class EventDispatcher { - protected emitter = new EventEmitter(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private isEventListenerObject = (listener: any): listener is EventListenerObject => (listener as EventListenerObject).handleEvent !== undefined; - - public addEventListener(type: K, listener: (this: this, ev: T[K]) => void): void; - public addEventListener(type: K, listener: EventListener): void; - public addEventListener(type: string, listener: EventListener | ((ev: T[K]) => void)): void { - if (listener) { - const handler = this.isEventListenerObject(listener) ? listener.handleEvent : listener; - this.emitter.addListener(type, handler); - } - } - - public removeEventListener(type: K, callback: (this: this, ev: T[K]) => void): void; - public removeEventListener(type: K, callback: EventListener): void; - public removeEventListener(type: K, callback: EventListener | ((ev: T[K]) => void)): void { - if (callback) { - const handler = this.isEventListenerObject(callback) ? callback.handleEvent : callback; - this.emitter.removeListener(type as string, handler); - } - } - - public dispatchEvent(event: Event): boolean { - return this.emitter.emit(event.type, event); - } -} - -/** - * @hidden - */ +/** @hidden Custom event. */ export class DOMEvent implements Event { - /** - * Type of the event + * Type of the event. */ public type: string; @@ -130,22 +93,22 @@ export class DOMEvent implements Event { /** * @hidden */ - public AT_TARGET: number; + public AT_TARGET: 2 = 2; /** * @hidden */ - public BUBBLING_PHASE: number; + public BUBBLING_PHASE: 3 = 3; /** * @hidden */ - public CAPTURING_PHASE: number; + public CAPTURING_PHASE: 1 = 1; /** * @hidden */ - public NONE: number; + public NONE: 0 = 0; constructor(target: EventTarget, type: string) { this.target = target; diff --git a/src/index.ts b/src/index.ts index cbdcf322..5edff60f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -23,19 +23,19 @@ * SOFTWARE. */ -import { BluetoothImpl, BluetoothOptions } from './bluetooth'; +import { Bluetooth, BluetoothOptions } from './bluetooth'; /** - * Default bluetooth instance synonymous with `navigator.bluetooth` + * Default bluetooth instance synonymous with `navigator.bluetooth`. */ -export const bluetooth = new BluetoothImpl(); +export const bluetooth = new Bluetooth(); /** - * Bluetooth class for creating new instances + * Bluetooth class for creating new instances. */ -export { BluetoothImpl as Bluetooth, BluetoothOptions }; +export { Bluetooth, BluetoothOptions }; /** - * Helper methods and enums + * Helper methods and enums. */ export * from './uuid'; diff --git a/src/server.ts b/src/server.ts index 93df7926..a7dddcb1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -23,45 +23,45 @@ * SOFTWARE. */ +import { adapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; -import { adapter } from './adapters'; -import { BluetoothRemoteGATTServiceImpl } from './service'; +import { BluetoothRemoteGATTService } from './service'; import { DOMEvent } from './events'; -import { BluetoothDeviceImpl } from './device'; +import type { BluetoothDevice } from './device'; /** - * Bluetooth Remote GATT Server class + * Bluetooth Remote GATT Server class. */ -export class BluetoothRemoteGATTServerImpl implements BluetoothRemoteGATTServer { +export class BluetoothRemoteGATTServer extends EventTarget { + private _connected = false; + private handle: string = undefined; + private services: BluetoothRemoteGATTService[] = undefined; /** - * The device the gatt server is related to + * The device the gatt server is related to. */ - public readonly device: BluetoothDeviceImpl = undefined; + public readonly device: BluetoothDevice = undefined; - private _connected = false; /** - * Whether the gatt server is connected + * Whether the gatt server is connected. */ public get connected(): boolean { return this._connected; } - private handle: string = undefined; - private services: Array = undefined; - /** - * Server constructor - * @param device Device the gatt server relates to + * Server constructor. + * @param device Device the gatt server relates to. */ - constructor(device: BluetoothDeviceImpl) { + constructor(device: BluetoothDevice) { + super(); this.device = device; this.handle = this.device.id; } /** - * Connect the gatt server - * @returns Promise containing the gatt server + * Connect the gatt server. + * @returns Promise containing the gatt server. */ public async connect(): Promise { if (this.connected) { @@ -80,7 +80,7 @@ export class BluetoothRemoteGATTServerImpl implements BluetoothRemoteGATTServer } /** - * Disconnect the gatt server + * Disconnect the gatt server. */ public disconnect(): void { adapter.disconnect(this.handle); @@ -88,9 +88,9 @@ export class BluetoothRemoteGATTServerImpl implements BluetoothRemoteGATTServer } /** - * Gets a single primary service contained in the gatt server - * @param service service UUID - * @returns Promise containing the service + * Gets a single primary service contained in the gatt server. + * @param service Service UUID. + * @returns Promise containing the service. */ public async getPrimaryService(service: BluetoothServiceUUID): Promise { if (!this.connected) { @@ -110,9 +110,9 @@ export class BluetoothRemoteGATTServerImpl implements BluetoothRemoteGATTServer } /** - * Gets a list of primary services contained in the gatt server - * @param service service UUID - * @returns Promise containing an array of services + * Gets a list of primary services contained in the gatt server. + * @param service Service UUID. + * @returns Promise containing an array of services. */ public async getPrimaryServices(service?: BluetoothServiceUUID): Promise> { if (!this.connected) { @@ -125,7 +125,7 @@ export class BluetoothRemoteGATTServerImpl implements BluetoothRemoteGATTServer Object.assign(serviceInfo, { device: this.device }); - return new BluetoothRemoteGATTServiceImpl(serviceInfo); + return new BluetoothRemoteGATTService(serviceInfo); }); } diff --git a/src/service.ts b/src/service.ts index b758454a..2ccaba7e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -23,55 +23,81 @@ * SOFTWARE. */ -import { adapter } from './adapters'; -import { BluetoothDeviceImpl } from './device'; -import { BluetoothRemoteGATTCharacteristicImpl, CharacteristicEvents } from './characteristic'; +import { adapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; -import { EventDispatcher, DOMEvent } from './events'; - -/** - * @hidden - */ -export interface ServiceEvents extends CharacteristicEvents { - /** - * Service added event - */ +import { DOMEvent } from './events'; +import { + BluetoothRemoteGATTCharacteristicEventMap, + BluetoothRemoteGATTCharacteristic, +} from './characteristic'; +import type { BluetoothDevice } from './device'; +import type { CustomEventListener } from "./common"; + +/** @hidden Events for {@link BluetoothRemoteGATTService} */ +export interface BluetoothRemoteGATTServiceEventMap extends BluetoothRemoteGATTCharacteristicEventMap { + /** Service added event. */ serviceadded: Event; - /** - * Service changed event - */ + /** Service changed event. */ servicechanged: Event; - /** - * Service removed event - */ + /** Service removed event. */ serviceremoved: Event; } +/** @hidden Type-safe events for {@link BluetoothRemoteGATTService}. */ +export interface BluetoothRemoteGATTService extends EventTarget { + /** @hidden */ + addEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + /** @hidden */ + addEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | AddEventListenerOptions, + ): void; + /** @hidden */ + removeEventListener( + type: K, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; + /** @hidden */ + removeEventListener( + type: string, + listener: CustomEventListener, + options?: boolean | EventListenerOptions, + ): void; +} + /** * Bluetooth Remote GATT Service class */ -export class BluetoothRemoteGATTServiceImpl extends EventDispatcher implements BluetoothRemoteGATTService { +export class BluetoothRemoteGATTService extends EventTarget { + private handle: string = undefined; + private services: Array = undefined; + private characteristics: Array = undefined; + private _oncharacteristicvaluechanged: (ev: Event) => void; + private _onserviceadded: (ev: Event) => void; + private _onservicechanged: (ev: Event) => void; + private _onserviceremoved: (ev: Event) => void; /** - * The device the service is related to + * The device the service is related to. */ - public readonly device: BluetoothDeviceImpl = undefined; + public readonly device: BluetoothDevice = undefined; /** - * The unique identifier of the service + * The unique identifier of the service. */ public readonly uuid: string = undefined; /** - * Whether the service is a primary one + * Whether the service is a primary one. */ public readonly isPrimary: boolean = false; - private handle: string = undefined; - private services: Array = undefined; - private characteristics: Array = undefined; - - private _oncharacteristicvaluechanged: (ev: Event) => void; public set oncharacteristicvaluechanged(fn: (ev: Event) => void) { if (this._oncharacteristicvaluechanged) { this.removeEventListener('characteristicvaluechanged', this._oncharacteristicvaluechanged); @@ -83,7 +109,6 @@ export class BluetoothRemoteGATTServiceImpl extends EventDispatcher void; public set onserviceadded(fn: (ev: Event) => void) { if (this._onserviceadded) { this.removeEventListener('serviceadded', this._onserviceadded); @@ -95,7 +120,6 @@ export class BluetoothRemoteGATTServiceImpl extends EventDispatcher void; public set onservicechanged(fn: (ev: Event) => void) { if (this._onservicechanged) { this.removeEventListener('servicechanged', this._onservicechanged); @@ -107,7 +131,6 @@ export class BluetoothRemoteGATTServiceImpl extends EventDispatcher void; public set onserviceremoved(fn: (ev: Event) => void) { if (this._onserviceremoved) { this.removeEventListener('serviceremoved', this._onserviceremoved); @@ -123,7 +146,7 @@ export class BluetoothRemoteGATTServiceImpl extends EventDispatcher) { + constructor(init: Partial) { super(); this.device = init.device; @@ -175,7 +198,7 @@ export class BluetoothRemoteGATTServiceImpl extends EventDispatcher Date: Sun, 4 Jun 2023 11:07:08 -0700 Subject: [PATCH 2/4] fix: replace atoi with (supposedly) safer strtol. --- lib/bindings.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/bindings.cpp b/lib/bindings.cpp index 06f06977..db255ec7 100644 --- a/lib/bindings.cpp +++ b/lib/bindings.cpp @@ -12,7 +12,12 @@ Napi::Value GetAdapters(const Napi::CallbackInfo &info) { // Respect `SIMPLEBLE_ADAPTER` if it exists. const char* adapterIndexEnv = std::getenv("SIMPLEBLE_ADAPTER"); if (adapterIndexEnv) { - const int adapterIndex = std::atoi(adapterIndexEnv); + int adapterIndex = -1; + char* end; + long index = std::strtol(adapterIndexEnv, &end, 10); + if (end != adapterIndexEnv && *end == '\0') { + adapterIndex = static_cast(index); + } if (adapterIndex < 0 || adapterIndex >= count) { Napi::RangeError::New(env, "SIMPLEBLE_ADAPTER is out of range") .ThrowAsJavaScriptException(); From a90bfddc7fd6a628701eeb41a02979bc0f9c187a Mon Sep 17 00:00:00 2001 From: Alex Shaw Date: Thu, 8 Jun 2023 06:59:03 -0700 Subject: [PATCH 3/4] feat: add AsyncIterableIterator --- src/bluetooth.ts | 126 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 10 deletions(-) diff --git a/src/bluetooth.ts b/src/bluetooth.ts index d495d056..49513169 100644 --- a/src/bluetooth.ts +++ b/src/bluetooth.ts @@ -31,6 +31,16 @@ import { BluetoothDevice, } from './device'; +interface Filtered { + filters: Array; + optionalServices?: Array; +} + +interface AcceptAll { + acceptAllDevices: boolean; + optionalServices?: Array; +} + /** * Bluetooth options. */ @@ -253,16 +263,6 @@ export class Bluetooth extends EventTarget { throw new Error('requestDevice error: request in progress'); } - interface Filtered { - filters: Array; - optionalServices?: Array; - } - - interface AcceptAll { - acceptAllDevices: boolean; - optionalServices?: Array; - } - const isFiltered = (maybeFiltered: RequestDeviceOptions): maybeFiltered is Filtered => (maybeFiltered as Filtered).filters !== undefined; @@ -360,6 +360,112 @@ export class Bluetooth extends EventTarget { }); } + /** + * Scans for multiple devices. + * @param options Options to use when scanning. + */ + async *requestLEDevices(options: RequestDeviceOptions = { filters: [] }): AsyncIterableIterator { + if (this.scanner !== undefined) { + throw new Error('requestDevices error: request in progress'); + } + + const isFiltered = (maybeFiltered: RequestDeviceOptions): maybeFiltered is Filtered => + (maybeFiltered as Filtered).filters !== undefined; + + const isAcceptAll = (maybeAcceptAll: RequestDeviceOptions): maybeAcceptAll is AcceptAll => + (maybeAcceptAll as AcceptAll).acceptAllDevices === true; + + let searchUUIDs = []; + + if (isFiltered(options)) { + // Must have a filter + if (options.filters.length === 0) { + throw new TypeError('requestDevices error: no filters specified'); + } + + // Don't allow empty filters + const emptyFilter = options.filters.some(filter => { + return (Object.keys(filter).length === 0); + }); + if (emptyFilter) { + throw new TypeError('requestDevices error: empty filter specified'); + } + + // Don't allow empty namePrefix + const emptyPrefix = options.filters.some(filter => { + return (typeof filter.namePrefix !== 'undefined' && filter.namePrefix === ''); + }); + if (emptyPrefix) { + throw new TypeError('requestDevices error: empty namePrefix specified'); + } + + options.filters.forEach(filter => { + if (filter.services) searchUUIDs = searchUUIDs.concat(filter.services.map(BluetoothUUID.getService)); + + // Unique-ify + searchUUIDs = searchUUIDs.filter((item, index, array) => { + return array.indexOf(item) === index; + }); + }); + } else if (!isAcceptAll(options)) { + throw new TypeError('requestDevices error: specify filters or acceptAllDevices'); + } + + const queue: Array<{resolve: (data: BluetoothDevice) => void}> = []; + + adapter.startScan(searchUUIDs, deviceInfo => { + let validServices = []; + + const complete = (bluetoothDevice: BluetoothDevice) => { + this.allowedDevices.add(bluetoothDevice.id); + //this.cancelRequest(); + if (queue.length) { + const next = queue.shift()!; + next.resolve(bluetoothDevice); + } + }; + + // filter devices if filters specified + if (isFiltered(options)) { + deviceInfo = this.filterDevice(options.filters, deviceInfo, validServices); + } + + if (deviceInfo) { + // Add additional services + if (options.optionalServices) { + validServices = validServices.concat(options.optionalServices.map(BluetoothUUID.getService)); + } + + // Set unique list of allowed services + const allowedServices = validServices.filter((item, index, array) => { + return array.indexOf(item) === index; + }); + Object.assign(deviceInfo, { + _bluetooth: this, + _allowedServices: allowedServices + }); + + const bluetoothDevice = new BluetoothDevice(deviceInfo, () => this.forgetDevice(deviceInfo.id)); + + const selectFn = () => { + complete.call(this, bluetoothDevice); + }; + + if (!this.deviceFound || this.deviceFound(bluetoothDevice, selectFn.bind(this)) === true) { + complete.call(this, bluetoothDevice); + } + } + }); + + while (true) { + const promise = new Promise((resolve) => { + queue.push({resolve}); + }); + const data = await promise; + yield data; + } + } + /** * Get all bluetooth devices */ From f2e54462b83458eafaba3ea4a9f2b8c5fe602983 Mon Sep 17 00:00:00 2001 From: Alex Shaw Date: Sun, 10 Mar 2024 01:45:33 -0800 Subject: [PATCH 4/4] fix: remove eslint and constructor errors --- src/adapter/adapter.ts | 6 +- src/bluetooth.ts | 42 ++++++++--- src/characteristic.ts | 11 ++- src/device.ts | 8 ++- src/events.ts | 156 ----------------------------------------- src/server.ts | 4 +- src/service.ts | 10 +-- tsconfig.json | 3 +- 8 files changed, 56 insertions(+), 184 deletions(-) delete mode 100644 src/events.ts diff --git a/src/adapter/adapter.ts b/src/adapter/adapter.ts index fe244435..67d60725 100644 --- a/src/adapter/adapter.ts +++ b/src/adapter/adapter.ts @@ -22,13 +22,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -import { adapters, isEnabled } from "./adapters"; +import { adapters, isEnabled } from './adapters'; import { BluetoothUUID } from '../uuid'; import type { BluetoothDevice } from '../device'; import type { BluetoothRemoteGATTService } from '../service'; import type { BluetoothRemoteGATTCharacteristic } from '../characteristic'; import type { BluetoothRemoteGATTDescriptor } from '../descriptor'; -import type { CustomEventListener } from "../common"; +import type { CustomEventListener } from '../common'; import type { Adapter, Peripheral, @@ -160,7 +160,7 @@ export class BluetoothAdapter extends EventTarget { this.servicesByPeripheral.set(peripheral, services); } - private get state(): boolean { + public get state(): boolean { const adapterEnabled = isEnabled(); return !!adapterEnabled; } diff --git a/src/bluetooth.ts b/src/bluetooth.ts index 49513169..a5c7d01e 100644 --- a/src/bluetooth.ts +++ b/src/bluetooth.ts @@ -23,9 +23,8 @@ * SOFTWARE. */ -import { adapter } from './adapter/adapter'; +import { adapter, type BluetoothAdapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; -import { DOMEvent } from './events'; import { BluetoothDeviceEventMap, BluetoothDevice, @@ -41,6 +40,28 @@ interface AcceptAll { optionalServices?: Array; } +/** @hidden Interface for creating an availability event. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface ValueEventInit extends EventInit { + value?: T | null; +} + +/** + * Bluetooth availability event. + * + * @privateRemarks + * As the spec notes, this is generic and likely to be moved to HTML or DOM. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class ValueEvent extends Event { + readonly value: T | null; + + constructor(type: string, initDict: ValueEventInit | undefined = {}) { + super(type, initDict); + this.value = initDict.value; + } +} + /** * Bluetooth options. */ @@ -69,7 +90,7 @@ export interface BluetoothOptions { /** @hidden Events for {@link BluetoothDevice} */ export interface BluetoothEventMap extends BluetoothDeviceEventMap { /** Bluetooth Availability Changed event. */ - availabilitychanged: Event; + availabilitychanged: ValueEvent; } /** @@ -105,9 +126,12 @@ export class Bluetooth extends EventTarget { this.scanTime = options.scanTime * 1000; } - adapter.addEventListener("enabledchanged", (_) => { - // TODO: WebBluetooth says e.value should be a boolean. - this.dispatchEvent(new DOMEvent(this, 'availabilitychanged')); + adapter.addEventListener('enabledchanged', (evt) => { + this.dispatchEvent( + new ValueEvent('availabilitychanged', { + value: (evt.target as BluetoothAdapter).state + }) + ); }); } @@ -420,7 +444,7 @@ export class Bluetooth extends EventTarget { this.allowedDevices.add(bluetoothDevice.id); //this.cancelRequest(); if (queue.length) { - const next = queue.shift()!; + const next = queue.shift(); next.resolve(bluetoothDevice); } }; @@ -458,8 +482,8 @@ export class Bluetooth extends EventTarget { }); while (true) { - const promise = new Promise((resolve) => { - queue.push({resolve}); + const promise = new Promise((resolve) => { + queue.push({ resolve }); }); const data = await promise; yield data; diff --git a/src/characteristic.ts b/src/characteristic.ts index 96f76e45..534f9869 100644 --- a/src/characteristic.ts +++ b/src/characteristic.ts @@ -26,8 +26,7 @@ import { adapter } from './adapter/adapter'; import { BluetoothRemoteGATTDescriptor } from './descriptor'; import { BluetoothUUID } from './uuid'; -import { DOMEvent } from './events'; -import { CustomEventListener, isView } from "./common"; +import { CustomEventListener, isView } from './common'; import type { BluetoothRemoteGATTService } from './service'; /** @hidden Events for {@link BluetoothRemoteGATTCharacteristic} */ @@ -124,10 +123,10 @@ export class BluetoothRemoteGATTCharacteristic extends EventTarget { private setValue(value?: DataView, emit?: boolean) { this._value = value; if (emit) { - this.dispatchEvent(new DOMEvent(this, 'characteristicvaluechanged')); - this.service.dispatchEvent(new DOMEvent(this, 'characteristicvaluechanged')); - this.service.device.dispatchEvent(new DOMEvent(this, 'characteristicvaluechanged')); - this.service.device._bluetooth.dispatchEvent(new DOMEvent(this, 'characteristicvaluechanged')); + this.dispatchEvent(new Event('characteristicvaluechanged', { bubbles: true })); + this.service.dispatchEvent(new Event('characteristicvaluechanged', { bubbles: true })); + this.service.device.dispatchEvent(new Event('characteristicvaluechanged', { bubbles: true })); + this.service.device._bluetooth.dispatchEvent(new Event('characteristicvaluechanged', { bubbles: true })); } } diff --git a/src/device.ts b/src/device.ts index 45fb2ca9..146573c1 100644 --- a/src/device.ts +++ b/src/device.ts @@ -50,7 +50,7 @@ export class BluetoothAdvertisingEvent extends Event { readonly serviceData: BluetoothServiceData; constructor(dict: BluetoothAdvertisingEventInit) { - super("advertisementreceived", dict); + super('advertisementreceived', dict); this.device = dict.device; this.uuids = dict.uuids; this.name = dict.name; @@ -228,4 +228,10 @@ export class BluetoothDevice extends EventTarget { public async forget(): Promise { this.forgetFn(); } + + /** @hidden Handle a disconnect. */ + public _handleDisconnect() { + this.dispatchEvent(new Event('gattserverdisconnected', { bubbles: true })); + this._bluetooth.dispatchEvent(new Event('gattserverdisconnected', { bubbles: true })); + } } diff --git a/src/events.ts b/src/events.ts deleted file mode 100644 index 0d03d307..00000000 --- a/src/events.ts +++ /dev/null @@ -1,156 +0,0 @@ -/* -* Node Web Bluetooth -* Copyright (c) 2019 Rob Moran -* -* The MIT License (MIT) -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*/ - -/** @hidden Custom event. */ -export class DOMEvent implements Event { - /** - * Type of the event. - */ - public type: string; - - /** - * @hidden - */ - public target: EventTarget; - - /** - * @hidden - */ - public currentTarget: EventTarget; - - /** - * @hidden - */ - public srcElement: EventTarget; - - /** - * @hidden - */ - public timeStamp: number; - - /** - * @hidden - */ - public bubbles = true; - - /** - * @hidden - */ - public cancelable = false; - - /** - * @hidden - */ - public cancelBubble = false; - - /** - * @hidden - */ - public composed = false; - - /** - * @hidden - */ - public defaultPrevented = false; - - /** - * @hidden - */ - public eventPhase = 0; - - /** - * @hidden - */ - public isTrusted = true; - - /** - * @hidden - */ - public returnValue = true; - - /** - * @hidden - */ - public AT_TARGET: 2 = 2; - - /** - * @hidden - */ - public BUBBLING_PHASE: 3 = 3; - - /** - * @hidden - */ - public CAPTURING_PHASE: 1 = 1; - - /** - * @hidden - */ - public NONE: 0 = 0; - - constructor(target: EventTarget, type: string) { - this.target = target; - this.srcElement = target; - this.currentTarget = target; - this.type = type; - } - - /** - * @hidden - */ - public composedPath(): Array { - return []; - } - - /** - * @hidden - */ - public initEvent(type: string, bubbles?: boolean, cancelable?: boolean): void { - this.type = type; - this.bubbles = bubbles; - this.cancelable = cancelable; - } - - /** - * @hidden - */ - public preventDefault(): void { - this.defaultPrevented = true; - } - - /** - * @hidden - */ - public stopImmediatePropagation(): void { - return; - } - - /** - * @hidden - */ - public stopPropagation(): void { - return; - } -} diff --git a/src/server.ts b/src/server.ts index a7dddcb1..bef70c49 100644 --- a/src/server.ts +++ b/src/server.ts @@ -26,7 +26,6 @@ import { adapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; import { BluetoothRemoteGATTService } from './service'; -import { DOMEvent } from './events'; import type { BluetoothDevice } from './device'; /** @@ -71,8 +70,7 @@ export class BluetoothRemoteGATTServer extends EventTarget { await adapter.connect(this.handle, () => { this.services = undefined; this._connected = false; - this.device.dispatchEvent(new DOMEvent(this.device, 'gattserverdisconnected')); - this.device._bluetooth.dispatchEvent(new DOMEvent(this.device, 'gattserverdisconnected')); + this.device._handleDisconnect(); }); this._connected = true; diff --git a/src/service.ts b/src/service.ts index 2ccaba7e..41365686 100644 --- a/src/service.ts +++ b/src/service.ts @@ -25,13 +25,12 @@ import { adapter } from './adapter/adapter'; import { BluetoothUUID } from './uuid'; -import { DOMEvent } from './events'; import { BluetoothRemoteGATTCharacteristicEventMap, BluetoothRemoteGATTCharacteristic, } from './characteristic'; import type { BluetoothDevice } from './device'; -import type { CustomEventListener } from "./common"; +import type { CustomEventListener } from './common'; /** @hidden Events for {@link BluetoothRemoteGATTService} */ export interface BluetoothRemoteGATTServiceEventMap extends BluetoothRemoteGATTCharacteristicEventMap { @@ -155,9 +154,10 @@ export class BluetoothRemoteGATTService extends EventTarget { this.handle = this.uuid; - this.dispatchEvent(new DOMEvent(this, 'serviceadded')); - this.device.dispatchEvent(new DOMEvent(this, 'serviceadded')); - this.device._bluetooth.dispatchEvent(new DOMEvent(this, 'serviceadded')); + // TODO: When is serviceremoved fired? + this.dispatchEvent(new Event('serviceadded', { bubbles: true })); + this.device.dispatchEvent(new Event('serviceadded', { bubbles: true })); + this.device._bluetooth.dispatchEvent(new Event('serviceadded', { bubbles: true })); } /** diff --git a/tsconfig.json b/tsconfig.json index 44498013..647323b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "target": "es2016", + "module": "commonjs", "alwaysStrict": true, "downlevelIteration": true, "noImplicitReturns": true,