Source: easyble.js

/**
 * File: easyble.js
 * Description: Library for making BLE programming easier.
 * Author: Miki
 *
 * Note: The object type called "device" below, is the "DeviceInfo"
 * object obtained by calling evothings.ble.startScan, enhanced with
 * additional properties and functions to allow easy access to
 * object methods. Properties are also added to the Characteristic
 * and Descriptor object. Added properties are prefixed with two
 * underscores.
 */

// We assume the "evothings" library was loaded first.

// Load dependent script.
evothings.loadScript('libs/evothings/util/util.js');

// Object that holds BLE data and functions.
evothings.easyble = (function()
{
	var base64 = cordova.require('cordova/base64');

	/** Main object in the EasyBLE API. */
	var easyble = {};

	/**
	 * Set to true to report found devices only once,
	 * set to false to report continuously.
	 */
	var reportDeviceOnce = false;

	var serviceFilter = false;

	/** Internal properties and functions. */
	var internal = {};

	/** Internal variable used to track reading of service data. */
	var readCounter = 0;

	/** Table of discovered devices. */
	internal.knownDevices = {};

	/** Table of connected devices. */
	internal.connectedDevices = {};

	/**
	 * Set to true to report found devices only once.
	 * Set to false to report continuously.
	 * The default is to report continously.
	 */
	easyble.reportDeviceOnce = function(reportOnce)
	{
		reportDeviceOnce = reportOnce;
	};

	/**
	* Set to an Array of UUID strings to enable filtering of devices found by startScan().
	* Set to false to disable filtering.
	* The default is to not filter.
	* An empty array will cause no devices to be reported.
	*/
	easyble.filterDevicesByService = function(services)
	{
		serviceFilter = services;
	};

	/** Start scanning for devices. */
	easyble.startScan = function(win, fail)
	{
		easyble.stopScan();
		internal.knownDevices = {};
		evothings.ble.startScan(function(device)
		{
			// Ensure we have advertisementData.
			internal.ensureAdvertisementData(device);

			// Check if the device matches the filter, if we have a filter.
			if(!internal.deviceMatchesServiceFilter(device)) {
				return;
			}

			// Check if we already have got the device.
			var existingDevice = internal.knownDevices[device.address]
			if (existingDevice)
			{
				// Do not report device again if flag is set.
				if (reportDeviceOnce) { return; }

				// Flag not set, report device again.
				existingDevice.rssi = device.rssi;
				existingDevice.name = device.name;
				existingDevice.scanRecord = device.scanRecord;
				existingDevice.advertisementData = device.advertisementData;
				win(existingDevice);
				return;
			}

			// New device, add to known devices.
			internal.knownDevices[device.address] = device;

			// Add methods to the device info object.
			internal.addMethodsToDeviceObject(device);

			// Call callback function with device info.
			win(device);
		},
		function(errorCode)
		{
			fail(errorCode);
		});
	};

	/** Stop scanning for devices. */
	easyble.stopScan = function()
	{
		evothings.ble.stopScan();
	};

	/** Close all connected devices. */
	easyble.closeConnectedDevices = function()
	{
		for (var key in internal.connectedDevices)
		{
			var device = internal.connectedDevices[key];
			device && device.close();
			internal.connectedDevices[key] = null;
		}
	};

	/**
	* If device has advertisementData, does nothing.
	* If device instead has scanRecord, creates advertisementData.
	* See ble.js for AdvertisementData reference.
	*/
	internal.ensureAdvertisementData = function(device)
	{
		// If device object already has advertisementData we
		// do not need to parse the scanRecord.
		if (device.advertisementData) { return; }

		// Must have scanRecord yo continue.
		if (!device.scanRecord) { return; }

		// Here we parse BLE/GAP Scan Response Data.
		// See the Bluetooth Specification, v4.0, Volume 3, Part C, Section 11,
		// for details.

		var byteArray = evothings.util.base64DecToArr(device.scanRecord);
		var pos = 0;
		var advertisementData = {};
		var serviceUUIDs;
		var serviceData;

		// The scan record is a list of structures.
		// Each structure has a length byte, a type byte, and (length-1) data bytes.
		// The format of the data bytes depends on the type.
		// Malformed scanRecords will likely cause an exception in this function.
		while (pos < byteArray.length)
		{
			var length = byteArray[pos++];
			if (length == 0)
			{
				break;
			}
			length -= 1;
			var type = byteArray[pos++];

			// Parse types we know and care about.
			// Skip other types.

			var BLUETOOTH_BASE_UUID = '-0000-1000-8000-00805f9b34fb'

			// Convert 16-byte Uint8Array to RFC-4122-formatted UUID.
			function arrayToUUID(array, offset)
			{
				var k=0;
				var string = '';
				var UUID_format = [4, 2, 2, 2, 6];
				for (var l=0; l<UUID_format.length; l++)
				{
					if (l != 0)
					{
						string += '-';
					}
					for (var j=0; j<UUID_format[l]; j++, k++)
					{
						string += evothings.util.toHexString(array[offset+k], 1);
					}
				}
				return string;
			}

			if (type == 0x02 || type == 0x03) // 16-bit Service Class UUIDs.
			{
				serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
				for(var i=0; i<length; i+=2)
				{
					serviceUUIDs.push(
						'0000' +
						evothings.util.toHexString(
							evothings.util.littleEndianToUint16(byteArray, pos + i),
							2) +
						BLUETOOTH_BASE_UUID);
				}
			}
			if (type == 0x04 || type == 0x05) // 32-bit Service Class UUIDs.
			{
				serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
				for (var i=0; i<length; i+=4)
				{
					serviceUUIDs.push(
						evothings.util.toHexString(
							evothings.util.littleEndianToUint32(byteArray, pos + i),
							4) +
						BLUETOOTH_BASE_UUID);
				}
			}
			if (type == 0x06 || type == 0x07) // 128-bit Service Class UUIDs.
			{
				serviceUUIDs = serviceUUIDs ? serviceUUIDs : [];
				for (var i=0; i<length; i+=16)
				{
					serviceUUIDs.push(arrayToUUID(byteArray, pos + i));
				}
			}
			if (type == 0x08 || type == 0x09) // Local Name.
			{
				advertisementData.kCBAdvDataLocalName = evothings.ble.fromUtf8(
					new Uint8Array(byteArray.buffer, pos, length));
			}
			if (type == 0x0a) // TX Power Level.
			{
				advertisementData.kCBAdvDataTxPowerLevel =
					evothings.util.littleEndianToInt8(byteArray, pos);
			}
			if (type == 0x16) // Service Data, 16-bit UUID.
			{
				serviceData = serviceData ? serviceData : {};
				var uuid =
					'0000' +
					evothings.util.toHexString(
						evothings.util.littleEndianToUint16(byteArray, pos),
						2) +
					BLUETOOTH_BASE_UUID;
				var data = new Uint8Array(byteArray.buffer, pos+2, length-2);
				serviceData[uuid] = base64.fromArrayBuffer(data);
			}
			if (type == 0x20) // Service Data, 32-bit UUID.
			{
				serviceData = serviceData ? serviceData : {};
				var uuid =
					evothings.util.toHexString(
						evothings.util.littleEndianToUint32(byteArray, pos),
						4) +
					BLUETOOTH_BASE_UUID;
				var data = new Uint8Array(byteArray.buffer, pos+4, length-4);
				serviceData[uuid] = base64.fromArrayBuffer(data);
			}
			if (type == 0x21) // Service Data, 128-bit UUID.
			{
				serviceData = serviceData ? serviceData : {};
				var uuid = arrayToUUID(byteArray, pos);
				var data = new Uint8Array(byteArray.buffer, pos+16, length-16);
				serviceData[uuid] = base64.fromArrayBuffer(data);
			}
			if (type == 0xff) // Manufacturer-specific Data.
			{
				// Annoying to have to transform base64 back and forth,
				// but it has to be done in order to maintain the API.
				advertisementData.kCBAdvDataManufacturerData =
					base64.fromArrayBuffer(new Uint8Array(byteArray.buffer, pos, length));
			}

			pos += length;
		}
		advertisementData.kCBAdvDataServiceUUIDs = serviceUUIDs;
		advertisementData.kCBAdvDataServiceData = serviceData;
		device.advertisementData = advertisementData;

		/*
		// Log raw data for debugging purposes.
		var srs = ''
		for(var i=0; i<byteArray.length; i++) {
			srs += evothings.util.toHexString(byteArray[i], 1);
		}
		console.log("scanRecord: "+srs);

		console.log(JSON.stringify(advertisementData));
		*/
	}

	/**
	* Returns true if the device matches the serviceFilter, or if there is no filter.
	* Returns false otherwise.
	*/
	internal.deviceMatchesServiceFilter = function(device)
	{
		if (!serviceFilter) { return true; }

		var advertisementData = device.advertisementData;
		if (advertisementData)
		{
			if (advertisementData.kCBAdvDataServiceUUIDs)
			{
				for (var i in advertisementData)
				{
					for (var j in serviceFilter)
					{
						if (advertisementData[i].toLowerCase() ==
							serviceFilter[j].toLowerCase())
						{
							return true;
						}
					}
				}
			}
		}
		return false;
	}

	/**
	 * Add functions to the device object to allow calling them
	 * in an object-oriented style.
	 */
	internal.addMethodsToDeviceObject = function(device)
	{
		/** Connect to the device. */
		device.connect = function(win, fail)
		{
			internal.connectToDevice(device, win, fail);
		};

		/** Close the device. */
		device.close = function()
		{
			device.deviceHandle && evothings.ble.close(device.deviceHandle);
		};

		/** Read devices RSSI. Device must be connected. */
		device.readRSSI = function(win, fail)
		{
			evothings.ble.rssi(device.deviceHandle, win, fail);
		};

		/**
		 * Read all service info for the specified service UUIDs.
		 * @param serviceUUIDs - array of UUID strings
		 * @param win - success callback
		 * @param fail - error callback
		 * If serviceUUIDs is null, info for all services is read
		 * (this can be time-consuming compared to reading a
		 * selected number of services).
		 */
		device.readServices = function(serviceUUIDs, win, fail)
		{
			internal.readServices(device, serviceUUIDs, win, fail);
		};

		/** Read value of characteristic. */
		device.readCharacteristic = function(characteristicUUID, win, fail)
		{
			internal.readCharacteristic(device, characteristicUUID, win, fail);
		};

		/** Read value of descriptor. */
		device.readDescriptor = function(characteristicUUID, descriptorUUID, win, fail)
		{
			internal.readDescriptor(device, characteristicUUID, descriptorUUID, win, fail);
		};

		/** Write value of characteristic. */
		device.writeCharacteristic = function(characteristicUUID, value, win, fail)
		{
			internal.writeCharacteristic(device, characteristicUUID, value, win, fail);
		};

		/** Write value of descriptor. */
		device.writeDescriptor = function(characteristicUUID, descriptorUUID, value, win, fail)
		{
			internal.writeDescriptor(device, characteristicUUID, descriptorUUID, value, win, fail);
		};

		/** Subscribe to characteristic value updates. */
		device.enableNotification = function(characteristicUUID, win, fail)
		{
			internal.enableNotification(device, characteristicUUID, win, fail);
		};

		/** Unsubscribe from characteristic updates. */
		device.disableNotification = function(characteristicUUID, win, fail)
		{
			internal.disableNotification(device, characteristicUUID, win, fail);
		};
	};

	/** Connect to a device. */
	internal.connectToDevice = function(device, win, fail)
	{
		evothings.ble.connect(device.address, function(connectInfo)
		{
			if (connectInfo.state == 2) // connected
			{
				device.deviceHandle = connectInfo.deviceHandle;
				device.__uuidMap = {};
				internal.connectedDevices[device.address] = device;

				win(device);
			}
			else if (connectInfo.state == 0) // disconnected
			{
				internal.connectedDevices[device.address] = null;
				// TODO: How to signal disconnect?
				// Call error callback?
				// Additional callback? (connect, disconnect, fail)
				// Additional parameter on win callback with connect state?
				// (Last one is the best option I think).
				fail && fail('disconnected');
			}
		},
		function(errorCode)
		{
			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.
	 */
	internal.readServices = function(device, serviceUUIDs, win, 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, win, 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.
	 */
	internal.readCharacteristicsForServices = function(device, serviceUUIDs, win, 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;

					// Read descriptors for characteristic.
					evothings.ble.descriptors(
						device.deviceHandle,
						characteristic.handle,
						descriptorsCallbackFun(characteristic),
						function(errorCode)
						{
							fail(errorCode);
						});
				}
			};
		};

		var descriptorsCallbackFun = function(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;
				}
				if (0 == readCounter)
				{
					// Everything is read.
					win(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('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);
					});
			}
		}
	};

	internal.readCharacteristic = function(device, characteristicUUID, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();

		var characteristic = device.__uuidMap[characteristicUUID];
		if (!characteristic)
		{
			fail('Characteristic not found: ' + characteristicUUID);
			return;
		}

		evothings.ble.readCharacteristic(
			device.deviceHandle,
			characteristic.handle,
			win,
			fail);
	};

	internal.readDescriptor = function(device, characteristicUUID, descriptorUUID, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();
		descriptorUUID = descriptorUUID.toLowerCase();

		var descriptor = device.__uuidMap[characteristicUUID + ':' + descriptorUUID];
		if (!descriptor)
		{
			fail('Descriptor not found: ' + descriptorUUID);
			return;
		}

		evothings.ble.readDescriptor(
			device.deviceHandle,
			descriptor.handle,
			value,
			function()
			{
				win();
			},
			function(errorCode)
			{
				fail(errorCode);
			});
	};

	internal.writeCharacteristic = function(device, characteristicUUID, value, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();

		var characteristic = device.__uuidMap[characteristicUUID];
		if (!characteristic)
		{
			fail('Characteristic not found: ' + characteristicUUID);
			return;
		}

		evothings.ble.writeCharacteristic(
			device.deviceHandle,
			characteristic.handle,
			value,
			function()
			{
				win();
			},
			function(errorCode)
			{
				fail(errorCode);
			});
	};

	internal.writeDescriptor = function(device, characteristicUUID, descriptorUUID, value, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();
		descriptorUUID = descriptorUUID.toLowerCase();

		var descriptor = device.__uuidMap[characteristicUUID + ':' + descriptorUUID];
		if (!descriptor)
		{
			fail('Descriptor not found: ' + descriptorUUID);
			return;
		}

		evothings.ble.writeDescriptor(
			device.deviceHandle,
			descriptor.handle,
			value,
			function()
			{
				win();
			},
			function(errorCode)
			{
				fail(errorCode);
			});
	};

	internal.enableNotification = function(device, characteristicUUID, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();

		var characteristic = device.__uuidMap[characteristicUUID];
		if (!characteristic)
		{
			fail('Characteristic not found: ' + characteristicUUID);
			return;
		}

		evothings.ble.enableNotification(
			device.deviceHandle,
			characteristic.handle,
			win,
			fail);
	};

	internal.disableNotification = function(device, characteristicUUID, win, fail)
	{
		characteristicUUID = characteristicUUID.toLowerCase();

		var characteristic = device.__uuidMap[characteristicUUID];
		if (!characteristic)
		{
			fail('Characteristic not found: ' + characteristicUUID);
			return;
		}

		evothings.ble.disableNotification(
			device.deviceHandle,
			characteristic.handle,
			win,
			fail);
	};

	// Deprecated. Defined here for backwards compatibility.
	easyble.printObject = evothings.printObject;

	easyble.reset = function()
	{
		evothings.ble.reset();
	};

	return easyble;
})();