UNPKG

bleat

Version:

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

320 lines (292 loc) 15.9 kB
/* @license * * BLE Abstraction Tool: noble 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. define(['noble', 'bleat', 'bluetooth.helpers'], factory); } else if (typeof exports === 'object') { // Node. Does not work with strict CommonJS module.exports = function(bleat) { return factory(require('noble'), bleat, require('./bluetooth.helpers')); }; } else { // Browser globals with support for web workers (root is window) factory(root.noble, root.bleat, root.bleatHelpers); } }(this, function(noble, bleat, helpers) { "use strict"; // Guard against bleat being navigator.bluetooth if (!bleat._addAdapter) return; function checkForError(errorFn, continueFn) { return function(error) { if (error) errorFn(error); else if (typeof continueFn === "function") { var args = [].slice.call(arguments, 1); continueFn.apply(this, args); } }; } function bufferToDataView(buffer) { // Buffer to ArrayBuffer var arrayBuffer = new Uint8Array(buffer).buffer; return new DataView(arrayBuffer); } function dataViewToBuffer(dataView) { // DataView to TypedArray var typedArray = new Uint8Array(dataView.buffer); return new Buffer(typedArray); } // https://github.com/sandeepmistry/noble if (noble) { bleat._addAdapter("noble", { deviceHandles: {}, serviceHandles: {}, characteristicHandles: {}, descriptorHandles: {}, charNotifies: {}, foundFn: null, initialised: false, init: function(continueFn, errorFn) { if (this.initialised) return continueFn(); noble.on('discover', function(deviceInfo) { if (this.foundFn) { var deviceID = (deviceInfo.address && deviceInfo.address !== "unknown") ? deviceInfo.address : deviceInfo.id; if (!this.deviceHandles[deviceID]) this.deviceHandles[deviceID] = deviceInfo; var serviceUUIDs = []; if (deviceInfo.advertisement.serviceUuids) { deviceInfo.advertisement.serviceUuids.forEach(function(serviceUUID) { serviceUUIDs.push(helpers.getCanonicalUUID(serviceUUID)); }); } var manufacturerData = {}; if (deviceInfo.advertisement.manufacturerData) { // First 2 bytes are 16-bit company identifier var company = deviceInfo.advertisement.manufacturerData.readUInt16LE(0); company = ("0000" + company.toString(16)).slice(-4); // Remove company ID var buffer = deviceInfo.advertisement.manufacturerData.slice(2); manufacturerData[company] = bufferToDataView(buffer); } var serviceData = {}; if (deviceInfo.advertisement.serviceData) { deviceInfo.advertisement.serviceData.forEach(function(serviceAdvert) { serviceData[helpers.getCanonicalUUID(serviceAdvert.uuid)] = bufferToDataView(serviceAdvert.data); }); } this.foundFn({ _handle: deviceID, id: deviceID, name: deviceInfo.advertisement.localName, uuids: serviceUUIDs, adData: { manufacturerData: manufacturerData, serviceData: serviceData, txPower: deviceInfo.advertisement.txPowerLevel, rssi: deviceInfo.rssi } }); } }.bind(this)); this.initialised = true; continueFn(); }, startScan: function(serviceUUIDs, foundFn, completeFn, errorFn) { this.init(function() { this.deviceHandles = {}; var stateCB = function(state) { if (state === "poweredOn") { if (serviceUUIDs.length === 0) this.foundFn = foundFn; else this.foundFn = function(device) { serviceUUIDs.forEach(function(serviceUUID) { if (device.uuids.indexOf(serviceUUID) >= 0) { foundFn(device); return; } }); }; noble.startScanning([], false, checkForError(errorFn, completeFn)); } else errorFn("adapter not enabled"); }.bind(this); if (noble.state === "unknown") noble.once('stateChange', stateCB.bind(this)); else stateCB(noble.state); }.bind(this), errorFn); }, stopScan: function(errorFn) { this.foundFn = null; noble.stopScanning(); }, connect: function(handle, connectFn, disconnectFn, errorFn) { var baseDevice = this.deviceHandles[handle]; baseDevice.once("connect", connectFn); baseDevice.once("disconnect", function() { this.serviceHandles = {}; this.characteristicHandles = {}; this.descriptorHandles = {}; this.charNotifies = {}; disconnectFn(); }.bind(this)); baseDevice.connect(checkForError(errorFn)); }, disconnect: function(handle, errorFn) { this.deviceHandles[handle].disconnect(checkForError(errorFn)); }, discoverServices: function(handle, serviceUUIDs, completeFn, errorFn) { var baseDevice = this.deviceHandles[handle]; baseDevice.discoverServices([], checkForError(errorFn, function(services) { var discovered = []; services.forEach(function(serviceInfo) { var serviceUUID = helpers.getCanonicalUUID(serviceInfo.uuid); if (serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { if (!this.serviceHandles[serviceUUID]) this.serviceHandles[serviceUUID] = serviceInfo; discovered.push({ _handle: serviceUUID, uuid: serviceUUID, primary: true }); } }, this); completeFn(discovered); }.bind(this))); }, discoverIncludedServices: function(handle, serviceUUIDs, completeFn, errorFn) { var serviceInfo = this.serviceHandles[handle]; serviceInfo.discoverIncludedServices([], checkForError(errorFn, function(services) { var discovered = []; services.forEach(function(serviceInfo) { var serviceUUID = helpers.getCanonicalUUID(serviceInfo.uuid); if (serviceUUIDs.length === 0 || serviceUUIDs.indexOf(serviceUUID) >= 0) { if (!this.serviceHandles[serviceUUID]) this.serviceHandles[serviceUUID] = serviceInfo; discovered.push({ _handle: serviceUUID, uuid: serviceUUID, primary: false }); } }, this); completeFn(discovered); }.bind(this))); }, discoverCharacteristics: function(handle, characteristicUUIDs, completeFn, errorFn) { var serviceInfo = this.serviceHandles[handle]; serviceInfo.discoverCharacteristics([], checkForError(errorFn, function(characteristics) { var discovered = []; characteristics.forEach(function(characteristicInfo) { var charUUID = helpers.getCanonicalUUID(characteristicInfo.uuid); if (characteristicUUIDs.length === 0 || characteristicUUIDs.indexOf(charUUID) >= 0) { if (!this.characteristicHandles[charUUID]) this.characteristicHandles[charUUID] = characteristicInfo; discovered.push({ _handle: charUUID, uuid: charUUID, properties: { broadcast: (characteristicInfo.properties.indexOf("broadcast") >= 0), read: (characteristicInfo.properties.indexOf("read") >= 0), writeWithoutResponse: (characteristicInfo.properties.indexOf("writeWithoutResponse") >= 0), write: (characteristicInfo.properties.indexOf("write") >= 0), notify: (characteristicInfo.properties.indexOf("notify") >= 0), indicate: (characteristicInfo.properties.indexOf("indicate") >= 0), authenticatedSignedWrites: (characteristicInfo.properties.indexOf("authenticatedSignedWrites") >= 0), reliableWrite: (characteristicInfo.properties.indexOf("reliableWrite") >= 0), writableAuxiliaries: (characteristicInfo.properties.indexOf("writableAuxiliaries") >= 0) } }); characteristicInfo.on('data', function(data, isNotification) { if (isNotification === true && typeof this.charNotifies[charUUID] === "function") { var dataView = bufferToDataView(data); this.charNotifies[charUUID](dataView); } }.bind(this)); } }, this); completeFn(discovered); }.bind(this))); }, discoverDescriptors: function(handle, descriptorUUIDs, completeFn, errorFn) { var characteristicInfo = this.characteristicHandles[handle]; characteristicInfo.discoverDescriptors(checkForError(errorFn, function(descriptors) { var discovered = []; descriptors.forEach(function(descriptorInfo) { var descUUID = helpers.getCanonicalUUID(descriptorInfo.uuid); if (descriptorUUIDs.length === 0 || descriptorUUIDs.indexOf(descUUID) >= 0) { var descHandle = characteristicInfo.uuid + "-" + descriptorInfo.uuid; if (!this.descriptorHandles[descHandle]) this.descriptorHandles[descHandle] = descriptorInfo; discovered.push({ _handle: descHandle, uuid: descUUID }); } }, this); completeFn(discovered); }.bind(this))); }, readCharacteristic: function(handle, completeFn, errorFn) { this.characteristicHandles[handle].read(checkForError(errorFn, function(data) { var dataView = bufferToDataView(data); completeFn(dataView); })); }, writeCharacteristic: function(handle, dataView, completeFn, errorFn) { var buffer = dataViewToBuffer(dataView); this.characteristicHandles[handle].write(buffer, true, checkForError(errorFn, completeFn)); }, enableNotify: function(handle, notifyFn, completeFn, errorFn) { if (this.charNotifies[handle]) { this.charNotifies[handle] = notifyFn; return completeFn(); } this.characteristicHandles[handle].once("notify", function(state) { if (state !== true) return errorFn("notify failed to enable"); this.charNotifies[handle] = notifyFn; completeFn(); }.bind(this)); this.characteristicHandles[handle].notify(true, checkForError(errorFn)); }, disableNotify: function(handle, completeFn, errorFn) { if (!this.charNotifies[handle]) { return completeFn(); } this.characteristicHandles[handle].once("notify", function(state) { if (state !== false) return errorFn("notify failed to disable"); if (this.charNotifies[handle]) delete this.charNotifies[handle]; completeFn(); }.bind(this)); this.characteristicHandles[handle].notify(false, checkForError(errorFn)); }, readDescriptor: function(handle, completeFn, errorFn) { this.descriptorHandles[handle].readValue(checkForError(errorFn, function(data) { var dataView = bufferToDataView(data); completeFn(dataView); })); }, writeDescriptor: function(handle, dataView, completeFn, errorFn) { var buffer = dataViewToBuffer(dataView); this.descriptorHandles[handle].writeValue(buffer, checkForError(errorFn, completeFn)); } }); } }));