UNPKG

bleat

Version:

Abstraction library following Web Bluetooth specification for hiding differences in JavaScript BLE APIs

907 lines (809 loc) 25.6 kB
/* @license * * BLE Abstraction Tool: Evothings BLE plugin adapter * * The MIT License (MIT) * * Copyright (c) 2016 Rob Moran * * 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. */ // https://github.com/umdjs/umd ;(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. Register as an anonymous module. // Not supported by Cordova. define(['bleat', 'bluetooth.helpers'], factory.bind(this, root)); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS // Not supported by Cordova. module.exports = function(bleat) { return factory(root, bleat, require('./bluetooth.helpers')); }; } else { // Browser globals with support for web workers (root is window) // Used with Cordova. factory(root, root.bleat, root.bleatHelpers); } })(this, function(root, bleat, helpers) { "use strict"; // Guard against bleat being navigator.bluetooth if (!bleat._addAdapter) return; // Object that holds Bleat adapter functions. var adapter = {}; var mDeviceIdToDeviceHandle = {}; var mServiceHandleToDeviceHandle = {}; var mCharacteristicHandleToDeviceHandle = {}; var mDescriptorHandleToDeviceHandle = {}; var mCharacteristicHandleToCCCDHandle = {}; // Add adapter object to Bleat. Adapter functions are defined below. bleat._addAdapter('evothings', adapter); function init(readyFn) { if (root.evothings && evothings.ble) readyFn(); else document.addEventListener("deviceready", readyFn); } // Begin scanning for devices adapter.startScan = function( serviceUUIDs, // String[] serviceUUIDs advertised service UUIDs to restrict results by foundFn, // Function(Object deviceInfo) function called with each discovered deviceInfo completeFn, // Function() function called once starting scanning errorFn // Function(String errorMsg) function called if error occurs ) { init(function() { evothings.ble.stopScan(); evothings.ble.startScan( serviceUUIDs, function(deviceInfo) { if (foundFn) { foundFn(createBleatDeviceObject(deviceInfo)); } }, function(error) { if (errorFn) { errorFn(error); } }); if (completeFn) { completeFn(); } }); }; // Stop scanning for devices adapter.stopScan = function( errorFn // Function(String errorMsg) function called if error occurs ) { init(function() { evothings.ble.stopScan(); }); }; // Connect to a device adapter.connect = function( handle, // String handle device handle connectFn, // Function() function called when device connected disconnectFn, // Function() function called when device disconnected errorFn // Function(String errorMsg) function called if error occurs ) { // Check that device is not already connected. var deviceHandle = mDeviceIdToDeviceHandle[handle]; if (deviceHandle) { if (errorFn) { errorFn('device already connected'); } return; } // Connect to the device. evothings.ble.connect( handle, // Connect success. function(connectInfo) { // Connected. if (2 === connectInfo.state && connectFn) { mDeviceIdToDeviceHandle[handle] = connectInfo.deviceHandle; connectFn(); } // Disconnected. else if (0 === connectInfo.state && disconnectFn) { disconnectDevice(handle); disconnectFn(); } }, // Connect error. function(error) { if (errorFn) { errorFn(error); } }); }; // Disconnect from a device adapter.disconnect = function( handle, // String handle device handle errorFn // Function(String errorMsg) function called if error occurs ) { disconnectDevice(handle); }; // Discover services on a device adapter.discoverServices = function( handle, // String handle device handle serviceUUIDs, // String[] serviceUUIDs service UUIDs to restrict results by completeFn, // Function(Object[] serviceInfo) function called when discovery completed errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromDeviceId(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.services( deviceHandle, function(services) { // Collect found services. var discoveredServices = []; services.forEach(function(serviceInfo) { var serviceUUID = helpers.getCanonicalUUID(serviceInfo.uuid); // Filter services. var includeService = !serviceUUIDs || 0 === serviceUUIDs.length || serviceUUIDs.indexOf(serviceUUID) >= 0; if (includeService) { // Set device for service. mServiceHandleToDeviceHandle[serviceInfo.handle] = deviceHandle; // Add the service. discoveredServices.push( { _handle: serviceInfo.handle, uuid: serviceUUID, primary: true }); } }); // Return result. if (completeFn) { completeFn(discoveredServices); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Discover included services on a service adapter.discoverIncludedServices = function( handle, // String handle service handle serviceUUIDs, // String[] serviceUUIDs service UUIDs to restrict results by completeFn, // Function(Object[] serviceInfo) function called when discovery completed errorFn // Function(String errorMsg) function called if error occurs ) { // Not implemented in the BLE plugin. completeFn([]); }; // Discover characteristics on a service adapter.discoverCharacteristics = function( handle, // String handle service handle characteristicUUIDs, // String[] characteristicUUIDs characteristic UUIDs to restrict results by completeFn, // Function(Object[] characteristicInfo) function called when discovery completed errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromServiceHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.characteristics( deviceHandle, handle, function(characteristics) { // Collect found characteristics. var discoveredCharacteristics = []; characteristics.forEach(function(characteristicInfo) { var characteristicUUID = helpers.getCanonicalUUID(characteristicInfo.uuid); // Filter characteristics. var includeCharacteristic = !characteristicUUIDs || 0 === characteristicUUIDs.length || characteristicUUIDs.indexOf(characteristicUUID) >= 0; if (includeCharacteristic) { // Set device for characteristic. mCharacteristicHandleToDeviceHandle[ characteristicInfo.handle] = deviceHandle; // Add the characteristic. // For the characteristic property constants, see: // https://github.com/evothings/cordova-ble/blob/master/ble.js#L256 // Goes without saying they should have symbolic names!! // Created issue: https://github.com/evothings/cordova-ble/issues/90 discoveredCharacteristics.push( { _handle: characteristicInfo.handle, uuid: characteristicUUID, properties: { broadcast: characteristicInfo.property & 1, read: characteristicInfo.property & 2, writeWithoutResponse: (characteristicInfo.property & 4) && // AND or OR? (characteristicInfo.writeType & 1), write: characteristicInfo.property & 8, notify: characteristicInfo.property & 16, indicate: characteristicInfo.property & 32, authenticatedSignedWrites: (characteristicInfo.property & 64) && // AND or OR? (characteristicInfo.writeType & 4), reliableWrite: false, writableAuxiliaries: false } }); } }); // Return result. if (completeFn) { completeFn(discoveredCharacteristics); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Discover descriptors on a characteristic adapter.discoverDescriptors = function( handle, // String handle characteristic handle descriptorUUIDs, // String[] descriptorUUIDs descriptor UUIDs to restrict results by completeFn, // Function(Object[] descriptorInfo) function called when discovery completed errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromCharacteristicHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.descriptors( deviceHandle, handle, function(descriptors) { // Collect found descriptors. var discoveredDescriptors = []; descriptors.forEach(function(descriptorInfo) { var descriptorUUID = helpers.getCanonicalUUID(descriptorInfo.uuid); // If this is the CCCD we save it for use in enableNotify. if (descriptorUUID === '00002902-0000-1000-8000-00805f9b34fb') { mCharacteristicHandleToCCCDHandle[handle] = descriptorInfo.handle; } // Filter descriptors. var includeDescriptor = !descriptorUUIDs || 0 === descriptorUUIDs.length || descriptorUUIDs.indexOf(descriptorUUID) >= 0; if (includeDescriptor) { // Set device for descriptor. mDescriptorHandleToDeviceHandle[descriptorInfo.handle] = deviceHandle; // Add the descriptor. discoveredDescriptors.push( { _handle: descriptorInfo.handle, uuid: descriptorUUID }); } }); // Return result. if (completeFn) { completeFn(discoveredDescriptors); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Read a characteristic value adapter.readCharacteristic = function( handle, // String handle characteristic handle completeFn, // Function(DataView value) function called when read completes errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromCharacteristicHandle(handle, errorFn); if (!deviceHandle) { return; } // TODO: Re-enable notification on iOS if there was one, see issue: // https://github.com/evothings/cordova-ble/issues/61 // Currently we do not work around this limitation. evothings.ble.readCharacteristic( deviceHandle, handle, function(data) { if (completeFn) { completeFn(bufferToDataView(data)); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Write a characteristic value adapter.writeCharacteristic = function( handle, // String handle characteristic handle value, // DataView value value to write completeFn, // Function() function called when write completes errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromCharacteristicHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.writeCharacteristic( deviceHandle, handle, value, function() { if (completeFn) { completeFn(); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Enable value change notifications on a characteristic adapter.enableNotify = function( handle, // String handle characteristic handle notifyFn, // Function(DataView value) function called when value changes completeFn, // Function() function called when notifications enabled errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromCharacteristicHandle(handle, errorFn); if (!deviceHandle) { return; } // TODO: Android needs the CCCD written to for notifications // Should be encapsulated in native android layer, see issue: // https://github.com/evothings/cordova-ble/issues/30 // Write the CCCD regardless of platform, makes no harm on iOS. writeCCCD( deviceHandle, handle, enableNotification, function(error) { if (errorFn) { errorFn(error); } }); function enableNotification() { evothings.ble.enableNotification( deviceHandle, handle, function(data) { if (notifyFn) { notifyFn(bufferToDataView(data)); } }, function(error) { if (errorFn) { errorFn(error); } }); // Notifications "should have" been enabled. if (completeFn) { completeFn(); } } }; // Disable value change notifications on a characteristic adapter.disableNotify = function( handle, // String handle characteristic handle completeFn, // Function() function called when notifications disabled errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromCharacteristicHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.disableNotification( deviceHandle, handle, function() { if (completeFn) { completeFn(); } }, function(error) { if (errorFn) { errorFn(error); } }); // TODO: iOS doesn't call back after disable, see issue: // https://github.com/evothings/cordova-ble/issues/65 // Hack to compensate. if (platformIsIOS()) { setTimeout(completeFn, 0); // Timeout perhaps not needed. } }; // Read a descriptor value adapter.readDescriptor = function( handle, // String handle descriptor handle completeFn, // Function(DataView value) function called when read completes errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromDescriptorHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.readDescriptor( deviceHandle, handle, function(data) { if (completeFn) { completeFn(bufferToDataView(data)); } }, function(error) { if (errorFn) { errorFn(error); } }); }; // Write a descriptor value adapter.writeDescriptor = function( handle, // String handle descriptor handle value, // DataView value value to write completeFn, // Function() function called when write completes errorFn // Function(String errorMsg) function called if error occurs ) { var deviceHandle = getDeviceHandleFromDescriptorHandle(handle, errorFn); if (!deviceHandle) { return; } evothings.ble.writeDescriptor( deviceHandle, handle, value, function() { if (completeFn) { completeFn(); } }, function(error) { if (errorFn) { errorFn(error); } }); }; function disconnectDevice(handle) { var deviceHandle = mDeviceIdToDeviceHandle[handle]; if (deviceHandle) { // Disconnect the device. evothings.ble.close(deviceHandle); // Delete device handle mapping. delete mDeviceIdToDeviceHandle[handle]; // Delete related mappings for service handles etc. deleteDeviceHandleMappings(deviceHandle, mServiceHandleToDeviceHandle); deleteDeviceHandleMappings(deviceHandle, mCharacteristicHandleToDeviceHandle, true); deleteDeviceHandleMappings(deviceHandle, mDescriptorHandleToDeviceHandle); } } function deleteDeviceHandleMappings(deviceHandle, map, isCharateristicsMap) { for (var key in map) { if (deviceHandle === map[key]) { // Delete the mapping. delete map[key]; // If mapping for this key exists (yes it is a hack to do this here). if (isCharateristicsMap && mCharacteristicHandleToCCCDHandle[key]) { delete mCharacteristicHandleToCCCDHandle[key]; } } } } function getDeviceHandleFromDeviceId(handle, errorFn) { var deviceHandle = mDeviceIdToDeviceHandle[handle]; if (!deviceHandle) { if (errorFn) { errorFn('Device does not exist for device id: ' + handle); } return null; } return deviceHandle; } function getDeviceHandleFromServiceHandle(handle, errorFn) { var deviceHandle = mServiceHandleToDeviceHandle[handle]; if (!deviceHandle) { if (errorFn) { errorFn('Device does not exist for service handle: ' + handle); } return null; } return deviceHandle; } function getDeviceHandleFromCharacteristicHandle(handle, errorFn) { var deviceHandle = mCharacteristicHandleToDeviceHandle[handle]; if (!deviceHandle) { if (errorFn) { errorFn('Device does not exist for characteristic handle: ' + handle); } return null; } return deviceHandle; } function writeCCCD(deviceHandle, characteristicHandle, successCallback, errorCallback) { // Do we have a saved descriptor handle from descriptor discovery? var cccdHandle = mCharacteristicHandleToCCCDHandle[characteristicHandle]; if (cccdHandle) { writeTheCCCD(cccdHandle); } else { discoverTheCCCD(); } function writeTheCCCD(cccdHandle) { evothings.ble.writeDescriptor( deviceHandle, cccdHandle, new Uint8Array([1,0]), function() { successCallback(); }, function(error) { errorCallback(error); }); } function discoverTheCCCD() { adapter.discoverDescriptors( characteristicHandle, '00002902-0000-1000-8000-00805f9b34fb', // CCCD UUID function(descriptors) { var cccdHandle = mCharacteristicHandleToCCCDHandle[characteristicHandle]; if (cccdHandle) { writeTheCCCD(cccdHandle); } else { errorCallback('Could not find CCCD for characteristic: ' + characteristicHandle); } }, function(error) { errorCallback(error); return; }); } } /** * Create a Bleat deviceInfo object based on the device info from the BLE plugin. * @param deviceInfo BLE plugin deviceInfo object (source). * @return Bleat deviceInfo object. */ function createBleatDeviceObject(deviceInfo) { // Bleat device object. var device = {}; // Device handle and id. device._handle = deviceInfo.address; device.id = deviceInfo.address; // Use the advertised name as default. Use name in // advertisement data if available (see below). device.name = deviceInfo.name; // Array or service UUIDs (populated below). device.uuids = []; // Object that holds advertisement data. device.adData = {}; // RSSI value. device.adData.rssi = deviceInfo.rssi; // txPower not available. device.adData.txPower = null; // Service data (set below). device.adData.serviceData = {}; // Manufacturer data (set below). device.adData.manufacturerData = null; if (deviceInfo.advertisementData) { parseiOSAdvertisementData(deviceInfo, device); } else if (deviceInfo.scanRecord) { parseScanRecordAdvertisementData(deviceInfo, device); } return device; } /** * @param deviceInfo BLE plugin deviceInfo object (source). * @param device Bleat deviceInfo object (destination). */ function parseiOSAdvertisementData(deviceInfo, device) { // On iOS advertisement data is available in predefined fields. if (deviceInfo.advertisementData) { // Device name. if (deviceInfo.advertisementData.kCBAdvDataLocalName) { device.name = deviceInfo.advertisementData.kCBAdvDataLocalName; } // txPower. if (deviceInfo.advertisementData.kCBAdvDataTxPowerLevel) { device.adData.txPower = deviceInfo.advertisementData.kCBAdvDataTxPowerLevel; } // Service UUIDs. if (deviceInfo.advertisementData.kCBAdvDataServiceUUIDs) { deviceInfo.advertisementData.kCBAdvDataServiceUUIDs.forEach(function(serviceUUID) { device.uuids.push(helpers.getCanonicalUUID(serviceUUID)); }); } // Service data. if (deviceInfo.advertisementData.kCBAdvDataServiceData) { for (var uuid in deviceInfo.advertisementData.kCBAdvDataServiceData) { var data = deviceInfo.advertisementData.kCBAdvDataServiceData[uuid]; device.adData.serviceData[helpers.getCanonicalUUID(uuid)] = bufferToDataView(base64DecToArr(data)); } } // Manufacturer data. // TODO: Create map with company identifier (see Noble adapter). if (deviceInfo.advertisementData.kCBAdvDataManufacturerData) { // Save raw data as well. device.adData.manufacturerDataRaw = deviceInfo.advertisementData.kCBAdvDataManufacturerData; } } } /** * Decode the scan record. Data is encoded using a length byte followed by data. * @param deviceInfo BLE plugin deviceInfo object (source). * @param device Bleat deviceInfo object (destination). */ function parseScanRecordAdvertisementData(deviceInfo, device) { var byteArray = base64DecToArr(deviceInfo.scanRecord); var pos = 0; while (pos < byteArray.length) { var length = byteArray[pos++]; if (length === 0) break; length -= 1; var type = byteArray[pos++]; var i; // Local Name. if (type === 0x08 || type === 0x09) { // Convert UTF8 encoded buffer and strip null characters from the resulting string. device.name = evothings.ble.fromUtf8( new Uint8Array(byteArray.buffer, pos, length)).replace('\0', ''); } // TX Power Level. else if (type === 0x0a) { device.adData.txPower = littleEndianToInt8(byteArray, pos); } // 16-bit Service Class UUID. else if (type === 0x02 || type === 0x03) { for (i = 0; i < length; i += 2) { device.uuids.push( helpers.getCanonicalUUID( littleEndianToUint16(byteArray, pos + i).toString(16))); } } // 32-bit Service Class UUID. else if (type === 0x04 || type === 0x05) { for (i = 0; i < length; i += 4) { device.uuids.push( helpers.getCanonicalUUID( littleEndianToUint32(byteArray, pos + i).toString(16))); } } // 128-bit Service Class UUID. else if (type === 0x06 || type === 0x07) { for (i = 0; i < length; i += 16) { device.uuids.push( helpers.getCanonicalUUID(arrayToUUID(byteArray, pos + i))); } } pos += length; } } /* Not used. function stringToArrayBuffer(string) { var buffer = new ArrayBuffer(string.length); var bufferView = new Uint8Array(buffer); for (var i = 0; i < string.length; ++i) { bufferView[i] = string.charCodeAt(i); } return buffer; } function arrayBufferToString(buffer) { return String.fromCharCode.apply(null, new Uint8Array(buffer)); } */ // Code from https://github.com/evothings/evothings-libraries/blob/master/libs/evothings/easyble/easyble.js // Should be encapsulated in the native Android implementation, see issue: // https://github.com/evothings/cordova-ble/issues/62 function b64ToUint6(nChr) { return nChr > 64 && nChr < 91 ? nChr - 65 : nChr > 96 && nChr < 123 ? nChr - 71 : nChr > 47 && nChr < 58 ? nChr + 4 : nChr === 43 ? 62 : nChr === 47 ? 63 : 0; } function base64DecToArr(sBase64, nBlocksSize) { var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""); var nInLen = sB64Enc.length; var nOutLen = nBlocksSize ? Math.ceil((nInLen * 3 + 1 >> 2) / nBlocksSize) * nBlocksSize : nInLen * 3 + 1 >> 2; var taBytes = new Uint8Array(nOutLen); for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { nMod4 = nInIdx & 3; nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; if (nMod4 === 3 || nInLen - nInIdx === 1) { for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; } nUint24 = 0; } } return taBytes; } /** * Interpret byte buffer as little endian 8 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. */ function littleEndianToInt8(data, offset) { var x = data[offset]; if (x & 0x80) x = x - 256; return x; } function littleEndianToUint16(data, offset) { return (data[offset + 1] << 8) + data[offset]; } function littleEndianToUint32(data, offset) { return (data[offset + 3] << 24) + (data[offset + 2] << 16) + (data[offset + 1] << 8) + data[offset]; } function arrayToUUID(array, offset) { var uuid = ""; for (var i = 0; i < 16; i++) { uuid += ("00" + array[offset + i].toString(16)).slice(-2); } return uuid; } function bufferToDataView(buffer) { // Buffer to ArrayBuffer var arrayBuffer = new Uint8Array(buffer).buffer; return new DataView(arrayBuffer); } /* Not used. function dataViewToBuffer(dataView) { // DataView to TypedArray var typedArray = new Uint8Array(dataView.buffer); return new Buffer(typedArray); }*/ function getPlatform() { if (root.cordova) { return root.cordova.platformId; } else { return null; } } function platformIsIOS() { return 'ios' === getPlatform(); } function platformIsAndroid() { return 'android' === getPlatform(); } });