UNPKG

@ermitsrl/bluetooth-le

Version:

Capacitor plugin for Bluetooth Low Energy

830 lines (815 loc) 32.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var core = require('@capacitor/core'); var throat = require('throat'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var throat__default = /*#__PURE__*/_interopDefaultLegacy(throat); /** * Android scan mode */ exports.ScanMode = void 0; (function (ScanMode) { /** * Perform Bluetooth LE scan in low power mode. This mode is enforced if the scanning application is not in foreground. * https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_POWER */ ScanMode[ScanMode["SCAN_MODE_LOW_POWER"] = 0] = "SCAN_MODE_LOW_POWER"; /** * Perform Bluetooth LE scan in balanced power mode. (default) Scan results are returned at a rate that provides a good trade-off between scan frequency and power consumption. * https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_BALANCED */ ScanMode[ScanMode["SCAN_MODE_BALANCED"] = 1] = "SCAN_MODE_BALANCED"; /** * Scan using highest duty cycle. It's recommended to only use this mode when the application is running in the foreground. * https://developer.android.com/reference/android/bluetooth/le/ScanSettings#SCAN_MODE_LOW_LATENCY */ ScanMode[ScanMode["SCAN_MODE_LOW_LATENCY"] = 2] = "SCAN_MODE_LOW_LATENCY"; })(exports.ScanMode || (exports.ScanMode = {})); /** * Convert an array of numbers into a DataView. */ function numbersToDataView(value) { return new DataView(Uint8Array.from(value).buffer); } /** * Convert a DataView into an array of numbers. */ function dataViewToNumbers(value) { return Array.from(new Uint8Array(value.buffer)); } /** * Convert a string into a DataView. */ function textToDataView(value) { return numbersToDataView(value.split('').map((s) => s.charCodeAt(0))); } /** * Convert a DataView into a string. */ function dataViewToText(value) { return String.fromCharCode(...dataViewToNumbers(value)); } /** * Convert a 16 bit UUID into a 128 bit UUID string * @param value number, e.g. 0x180d * @return string, e.g. '0000180d-0000-1000-8000-00805f9b34fb' */ function numberToUUID(value) { return `0000${value.toString(16).padStart(4, '0')}-0000-1000-8000-00805f9b34fb`; } function hexStringToDataView(value) { const numbers = value .trim() .split(' ') .filter((e) => e !== '') .map((s) => parseInt(s, 16)); return numbersToDataView(numbers); } function dataViewToHexString(value) { return dataViewToNumbers(value) .map((n) => { let s = n.toString(16); if (s.length == 1) { s = '0' + s; } return s; }) .join(' '); } function webUUIDToString(uuid) { if (typeof uuid === 'string') { return uuid; } else if (typeof uuid === 'number') { return numberToUUID(uuid); } else { throw new Error('Invalid UUID'); } } function mapToObject(map) { const obj = {}; if (!map) { return undefined; } map.forEach((value, key) => { obj[key.toString()] = value; }); return obj; } const BluetoothLe = core.registerPlugin('BluetoothLe', { web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.BluetoothLeWeb()), }); function getQueue(enabled) { if (enabled) { return throat__default["default"](1); } else { return (fn) => fn(); } } function validateUUID(uuid) { if (typeof uuid !== 'string') { throw new Error(`Invalid UUID type ${typeof uuid}. Expected string.`); } uuid = uuid.toLowerCase(); const is128BitUuid = uuid.search(/^[0-9a-f]{8}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{4}\b-[0-9a-f]{12}$/) >= 0; if (!is128BitUuid) { throw new Error(`Invalid UUID format ${uuid}. Expected 128 bit string (e.g. "0000180d-0000-1000-8000-00805f9b34fb").`); } return uuid; } class BleClientClass { constructor() { this.scanListener = null; this.eventListeners = new Map(); this.queue = getQueue(true); } enableQueue() { this.queue = getQueue(true); } disableQueue() { this.queue = getQueue(false); } async initialize(options) { await this.queue(async () => { await BluetoothLe.initialize(options); }); } /** * Reports whether BLE is enabled on this device. * Always returns `true` on **web**. * @deprecated Use `isEnabled` instead. */ async getEnabled() { return this.isEnabled(); } async isEnabled() { const enabled = await this.queue(async () => { const result = await BluetoothLe.isEnabled(); return result.value; }); return enabled; } async enable() { await this.queue(async () => { await BluetoothLe.enable(); }); } async disable() { await this.queue(async () => { await BluetoothLe.disable(); }); } async startEnabledNotifications(callback) { await this.queue(async () => { var _a; const key = `onEnabledChanged`; await ((_a = this.eventListeners.get(key)) === null || _a === void 0 ? void 0 : _a.remove()); const listener = await BluetoothLe.addListener(key, (result) => { callback(result.value); }); this.eventListeners.set(key, listener); await BluetoothLe.startEnabledNotifications(); }); } async stopEnabledNotifications() { await this.queue(async () => { var _a; const key = `onEnabledChanged`; await ((_a = this.eventListeners.get(key)) === null || _a === void 0 ? void 0 : _a.remove()); this.eventListeners.delete(key); await BluetoothLe.stopEnabledNotifications(); }); } async isLocationEnabled() { const enabled = await this.queue(async () => { const result = await BluetoothLe.isLocationEnabled(); return result.value; }); return enabled; } async openLocationSettings() { await this.queue(async () => { await BluetoothLe.openLocationSettings(); }); } async openBluetoothSettings() { await this.queue(async () => { await BluetoothLe.openBluetoothSettings(); }); } async openAppSettings() { await this.queue(async () => { await BluetoothLe.openAppSettings(); }); } async setDisplayStrings(displayStrings) { await this.queue(async () => { await BluetoothLe.setDisplayStrings(displayStrings); }); } async requestDevice(options) { options = options ? this.validateRequestBleDeviceOptions(options) : undefined; const result = await this.queue(async () => { const device = await BluetoothLe.requestDevice(options); return device; }); return result; } async requestLEScan(options, callback) { options = this.validateRequestBleDeviceOptions(options); await this.queue(async () => { var _a; await ((_a = this.scanListener) === null || _a === void 0 ? void 0 : _a.remove()); this.scanListener = await BluetoothLe.addListener('onScanResult', (resultInternal) => { const result = Object.assign(Object.assign({}, resultInternal), { manufacturerData: this.convertObject(resultInternal.manufacturerData), serviceData: this.convertObject(resultInternal.serviceData), rawAdvertisement: resultInternal.rawAdvertisement ? this.convertValue(resultInternal.rawAdvertisement) : undefined }); callback(result); }); await BluetoothLe.requestLEScan(options); }); } async stopLEScan() { await this.queue(async () => { var _a; await ((_a = this.scanListener) === null || _a === void 0 ? void 0 : _a.remove()); this.scanListener = null; await BluetoothLe.stopLEScan(); }); } async getDevices(deviceIds) { return this.queue(async () => { const result = await BluetoothLe.getDevices({ deviceIds }); return result.devices; }); } async getConnectedDevices(services) { return this.queue(async () => { const result = await BluetoothLe.getConnectedDevices({ services }); return result.devices; }); } // define networks typo async scanNetworks(deviceId) { return this.queue(async () => { const result = await BluetoothLe.scanNetworks({ deviceId }); return result.networks; }); } // send wifi credentials to device async provision(deviceId, ssid, password = '') { await this.queue(async () => { await BluetoothLe.provision({ deviceId, ssid, password }); }); } async connect(deviceId, proofOfPossession, onDisconnect, options) { await this.queue(async () => { var _a; if (onDisconnect) { const key = `disconnected|${deviceId}`; await ((_a = this.eventListeners.get(key)) === null || _a === void 0 ? void 0 : _a.remove()); const listener = await BluetoothLe.addListener(key, () => { onDisconnect(deviceId); }); this.eventListeners.set(key, listener); } await BluetoothLe.connect(Object.assign({ deviceId, proofOfPossession }, options)); }); } async createBond(deviceId) { await this.queue(async () => { await BluetoothLe.createBond({ deviceId }); }); } async isBonded(deviceId) { const isBonded = await this.queue(async () => { const result = await BluetoothLe.isBonded({ deviceId }); return result.value; }); return isBonded; } async setPin(deviceId) { const setPin = await this.queue(async () => { const result = await BluetoothLe.setPin({ deviceId }); return result.value; }); return setPin; } async disconnect(deviceId) { await this.queue(async () => { await BluetoothLe.disconnect({ deviceId }); }); } async getServices(deviceId) { const services = await this.queue(async () => { const result = await BluetoothLe.getServices({ deviceId }); return result.services; }); return services; } async discoverServices(deviceId) { await this.queue(async () => { await BluetoothLe.discoverServices({ deviceId }); }); } async readRssi(deviceId) { const value = await this.queue(async () => { const result = await BluetoothLe.readRssi({ deviceId }); return parseFloat(result.value); }); return value; } async read(deviceId, service, characteristic, options) { service = validateUUID(service); characteristic = validateUUID(characteristic); const value = await this.queue(async () => { const result = await BluetoothLe.read(Object.assign({ deviceId, service, characteristic }, options)); return this.convertValue(result.value); }); return value; } async write(deviceId, service, characteristic, value, options) { service = validateUUID(service); characteristic = validateUUID(characteristic); return this.queue(async () => { if (!(value === null || value === void 0 ? void 0 : value.buffer)) { throw new Error('Invalid data.'); } let writeValue = value; if (core.Capacitor.getPlatform() !== 'web') { // on native we can only write strings writeValue = dataViewToHexString(value); } await BluetoothLe.write(Object.assign({ deviceId, service, characteristic, value: writeValue }, options)); }); } async writeWithoutResponse(deviceId, service, characteristic, value, options) { service = validateUUID(service); characteristic = validateUUID(characteristic); await this.queue(async () => { if (!(value === null || value === void 0 ? void 0 : value.buffer)) { throw new Error('Invalid data.'); } let writeValue = value; if (core.Capacitor.getPlatform() !== 'web') { // on native we can only write strings writeValue = dataViewToHexString(value); } await BluetoothLe.writeWithoutResponse(Object.assign({ deviceId, service, characteristic, value: writeValue }, options)); }); } async readDescriptor(deviceId, service, characteristic, descriptor, options) { service = validateUUID(service); characteristic = validateUUID(characteristic); descriptor = validateUUID(descriptor); const value = await this.queue(async () => { const result = await BluetoothLe.readDescriptor(Object.assign({ deviceId, service, characteristic, descriptor }, options)); return this.convertValue(result.value); }); return value; } async writeDescriptor(deviceId, service, characteristic, descriptor, value, options) { service = validateUUID(service); characteristic = validateUUID(characteristic); descriptor = validateUUID(descriptor); return this.queue(async () => { if (!(value === null || value === void 0 ? void 0 : value.buffer)) { throw new Error('Invalid data.'); } let writeValue = value; if (core.Capacitor.getPlatform() !== 'web') { // on native we can only write strings writeValue = dataViewToHexString(value); } await BluetoothLe.writeDescriptor(Object.assign({ deviceId, service, characteristic, descriptor, value: writeValue }, options)); }); } async startNotifications(deviceId, service, characteristic, callback) { service = validateUUID(service); characteristic = validateUUID(characteristic); await this.queue(async () => { var _a; const key = `notification|${deviceId}|${service}|${characteristic}`; await ((_a = this.eventListeners.get(key)) === null || _a === void 0 ? void 0 : _a.remove()); const listener = await BluetoothLe.addListener(key, (event) => { callback(this.convertValue(event === null || event === void 0 ? void 0 : event.value)); }); this.eventListeners.set(key, listener); await BluetoothLe.startNotifications({ deviceId, service, characteristic, }); }); } async stopNotifications(deviceId, service, characteristic) { service = validateUUID(service); characteristic = validateUUID(characteristic); await this.queue(async () => { var _a; const key = `notification|${deviceId}|${service}|${characteristic}`; await ((_a = this.eventListeners.get(key)) === null || _a === void 0 ? void 0 : _a.remove()); this.eventListeners.delete(key); await BluetoothLe.stopNotifications({ deviceId, service, characteristic, }); }); } validateRequestBleDeviceOptions(options) { if (options.services) { options.services = options.services.map(validateUUID); } if (options.optionalServices) { options.optionalServices = options.optionalServices.map(validateUUID); } return options; } convertValue(value) { if (typeof value === 'string') { return hexStringToDataView(value); } else if (value === undefined) { return new DataView(new ArrayBuffer(0)); } return value; } convertObject(obj) { if (obj === undefined) { return undefined; } const result = {}; for (const key of Object.keys(obj)) { result[key] = this.convertValue(obj[key]); } return result; } } const BleClient = new BleClientClass(); async function runWithTimeout(promise, time, exception) { let timer; return Promise.race([ promise, new Promise((_, reject) => { timer = setTimeout(() => reject(exception), time); }), ]).finally(() => clearTimeout(timer)); } class BluetoothLeWeb extends core.WebPlugin { constructor() { super(...arguments); this.deviceMap = new Map(); this.discoveredDevices = new Map(); this.scan = null; this.DEFAULT_CONNECTION_TIMEOUT = 10000; this.onAdvertisementReceivedCallback = this.onAdvertisementReceived.bind(this); this.onDisconnectedCallback = this.onDisconnected.bind(this); this.onCharacteristicValueChangedCallback = this.onCharacteristicValueChanged.bind(this); } async initialize() { if (typeof navigator === 'undefined' || !navigator.bluetooth) { throw this.unavailable('Web Bluetooth API not available in this browser.'); } const isAvailable = await navigator.bluetooth.getAvailability(); if (!isAvailable) { throw this.unavailable('No Bluetooth radio available.'); } } async isEnabled() { // not available on web return { value: true }; } async enable() { throw this.unavailable('enable is not available on web.'); } async disable() { throw this.unavailable('disable is not available on web.'); } async startEnabledNotifications() { // not available on web } async stopEnabledNotifications() { // not available on web } async isLocationEnabled() { throw this.unavailable('isLocationEnabled is not available on web.'); } async openLocationSettings() { throw this.unavailable('openLocationSettings is not available on web.'); } async openBluetoothSettings() { throw this.unavailable('openBluetoothSettings is not available on web.'); } async openAppSettings() { throw this.unavailable('openAppSettings is not available on web.'); } async setDisplayStrings() { // not available on web } async requestDevice(options) { const filters = this.getFilters(options); const device = await navigator.bluetooth.requestDevice({ filters: filters.length ? filters : undefined, optionalServices: options === null || options === void 0 ? void 0 : options.optionalServices, acceptAllDevices: filters.length === 0, }); this.deviceMap.set(device.id, device); const bleDevice = this.getBleDevice(device); return bleDevice; } async requestLEScan(options) { this.requestBleDeviceOptions = options; const filters = this.getFilters(options); await this.stopLEScan(); this.discoveredDevices = new Map(); navigator.bluetooth.removeEventListener('advertisementreceived', this.onAdvertisementReceivedCallback); navigator.bluetooth.addEventListener('advertisementreceived', this.onAdvertisementReceivedCallback); this.scan = await navigator.bluetooth.requestLEScan({ filters: filters.length ? filters : undefined, acceptAllAdvertisements: filters.length === 0, keepRepeatedDevices: options === null || options === void 0 ? void 0 : options.allowDuplicates, }); } onAdvertisementReceived(event) { var _a, _b; const deviceId = event.device.id; this.deviceMap.set(deviceId, event.device); const isNew = !this.discoveredDevices.has(deviceId); if (isNew || ((_a = this.requestBleDeviceOptions) === null || _a === void 0 ? void 0 : _a.allowDuplicates)) { this.discoveredDevices.set(deviceId, true); const device = this.getBleDevice(event.device); const result = { device, localName: device.name, rssi: event.rssi, txPower: event.txPower, manufacturerData: mapToObject(event.manufacturerData), serviceData: mapToObject(event.serviceData), uuids: (_b = event.uuids) === null || _b === void 0 ? void 0 : _b.map(webUUIDToString), }; this.notifyListeners('onScanResult', result); } } async stopLEScan() { var _a; if ((_a = this.scan) === null || _a === void 0 ? void 0 : _a.active) { this.scan.stop(); } this.scan = null; } async getDevices(_options) { const devices = await navigator.bluetooth.getDevices(); const bleDevices = devices.map((device) => { this.deviceMap.set(device.id, device); const bleDevice = this.getBleDevice(device); return bleDevice; }); return { devices: bleDevices }; } async getConnectedDevices(_options) { const devices = await navigator.bluetooth.getDevices(); const bleDevices = devices .filter((device) => { var _a; return (_a = device.gatt) === null || _a === void 0 ? void 0 : _a.connected; }) .map((device) => { this.deviceMap.set(device.id, device); const bleDevice = this.getBleDevice(device); return bleDevice; }); return { devices: bleDevices }; } async scanNetworks(_options) { throw this.unavailable('scanNetworks is not available on web.'); } async provision(_options) { throw this.unavailable('provision is not available on web.'); } async connect(options) { var _a, _b; const device = this.getDeviceFromMap(options.deviceId); device.removeEventListener('gattserverdisconnected', this.onDisconnectedCallback); device.addEventListener('gattserverdisconnected', this.onDisconnectedCallback); const timeoutError = Symbol(); if (device.gatt === undefined) { throw new Error('No gatt server available.'); } try { const timeout = (_a = options.timeout) !== null && _a !== void 0 ? _a : this.DEFAULT_CONNECTION_TIMEOUT; await runWithTimeout(device.gatt.connect(), timeout, timeoutError); } catch (error) { // cancel pending connect call, does not work yet in chromium because of a bug: // https://bugs.chromium.org/p/chromium/issues/detail?id=684073 await ((_b = device.gatt) === null || _b === void 0 ? void 0 : _b.disconnect()); if (error === timeoutError) { throw new Error('Connection timeout'); } else { throw error; } } } onDisconnected(event) { const deviceId = event.target.id; const key = `disconnected|${deviceId}`; this.notifyListeners(key, null); } async createBond(_options) { throw this.unavailable('createBond is not available on web.'); } async isBonded(_options) { throw this.unavailable('isBonded is not available on web.'); } async setPin(_options) { throw this.unavailable('setPin is not available on web.'); } async disconnect(options) { var _a; (_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.disconnect(); } async getServices(options) { var _a, _b; const services = (_b = (await ((_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.getPrimaryServices()))) !== null && _b !== void 0 ? _b : []; const bleServices = []; for (const service of services) { const characteristics = await service.getCharacteristics(); const bleCharacteristics = []; for (const characteristic of characteristics) { bleCharacteristics.push({ uuid: characteristic.uuid, properties: this.getProperties(characteristic), descriptors: await this.getDescriptors(characteristic), }); } bleServices.push({ uuid: service.uuid, characteristics: bleCharacteristics }); } return { services: bleServices }; } async getDescriptors(characteristic) { try { const descriptors = await characteristic.getDescriptors(); return descriptors.map((descriptor) => ({ uuid: descriptor.uuid, })); } catch (_a) { return []; } } getProperties(characteristic) { return { broadcast: characteristic.properties.broadcast, read: characteristic.properties.read, writeWithoutResponse: characteristic.properties.writeWithoutResponse, write: characteristic.properties.write, notify: characteristic.properties.notify, indicate: characteristic.properties.indicate, authenticatedSignedWrites: characteristic.properties.authenticatedSignedWrites, reliableWrite: characteristic.properties.reliableWrite, writableAuxiliaries: characteristic.properties.writableAuxiliaries, }; } async getCharacteristic(options) { var _a; const service = await ((_a = this.getDeviceFromMap(options.deviceId).gatt) === null || _a === void 0 ? void 0 : _a.getPrimaryService(options === null || options === void 0 ? void 0 : options.service)); return service === null || service === void 0 ? void 0 : service.getCharacteristic(options === null || options === void 0 ? void 0 : options.characteristic); } async getDescriptor(options) { const characteristic = await this.getCharacteristic(options); return characteristic === null || characteristic === void 0 ? void 0 : characteristic.getDescriptor(options === null || options === void 0 ? void 0 : options.descriptor); } async discoverServices(_options) { throw this.unavailable('discoverServices is not available on web.'); } async readRssi(_options) { throw this.unavailable('readRssi is not available on web.'); } async read(options) { const characteristic = await this.getCharacteristic(options); const value = await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.readValue()); return { value }; } async write(options) { const characteristic = await this.getCharacteristic(options); let dataView; if (typeof options.value === 'string') { dataView = hexStringToDataView(options.value); } else { dataView = options.value; } await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.writeValueWithResponse(dataView)); } async writeWithoutResponse(options) { const characteristic = await this.getCharacteristic(options); let dataView; if (typeof options.value === 'string') { dataView = hexStringToDataView(options.value); } else { dataView = options.value; } await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.writeValueWithoutResponse(dataView)); } async readDescriptor(options) { const descriptor = await this.getDescriptor(options); const value = await (descriptor === null || descriptor === void 0 ? void 0 : descriptor.readValue()); return { value }; } async writeDescriptor(options) { const descriptor = await this.getDescriptor(options); let dataView; if (typeof options.value === 'string') { dataView = hexStringToDataView(options.value); } else { dataView = options.value; } await (descriptor === null || descriptor === void 0 ? void 0 : descriptor.writeValue(dataView)); } async startNotifications(options) { const characteristic = await this.getCharacteristic(options); characteristic === null || characteristic === void 0 ? void 0 : characteristic.removeEventListener('characteristicvaluechanged', this.onCharacteristicValueChangedCallback); characteristic === null || characteristic === void 0 ? void 0 : characteristic.addEventListener('characteristicvaluechanged', this.onCharacteristicValueChangedCallback); await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.startNotifications()); } onCharacteristicValueChanged(event) { var _a, _b; const characteristic = event.target; const key = `notification|${(_a = characteristic.service) === null || _a === void 0 ? void 0 : _a.device.id}|${(_b = characteristic.service) === null || _b === void 0 ? void 0 : _b.uuid}|${characteristic.uuid}`; this.notifyListeners(key, { value: characteristic.value, }); } async stopNotifications(options) { const characteristic = await this.getCharacteristic(options); await (characteristic === null || characteristic === void 0 ? void 0 : characteristic.stopNotifications()); } getFilters(options) { var _a; const filters = []; for (const service of (_a = options === null || options === void 0 ? void 0 : options.services) !== null && _a !== void 0 ? _a : []) { filters.push({ services: [service], name: options === null || options === void 0 ? void 0 : options.name, namePrefix: options === null || options === void 0 ? void 0 : options.namePrefix, }); } if (((options === null || options === void 0 ? void 0 : options.name) || (options === null || options === void 0 ? void 0 : options.namePrefix)) && filters.length === 0) { filters.push({ name: options.name, namePrefix: options.namePrefix, }); } return filters; } getDeviceFromMap(deviceId) { const device = this.deviceMap.get(deviceId); if (device === undefined) { throw new Error('Device not found. Call "requestDevice", "requestLEScan" or "getDevices" first.'); } return device; } getBleDevice(device) { var _a; const bleDevice = { deviceId: device.id, // use undefined instead of null if name is not available name: (_a = device.name) !== null && _a !== void 0 ? _a : undefined, }; return bleDevice; } } var web = /*#__PURE__*/Object.freeze({ __proto__: null, BluetoothLeWeb: BluetoothLeWeb }); exports.BleClient = BleClient; exports.BluetoothLe = BluetoothLe; exports.dataViewToHexString = dataViewToHexString; exports.dataViewToNumbers = dataViewToNumbers; exports.dataViewToText = dataViewToText; exports.hexStringToDataView = hexStringToDataView; exports.mapToObject = mapToObject; exports.numberToUUID = numberToUUID; exports.numbersToDataView = numbersToDataView; exports.textToDataView = textToDataView; exports.webUUIDToString = webUUIDToString; //# sourceMappingURL=plugin.cjs.js.map