UNPKG

@manekinekko/angular-web-bluetooth

Version:
675 lines (664 loc) 27.1 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, InjectionToken, NgModule } from '@angular/core'; import { from, throwError, EMPTY, fromEvent } from 'rxjs'; import { filter, mergeMap, map, takeUntil } from 'rxjs/operators'; import { CommonModule } from '@angular/common'; class BrowserWebBluetooth { constructor() { this.ble = navigator.bluetooth; if (!this.ble) { throw new Error('Your browser does not support Smart Bluetooth. See http://caniuse.com/#search=Bluetooth for more details.'); } } requestDevice(options) { return this.ble.requestDevice(options); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BrowserWebBluetooth, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BrowserWebBluetooth }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BrowserWebBluetooth, decorators: [{ type: Injectable }], ctorParameters: () => [] }); class ServerWebBluetooth { static instance() { // mocked object for now return {}; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ServerWebBluetooth, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ServerWebBluetooth }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ServerWebBluetooth, decorators: [{ type: Injectable }] }); class ConsoleLoggerService { log(...args) { console.log.apply(console, args); } error(...args) { console.error.apply(console, args); } warn(...args) { console.warn.apply(console, args); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ConsoleLoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ConsoleLoggerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: ConsoleLoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class NoLoggerService { log(...args) { } error(...args) { } warn(...args) { } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: NoLoggerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: NoLoggerService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: NoLoggerService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); class BluetoothCore { constructor(webBle, console) { this.webBle = webBle; this.console = console; this.device$ = new EventEmitter(); this.gatt$ = new EventEmitter(); this.characteristicValueChanges$ = new EventEmitter(); this.gattServer = null; } getDevice$() { return this.device$; } getGATT$() { return this.gatt$; } streamValues$() { return this.characteristicValueChanges$.pipe(filter(value => value && value.byteLength > 0)); } /** * Run the discovery process and read the value form the provided service and characteristic * @param options the ReadValueOptions */ async value(options) { this.console.log('[BLE::Info] Reading value with options %o', options); if (typeof options.acceptAllDevices === 'undefined') { options.acceptAllDevices = true; } if (typeof options.optionalServices === 'undefined') { options.optionalServices = [options.service]; } else { options.optionalServices = [...options.optionalServices]; } this.console.log('[BLE::Info] Reading value with options %o', options); try { const device = await this.discover({ acceptAllDevices: options.acceptAllDevices, optionalServices: options.optionalServices }); this.console.log('[BLE::Info] Device info %o', device); const gatt = await this.connectDevice(device); this.console.log('[BLE::Info] GATT info %o', gatt); const primaryService = await this.getPrimaryService(gatt, options.service); this.console.log('[BLE::Info] Primary Service info %o', primaryService); const characteristic = await this.getCharacteristic(primaryService, options.characteristic); this.console.log('[BLE::Info] Characteristic info %o', characteristic); const value = await characteristic.readValue(); this.console.log('[BLE::Info] Value info %o', value); return value; } catch (error) { throw new Error(error); } } value$(options) { return from(this.value(options)); } /** * Run the discovery process. * * @param Options such as filters and optional services * @return The GATT server for the chosen device */ async discover(options = {}) { options.optionalServices = options.optionalServices || ['generic_access']; this.console.log('[BLE::Info] Requesting devices with options %o', options); let device = null; try { device = await this.webBle.requestDevice(options); device.addEventListener('gattserverdisconnected', this.onDeviceDisconnected.bind(this)); if (device) { this.device$.emit(device); } else { this.device$.error(`[BLE::Error] Can not get the Bluetooth Remote GATT Server. Abort.`); } } catch (error) { this.console.error(error); } return device; } /** * This handler will trigger when the client disconnets from the server. * * @param event The onDeviceDisconnected event */ onDeviceDisconnected(event) { const disconnectedDevice = event.target; this.console.log('[BLE::Info] disconnected device %o', disconnectedDevice); this.device$.emit(undefined); } /** * Run the discovery process. * * @param Options such as filters and optional services * @return Emites the value of the requested service read from the device */ discover$(options) { return from(this.discover(options)).pipe(mergeMap((device) => this.connectDevice$(device))); } /** * Connect to current device. * * @return Emites the gatt server instance of the requested device */ async connectDevice(device) { if (device === null || typeof device.gatt === "undefined") { this.console.error('[BLE::Error] Was not able to connect to Bluetooth Remote GATT Server'); this.gatt$.error(null); return null; } this.console.log('[BLE::Info] Connecting to Bluetooth Remote GATT Server of %o', device); try { const gattServer = await device.gatt.connect(); this.gattServer = gattServer; this.gatt$.emit(gattServer); return gattServer; } catch (error) { // probably the user has canceled the discovery Promise.reject(`${error.message}`); this.gatt$.error(`${error.message}`); } return null; } /** * Connect to current device. * * @return Emites the gatt server instance of the requested device */ connectDevice$(device) { return from(this.connectDevice(device)); } /** * Disconnect the current connected device */ disconnectDevice() { if (!this.gattServer) { return; } this.console.log('[BLE::Info] Disconnecting from Bluetooth Device %o', this.gattServer); if (this.gattServer.connected) { this.gattServer.disconnect(); } else { this.console.log('[BLE::Info] Bluetooth device is already disconnected'); } } /** * Requests the primary service. * * @param gatt The BluetoothRemoteGATTServer sever * @param service The UUID of the primary service * @return The remote service (as a Promise) */ async getPrimaryService(gatt, service) { try { const remoteService = await gatt.getPrimaryService(service); return await Promise.resolve(remoteService); } catch (error) { return await Promise.reject(`${error.message} (${service})`); } } /** * Requests the primary service. * * @param gatt The BluetoothRemoteGATTServer sever * @param service The UUID of the primary service * @return The remote service (as an observable). */ getPrimaryService$(gatt, service) { this.console.log('[BLE::Info] Getting primary service "%s" (if available) of %o', service, gatt); if (gatt) { return from(this.getPrimaryService(gatt, service)); } else { return throwError(() => new Error('[BLE::Error] Was not able to connect to the Bluetooth Remote GATT Server')); } } /** * Requests a characteristic from the primary service. * * @param primaryService The primary service. * @param characteristic The characteristic's UUID. * @returns The characteristic description (as a Promise). */ async getCharacteristic(primaryService, characteristic) { this.console.log('[BLE::Info] Getting Characteristic "%s" of %o', characteristic, primaryService); try { const char = await primaryService.getCharacteristic(characteristic); // listen for characteristic value changes if (char.properties.notify) { char.startNotifications().then(_ => { this.console.log('[BLE::Info] Starting notifications of "%s"', characteristic); char.addEventListener('characteristicvaluechanged', this.onCharacteristicChanged.bind(this)); }, (error) => { Promise.reject(`${error.message} (${characteristic})`); }); } else { char.addEventListener('characteristicvaluechanged', this.onCharacteristicChanged.bind(this)); } return char; } catch (rejectionError) { Promise.reject(`${rejectionError.message} (${characteristic})`); } return null; } /** * Requests a characteristic from the primary service. * * @param primaryService The primary service. * @param characteristic The characteristic's UUID. * @returns The characteristic description (as a Observable). */ getCharacteristic$(primaryService, characteristic) { this.console.log('[BLE::Info] Getting Characteristic "%s" of %o', characteristic, primaryService); return from(this.getCharacteristic(primaryService, characteristic)); } /** * Sets the characteristic's state. * * @param service The parent service of the characteristic. * @param characteristic The requested characteristic * @param state An ArrayBuffer containing the value of the characteristic. * @return The primary service (useful for chaining). */ setCharacteristicState(service, characteristic, state) { const primaryService = this.getPrimaryService$(this.gattServer, service); primaryService .pipe(mergeMap((_primaryService) => this.getCharacteristic$(_primaryService, characteristic))) .subscribe((characteristic) => this.writeValue$(characteristic, state)); return primaryService; } /** * Enables the specified characteristic of a given service. * * @param service The parent service of the characteristic. * @param characteristic The requested characteristic * @return The primary service (useful for chaining). */ enableCharacteristic(service, characteristic, state) { state = state || new Uint8Array([1]); return this.setCharacteristicState(service, characteristic, state); } /** * Disables the specified characteristic of a given service. * * @param service The parent service of the characteristic. * @param characteristic The requested characteristic. * @return The primary service (useful for chaining). */ disbaleCharacteristic(service, characteristic, state) { state = state || new Uint8Array([0]); return this.setCharacteristicState(service, characteristic, state); } /** * Dispatches new values emitted by a characteristic. * * @param event the distpatched event. */ onCharacteristicChanged(event) { this.console.log('[BLE::Info] Dispatching new characteristic value %o', event); const value = event.target.value; this.characteristicValueChanges$.emit(value); } /** * Reads a value from the characteristics, as a DataView. * * @param characteristic The requested characteristic. * @return the DataView value (as an Observable). */ readValue$(characteristic) { this.console.log('[BLE::Info] Reading Characteristic %o', characteristic); return from(characteristic .readValue() .then((data) => Promise.resolve(data), (error) => Promise.reject(`${error.message}`))); } /** * Writes a value into the specified characteristic. * * @param characteristic The requested characteristic. * @param value The value to be written (as an ArrayBuffer or Uint8Array). * @return an void Observable. */ writeValue$(characteristic, value) { if (characteristic === null) { this.console.error('[BLE::Error] Was not able to write characteristic'); return null; } this.console.log('[BLE::Info] Writing Characteristic %o', characteristic); return from(characteristic.writeValue(value).then(_ => Promise.resolve(), (error) => Promise.reject(`${error.message}`))); } /** * A stream of DataView values emitted by the specified characteristic. * * @param characteristic The characteristic which value you want to observe * @return The stream of DataView values. */ observeValue$(characteristic) { if (characteristic === null || typeof characteristic.service === 'undefined') { this.console.error('[BLE::Error] Was not able to read characteristic'); return EMPTY; } characteristic.startNotifications(); const disconnected = fromEvent(characteristic.service.device, 'gattserverdisconnected'); return fromEvent(characteristic, 'characteristicvaluechanged') .pipe(map((event) => event.target.value), takeUntil(disconnected)); } /** * A utility method to convert LE to an unsigned 16-bit integer values. * * @param data The DataView binary data. * @param byteOffset The offset, in byte, from the start of the view where to read the data. * @return An unsigned 16-bit integer number. */ littleEndianToUint16(data, byteOffset) { return (this.littleEndianToUint8(data, byteOffset + 1) << 8) + this.littleEndianToUint8(data, byteOffset); } /** * A utility method to convert LE to an unsigned 8-bit integer values. * * @param data The DataView binary data. * @param byteOffset The offset, in byte, from the start of the view where to read the data. * @return An unsigned 8-bit integer number. */ littleEndianToUint8(data, byteOffset) { return data.getUint8(byteOffset); } /** * Sends random data (for testing purposes only). * * @return Random unsigned 8-bit integer values. */ fakeNext(fakeValue) { if (fakeValue === undefined) { fakeValue = () => { const dv = new DataView(new ArrayBuffer(8)); dv.setUint8(0, (Math.random() * 110) | 0); return dv; }; } this.characteristicValueChanges$.emit(fakeValue()); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BluetoothCore, deps: [{ token: BrowserWebBluetooth }, { token: ConsoleLoggerService }], target: i0.ɵɵFactoryTarget.Injectable }); } static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BluetoothCore, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: BluetoothCore, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: BrowserWebBluetooth }, { type: ConsoleLoggerService }] }); function browserWebBluetooth() { return new BrowserWebBluetooth(); } function consoleLoggerServiceConfig(options) { if (options && options.enableTracing) { return new ConsoleLoggerService(); } else { return new NoLoggerService(); } } function makeMeTokenInjector() { return new InjectionToken('AWBOptions'); } class WebBluetoothModule { static forRoot(options = {}) { return { ngModule: WebBluetoothModule, providers: [ BluetoothCore, { provide: BrowserWebBluetooth, useFactory: browserWebBluetooth }, { provide: makeMeTokenInjector, useValue: options }, { provide: ConsoleLoggerService, useFactory: consoleLoggerServiceConfig, deps: [makeMeTokenInjector] } ] }; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: WebBluetoothModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.1.3", ngImport: i0, type: WebBluetoothModule, imports: [CommonModule] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: WebBluetoothModule, imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.1.3", ngImport: i0, type: WebBluetoothModule, decorators: [{ type: NgModule, args: [{ imports: [CommonModule] }] }] }); // http://processors.wiki.ti.com/images/a/a8/BLE_SensorTag_GATT_Server.pdf // prettier-ignore const TiTag = { DEVICE_INFORMATION: { SERVICE: 'f000180a-0451-4000-b000-000000000000', SYSTEM_ID: 'f0002a23-0451-4000-b000-000000000000', MODEL_NUMBER: 'f0002a24-0451-4000-b000-000000000000', SERIAL_NUMBER: 'f0002a25-0451-4000-b000-000000000000', FIRMWARE_REV: 'f0002a26-0451-4000-b000-000000000000', HARDWARE_REV: 'f0002a27-0451-4000-b000-000000000000', SOFTWARE_REV: 'f0002a28-0451-4000-b000-000000000000', MANIFACTURER: 'f0002a29-0451-4000-b000-000000000000', IEEE11073: 'f0002a2a-0451-4000-b000-000000000000', PNP_ID: 'f0002a50-0451-4000-b000-000000000000' }, BATTERY: { SERVICE: 'f000180f-0451-4000-b000-000000000000', LEVEL: 'f0002a19-0451-4000-b000-000000000000' }, TEMPERATURE: { SERVICE: 'f000aa00-0451-4000-b000-000000000000', DATA: 'f000aa01-0451-4000-b000-000000000000', CONFIGURATION: 'f000aa02-0451-4000-b000-000000000000', PERIOD: 'f000aa03-0451-4000-b000-000000000000' }, HUMIDITY: { SERVICE: 'f000aa20-0451-4000-b000-000000000000', DATA: 'f000aa21-0451-4000-b000-000000000000', CONFIGURATION: 'f000aa22-0451-4000-b000-000000000000', PERIOD: 'f000aa23-0451-4000-b000-000000000000' }, BAROMETER: { SERVICE: 'f000aa40-0451-4000-b000-000000000000', DATA: 'f000aa41-0451-4000-b000-000000000000', CONFIGURATION: 'f000aa42-0451-4000-b000-000000000000', PERIOD: 'f000aa44-0451-4000-b000-000000000000' }, // service not available in model CC2650 // ACCELEROMETER : { // SERVICE : 'f000aa10-0451-4000-b000-000000000000', // DATA : 'f000aa11-0451-4000-b000-000000000000', // CONFIGURATION : 'f000aa12-0451-4000-b000-000000000000', // PERIOD : 'f000aa13-0451-4000-b000-000000000000' // }, // service not available in model CC2650 // MAGNETOMETER : { // SERVICE : 'f000aa30-0451-4000-b000-000000000000', // DATA : 'f000aa31-0451-4000-b000-000000000000', // CONFIGURATION : 'f000aa32-0451-4000-b000-000000000000', // PERIOD : 'f000aa33-0451-4000-b000-000000000000' // }, // service not available in model CC2650 // GYROSCOPE : { // SERVICE : 'f000aa50-0451-4000-b000-000000000000', // DATA : 'f000aa51-0451-4000-b000-000000000000', // CONFIGURATION : 'f000aa52-0451-4000-b000-000000000000', // PERIOD : 'f000aa53-0451-4000-b000-000000000000' // }, MOVEMENT: { SERVICE: 'f000aa80-0451-4000-b000-000000000000', DATA: 'f000aa81-0451-4000-b000-000000000000', CONFIGURATION: 'f000aa82-0451-4000-b000-000000000000', PERIOD: 'f000aa83-0451-4000-b000-000000000000' }, LIGHT: { SERVICE: 'f000aa70-0451-4000-b000-000000000000', DATA: 'f000aa71-0451-4000-b000-000000000000', CONFIGURATION: 'f000aa72-0451-4000-b000-000000000000', PERIOD: 'f000aa73-0451-4000-b000-000000000000' }, KEYPRESS: { SERVICE: 'f000ffe0-0451-4000-b000-000000000000', STATE: 'f000ffe1-0451-4000-b000-000000000000' }, __REGISTER__: { SERVICE: 'f000ac00-0451-4000-b000-000000000000', DATA: 'f000ac01-0451-4000-b000-000000000000', ADDRESS: 'f000ac02-0451-4000-b000-000000000000', DEVICE_ID: 'f000ac03-0451-4000-b000-000000000000' }, CONTROL: { SERVICE: 'f000ccc0-0451-4000-b000-000000000000', CURRENT_USED_PARAMETERS: 'f000ccc1-0451-4000-b000-000000000000', REQUEST_NEW_PARAMETERS: 'f000ccc2-0451-4000-b000-000000000000', DISCONNECT_REQUEST: 'f000ccc3-0451-4000-b000-000000000000' }, OAD: { SERVICE: 'f000ffc0-0451-4000-b000-000000000000', IMAGE_NOTIFY: 'f000ffc1-0451-4000-b000-000000000000', IMAGE_BLOCK_REQUEST: 'f000ffc2-0451-4000-b000-000000000000', IMAGE_COUNT: 'f000ffc3-0451-4000-b000-000000000000', IMAGE_STATUS: 'f000ffc4-0451-4000-b000-000000000000' }, IO: { SERVICE: 'f000aa64-0451-4000-b000-000000000000', DATA: 'f000aa65-0451-4000-b000-000000000000', CONFIG: 'f000aa66-0451-4000-b000-000000000000' } }; const TI_SENSORAG_SERVICES = Object.keys(TiTag).map((key) => TiTag[key].SERVICE); /* * Fake Web Bluetooth implementation * Replace real browser Bluetooth objects by a much simpler objects that implement some required functionalities */ class FakeBluetoothDevice { constructor(id, name) { this.id = id; this.name = name; this.gatt = null; this.listeners = { gattserverdisconnected: [] }; } addEventListener(type, listener) { this.listeners[type] = [ ...this.listeners[type], listener ]; } disconnect() { const mockedEvent = { target: this }; this.listeners['gattserverdisconnected'].forEach(listener => listener(mockedEvent)); } clear() { this.id = ""; this.name = ""; this.listeners = { gattserverdisconnected: [] }; } } class FakeBluetoothRemoteGATTServer { constructor(device, services) { this.device = device; this.services = services; this.connected = false; device.gatt = this; } connect() { this.connected = true; return Promise.resolve(this); } getPrimaryService(service) { return Promise.resolve(this.services[service].service); } disconnect() { this.device.disconnect(); this.connected = false; } } class FakeBluetoothRemoteGATTService { constructor(device, characteristics) { this.device = device; this.characteristics = characteristics; this.characteristics.service = this; } getCharacteristic(characteristic) { return Promise.resolve(this.characteristics[characteristic]); } } class FakeBluetoothRemoteGATTCharacteristic { constructor(properties, initialValue) { this.listeners = { characteristicvaluechanged: [] }; this.properties = properties; this.value = initialValue; this.initialValue = initialValue; } readValue() { return Promise.resolve(this.value); } addEventListener(type, listener) { this.listeners[type] = [ ...this.listeners[type], listener ]; } changeValue(value) { this.value = value; const mockedEvent = { target: this }; this.listeners['characteristicvaluechanged'].forEach(listener => listener(mockedEvent)); } clear() { this.value = this.initialValue; this.listeners = { characteristicvaluechanged: [] }; } } /* * Public API Surface of angular-web-bluetooth */ /** * Generated bundle index. Do not edit. */ export { BluetoothCore, BrowserWebBluetooth, ConsoleLoggerService, FakeBluetoothDevice, FakeBluetoothRemoteGATTCharacteristic, FakeBluetoothRemoteGATTServer, FakeBluetoothRemoteGATTService, NoLoggerService, ServerWebBluetooth, TI_SENSORAG_SERVICES, TiTag, WebBluetoothModule, browserWebBluetooth, consoleLoggerServiceConfig, makeMeTokenInjector }; //# sourceMappingURL=manekinekko-angular-web-bluetooth.mjs.map