// File eddystone.dist.js 2016-08-15T11:27:33Z // This file was generated by buildDistFiles.rb // -------------------------------------------------- // File: evothings-dist-base.js // // Set up definitions needed by Evothings JavaScript libraries // distribution files. // // Global holding everything. window.evothings = window.evothings || {}; // Define an empty No Operation function. This function is called // in place of async script loading, since we build a single merged file. evothings.__NOOP_FUN__ = function() {}; // -------------------------------------------------- // File: evothings.js // // Here we define common function such as async script loading and OS detection. ;(function() { window.evothings = window.evothings || {}; /** * @namespace * @description
Functions for loading scripts asynchronously, * detecting platform, and other common application functionality.
* @alias evothings * @public */ var evothings = window.evothings; /* ------------------ Script loading ------------------ */ var mScriptLoadingCounter = 0; var mLoadedScripts = {}; var mScriptsLoadedCallbacks = []; /** * Make sure to catch any DOMContentLoaded events occurring before * asynchronous loading of scripts. Those scripts, like ui.js, should check * this variable before listening for the event. */ evothings.gotDOMContentLoaded = false; window.addEventListener('DOMContentLoaded', function(e) { evothings.gotDOMContentLoaded = true; }) /** * Load a script. * @param {string} url - URL or path to the script. Relative paths are * relative to the HTML file that initiated script loading. * @param {function} successCallback - Optional parameterless function that * will be called when the script has loaded. * @param {function} errorCallback - Optional function that will be called * if loading the script fails, takes an error object as parameter. * @public */ evothings.loadScript = function(url, successCallback, errorCallback) { // If script is already loaded call callback directly and return. if (mLoadedScripts[url] == 'loadingcomplete') { successCallback && successCallback(); return; } // Add script to dictionary of loaded scripts. mLoadedScripts[url] = 'loadingstarted'; ++mScriptLoadingCounter; // Create script tag. var script = document.createElement('script'); script.type = 'text/javascript'; script.src = url; // Bind the onload event. script.onload = function() { // Mark as loaded. mLoadedScripts[url] = 'loadingcomplete'; --mScriptLoadingCounter; // Call success callback if given. successCallback && successCallback(); // Call scripts loaded callbacks if this was the last script loaded. if (0 == mScriptLoadingCounter) { for (var i = 0; i < mScriptsLoadedCallbacks.length; ++i) { var loadedCallback = mScriptsLoadedCallbacks[i]; loadedCallback && loadedCallback(); } // Clear callbacks - should we do this??? mScriptsLoadedCallbacks = []; } }; // onerror fires for things like malformed URLs and 404's. // If this function is called, the matching onload will not be called and // scriptsLoaded will not fire. script.onerror = function(error) { errorCallback && errorCallback(error); }; // Attaching the script tag to the document starts loading the script. document.head.appendChild(script); }; /** * Load array of scripts. * @param {array} array - Array of URL or path name stringa. * Relative paths are relative to the HTML file that initiated * script loading. * @param {function} loadedCallback - Optional parameterless * function called when all scripts in the array has loaded. * @public */ evothings.loadScripts = function(array, loadedCallback) { var lib = array.shift(); if (!lib) { // Array is empty and all scripts are loaded. loadedCallback && loadedCallback(); } else { // Load next script. evothings.loadScript(lib, function() { evothings.loadScripts(array, loadedCallback); }); } }; /** * Experimental. * Mark a script as loaded. This is useful if a script is designed * to be included both in HTML and in JavaScript. * @param {string} pathOrURL - URL or path to the script. Relative paths are * relative to the HTML file that initiated script loading. * @public */ evothings.markScriptAsLoaded = function(pathOrURL) { mLoadedScripts[url] = 'loadingcomplete'; }; /** *Add a callback that will be called when all scripts are loaded.
*It is good practise to always use this function when * loading script asynchronously or using a library that does so.
* @param {function} callback - Parameterless function that will * be called when all scripts have finished loading. * @public */ evothings.scriptsLoaded = function(callback) { // If scripts are already loaded call the callback directly, // else add the callback to the callbacks array. if (0 != Object.keys(mLoadedScripts).length && 0 == mScriptLoadingCounter) { callback && callback(); } else { mScriptsLoadedCallbacks.push(callback); } }; /* ------------------ Debugging ------------------ */ /** * Print a JavaScript object (dictionary). For debugging. * * @param {Object} obj - Object to print. * @param {function} printFun - print function (optional - defaults to * console.log if not given). * * @example * var obj = { company: 'Evothings', field: 'IoT' }; * evothings.printObject(obj); * evothings.printObject(obj, console.log); * * @public */ evothings.printObject = function(obj, printFun) { printFun = printFun || console.log; function print(obj, level) { var indent = new Array(level + 1).join(' '); for (var prop in obj) { if (obj.hasOwnProperty(prop)) { var value = obj[prop]; if (typeof value == 'object') { printFun(indent + prop + ':'); print(value, level + 1); } else { printFun(indent + prop + ': ' + value); } } } } print(obj, 0); }; /* ------------------ Platform check ------------------ */ /** * @namespace * @description Namespace for platform check functions. */ evothings.os = {}; /** * Returns true if current platform is iOS, false if not. * @return {boolean} true if platform is iOS, false if not. * @public */ evothings.os.isIOS = function() { return /iP(hone|ad|od)/.test(navigator.userAgent); }; /** * Returns true if current platform is iOS 7, false if not. * @return {boolean} true if platform is iOS 7, false if not. * @public */ evothings.os.isIOS7 = function() { return /iP(hone|ad|od).*OS 7/.test(navigator.userAgent); }; /** * Returns true if current platform is Android, false if not. * @return {boolean} true if platform is Android, false if not. * @public */ evothings.os.isAndroid = function() { return /Android|android/.test(navigator.userAgent); }; /** * Returns true if current platform is Windows Phone, false if not. * @return {boolean} true if platform is Windows Phone, false if not. * @public */ evothings.os.isWP = function() { return /Windows Phone/.test(navigator.userAgent); }; })(); // -------------------------------------------------- // File: util.js evothings = window.evothings || {}; /** * @namespace * @author Aaron Ardiri * @author Fredrik Eldh * @description Utilities for byte arrays. */ evothings.util = {}; ;(function() { /** * 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. * @public */ evothings.util.littleEndianToInt8 = function(data, offset) { var x = evothings.util.littleEndianToUint8(data, offset) if (x & 0x80) x = x - 256 return x } /** * Interpret byte buffer as unsigned little endian 8 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.littleEndianToUint8 = function(data, offset) { return data[offset] } /** * Interpret byte buffer as little endian 16 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.littleEndianToInt16 = function(data, offset) { return (evothings.util.littleEndianToInt8(data, offset + 1) << 8) + evothings.util.littleEndianToUint8(data, offset) } /** * Interpret byte buffer as unsigned little endian 16 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.littleEndianToUint16 = function(data, offset) { return (evothings.util.littleEndianToUint8(data, offset + 1) << 8) + evothings.util.littleEndianToUint8(data, offset) } /** * Interpret byte buffer as unsigned little endian 32 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.littleEndianToUint32 = function(data, offset) { return (evothings.util.littleEndianToUint8(data, offset + 3) << 24) + (evothings.util.littleEndianToUint8(data, offset + 2) << 16) + (evothings.util.littleEndianToUint8(data, offset + 1) << 8) + evothings.util.littleEndianToUint8(data, offset) } /** * Interpret byte buffer as signed big endian 16 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.bigEndianToInt16 = function(data, offset) { return (evothings.util.littleEndianToInt8(data, offset) << 8) + evothings.util.littleEndianToUint8(data, offset + 1) } /** * Interpret byte buffer as unsigned big endian 16 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.bigEndianToUint16 = function(data, offset) { return (evothings.util.littleEndianToUint8(data, offset) << 8) + evothings.util.littleEndianToUint8(data, offset + 1) } /** * Interpret byte buffer as unsigned big endian 32 bit integer. * Returns converted number. * @param {ArrayBuffer} data - Input buffer. * @param {number} offset - Start of data. * @return Converted number. * @public */ evothings.util.bigEndianToUint32 = function(data, offset) { return (evothings.util.littleEndianToUint8(data, offset) << 24) + (evothings.util.littleEndianToUint8(data, offset + 1) << 16) + (evothings.util.littleEndianToUint8(data, offset + 2) << 8) + evothings.util.littleEndianToUint8(data, offset + 3) } /** * Converts a single Base64 character to a 6-bit integer. * @private */ 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; } /** * Decodes a Base64 string. Returns a Uint8Array. * nBlocksSize is optional. * @param {String} sBase64 * @param {int} nBlocksSize * @return {Uint8Array} * @public */ evothings.util.base64DecToArr = function(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; } /** * Returns the integer i in hexadecimal string form, * with leading zeroes, such that * the resulting string is at least byteCount*2 characters long. * @param {int} i * @param {int} byteCount * @public */ evothings.util.toHexString = function(i, byteCount) { var string = (new Number(i)).toString(16); while(string.length < byteCount*2) { string = '0'+string; } return string; } /** * Takes a ArrayBuffer or TypedArray and returns its hexadecimal representation. * No spaces or linebreaks. * @param data * @public */ evothings.util.typedArrayToHexString = function(data) { // view data as a Uint8Array, unless it already is one. if(data.buffer) { if(!(data instanceof Uint8Array)) data = new Uint8Array(data.buffer); } else if(data instanceof ArrayBuffer) { data = new Uint8Array(data); } else { throw "not an ArrayBuffer or TypedArray."; } var str = ''; for(var i=0; iAn all-in-one file with this library and helper libraries included is * available in file easyble.dist.js. Include this file in index.html (recommended).
*If you include easyble.js
rather than easyble.dist.js
it is safe practise to call function {@link evothings.scriptsLoaded}
* to ensure dependent libraries are loaded before calling functions
* in this library (in this case you also need to have the dependent library folders).
On Android you can disable automatic write of notify/indicate and write * the configuration descriptor yourself, supply an options object as * last parameter, see example below.
* @param {string} serviceUUID - UUID of service that has the given * characteristic (previous versions of this library allowed leaving out * the service UUID, this is unsafe practice and has been deprecated, always * specify the service UUID). * @param {string} characteristicUUID - UUID of characteristic to subscribe to. * @param {evothings.easyble.dataCallback} success - Success callback: * success(data). * @param {evothings.easyble.failCallback} fail - Error callback: fail(error). * @param {evothings.easyble.NotificationOptions} [options] - Optional settings. * @public * @instance * @example * // Example call: * device.enableNotification( * serviceUUID, * characteristicUUID, * function(data) * { * console.log('characteristic data: ' + evothings.ble.fromUtf8(data)); * }, * function(errorCode) * { * console.log('enableNotification error: ' + errorCode); * }); * * // Turn off automatic writing of the config descriptor (for special cases): * device.enableNotification( * serviceUUID, * characteristicUUID, * function(data) * { * console.log('characteristic data: ' + evothings.ble.fromUtf8(data)); * }, * function(errorCode) * { * console.log('enableNotification error: ' + errorCode); * }, * { writeConfigDescriptor: false }); */ device.enableNotification = function(arg1, arg2, arg3, arg4, arg5) { if ('function' == typeof arg2) { // Service UUID is missing. internal.enableNotification(device, arg1, arg2, arg3, arg4); } else { // Service UUID is present. internal.enableServiceNotification(device, arg1, arg2, arg3, arg4, arg5); } }; /** * Deprecated. * Use function {@link evothings.easyble.EasyBLEDevice#enableNotification} * @deprecated * @instance */ device.enableServiceNotification = function( serviceUUID, characteristicUUID, success, fail, options) { internal.enableServiceNotification( device, serviceUUID, characteristicUUID, success, fail, options); }; /** * Unsubscribe from characteristic updates to stop notifications. *On Android you can disable automatic write of notify/indicate and write * the configuration descriptor yourself, supply an options object as * last parameter, see example below.
* @param {string} serviceUUID - UUID of service that has the given * characteristic (previous versions of this library allowed leaving out * the service UUID, this is unsafe practice and has been deprecated, always * specify the service UUID). * @param serviceUUID - UUID of service that has the given characteristic. * @param characteristicUUID - UUID of characteristic to unsubscribe from. * @param {evothings.easyble.emptyCallback} success - Success callback: success() * @param {evothings.easyble.failCallback} fail - Error callback: fail(error) * @param {evothings.easyble.NotificationOptions} [options] - Optional settings. * @public * @instance * @example * // Example call: * device.disableNotification( * serviceUUID, * characteristicUUID, * function() * { * console.log('characteristic notification disabled'); * }, * function(errorCode) * { * console.log('disableNotification error: ' + errorCode); * }); */ device.disableNotification = function(arg1, arg2, arg3, arg4, arg5) { if ('function' == typeof arg2) { // Service UUID is missing. internal.disableNotification(device, arg1, arg2, arg3, arg4); } else { // Service UUID is present. internal.disableServiceNotification(device, arg1, arg2, arg3, arg4, arg5); } }; /** * Deprecated. * Use function {@link evothings.easyble.EasyBLEDevice#disableNotification} * @deprecated * @instance */ device.disableServiceNotification = function( serviceUUID, characteristicUUID, success, fail, options) { internal.disableServiceNotification( device, serviceUUID, characteristicUUID, success, fail, options); }; }; /** * Connect to a device. * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.connectToDevice = function(device, success, fail) { // Check that device is not already connected. if (device.__isConnected) { fail(evothings.easyble.error.DEVICE_ALREADY_CONNECTED); return; } evothings.ble.connect( device.address, // Success callback. function(connectInfo) { // DEBUG LOG console.log('BLE connect state: ' + connectInfo.state); if (connectInfo.state == 2) // connected { device.deviceHandle = connectInfo.deviceHandle; device.__uuidMap = {}; device.__serviceMap = {}; device.__isConnected = true; internal.connectedDevices[device.address] = device; success(device); } else if (connectInfo.state == 0) // disconnected { device.__isConnected = false; internal.connectedDevices[device.address] = null; // TODO: Perhaps this should be redesigned, as disconnect is // more of a status change than an error? What do you think? fail && fail(evothings.easyble.error.DISCONNECTED); } }, // Error callback. function(errorCode) { // DEBUG LOG console.log('BLE connect error: ' + errorCode); // Set isConnected to false on error. device.__isConnected = false; internal.connectedDevices[device.address] = null; fail(errorCode); }); }; /** * Obtain device services, them read characteristics and descriptors * for the services with the given uuid(s). * If serviceUUIDs is null, info is read for all services. * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.readServices = function(device, serviceUUIDs, success, fail) { // Read services. evothings.ble.services( device.deviceHandle, function(services) { // Array that stores services. device.__services = []; for (var i = 0; i < services.length; ++i) { var service = services[i]; service.uuid = service.uuid.toLowerCase(); device.__services.push(service); device.__uuidMap[service.uuid] = service; } internal.readCharacteristicsForServices( device, serviceUUIDs, success, fail); }, function(errorCode) { fail(errorCode); }); }; /** * Read characteristics and descriptors for the services with the given uuid(s). * If serviceUUIDs is null, info for all services are read. * Internal function. * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.readCharacteristicsForServices = function(device, serviceUUIDs, success, fail) { var characteristicsCallbackFun = function(service) { // Array with characteristics for service. service.__characteristics = []; return function(characteristics) { --readCounter; // Decrements the count added by services. readCounter += characteristics.length; for (var i = 0; i < characteristics.length; ++i) { var characteristic = characteristics[i]; characteristic.uuid = characteristic.uuid.toLowerCase(); service.__characteristics.push(characteristic); device.__uuidMap[characteristic.uuid] = characteristic; device.__serviceMap[service.uuid + ':' + characteristic.uuid] = characteristic; // DEBUG LOG //console.log('storing service:characteristic key: ' + service.uuid + ':' + characteristic.uuid); //if (!characteristic) //{ // console.log(' --> characteristic is null!') //} // Read descriptors for characteristic. evothings.ble.descriptors( device.deviceHandle, characteristic.handle, descriptorsCallbackFun(service, characteristic), function(errorCode) { fail(errorCode); }); } }; }; /** * @private */ var descriptorsCallbackFun = function(service, characteristic) { // Array with descriptors for characteristic. characteristic.__descriptors = []; return function(descriptors) { --readCounter; // Decrements the count added by characteristics. for (var i = 0; i < descriptors.length; ++i) { var descriptor = descriptors[i]; descriptor.uuid = descriptor.uuid.toLowerCase(); characteristic.__descriptors.push(descriptor); device.__uuidMap[characteristic.uuid + ':' + descriptor.uuid] = descriptor; device.__serviceMap[service.uuid + ':' + characteristic.uuid + ':' + descriptor.uuid] = descriptor; } if (0 == readCounter) { // Everything is read. success(device); } }; }; // Initialize read counter. readCounter = 0; if (null != serviceUUIDs) { // Read info for service UUIDs. readCounter = serviceUUIDs.length; for (var i = 0; i < serviceUUIDs.length; ++i) { var uuid = serviceUUIDs[i].toLowerCase(); var service = device.__uuidMap[uuid]; if (!service) { fail(evothings.easyble.error.SERVICE_NOT_FOUND + ' ' + uuid); return; } // Read characteristics for service. Will also read descriptors. evothings.ble.characteristics( device.deviceHandle, service.handle, characteristicsCallbackFun(service), function(errorCode) { fail(errorCode); }); } } else { // Read info for all services. readCounter = device.__services.length; for (var i = 0; i < device.__services.length; ++i) { // Read characteristics for service. Will also read descriptors. var service = device.__services[i]; evothings.ble.characteristics( device.deviceHandle, service.handle, characteristicsCallbackFun(service), function(errorCode) { fail(errorCode); }); } } }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.readCharacteristic = function(device, characteristicUUID, success, fail) { characteristicUUID = characteristicUUID.toLowerCase(); var characteristic = device.__uuidMap[characteristicUUID]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + characteristicUUID); return; } evothings.ble.readCharacteristic( device.deviceHandle, characteristic.handle, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.readServiceCharacteristic = function( device, serviceUUID, characteristicUUID, success, fail) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase(); var characteristic = device.__serviceMap[key]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + key); return; } evothings.ble.readCharacteristic( device.deviceHandle, characteristic.handle, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.readDescriptor = function( device, characteristicUUID, descriptorUUID, success, fail) { characteristicUUID = characteristicUUID.toLowerCase(); descriptorUUID = descriptorUUID.toLowerCase(); var descriptor = device.__uuidMap[characteristicUUID + ':' + descriptorUUID]; if (!descriptor) { fail(evothings.easyble.error.DESCRIPTOR_NOT_FOUND + ' ' + descriptorUUID); return; } evothings.ble.readDescriptor( device.deviceHandle, descriptor.handle, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.readServiceDescriptor = function( device, serviceUUID, characteristicUUID, descriptorUUID, success, fail) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase() + ':' + descriptorUUID.toLowerCase(); var descriptor = device.__serviceMap[key]; if (!descriptor) { fail(evothings.easyble.error.DESCRIPTOR_NOT_FOUND + ' ' + key); return; } evothings.ble.readDescriptor( device.deviceHandle, descriptor.handle, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.writeCharacteristic = function( device, characteristicUUID, value, success, fail) { characteristicUUID = characteristicUUID.toLowerCase(); var characteristic = device.__uuidMap[characteristicUUID]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + characteristicUUID); return; } evothings.ble.writeCharacteristic( device.deviceHandle, characteristic.handle, value, function() { success(); }, function(errorCode) { fail(errorCode); }); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.writeServiceCharacteristic = function( device, serviceUUID, characteristicUUID, value, success, fail) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase(); var characteristic = device.__serviceMap[key]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + key); return; } evothings.ble.writeCharacteristic( device.deviceHandle, characteristic.handle, value, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.writeServiceCharacteristicWithoutResponse = function( device, serviceUUID, characteristicUUID, value, success, fail) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase(); // DEBUG LOG //console.log('internal.writeServiceCharacteristicWithoutResponse key: ' + key) //console.log('internal.writeServiceCharacteristicWithoutResponse serviceMap:') for (var theKey in device.__serviceMap) { console.log(' ' + theKey); } var characteristic = device.__serviceMap[key]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + key); return; } evothings.ble.writeCharacteristicWithoutResponse( device.deviceHandle, characteristic.handle, value, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.writeDescriptor = function( device, characteristicUUID, descriptorUUID, value, success, fail) { characteristicUUID = characteristicUUID.toLowerCase(); descriptorUUID = descriptorUUID.toLowerCase(); var descriptor = device.__uuidMap[characteristicUUID + ':' + descriptorUUID]; if (!descriptor) { fail(evothings.easyble.error.DESCRIPTOR_NOT_FOUND + ' ' + descriptorUUID); return; } evothings.ble.writeDescriptor( device.deviceHandle, descriptor.handle, value, function() { success(); }, function(errorCode) { fail(errorCode); }); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.writeServiceDescriptor = function( device, serviceUUID, characteristicUUID, descriptorUUID, value, success, fail) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase() + ':' + descriptorUUID.toLowerCase(); var descriptor = device.__serviceMap[key]; if (!descriptor) { fail(evothings.easyble.error.DESCRIPTOR_NOT_FOUND + ' ' + key); return; } evothings.ble.writeDescriptor( device.deviceHandle, descriptor.handle, value, success, fail); }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.enableNotification = function( device, characteristicUUID, success, fail, options) { characteristicUUID = characteristicUUID.toLowerCase(); var characteristic = device.__uuidMap[characteristicUUID]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + characteristicUUID); return; } evothings.ble.enableNotification( device.deviceHandle, characteristic.handle, success, fail, options); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.enableServiceNotification = function( device, serviceUUID, characteristicUUID, success, fail, options) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase(); var characteristic = device.__serviceMap[key]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + key); return; } evothings.ble.enableNotification( device.deviceHandle, characteristic.handle, success, fail, options); }; /** * Called from evothings.easyble.EasyBLEDevice. * @deprecated Naming is a bit confusing, internally functions * named "xxxServiceYYY" are the "future-safe" onces, but in * the public API functions "xxxYYY" are new "future-safe" * (and backwards compatible). * @private */ internal.disableNotification = function( device, characteristicUUID, success, fail, options) { characteristicUUID = characteristicUUID.toLowerCase(); var characteristic = device.__uuidMap[characteristicUUID]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + characteristicUUID); return; } evothings.ble.disableNotification( device.deviceHandle, characteristic.handle, success, fail, options); }; /** * Called from evothings.easyble.EasyBLEDevice. * @private */ internal.disableServiceNotification = function( device, serviceUUID, characteristicUUID, success, fail, options) { var key = serviceUUID.toLowerCase() + ':' + characteristicUUID.toLowerCase(); var characteristic = device.__serviceMap[key]; if (!characteristic) { fail(evothings.easyble.error.CHARACTERISTIC_NOT_FOUND + ' ' + key); return; } evothings.ble.disableNotification( device.deviceHandle, characteristic.handle, success, fail, options); }; /** * Prints and object for debugging purposes. * @deprecated. Defined here for backwards compatibility. * Use evothings.printObject(). * @public */ evothings.easyble.printObject = evothings.printObject; /** * Reset the BLE hardware. Can be time consuming. * @public */ evothings.easyble.reset = function() { evothings.ble.reset(); }; })(); // -------------------------------------------------- // File: eddystone.js // This library scans for Eddystone beacons and translates their // advertisements into user-friendly variables. // The protocol specification is available at: // https://github.com/google/eddystone ;(function() { // prerequisites evothings.__NOOP_FUN__([ 'libs/evothings/easyble/easyble.js', ]) /** * @namespace * @descriptionLibrary for Eddystone beacons.
*It is safe practise to call function {@link evothings.scriptsLoaded} * to ensure dependent libraries are loaded before calling functions * in this library.
*/ evothings.eddystone = {}; // constants var BLUETOOTH_BASE_UUID = '-0000-1000-8000-00805f9b34fb'; // false when scanning is off. true when on. var isScanning = false; /** * @description Starts scanning for Eddystone devices. *Found devices and errors will be reported to the supplied callbacks.
*Will keep scanning indefinitely until you call stopScan().
*To conserve energy, call stopScan() as soon as you've found the device * you're looking for.
*Calling startScan() while scanning is in progress will produce an error.
* * @param {evothings.eddystone.scanCallback} - Success function called * when a beacon is found. * @param {evothings.eddystone.failCallback} - Error callback: fail(error). * * @public * * @example * evothings.eddystone.startScan( * function(beacon) * { * console.log('Found beacon: ' + beacon.url); * }, * function(error) * { * console.log('Scan error: ' + error); * }); */ evothings.eddystone.startScan = function(scanCallback, failCallback) { // Internal callback variable names. var win = scanCallback; var fail = failCallback; // If scanning is already in progress, fail. if(isScanning) { fail("Scanning already in progress!"); return; } isScanning = true; // The device object given in this callback is reused by easyble. // Therefore we can store data in it and expect to have the data still be there // on the next callback with the same device. evothings.easyble.startScan( // Scan for Eddystone Service UUID. // This enables background scanning on iOS (and Android). ['0000FEAA-0000-1000-8000-00805F9B34FB'], function(device) { // A device might be an Eddystone if it has advertisementData... var ad = device.advertisementData; if(!ad) return; // With serviceData... var sd = ad.kCBAdvDataServiceData; if(!sd) return; // And the 0xFEAA service. var base64data = sd['0000feaa'+BLUETOOTH_BASE_UUID]; if(!base64data) return; var byteArray = evothings.util.base64DecToArr(base64data); // If the data matches one of the Eddystone frame formats, // we can forward it to the user. if(parseFrameUID(device, byteArray, win, fail)) return; if(parseFrameURL(device, byteArray, win, fail)) return; if(parseFrameTLM(device, byteArray, win, fail)) return; }, function(error) { fail(error); }); } /** * @description This function is a parameter to startScan() and * is called when a beacons is discovered/updated. * @callback evothings.eddystone.scanCallback * @param {evothings.eddystone.EddystoneDevice} beacon - Beacon * found during scanning. */ /** * @description This function is called when an operation fails. * @callback evothings.eddystone.failCallback * @param {string} errorString - A human-readable string that * describes the error that occurred. */ /** * @description Object representing a BLE device. Inherits from * {@link evothings.easyble.EasyBLEDevice}. * Which properties are available depends on which packets types broadcasted * by the beacon. Properties may be undefined. Typically properties are populated * as scanning processes. * @typedef {Object} evothings.eddystone.EddystoneDevice * @property {string} url - An Internet URL. * @property {number} txPower - A signed integer, the signal strength in decibels, * factory-measured at a range of 0 meters. * @property {Uint8Array} nid - 10-byte namespace ID. * @property {Uint8Array} bid - 6-byte beacon ID. * @property {number} voltage - Device's battery voltage, in millivolts, * or 0 (zero) if device is not battery-powered. * @property {number} temperature - Device's ambient temperature in 256:ths of * degrees Celcius, or 0x8000 if device has no thermometer. * @property {number} adv_cnt - Count of advertisement frames sent since device's startup. * @property {number} dsec_cnt - Time since device's startup, in deci-seconds * (10 units equals 1 second). */ /** * @description Stop scanning for Eddystone devices. * @public * @example * evothings.eddystone.stopScan(); */ evothings.eddystone.stopScan = function() { evothings.easyble.stopScan(); isScanning = false; } /** * @description Calculate the accuracy (distance in meters) of the beacon. *The beacon distance calculation uses txPower at 1 meters, but the * Eddystone protocol reports the value at 0 meters. 41dBm is the signal * loss that occurs over 1 meter, this value is subtracted by default * from the reported txPower. You can tune the calculation by adding * or subtracting to param txPower.
*
Note that the returned distance value is not accurate, and that * it fluctuates over time. Sampling/filtering over time is recommended * to obtain a stable value.
* @public * @param txPower The txPower of the beacon. * @param rssi The RSSI of the beacon, subtract or add to this value to * tune the dBm strength. 41dBm is subtracted from this value in the * distance algorithm used by calculateAccuracy. * @return Distance in meters, or null if unable to compute distance * (occurs for example when txPower or rssi is undefined). * @example * // Note that beacon.txPower and beacon.rssi many be undefined, * // in which case calculateAccuracy returns null. This happens * // before txPower and rssi have been reported by the beacon. * var distance = evothings.eddystone.calculateAccuracy( * beacon.txPower, beacon.rssi); */ evothings.eddystone.calculateAccuracy = function(txPower, rssi) { if (!rssi || rssi >= 0 || !txPower) { return null } // Algorithm // http://developer.radiusnetworks.com/2014/12/04/fundamentals-of-beacon-ranging.html // http://stackoverflow.com/questions/21338031/radius-networks-ibeacon-ranging-fluctuation // The beacon distance formula uses txPower at 1 meters, but the Eddystone // protocol reports the value at 0 meters. 41dBm is the signal loss that // occurs over 1 meter, so we subtract that from the reported txPower. var ratio = rssi * 1.0 / (txPower - 41) if (ratio < 1.0) { return Math.pow(ratio, 10) } else { var accuracy = (0.89976) * Math.pow(ratio, 7.7095) + 0.111 return accuracy } } /** * Create a low-pass filter. * @param cutOff The filter cut off value. * @return Object with two functions: filter(value), value() * @example * // Create filter with cut off 0.8 * var lowpass = evothings.eddystone.createLowPassFilter(0.8) * // Filter value (returns current filter value) * distance = lowpass.filter(distance) * // Get current value * distance = lowpass.value() */ evothings.eddystone.createLowPassFilter = function(cutOff, state) { // Filter cut off. if (undefined === cutOff) { cutOff = 0.8 } // Current value of the filter. if (undefined === state) { state = 0.0 } // Return object with filter functions. return { // This function will filter the given value. // Returns the current value of the filter. filter: function(value) { state = (value * (1.0 - cutOff)) + (state * cutOff) return state }, // This function returns the current value of the filter. value: function() { return state } } } // Return true on frame type recognition, false otherwise. function parseFrameUID(device, data, win, fail) { if(data[0] != 0x00) return false; // The UID frame has 18 bytes + 2 bytes reserved for future use // https://github.com/google/eddystone/tree/master/eddystone-uid // Check that we got at least 18 bytes. if(data.byteLength < 18) { fail("UID frame: invalid byteLength: "+data.byteLength); return true; } device.txPower = evothings.util.littleEndianToInt8(data, 1); device.nid = data.subarray(2, 12); // Namespace ID. device.bid = data.subarray(12, 18); // Beacon ID. win(device); return true; } function parseFrameURL(device, data, win, fail) { if(data[0] != 0x10) return false; if(data.byteLength < 4) { fail("URL frame: invalid byteLength: "+data.byteLength); return true; } device.txPower = evothings.util.littleEndianToInt8(data, 1); // URL scheme prefix var url; switch(data[2]) { case 0: url = 'http://www.'; break; case 1: url = 'https://www.'; break; case 2: url = 'http://'; break; case 3: url = 'https://'; break; default: fail("URL frame: invalid prefix: "+data[2]); return true; } // Process each byte in sequence. var i = 3; while(i < data.byteLength) { var c = data[i]; // A byte is either a top-domain shortcut, or a printable ascii character. if(c < 14) { switch(c) { case 0: url += '.com/'; break; case 1: url += '.org/'; break; case 2: url += '.edu/'; break; case 3: url += '.net/'; break; case 4: url += '.info/'; break; case 5: url += '.biz/'; break; case 6: url += '.gov/'; break; case 7: url += '.com'; break; case 8: url += '.org'; break; case 9: url += '.edu'; break; case 10: url += '.net'; break; case 11: url += '.info'; break; case 12: url += '.biz'; break; case 13: url += '.gov'; break; } } else if(c < 32 || c >= 127) { // Unprintables are not allowed. fail("URL frame: invalid character: "+data[2]); return true; } else { url += String.fromCharCode(c); } i += 1; } // Set URL field of the device. device.url = url; win(device); return true; } function parseFrameTLM(device, data, win, fail) { if(data[0] != 0x20) return false; if(data[1] != 0x00) { fail("TLM frame: unknown version: "+data[1]); return true; } if(data.byteLength != 14) { fail("TLM frame: invalid byteLength: "+data.byteLength); return true; } device.voltage = evothings.util.bigEndianToUint16(data, 2); var temp = evothings.util.bigEndianToUint16(data, 4); if(temp == 0x8000) { device.temperature = 0x8000; } else { device.temperature = evothings.util.bigEndianToInt16(data, 4) / 256.0; } device.adv_cnt = evothings.util.bigEndianToUint32(data, 6); device.dsec_cnt = evothings.util.bigEndianToUint32(data, 10); win(device); return true; } var isAdvertising = false; function isValidHex(str, len) { return typeof str === "string" && str.length === len * 2 && /^[0-9A-F]+$/.test(str) } function txPowerLevel() { // ADVERTISE_TX_POWER_MEDIUM taken from // txeddystone_uid/MainActivity.java#L324-L340 (https://goo.gl/MqWqUu) return -26 } // From [txeddystone_uid/MainActivity.java](https://goo.gl/VGtnN1) function toByteArray(hexString) { var out = []; for (var i = 0; i < hexString.length; i += 2) { out.push((parseInt(hexString.charAt(i), 16) << 4) + parseInt(hexString.charAt(i + 1), 16)); } return out; } // Variable that points to the Cordova Base64 module, loaded lazily. var base64; function buildServiceData(namespace, instance) { if (!base64) { base64 = cordova.require('cordova/base64'); } var data = [0, txPowerLevel()]; Array.prototype.push.apply(data, toByteArray(namespace)); Array.prototype.push.apply(data, toByteArray(instance)); return base64.fromArrayBuffer(new Uint8Array(data)); } /** * @description Start Advertising Eddystone Beacon. *
Namespace may only be 10 hex bytes
*Instance may only be 6 hex bytes
*Will keep advertising indefinitely until you call stopAdvertise().
* @public * @param {string} namespace - Hex namespace string (10 bytes, 20 chars) * @param {string} instance - Hex instance string (6 bytes, 12 chars) * @param {evothings.eddystone.advertiseCallback} - Success function called when advertising started * @param {evothings.eddystone.failCallback} - Error callback: fail(error). * @example * evothings.eddystone.startAdvertise("0123456789ABCDEF0123", "0123456789AB"); */ evothings.eddystone.startAdvertise = function(namespace, instance, advertiseCallback, failCallback) { // Internal callback variable names. var win = advertiseCallback; var fail = failCallback; if(isAdvertising) { fail("Advertising already in progress!"); return; } if (!isValidHex(namespace, 10)) { fail("Invalid namespace, must be 10 hex bytes"); return; } if (!isValidHex(instance, 6)) { fail("Invalid instance, must be 6 hex bytes"); return; } var serviceData = buildServiceData(namespace, instance); var transmitData = { serviceUUIDs: ["0000FEAA-0000-1000-8000-00805F9B34FB"], serviceData: { "0000FEAA-0000-1000-8000-00805F9B34FB": serviceData } }; var settings = { broadcastData: transmitData, scanResponseData: transmitData }; evothings.ble.peripheral.startAdvertise( settings, function() { isAdvertising = true; win() }, function(error) { isAdvertising = false; fail(error) }); } /** * @description Stop Advertising Eddystone Beacon. * @public * @param {Function} onSuccess - Success function called when advertising stoppped * @param {evothings.eddystone.failCallback} - Error callback: fail(error). * @example * evothings.eddystone.stopAdvertise(); */ evothings.eddystone.stopAdvertise = function(onSuccess, failCallback) { isAdvertising = false; evothings.ble.peripheral.stopAdvertise(onSuccess, failCallback); } })(); // --------------------------------------------------