diff --git a/.gitignore b/.gitignore index de01ef30d..4adaf5da2 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,5 @@ results build/ node_modules/ npm-debug.log + +native/ diff --git a/.travis.yml b/.travis.yml index bb9bd07e3..f2cb33c16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,34 +1,23 @@ -os: - - linux - - osx -language: cpp -env: - - NODE_VERSION="0.8" - - NODE_VERSION="0.10" - - NODE_VERSION="0.12" - - NODE_VERSION="4" - - NODE_VERSION="6" - - NODE_VERSION="8" +language: node_js + matrix: - fast_finish: true -before_install: - - git clone https://github.com/creationix/nvm.git /tmp/.nvm; - - source /tmp/.nvm/nvm.sh; - - nvm install $NODE_VERSION; - - nvm use --delete-prefix $NODE_VERSION; - - if [ $TRAVIS_OS_NAME == "linux" ]; then - export CC="g++-4.8"; - fi - - if [ $NODE_VERSION == "0.8" ]; then - npm install -g npm@2; - fi + include: + - node_js: '8' + os: osx + + install: - - npm install + - yarn install --frozen-lockfile + - yarn build:source + - yarn ci + script: - - npm test -addons: - apt: - sources: - - ubuntu-toolchain-r-test - packages: - - g++-4.8 + - PUBLISH_BINARY=false + # if we are building a tag then publish + - if [[ $TRAVIS_BRANCH == `git describe --tags --always HEAD` ]]; then PUBLISH_BINARY=true; fi; + - > + if [ "$PUBLISH_BINARY" == true ]; then + yarn global add prebuild + prebuild -t 6.14.4 -t 8.12.0 -r node --upload $prebuild_upload + prebuild -t 2.0.0 -t 3.0.0 -t 4.0.4 -t 5.0.0 -r electron --upload $prebuild_upload + fi diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..80b7d2a5c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,33 @@ +# vs 2017 image because Windows SDK 10.0.15063.0 is not available on vs 2015 image +image: Previous Visual Studio 2017 + +platform: +- x86 +- x64 + +install: + - ps: Update-NodeJsInstallation (Get-NodeJsLatestBuild "8.12.0") $env:platform; + - ps: | + npm install -g node-gyp 2>&1 | out-null + if ($LASTEXITCODE -NE 0) { throw "Failed installing node-gyp"; } + - ps: node -v + - ps: npm -v + - ps: node-gyp -v + - cmd: yarn install --frozen-lockfile + - cmd: yarn build:source + - cmd: yarn ci + - ps: $publish_binary = 0 + - ps: if ($env:appveyor_repo_tag -match "true") { $publish_binary=1; } + - ps: echo "tag $env:appveyor_repo_tag_name branch $env:APPVEYOR_REPO_BRANCH" +build_script: + - ps: echo "publish $publish_binary" + - ps: $arch = $env:platform + - ps: if ($arch -match "x86") { $arch="ia32"; } + - ps: | + if ($publish_binary -Eq "1") { + yarn global add prebuild 2>&1 | write-host + prebuild -t 6.14.4 -t 8.12.0 -r node --upload $env:prebuild_upload 2>&1 | write-host + prebuild -t 2.0.0 -t 3.0.0 -t 4.0.4 -t 5.0.0 -r electron --upload $env:prebuild_upload 2>&1 | write-host + } + echo "done." + diff --git a/binding.gyp b/binding.gyp new file mode 100644 index 000000000..8e8d3d8ca --- /dev/null +++ b/binding.gyp @@ -0,0 +1,57 @@ +{ + 'targets': [ + { + 'target_name': 'noble', + 'include_dirs': [" { + if (state === 'poweredOn') { + console.log('Scanning'); + noble.startScanning([ECHO_SERVICE_UUID]); + } else { + noble.stopScanning(); + } +}); + +noble.on('discover', peripheral => { + // connect to the first peripheral that is scanned + noble.stopScanning(); + const name = peripheral.advertisement.localName; + console.log(`Connecting to '${name}' ${peripheral.id}`); + connectAndSetUp(peripheral); +}); + +function connectAndSetUp(peripheral) { + + peripheral.connect(error => { + console.log('Connected to', peripheral.id); + + // specify the services and characteristics to discover + const serviceUUIDs = [ECHO_SERVICE_UUID]; + const characteristicUUIDs = [ECHO_CHARACTERISTIC_UUID]; + + peripheral.discoverSomeServicesAndCharacteristics( + serviceUUIDs, + characteristicUUIDs, + onServicesAndCharacteristicsDiscovered + ); + }); + + peripheral.on('disconnect', () => console.log('disconnected')); +} + +function onServicesAndCharacteristicsDiscovered(error, services, characteristics) { + console.log('Discovered services and characteristics'); + const echoCharacteristic = characteristics[0]; + + // data callback receives notifications + echoCharacteristic.on('data', (data, isNotification) => { + console.log('Received: "' + data + '"'); + }); + + // subscribe to be notified whenever the peripheral update the characteristic + echoCharacteristic.subscribe(error => { + if (error) { + console.error('Error subscribing to echoCharacteristic'); + } else { + console.log('Subscribed for echoCharacteristic notifications'); + } + }); + + // create an interval to send data to the service + let count = 0; + setInterval(() => { + count++; + const message = new Buffer('hello, ble ' + count, 'utf-8'); + console.log("Sending: '" + message + "'"); + echoCharacteristic.write(message); + }, 2500); +} diff --git a/lib/mac/bindings.js b/lib/mac/bindings.js index 92e1d049c..3c267ffd8 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -1,12 +1,8 @@ -var os = require('os'); -var osRelease = parseFloat(os.release()); - -if (osRelease < 13 ) { - module.exports = require('./legacy'); -} else if (osRelease < 14) { - module.exports = require('./mavericks'); -} else if (osRelease < 17) { - module.exports = require('./yosemite'); -} else { - module.exports = require('./highsierra'); -} +const events = require('events'); +const util = require('util'); + +const NobleMac = require('bindings')('noble').NobleMac; + +util.inherits(NobleMac, events.EventEmitter); + +module.exports = new NobleMac(); diff --git a/lib/mac/highsierra.js b/lib/mac/highsierra.js deleted file mode 100644 index e9c4bd84f..000000000 --- a/lib/mac/highsierra.js +++ /dev/null @@ -1,840 +0,0 @@ -var events = require('events'); -var os = require('os'); -var util = require('util'); - -var debug = require('debug')('highsierra-bindings'); - -var XpcConnection = require('xpc-connection'); - -var localAddress = require('./local-address'); -var uuidToAddress = require('./uuid-to-address'); - -/** - * NobleBindings for mac - */ -var NobleBindings = function() { - this._peripherals = {}; - - this._xpcConnection = new XpcConnection('com.apple.bluetoothd'); - this._xpcConnection.on('error', function(message) {this.emit('xpcError', message);}.bind(this)); - this._xpcConnection.on('event', function(event) {this.emit('xpcEvent', event); }.bind(this)); -}; - -util.inherits(NobleBindings, events.EventEmitter); - -NobleBindings.prototype.sendXpcMessage = function(message) { - this._xpcConnection.sendMessage(message); -}; - -var nobleBindings = new NobleBindings(); - -// General xpc message handling -nobleBindings.on('xpcEvent', function(event) { - debug('xpcEvent: ' + JSON.stringify(event, undefined, 2)); - - var kCBMsgId = event.kCBMsgId; - var kCBMsgArgs = event.kCBMsgArgs; - this.emit('kCBMsgId' + kCBMsgId, kCBMsgArgs); -}); - -nobleBindings.on('xpcError', function(message) { - console.error('xpcError: ' + message); -}); - -nobleBindings.sendCBMsg = function(id, args) { - debug('sendCBMsg: ' + id + ', ' + JSON.stringify(args, undefined, 2)); - this.sendXpcMessage({kCBMsgId: id,kCBMsgArgs: args}); -}; - - - - -/** - * Init xpc connection to bluetoothd - * - * @discussion tested - */ -nobleBindings.init = function() { - this._xpcConnection.setup(); - - localAddress(function(address) { - if (address) { - this.emit('addressChange', address); - } - - this.sendCBMsg(1, { - kCBMsgArgName: 'node-' + (new Date()).getTime(), - kCBMsgArgOptions: { - kCBInitOptionShowPowerAlert: 0 - }, - kCBMsgArgType: 0 - }); - }.bind(this)); -}; - -nobleBindings.on('kCBMsgId4', function(args) { - var state = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'][args.kCBMsgArgState]; - debug('state change ' + state); - this.emit('stateChange', state); -}); - - - -/** - * Start scanning - * @param {Array} serviceUuids Scan for these UUIDs, if undefined then scan for all - * @param {Bool} allowDuplicates Scan can return duplicates - * - * @discussion tested - */ -nobleBindings.startScanning = function(serviceUuids, allowDuplicates) { - var args = { - kCBMsgArgOptions: {}, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - if (allowDuplicates) { - args.kCBMsgArgOptions.kCBScanOptionAllowDuplicates = 1; - } - - this.sendCBMsg(44, args); - this.emit('scanStart'); -}; - -/** - * Response message to start scanning - * - * @example - * // For `TI Sensortag` the message lookes like this: - * handleMsg: 37, { - * kCBMsgArgAdvertisementData = { - * kCBAdvDataIsConnectable = 1; - * kCBAdvDataLocalName = SensorTag; - * kCBAdvDataTxPowerLevel = 0; - * }; - * kCBMsgArgDeviceUUID = "<__NSConcreteUUID 0x6180000208e0> 53486C7A-DED2-4AA6-8913-387CD22F25D8"; - * kCBMsgArgName = SensorTag; - * kCBMsgArgRssi = "-68"; - * } - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId48', function(args) { - if (Object.keys(args.kCBMsgArgAdvertisementData).length === 0 || - (args.kCBMsgArgAdvertisementData.kCBAdvDataIsConnectable !== undefined && - Object.keys(args.kCBMsgArgAdvertisementData).length === 1)) { - return; - } - - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var advertisement = { - localName: args.kCBMsgArgAdvertisementData.kCBAdvDataLocalName || args.kCBMsgArgName, - txPowerLevel: args.kCBMsgArgAdvertisementData.kCBAdvDataTxPowerLevel, - manufacturerData: args.kCBMsgArgAdvertisementData.kCBAdvDataManufacturerData, - serviceData: [], - serviceUuids: [] - }; - var connectable = args.kCBMsgArgAdvertisementData.kCBAdvDataIsConnectable ? true : false; - var rssi = args.kCBMsgArgRssi; - var i; - - if (args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs) { - for(i = 0; i < args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs.length; i++) { - advertisement.serviceUuids.push(args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs[i].toString('hex')); - } - } - - var serviceData = args.kCBMsgArgAdvertisementData.kCBAdvDataServiceData; - if (serviceData) { - for (i = 0; i < serviceData.length; i += 2) { - var serviceDataUuid = serviceData[i].toString('hex'); - var data = serviceData[i + 1]; - - advertisement.serviceData.push({ - uuid: serviceDataUuid, - data: data - }); - } - } - - debug('peripheral ' + deviceUuid + ' discovered'); - - var uuid = new Buffer(deviceUuid, 'hex'); - uuid.isUuid = true; - - if (!this._peripherals[deviceUuid]) { - this._peripherals[deviceUuid] = {}; - } - - this._peripherals[deviceUuid].uuid = uuid; - this._peripherals[deviceUuid].connectable = connectable; - this._peripherals[deviceUuid].advertisement = advertisement; - this._peripherals[deviceUuid].rssi = rssi; - - (function(deviceUuid, advertisement, rssi) { - uuidToAddress(deviceUuid, function(error, address, addressType) { - address = address || 'unknown'; - addressType = addressType || 'unknown'; - - this._peripherals[deviceUuid].address = address; - this._peripherals[deviceUuid].addressType = addressType; - - this.emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi); - }.bind(this)); - }.bind(this))(deviceUuid, advertisement, rssi); -}); - - -/** - * Stop scanning - * - * @discussion tested - */ -nobleBindings.stopScanning = function() { - this.sendCBMsg(45, null); - this.emit('scanStop'); -}; - - - -/** - * Connect to peripheral - * @param {String} deviceUuid Peripheral uuid to connect to - * - * @discussion tested - */ -nobleBindings.connect = function(deviceUuid) { - this.sendCBMsg(46, { - kCBMsgArgOptions: { - kCBConnectOptionNotifyOnDisconnection: 1 - }, - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -nobleBindings.on('kCBMsgId49', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' connected'); - - this.emit('connect', deviceUuid); -}); - - - -/** - * Disconnect - * - * @param {String} deviceUuid Peripheral uuid to disconnect - * - * @discussion tested - */ -nobleBindings.disconnect = function(deviceUuid) { - this.sendCBMsg(47, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -/** - * Response to disconnect - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId50', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' disconnected'); - - this.emit('disconnect', deviceUuid); -}); - - - -/** - * Update RSSI - * - * @discussion tested - */ -nobleBindings.updateRssi = function(deviceUuid) { - this.sendCBMsg(61, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -/** - * Response to RSSI update - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId71', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var rssi = args.kCBMsgArgData; - - this._peripherals[deviceUuid].rssi = rssi; - - debug('peripheral ' + deviceUuid + ' RSSI update ' + rssi); - - this.emit('rssiUpdate', deviceUuid, rssi); -}); - - - -/** - * Discover services - * - * @param {String} deviceUuid Device UUID - * @param {Array} uuids Services to discover, if undefined then all - * - * @discussion tested - */ -nobleBindings.discoverServices = function(deviceUuid, uuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgUUIDs: [] - }; - - if (uuids) { - for(var i = 0; i < uuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(uuids[i], 'hex'); - } - } - - this.sendCBMsg(62, args); -}; - -/** - * Response to discover service - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId72', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceUuids = []; - - this._peripherals[deviceUuid].services = this._peripherals[deviceUuid].services || {}; - - if (args.kCBMsgArgServices) { - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var service = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - if (!this._peripherals[deviceUuid].services[service.uuid] ) { - this._peripherals[deviceUuid].services[service.uuid] = this._peripherals[deviceUuid].services[service.startHandle] = service; - } - - serviceUuids.push(service.uuid); - } - } - // TODO: result 24 => device not connected - - this.emit('servicesDiscover', deviceUuid, serviceUuids); -}); - - - -/** - * [discoverIncludedServices description] - * - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} serviceUuids - * - * @dicussion tested - */ -nobleBindings.discoverIncludedServices = function(deviceUuid, serviceUuid, serviceUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - this.sendCBMsg(74, args); -}; - -/** - * Response to dicover included services - * - * @dicussion tested - */ -nobleBindings.on('kCBMsgId76', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var includedServiceUuids = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices || {}; - - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var includedService = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - if (! this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.uuid]) { - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.startHandle] = includedService; - } - - includedServiceUuids.push(includedService.uuid); - } - - this.emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids); -}); - - - -/** - * Discover characteristic - * - * @param {String} deviceUuid Peripheral UUID - * @param {String} serviceUuid Service UUID - * @param {Array} characteristicUuids Characteristics to discover, all if empty - * - * @discussion tested - */ -nobleBindings.discoverCharacteristics = function(deviceUuid, serviceUuid, characteristicUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (characteristicUuids) { - for(var i = 0; i < characteristicUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(characteristicUuids[i], 'hex'); - } - } - - this.sendCBMsg(75, args); -}; - -/** - * Response to characteristic discovery - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId77', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var characteristics = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics || {}; - - for(var i = 0; i < args.kCBMsgArgCharacteristics.length; i++) { - var properties = args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicProperties; - - var characteristic = { - uuid: args.kCBMsgArgCharacteristics[i].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicHandle, - valueHandle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicValueHandle, - properties: [] - }; - - if (properties & 0x01) { - characteristic.properties.push('broadcast'); - } - - if (properties & 0x02) { - characteristic.properties.push('read'); - } - - if (properties & 0x04) { - characteristic.properties.push('writeWithoutResponse'); - } - - if (properties & 0x08) { - characteristic.properties.push('write'); - } - - if (properties & 0x10) { - characteristic.properties.push('notify'); - } - - if (properties & 0x20) { - characteristic.properties.push('indicate'); - } - - if (properties & 0x40) { - characteristic.properties.push('authenticatedSignedWrites'); - } - - if (properties & 0x80) { - characteristic.properties.push('extendedProperties'); - } - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.handle] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.valueHandle] = characteristic; - - characteristics.push({ - uuid: characteristic.uuid, - properties: characteristic.properties - }); - } - - this.emit('characteristicsDiscover', deviceUuid, serviceUuid, characteristics); -}); - - - -/** - * Read value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * - * @discussion tested - */ -nobleBindings.read = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(78 , { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -/** - * Response to read value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId83', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var isNotification = args.kCBMsgArgIsNotification ? true : false; - var data = args.kCBMsgArgData; - - var peripheral = this._peripherals[deviceUuid]; - - if (peripheral) { - for(var i in peripheral.services) { - if (peripheral.services[i].characteristics && - peripheral.services[i].characteristics[characteristicHandle]) { - - this.emit('read', deviceUuid, peripheral.services[i].uuid, - peripheral.services[i].characteristics[characteristicHandle].uuid, data, isNotification); - break; - } - } - } else { - console.warn('noble (highsierra): received read event from unknown peripheral: ' + deviceUuid + ' !'); - } -}); - - - -/** - * Write value - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} characteristicUuid - * @param {[Type]} data - * @param {Bool} withoutResponse - * - * @discussion tested - */ -nobleBindings.write = function(deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) { - this.sendCBMsg(79, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgData: data, - kCBMsgArgType: (withoutResponse ? 1 : 0) - }); - - if (withoutResponse) { - this.emit('write', deviceUuid, serviceUuid, characteristicUuid); - } -}; - -/** - * Response to write - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId84', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('write', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid); - break; - } - } -}); - - - -/** - * Broadcast - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} broadcast [description] - * @return {[type]} [description] - * - * @discussion The ids were incemented but there seems to be no CoreBluetooth function to call/verify this. - */ -nobleBindings.broadcast = function(deviceUuid, serviceUuid, characteristicUuid, broadcast) { - throw new Error('This OS does not support broadcast.'); -}; - - -/** - * Register notification hanlder - * - * @param {String} deviceUuid Peripheral UUID - * @param {String} serviceUuid Service UUID - * @param {String} characteristicUuid Charactereistic UUID - * @param {Bool} notify If want to get notification - * - * @discussion tested - */ -nobleBindings.notify = function(deviceUuid, serviceUuid, characteristicUuid, notify) { - this.sendCBMsg(81, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (notify ? 1 : 0) - }); -}; - -/** - * Response notification - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId86', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('notify', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - - - -/** - * Discover service descriptors - * - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} characteristicUuid - * - * @discussion tested - */ -nobleBindings.discoverDescriptors = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(82, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -/** - * Response to descriptor discovery - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId87', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var descriptors = []; //args.kCBMsgArgDescriptors; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors = {}; - - for(var j = 0; j < args.kCBMsgArgDescriptors.length; j++) { - var descriptor = { - uuid: args.kCBMsgArgDescriptors[j].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgDescriptors[j].kCBMsgArgDescriptorHandle - }; - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.uuid] = - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.handle] = descriptor; - - descriptors.push(descriptor.uuid); - } - - this.emit('descriptorsDiscover', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, descriptors); - break; - } - } -}); - - - -/** - * Read value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} descriptorUuid [description] - * - * @discussion tested - */ -nobleBindings.readValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) { - this.sendCBMsg(88, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle - }); -}; - -/** - * Response to read value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId90', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - var data = args.kCBMsgArgData; - - this.emit('handleRead', deviceUuid, descriptorHandle, data); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueRead', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid, data); - return; // break; - } - } - } -}); - - - -/** - * Write value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} descriptorUuid [description] - * @param {[type]} data [description] - * - * @discussion tested - */ -nobleBindings.writeValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { - this.sendCBMsg(89, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle, - kCBMsgArgData: data - }); -}; - -/** - * Response to write value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId91', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - - this.emit('handleWrite', deviceUuid, descriptorHandle); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueWrite', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid); - return; // break; - } - } - } -}); - - - -/** - * Reade value directly from handle - * - * @param {[type]} deviceUuid [description] - * @param {[type]} handle [description] - * - * @discussion tested - */ -nobleBindings.readHandle = function(deviceUuid, handle) { - this.sendCBMsg(88, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle - }); -}; - - - -/** - * Write value directly to handle - * - * @param {[type]} deviceUuid [description] - * @param {[type]} handle [description] - * @param {[type]} data [description] - * @param {[type]} withoutResponse [description] - * - * @discussion tested - */ -nobleBindings.writeHandle = function(deviceUuid, handle, data, withoutResponse) { - // TODO: use without response - this.sendCBMsg(89, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle, - kCBMsgArgData: data - }); -}; - - -// Exports -module.exports = nobleBindings; diff --git a/lib/mac/legacy.js b/lib/mac/legacy.js deleted file mode 100644 index 76854efcc..000000000 --- a/lib/mac/legacy.js +++ /dev/null @@ -1,629 +0,0 @@ -var events = require('events'); -var os = require('os'); -var util = require('util'); - -var debug = require('debug')('legacy-bindings'); - -var osRelease = os.release(); -var isLessThan10_8_5 = (parseFloat(osRelease) < 12.5); - -var localAddress = require('./local-address'); -var uuidToAddress = require('./uuid-to-address'); - -var XpcConnection = require('xpc-connection'); - -var NobleBindings = function() { - this._peripherals = {}; - - this._xpcConnection = new XpcConnection('com.apple.blued'); - - this._xpcConnection.on('error', function(message) { - this.emit('xpcError', message); - }.bind(this)); - - this._xpcConnection.on('event', function(event) { - this.emit('xpcEvent', event); - }.bind(this)); -}; - -util.inherits(NobleBindings, events.EventEmitter); - -NobleBindings.prototype.sendXpcMessage = function(message) { - this._xpcConnection.sendMessage(message); -}; - -var nobleBindings = new NobleBindings(); - -nobleBindings.on('xpcEvent', function(event) { - var kCBMsgId = event.kCBMsgId; - var kCBMsgArgs = event.kCBMsgArgs; - - debug('xpcEvent: ' + JSON.stringify(event, undefined, 2)); - - this.emit('kCBMsgId' + kCBMsgId, kCBMsgArgs); -}); - -/* - Result codes ... - - CBErrorUnknown, - - CBATTErrorInvalidHandle = 0x01, - CBATTErrorReadNotPermitted = 0x02, - CBATTErrorWriteNotPermitted = 0x03, - CBATTErrorInvalidPdu = 0x04, - CBATTErrorInsufficientAuthentication = 0x05, - CBATTErrorRequestNotSupported = 0x06, - CBATTErrorInvalidOffset = 0x07, - CBATTErrorInsufficientAuthorization = 0x08, - CBATTErrorPrepareQueueFull = 0x09, - CBATTErrorAttributeNotFound = 0x0A, - CBATTErrorAttributeNotLong = 0x0B, - CBATTErrorInsufficientEncryptionKeySize = 0x0C, - CBATTErrorInvalidAttributeValueLength = 0x0D, - CBATTErrorUnlikelyError = 0x0E, - CBATTErrorInsufficientEncryption = 0x0F, - CBATTErrorUnsupportedGroupType = 0x10, - CBATTErrorInsufficientResources = 0x11, -*/ - -nobleBindings.on('xpcError', function(message) { - console.error('xpcError: ' + message); -}); - -nobleBindings.sendCBMsg = function(id, args) { - debug('sendCBMsg: ' + id + ', ' + JSON.stringify(args, undefined, 2)); - this.sendXpcMessage({ - kCBMsgId: id, - kCBMsgArgs: args - }); -}; - -nobleBindings.init = function() { - this._xpcConnection.setup(); - - localAddress(function(address) { - if (address) { - this.emit('addressChange', address); - } - - this.sendCBMsg(1, { - kCBMsgArgAlert: 1, - kCBMsgArgName: 'node-' + (new Date()).getTime() - }); - }.bind(this)); -}; - -nobleBindings.on('kCBMsgId4', function(args) { - var state = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'][args.kCBMsgArgState]; - debug('state change ' + state); - this.emit('stateChange', state); -}); - -nobleBindings.startScanning = function(serviceUuids, allowDuplicates) { - var args = { - kCBMsgArgOptions: {}, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - if (allowDuplicates) { - args.kCBMsgArgOptions.kCBScanOptionAllowDuplicates = 1; - } - - this.sendCBMsg(isLessThan10_8_5 ? 7 : 23, args); - - this.emit('scanStart'); -}; - -nobleBindings.stopScanning = function() { - this.sendCBMsg(isLessThan10_8_5 ? 8 : 24, null); - - this.emit('scanStop'); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId13' : 'kCBMsgId31', function(args) { - var peripheralUuid = args.kCBMsgArgPeripheral.kCBMsgArgUUID.toString('hex'); - var peripheralHandle = args.kCBMsgArgPeripheral.kCBMsgArgPeripheralHandle; - var advertisement = { - localName: args.kCBMsgArgAdvertisementData.kCBAdvDataLocalName, - txPowerLevel: args.kCBMsgArgAdvertisementData.kCBAdvDataTxPowerLevel, - manufacturerData: args.kCBMsgArgAdvertisementData.kCBAdvDataManufacturerData, - serviceData: [], - serviceUuids: [] - }; - var rssi = args.kCBMsgArgRssi; - var i; - - if (args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs) { - for(i = 0; i < args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs.length; i++) { - advertisement.serviceUuids.push(args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs[i].toString('hex')); - } - } - - var serviceData = args.kCBMsgArgAdvertisementData.kCBAdvDataServiceData; - if (serviceData) { - for (i = 0; i < serviceData.length; i += 2) { - var serviceDataUuid = serviceData[i].toString('hex'); - var data = serviceData[i + 1]; - - advertisement.serviceData.push({ - uuid: serviceDataUuid, - data: data - }); - } - } - - debug('peripheral ' + peripheralUuid + ' discovered'); - - this._peripherals[peripheralUuid] = this._peripherals[peripheralHandle] = { - uuid: peripheralUuid, - address: undefined, - addressType: undefined, - handle: peripheralHandle, - advertisement: advertisement, - rssi: rssi - }; - - (function(peripheralUuid, peripheralHandle, advertisement, rssi) { - uuidToAddress(peripheralUuid, function(error, address, addressType) { - address = address || 'unknown'; - addressType = addressType || 'unknown'; - - this._peripherals[peripheralUuid].address = this._peripherals[peripheralHandle].address = address; - this._peripherals[peripheralUuid].addressType = this._peripherals[peripheralHandle].addressType = addressType; - - this.emit('discover', peripheralUuid, address, addressType, undefined, advertisement, rssi); - }.bind(this)); - }.bind(this))(peripheralUuid, peripheralHandle, advertisement, rssi); -}); - -nobleBindings.connect = function(peripheralUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 9 : 25, { - kCBMsgArgOptions: { - kCBConnectOptionNotifyOnDisconnection: 1 - }, - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId14' : 'kCBMsgId32', function(args) { - var peripheralUuid = args.kCBMsgArgUUID.toString('hex'); - // var peripheralHandle = args.kCBMsgArgPeripheralHandle; - - debug('peripheral ' + peripheralUuid + ' connected'); - - this.emit('connect', peripheralUuid); -}); - -nobleBindings.disconnect = function(peripheralUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 10 : 26, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId15' : 'kCBMsgId33', function(args) { - var peripheralUuid = args.kCBMsgArgUUID.toString('hex'); - // var peripheralHandle = args.kCBMsgArgPeripheralHandle; - - debug('peripheral ' + peripheralUuid + ' disconnected'); - - this.emit('disconnect', peripheralUuid); -}); - -nobleBindings.updateRssi = function(peripheralUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 16 : 35, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId20' : 'kCBMsgId41', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var rssi = args.kCBMsgArgData; - - this._peripherals[peripheralHandle].rssi = rssi; - - debug('peripheral ' + peripheralUuid + ' RSSI update ' + rssi); - - this.emit('rssiUpdate', peripheralUuid, rssi); -}); - -nobleBindings.discoverServices = function(peripheralUuid, uuids) { - var args = { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgUUIDs: [] - }; - - if (uuids) { - for(var i = 0; i < uuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(uuids[i], 'hex'); - } - } - - this.sendCBMsg(isLessThan10_8_5 ? 17 : 36, args); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId21' : 'kCBMsgId42', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var serviceUuids = []; - - this._peripherals[peripheralHandle].services = {}; - - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var service = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - this._peripherals[peripheralHandle].services[service.uuid] = this._peripherals[peripheralHandle].services[service.startHandle] = service; - - serviceUuids.push(service.uuid); - } - - this.emit('servicesDiscover', peripheralUuid, serviceUuids); -}); - -nobleBindings.discoverIncludedServices = function(peripheralUuid, serviceUuid, serviceUuids) { - var args = { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgServiceStartHandle: this._peripherals[peripheralUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[peripheralUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - this.sendCBMsg(isLessThan10_8_5 ? 25 : 46, args); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId27' : 'kCBMsgId48', function(args) { - var peripheralUuidHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralUuidHandle].uuid; - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[peripheralUuidHandle].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var includedServiceUuids = []; - - this._peripherals[peripheralUuidHandle].services[serviceStartHandle].includedServices = {}; - - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var includedService = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - this._peripherals[peripheralUuidHandle].services[serviceStartHandle].includedServices[includedServices.uuid] = - this._peripherals[peripheralUuidHandle].services[serviceStartHandle].includedServices[includedServices.startHandle] = includedService; - - includedServiceUuids.push(includedService.uuid); - } - - this.emit('includedServicesDiscover', peripheralUuid, serviceUuid, includedServiceUuids); -}); - -nobleBindings.discoverCharacteristics = function(peripheralUuid, serviceUuid, characteristicUuids) { - var args = { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgServiceStartHandle: this._peripherals[peripheralUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[peripheralUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (characteristicUuids) { - for(var i = 0; i < characteristicUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(characteristicUuids[i], 'hex'); - } - } - - this.sendCBMsg(isLessThan10_8_5 ? 26 : 47, args); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId28' : 'kCBMsgId49', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[peripheralHandle].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var characteristics = []; - - this._peripherals[peripheralHandle].services[serviceStartHandle].characteristics = {}; - - for(var i = 0; i < args.kCBMsgArgCharacteristics.length; i++) { - var properties = args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicProperties; - - var characteristic = { - uuid: args.kCBMsgArgCharacteristics[i].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicHandle, - valueHandle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicValueHandle, - properties: [] - }; - - if (properties & 0x01) { - characteristic.properties.push('broadcast'); - } - - if (properties & 0x02) { - characteristic.properties.push('read'); - } - - if (properties & 0x04) { - characteristic.properties.push('writeWithoutResponse'); - } - - if (properties & 0x08) { - characteristic.properties.push('write'); - } - - if (properties & 0x10) { - characteristic.properties.push('notify'); - } - - if (properties & 0x20) { - characteristic.properties.push('indicate'); - } - - if (properties & 0x40) { - characteristic.properties.push('authenticatedSignedWrites'); - } - - if (properties & 0x80) { - characteristic.properties.push('extendedProperties'); - } - - this._peripherals[peripheralHandle].services[serviceStartHandle].characteristics[characteristic.uuid] = - this._peripherals[peripheralHandle].services[serviceStartHandle].characteristics[characteristic.handle] = - this._peripherals[peripheralHandle].services[serviceStartHandle].characteristics[characteristic.valueHandle] = characteristic; - - characteristics.push({ - uuid: characteristic.uuid, - properties: characteristic.properties - }); - } - - this.emit('characteristicsDiscover', peripheralUuid, serviceUuid, characteristics); -}); - -nobleBindings.read = function(peripheralUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 29 : 50 , { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgCharacteristicHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId35' : 'kCBMsgId56', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheral = this._peripherals[peripheralHandle]; - - if (peripheral) { - var peripheralUuid = peripheral.uuid; - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var isNotification = args.kCBMsgArgIsNotification ? true : false; - var data = args.kCBMsgArgData; - - for(var i in peripheral.services) { - if (peripheral.services[i].characteristics && - peripheral.services[i].characteristics[characteristicHandle]) { - - this.emit('read', peripheralUuid, peripheral.services[i].uuid, - peripheral.services[i].characteristics[characteristicHandle].uuid, data, isNotification); - break; - } - } - } else { - console.warn('noble (mac legacy): received read event from unknown peripheral: ' + peripheralHandle + ' !'); - } -}); - -nobleBindings.write = function(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) { - this.sendCBMsg(isLessThan10_8_5 ? 30 : 51, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgCharacteristicHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgData: data, - kCBMsgArgType: (withoutResponse ? 1 : 0) - }); - - if (withoutResponse) { - this.emit('write', peripheralUuid, serviceUuid, characteristicUuid); - } -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId36' : 'kCBMsgId57', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - - for(var i in this._peripherals[peripheralHandle].services) { - if (this._peripherals[peripheralHandle].services[i].characteristics && - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle]) { - this.emit('write', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].uuid); - break; - } - } -}); - -nobleBindings.broadcast = function(peripheralUuid, serviceUuid, characteristicUuid, broadcast) { - this.sendCBMsg(isLessThan10_8_5 ? 31 : 52, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgCharacteristicHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (broadcast ? 1 : 0) - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId37' : 'kCBMsgId58', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[peripheralHandle].services) { - if (this._peripherals[peripheralHandle].services[i].characteristics && - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle]) { - this.emit('broadcast', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - -nobleBindings.notify = function(peripheralUuid, serviceUuid, characteristicUuid, notify) { - this.sendCBMsg(isLessThan10_8_5 ? 32 : 53, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgCharacteristicHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (notify ? 1 : 0) - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId38' : 'kCBMsgId59', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[peripheralHandle].services) { - if (this._peripherals[peripheralHandle].services[i].characteristics && - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle]) { - this.emit('notify', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - -nobleBindings.discoverDescriptors = function(peripheralUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 34 : 55, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgCharacteristicHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId39' : 'kCBMsgId60', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var descriptors = []; //args.kCBMsgArgDescriptors; - - for(var i in this._peripherals[peripheralHandle].services) { - if (this._peripherals[peripheralHandle].services[i].characteristics && - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle]) { - - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].descriptors = {}; - - for(var j = 0; j < args.kCBMsgArgDescriptors.length; j++) { - var descriptor = { - uuid: args.kCBMsgArgDescriptors[j].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgDescriptors[j].kCBMsgArgDescriptorHandle - }; - - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].descriptors[descriptor.uuid] = - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].descriptors[descriptor.handle] = descriptor; - - descriptors.push(descriptor.uuid); - } - - this.emit('descriptorsDiscover', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[characteristicHandle].uuid, descriptors); - break; - } - } -}); - -nobleBindings.readValue = function(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { - this.sendCBMsg(isLessThan10_8_5 ? 40 : 61, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgDescriptorHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId42' : 'kCBMsgId63', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - var data = args.kCBMsgArgData; - - this.emit('handleRead', peripheralUuid, descriptorHandle, data); - - for(var i in this._peripherals[peripheralHandle].services) { - for(var j in this._peripherals[peripheralHandle].services[i].characteristics) { - if (this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors && - this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueRead', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[j].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors[descriptorHandle].uuid, data); - return; // break; - } - } - } -}); - -nobleBindings.writeValue = function(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { - this.sendCBMsg(isLessThan10_8_5 ? 41 : 62, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgDescriptorHandle: this._peripherals[peripheralUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle, - kCBMsgArgData: data - }); -}; - -nobleBindings.on(isLessThan10_8_5 ? 'kCBMsgId43' : 'kCBMsgId64', function(args) { - var peripheralHandle = args.kCBMsgArgPeripheralHandle; - var peripheralUuid = this._peripherals[peripheralHandle].uuid; - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - - this.emit('handleWrite', peripheralUuid, descriptorHandle); - - for(var i in this._peripherals[peripheralHandle].services) { - for(var j in this._peripherals[peripheralHandle].services[i].characteristics) { - if (this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors && - this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueWrite', peripheralUuid, this._peripherals[peripheralHandle].services[i].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[j].uuid, - this._peripherals[peripheralHandle].services[i].characteristics[j].descriptors[descriptorHandle].uuid); - return; // break; - } - } - } -}); - -nobleBindings.readHandle = function(peripheralUuid, handle) { - this.sendCBMsg(isLessThan10_8_5 ? 40 : 61, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgDescriptorHandle: handle - }); -}; - -nobleBindings.writeHandle = function(peripheralUuid, handle, data, withoutResponse) { - // TODO: use without response - this.sendCBMsg(isLessThan10_8_5 ? 41 : 62, { - kCBMsgArgPeripheralHandle: this._peripherals[peripheralUuid].handle, - kCBMsgArgDescriptorHandle: handle, - kCBMsgArgData: data - }); -}; - -module.exports = nobleBindings; diff --git a/lib/mac/local-address.js b/lib/mac/local-address.js deleted file mode 100644 index 2a7f3e0ed..000000000 --- a/lib/mac/local-address.js +++ /dev/null @@ -1,18 +0,0 @@ -var child_process = require('child_process'); - -function localAddress(callback) { - child_process.exec('system_profiler SPBluetoothDataType', {}, function(error, stdout, stderr) { - var address = null; - - if (!error) { - var found = stdout.match(/\s+Address: (.*)/); - if (found) { - address = found[1].toLowerCase().replace(/-/g, ':'); - } - } - - callback(address); - }); -} - -module.exports = localAddress; diff --git a/lib/mac/mavericks.js b/lib/mac/mavericks.js deleted file mode 100644 index 82d467910..000000000 --- a/lib/mac/mavericks.js +++ /dev/null @@ -1,603 +0,0 @@ -var events = require('events'); -var os = require('os'); -var util = require('util'); - -var debug = require('debug')('mavericks-bindings'); - -var XpcConnection = require('xpc-connection'); - -var localAddress = require('./local-address'); -var uuidToAddress = require('./uuid-to-address'); - -var NobleBindings = function() { - this._peripherals = {}; - - this._xpcConnection = new XpcConnection('com.apple.blued'); - - this._xpcConnection.on('error', function(message) { - this.emit('xpcError', message); - }.bind(this)); - - this._xpcConnection.on('event', function(event) { - this.emit('xpcEvent', event); - }.bind(this)); -}; - -util.inherits(NobleBindings, events.EventEmitter); - -NobleBindings.prototype.sendXpcMessage = function(message) { - this._xpcConnection.sendMessage(message); -}; - -var nobleBindings = new NobleBindings(); - -nobleBindings.on('xpcEvent', function(event) { - var kCBMsgId = event.kCBMsgId; - var kCBMsgArgs = event.kCBMsgArgs; - - debug('xpcEvent: ' + JSON.stringify(event, undefined, 2)); - - this.emit('kCBMsgId' + kCBMsgId, kCBMsgArgs); -}); - -nobleBindings.on('xpcError', function(message) { - console.error('xpcError: ' + message); -}); - -nobleBindings.sendCBMsg = function(id, args) { - debug('sendCBMsg: ' + id + ', ' + JSON.stringify(args, undefined, 2)); - this.sendXpcMessage({ - kCBMsgId: id, - kCBMsgArgs: args - }); -}; - -nobleBindings.init = function() { - this._xpcConnection.setup(); - - localAddress(function(address) { - if (address) { - this.emit('addressChange', address); - } - - this.sendCBMsg(1, { - kCBMsgArgName: 'node-' + (new Date()).getTime(), - kCBMsgArgOptions: { - kCBInitOptionShowPowerAlert: 0 - }, - kCBMsgArgType: 0 - }); - }.bind(this)); -}; - -nobleBindings.on('kCBMsgId6', function(args) { - var state = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'][args.kCBMsgArgState]; - debug('state change ' + state); - this.emit('stateChange', state); -}); - -nobleBindings.startScanning = function(serviceUuids, allowDuplicates) { - var args = { - kCBMsgArgOptions: {}, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - if (allowDuplicates) { - args.kCBMsgArgOptions.kCBScanOptionAllowDuplicates = 1; - } - - this.sendCBMsg(29, args); - - this.emit('scanStart'); -}; - -nobleBindings.stopScanning = function() { - this.sendCBMsg(30, null); - - this.emit('scanStop'); -}; - -nobleBindings.on('kCBMsgId37', function(args) { - if (Object.keys(args.kCBMsgArgAdvertisementData).length === 0) { - return; - } - - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var advertisement = { - localName: args.kCBMsgArgAdvertisementData.kCBAdvDataLocalName || args.kCBMsgArgName, - txPowerLevel: args.kCBMsgArgAdvertisementData.kCBAdvDataTxPowerLevel, - manufacturerData: args.kCBMsgArgAdvertisementData.kCBAdvDataManufacturerData, - serviceData: [], - serviceUuids: [] - }; - var rssi = args.kCBMsgArgRssi; - var i; - - if (args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs) { - for(i = 0; i < args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs.length; i++) { - advertisement.serviceUuids.push(args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs[i].toString('hex')); - } - } - - var serviceData = args.kCBMsgArgAdvertisementData.kCBAdvDataServiceData; - if (serviceData) { - for (i = 0; i < serviceData.length; i += 2) { - var serviceDataUuid = serviceData[i].toString('hex'); - var data = serviceData[i + 1]; - - advertisement.serviceData.push({ - uuid: serviceDataUuid, - data: data - }); - } - } - - debug('peripheral ' + deviceUuid + ' discovered'); - - var uuid = new Buffer(deviceUuid, 'hex'); - uuid.isUuid = true; - - if(!this._peripherals[deviceUuid]) { - this._peripherals[deviceUuid] = {}; - } - this._peripherals[deviceUuid].uuid = uuid; - this._peripherals[deviceUuid].advertisement = advertisement; - this._peripherals[deviceUuid].rssi = rssi; - - (function(deviceUuid, advertisement, rssi) { - uuidToAddress(deviceUuid, function(error, address, addressType) { - address = address || 'unknown'; - addressType = addressType || 'unknown'; - - this._peripherals[deviceUuid].address = address; - this._peripherals[deviceUuid].addressType = addressType; - - this.emit('discover', deviceUuid, address, addressType, undefined, advertisement, rssi); - }.bind(this)); - }.bind(this))(deviceUuid, advertisement, rssi); -}); - -nobleBindings.connect = function(deviceUuid) { - this.sendCBMsg(31, { - kCBMsgArgOptions: { - kCBConnectOptionNotifyOnDisconnection: 1 - }, - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -nobleBindings.on('kCBMsgId38', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' connected'); - - this.emit('connect', deviceUuid); -}); - -nobleBindings.disconnect = function(deviceUuid) { - this.sendCBMsg(32, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -nobleBindings.on('kCBMsgId40', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' disconnected'); - - this.emit('disconnect', deviceUuid); -}); - -nobleBindings.updateRssi = function(deviceUuid) { - this.sendCBMsg(43, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -nobleBindings.on('kCBMsgId54', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var rssi = args.kCBMsgArgData; - - this._peripherals[deviceUuid].rssi = rssi; - - debug('peripheral ' + deviceUuid + ' RSSI update ' + rssi); - - this.emit('rssiUpdate', deviceUuid, rssi); -}); - -nobleBindings.discoverServices = function(deviceUuid, uuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgUUIDs: [] - }; - - if (uuids) { - for(var i = 0; i < uuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(uuids[i], 'hex'); - } - } - - this.sendCBMsg(44, args); -}; - -nobleBindings.on('kCBMsgId55', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceUuids = []; - - this._peripherals[deviceUuid].services = this._peripherals[deviceUuid].services || {}; - - if (args.kCBMsgArgServices) { - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var service = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - this._peripherals[deviceUuid].services[service.uuid] = this._peripherals[deviceUuid].services[service.startHandle] = service; - - serviceUuids.push(service.uuid); - } - } - // TODO: result 24 => device not connected - - this.emit('servicesDiscover', deviceUuid, serviceUuids); -}); - -nobleBindings.discoverIncludedServices = function(deviceUuid, serviceUuid, serviceUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - this.sendCBMsg(60, args); -}; - -nobleBindings.on('kCBMsgId62', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var includedServiceUuids = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices || {}; - - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var includedService = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - if (! this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.uuid]) { - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.startHandle] = includedService; - } - - includedServiceUuids.push(includedService.uuid); - } - - this.emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids); -}); - -nobleBindings.discoverCharacteristics = function(deviceUuid, serviceUuid, characteristicUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (characteristicUuids) { - for(var i = 0; i < characteristicUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(characteristicUuids[i], 'hex'); - } - } - - this.sendCBMsg(61, args); -}; - -nobleBindings.on('kCBMsgId63', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var characteristics = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics || {}; - - for(var i = 0; i < args.kCBMsgArgCharacteristics.length; i++) { - var properties = args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicProperties; - - var characteristic = { - uuid: args.kCBMsgArgCharacteristics[i].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicHandle, - valueHandle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicValueHandle, - properties: [] - }; - - if (properties & 0x01) { - characteristic.properties.push('broadcast'); - } - - if (properties & 0x02) { - characteristic.properties.push('read'); - } - - if (properties & 0x04) { - characteristic.properties.push('writeWithoutResponse'); - } - - if (properties & 0x08) { - characteristic.properties.push('write'); - } - - if (properties & 0x10) { - characteristic.properties.push('notify'); - } - - if (properties & 0x20) { - characteristic.properties.push('indicate'); - } - - if (properties & 0x40) { - characteristic.properties.push('authenticatedSignedWrites'); - } - - if (properties & 0x80) { - characteristic.properties.push('extendedProperties'); - } - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.handle] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.valueHandle] = characteristic; - - characteristics.push({ - uuid: characteristic.uuid, - properties: characteristic.properties - }); - } - - this.emit('characteristicsDiscover', deviceUuid, serviceUuid, characteristics); -}); - -nobleBindings.read = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(64 , { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -nobleBindings.on('kCBMsgId70', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var isNotification = args.kCBMsgArgIsNotification ? true : false; - var data = args.kCBMsgArgData; - - var peripheral = this._peripherals[deviceUuid]; - - if (peripheral) { - for(var i in peripheral.services) { - if (peripheral.services[i].characteristics && - peripheral.services[i].characteristics[characteristicHandle]) { - - this.emit('read', deviceUuid, peripheral.services[i].uuid, - peripheral.services[i].characteristics[characteristicHandle].uuid, data, isNotification); - break; - } - } - } else { - console.warn('noble (mac mavericks): received read event from unknown peripheral: ' + deviceUuid + ' !'); - } -}); - -nobleBindings.write = function(deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) { - this.sendCBMsg(65, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgData: data, - kCBMsgArgType: (withoutResponse ? 1 : 0) - }); - - if (withoutResponse) { - this.emit('write', deviceUuid, serviceUuid, characteristicUuid); - } -}; - -nobleBindings.on('kCBMsgId71', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('write', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid); - break; - } - } -}); - -nobleBindings.broadcast = function(deviceUuid, serviceUuid, characteristicUuid, broadcast) { - this.sendCBMsg(66, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (broadcast ? 1 : 0) - }); -}; - -nobleBindings.on('kCBMsgId72', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('broadcast', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - -nobleBindings.notify = function(deviceUuid, serviceUuid, characteristicUuid, notify) { - this.sendCBMsg(67, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (notify ? 1 : 0) - }); -}; - -nobleBindings.on('kCBMsgId73', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('notify', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - -nobleBindings.discoverDescriptors = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(69, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -nobleBindings.on('kCBMsgId75', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var descriptors = []; //args.kCBMsgArgDescriptors; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors = {}; - - for(var j = 0; j < args.kCBMsgArgDescriptors.length; j++) { - var descriptor = { - uuid: args.kCBMsgArgDescriptors[j].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgDescriptors[j].kCBMsgArgDescriptorHandle - }; - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.uuid] = - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.handle] = descriptor; - - descriptors.push(descriptor.uuid); - } - - this.emit('descriptorsDiscover', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, descriptors); - break; - } - } -}); - -nobleBindings.readValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) { - this.sendCBMsg(76, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle - }); -}; - -nobleBindings.on('kCBMsgId78', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - var data = args.kCBMsgArgData; - - this.emit('handleRead', deviceUuid, descriptorHandle, data); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueRead', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid, data); - return; // break; - } - } - } -}); - -nobleBindings.writeValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { - this.sendCBMsg(77, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle, - kCBMsgArgData: data - }); -}; - -nobleBindings.on('kCBMsgId79', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - - this.emit('handleWrite', deviceUuid, descriptorHandle); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueWrite', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid); - return; // break; - } - } - } -}); - -nobleBindings.readHandle = function(deviceUuid, handle) { - this.sendCBMsg(76, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle - }); -}; - -nobleBindings.writeHandle = function(deviceUuid, handle, data, withoutResponse) { - // TODO: use without response - this.sendCBMsg(77, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle, - kCBMsgArgData: data - }); -}; - -module.exports = nobleBindings; diff --git a/lib/mac/src/ble_manager.h b/lib/mac/src/ble_manager.h new file mode 100644 index 000000000..3ea5b7725 --- /dev/null +++ b/lib/mac/src/ble_manager.h @@ -0,0 +1,41 @@ +// +// ble_manager.h +// noble-mac-native +// +// Created by Georg Vienna on 28.08.18. +// + +#pragma once + +#import +#import +#include + +#include "callbacks.h" + +@interface BLEManager : NSObject { + Emit emit; + bool pendingRead; +} +@property (strong) CBCentralManager *centralManager; +@property dispatch_queue_t dispatchQueue; +@property NSMutableDictionary *peripherals; + +- (instancetype)init: (const Napi::Value&) receiver with: (const Napi::Function&) callback; +- (void)scan: (NSArray *)serviceUUIDs allowDuplicates: (BOOL)allowDuplicates; +- (void)stopScan; +- (BOOL)connect:(NSString*) uuid; +- (BOOL)disconnect:(NSString*) uuid; +- (BOOL)updateRSSI:(NSString*) uuid; +- (BOOL)discoverServices:(NSString*) uuid serviceUuids:(NSArray*) services; +- (BOOL)discoverIncludedServices:(NSString*) uuid forService:(NSString*) serviceUuid services:(NSArray*) serviceUuids; +- (BOOL)discoverCharacteristics:(NSString*) nsAddress forService:(NSString*) service characteristics:(NSArray*) characteristics; +- (BOOL)read:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid; +- (BOOL)write:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid data:(NSData*) data withoutResponse:(BOOL)withoutResponse; +- (BOOL)notify:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid on:(BOOL)on; +- (BOOL)discoverDescriptors:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid; +- (BOOL)readValue:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid descriptor:(NSString*) descriptorUuid; +- (BOOL)writeValue:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid descriptor:(NSString*) descriptorUuid data:(NSData*) data; +- (BOOL)readHandle:(NSString*) uuid handle:(NSNumber*) handle; +- (BOOL)writeHandle:(NSString*) uuid handle:(NSNumber*) handle data:(NSData*) data; +@end diff --git a/lib/mac/src/ble_manager.mm b/lib/mac/src/ble_manager.mm new file mode 100644 index 000000000..cee72ab83 --- /dev/null +++ b/lib/mac/src/ble_manager.mm @@ -0,0 +1,531 @@ +// +// ble_manager.mm +// noble-mac-native +// +// Created by Georg Vienna on 28.08.18. +// +#include "ble_manager.h" + +#import + +#include "objc_cpp.h" +#include + +#define LOGE(message, ...) \ +{\ + char buff[255];\ + snprintf(buff, sizeof(buff), ": " message, __VA_ARGS__);\ + std::string buffAsStdStr = __FUNCTION__;\ + buffAsStdStr += buff;\ + emit.Log(buffAsStdStr);\ +} + +const char* description(NSError *error) { + auto description = [error description]; + if(description != nil) { + auto str = [description UTF8String]; + if(str != NULL) { + return str; + } + } + return ""; +} + +@implementation BLEManager +- (instancetype)init: (const Napi::Value&) receiver with: (const Napi::Function&) callback { + if (self = [super init]) { + pendingRead = false; + // wrap cb before creating the CentralManager as it may call didUpdateState immediately + self->emit.Wrap(receiver, callback); + self.dispatchQueue = dispatch_queue_create("CBqueue", 0); + self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:self.dispatchQueue]; + self.peripherals = [NSMutableDictionary dictionaryWithCapacity:10]; + } + return self; +} + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central { + auto state = stateToString(central.state); + emit.RadioState(state); +} + +- (void)scan: (NSArray *)serviceUUIDs allowDuplicates: (BOOL)allowDuplicates { + NSMutableArray* advServicesUuid = [NSMutableArray arrayWithCapacity:[serviceUUIDs count]]; + [serviceUUIDs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [advServicesUuid addObject:[CBUUID UUIDWithString:obj]]; + }]; + NSDictionary *options = @{CBCentralManagerScanOptionAllowDuplicatesKey:[NSNumber numberWithBool:allowDuplicates]}; + [self.centralManager scanForPeripheralsWithServices:advServicesUuid options:options]; + emit.ScanState(true); +} + +- (void)stopScan { + [self.centralManager stopScan]; + emit.ScanState(false); +} + +- (void) centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { + std::string uuid = getUuid(peripheral); + + Peripheral p; + p.address = getAddress(uuid, &p.addressType); + IF(NSNumber*, connect, [advertisementData objectForKey:CBAdvertisementDataIsConnectable]) { + p.connectable = [connect boolValue]; + } + IF(NSString*, dataLocalName, [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]) { + p.name = std::make_pair([dataLocalName UTF8String], true); + } + if(!std::get<1>(p.name)) { + IF(NSString*, name, [peripheral name]) { + p.name = std::make_pair([name UTF8String], true); + } + } + IF(NSNumber*, txLevel, [advertisementData objectForKey:CBAdvertisementDataTxPowerLevelKey]) { + p.txPowerLevel = std::make_pair([txLevel intValue], true); + } + IF(NSData*, data, [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey]) { + const UInt8* bytes = (UInt8 *)[data bytes]; + std::get<0>(p.manufacturerData).assign(bytes, bytes+[data length]); + std::get<1>(p.manufacturerData) = true; + } + IF(NSDictionary*, dictionary, [advertisementData objectForKey:CBAdvertisementDataServiceDataKey]) { + for (CBUUID* key in dictionary) { + IF(NSData*, value, dictionary[key]) { + auto serviceUuid = [[key UUIDString] UTF8String]; + Data sData; + const UInt8* bytes = (UInt8 *)[value bytes]; + sData.assign(bytes, bytes+[value length]); + std::get<0>(p.serviceData).push_back(std::make_pair(serviceUuid, sData)); + } + } + std::get<1>(p.serviceData) = true; + } + IF(NSArray*, services, [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey]) { + for (CBUUID* service in services) { + std::get<0>(p.serviceUuids).push_back([[service UUIDString] UTF8String]); + } + std::get<1>(p.serviceUuids) = true; + } + + int rssi = [RSSI intValue]; + emit.Scan(uuid, rssi, p); +} + +- (BOOL)connect:(NSString*) uuid { + CBPeripheral *peripheral = [self.peripherals objectForKey:uuid]; + if(!peripheral) { + NSArray* peripherals = [self.centralManager retrievePeripheralsWithIdentifiers:@[[[NSUUID alloc] initWithUUIDString:uuid]]]; + peripheral = [peripherals firstObject]; + if(peripheral) { + peripheral.delegate = self; + [self.peripherals setObject:peripheral forKey:uuid]; + } else { + std::stringstream str; + str << "Peripheral with uuid "; + str << [uuid UTF8String]; + str << " not found!"; + emit.Connected([uuid UTF8String], str.str().c_str()); + return NO; + } + } + NSDictionary* options = @{CBConnectPeripheralOptionNotifyOnDisconnectionKey: [NSNumber numberWithBool:YES]}; + [self.centralManager connectPeripheral:peripheral options:options]; + return YES; +} + +- (void) centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral { + std::string uuid = getUuid(peripheral); + emit.Connected(uuid, ""); +} + +- (void) centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { + [self.peripherals removeObjectForKey:peripheral.identifier.UUIDString]; + std::string uuid = getUuid(peripheral); + std::string message = "Connection failed"; + if(error) { + message += ": "; + message += description(error); + } + emit.Connected(uuid, message); +} + +- (BOOL)disconnect:(NSString*) uuid { + IF(CBPeripheral*, peripheral, [self.peripherals objectForKey:uuid]) { + auto state = peripheral.state; + LOGE("disconnect called on %s state %ld", [uuid UTF8String], (long)state); + [self.centralManager cancelPeripheralConnection:peripheral]; + return YES; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +-(void) centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error { + [self.peripherals removeObjectForKey:peripheral.identifier.UUIDString]; + std::string uuid = getUuid(peripheral); + if(error) { + LOGE("error %s %s", uuid.c_str(), description(error)); + } + emit.Disconnected(uuid); +} + +- (BOOL)updateRSSI:(NSString*) uuid { + IF(CBPeripheral*, peripheral, [self.peripherals objectForKey:uuid]) { + [peripheral readRSSI]; + return YES; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheralDidUpdateRSSI:(CBPeripheral *)peripheral error:(NSError *)error { + std::string uuid = getUuid(peripheral); + NSNumber* rssi = peripheral.RSSI; + if(!error && rssi) { + emit.RSSI(uuid, [rssi longValue]); + } +} + +#pragma mark - Services + +-(BOOL) discoverServices:(NSString*) uuid serviceUuids:(NSArray*) services { + IF(CBPeripheral*, peripheral, [self.peripherals objectForKey:uuid]) { + NSMutableArray* servicesUuid = nil; + if(services) { + servicesUuid = [NSMutableArray arrayWithCapacity:[services count]]; + [services enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [servicesUuid addObject:[CBUUID UUIDWithString:obj]]; + }]; + } + [peripheral discoverServices:servicesUuid]; + return YES; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void) peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error { + std::string uuid = getUuid(peripheral); + std::vector services = getServices(peripheral.services); + if(error) { + LOGE("error %s %s", uuid.c_str(), description(error)); + } + emit.ServicesDiscovered(uuid, services); +} + +- (BOOL)discoverIncludedServices:(NSString*) uuid forService:(NSString*) serviceUuid services:(NSArray*) serviceUuids { + IF(CBPeripheral*, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBService*, service, [self getService:peripheral service:serviceUuid]) { + NSMutableArray* includedServices = nil; + if(serviceUuids) { + includedServices = [NSMutableArray arrayWithCapacity:[serviceUuids count]]; + [serviceUuids enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [includedServices addObject:[CBUUID UUIDWithString:obj]]; + }]; + } + [peripheral discoverIncludedServices:includedServices forService:service]; + return YES; + } + LOGE("service not found %s %s", [uuid UTF8String], [serviceUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverIncludedServicesForService:(CBService *)service error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [[service.UUID UUIDString] UTF8String]; + if(error) { + LOGE("error %s %s %s", uuid.c_str(), serviceUuid, description(error)); + } + std::vector services = getServices(service.includedServices); + emit.IncludedServicesDiscovered(uuid, serviceUuid, services); +} + +#pragma mark - Characteristics + +- (BOOL)discoverCharacteristics:(NSString*) uuid forService:(NSString*) serviceUuid characteristics:(NSArray*) characteristics { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBService*, service, [self getService:peripheral service:serviceUuid]) { + NSMutableArray* characteristicsUuid = nil; + if(characteristics) { + characteristicsUuid = [NSMutableArray arrayWithCapacity:[characteristics count]]; + [characteristics enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + [characteristicsUuid addObject:[CBUUID UUIDWithString:obj]]; + }]; + } + [peripheral discoverCharacteristics:characteristicsUuid forService:service]; + return YES; + } + LOGE("service not found %s %s", [uuid UTF8String], [serviceUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +-(void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [service.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s", uuid.c_str(), serviceUuid, description(error)); + } + auto characteristics = getCharacteristics(service.characteristics); + emit.CharacteristicsDiscovered(uuid, serviceUuid, characteristics); +} + +- (BOOL)read:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBCharacteristic*, characteristic, [self getCharacteristic:peripheral service:serviceUuid characteristic:characteristicUuid]) { + pendingRead = true; + [peripheral readValueForCharacteristic:characteristic]; + return YES; + } + LOGE("characteristic not found %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void) peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [characteristic.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, description(error)); + } + const UInt8* bytes = (UInt8 *)[characteristic.value bytes]; + Data data; + data.assign(bytes, bytes+[characteristic.value length]); + bool isNotification = !pendingRead && characteristic.isNotifying; + pendingRead = false; + emit.Read(uuid, serviceUuid, characteristicUuid, data, isNotification); +} + +- (BOOL)write:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid data:(NSData*) data withoutResponse:(BOOL)withoutResponse { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBCharacteristic*, characteristic, [self getCharacteristic:peripheral service:serviceUuid characteristic:characteristicUuid]) { + CBCharacteristicWriteType type = withoutResponse ? CBCharacteristicWriteWithoutResponse : CBCharacteristicWriteWithResponse; + [peripheral writeValue:data forCharacteristic:characteristic type:type]; + if (withoutResponse) { + emit.Write([uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String]); + } + return YES; + } + LOGE("characteristic not found %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +-(void) peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [characteristic.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, description(error)); + } + emit.Write(uuid, serviceUuid, characteristicUuid); +} + +- (BOOL)notify:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid on:(BOOL)on { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBCharacteristic*, characteristic, [self getCharacteristic:peripheral service:serviceUuid characteristic:characteristicUuid]) { + [peripheral setNotifyValue:on forCharacteristic:characteristic]; + return YES; + } + LOGE("characteristic not found %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheral:(CBPeripheral *)peripheral didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [characteristic.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, description(error)); + } + emit.Notify(uuid, serviceUuid, characteristicUuid, characteristic.isNotifying); +} + +#pragma mark - Descriptors + +- (BOOL)discoverDescriptors:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBCharacteristic*, characteristic, [self getCharacteristic:peripheral service:serviceUuid characteristic:characteristicUuid]) { + [peripheral discoverDescriptorsForCharacteristic:characteristic]; + return YES; + } + LOGE("characteristic not found %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheral:(CBPeripheral *)peripheral didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [characteristic.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, description(error)); + } + std::vector descriptors = getDescriptors(characteristic.descriptors); + emit.DescriptorsDiscovered(uuid, serviceUuid, characteristicUuid, descriptors); +} + +- (BOOL)readValue:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid descriptor:(NSString*) descriptorUuid { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBDescriptor*, descriptor, [self getDescriptor:peripheral service:serviceUuid characteristic:characteristicUuid descriptor:descriptorUuid]) { + [peripheral readValueForDescriptor:descriptor]; + return YES; + } + LOGE("descriptor not found %s %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String], [descriptorUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [descriptor.characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [descriptor.characteristic.UUID.UUIDString UTF8String]; + auto descriptorUuid = [descriptor.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, descriptorUuid, description(error)); + } + const UInt8* bytes = (UInt8 *)[descriptor.value bytes]; + Data data; + data.assign(bytes, bytes+[descriptor.value length]); + IF(NSNumber*, handle, [self getDescriptorHandle:descriptor]) { + emit.ReadHandle(uuid, [handle intValue], data); + } + emit.ReadValue(uuid, serviceUuid, characteristicUuid, descriptorUuid, data); +} + +- (BOOL)writeValue:(NSString*) uuid service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid descriptor:(NSString*) descriptorUuid data:(NSData*) data { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBDescriptor*, descriptor, [self getDescriptor:peripheral service:serviceUuid characteristic:characteristicUuid descriptor:descriptorUuid]) { + [peripheral writeValue:data forDescriptor:descriptor]; + return YES; + } + LOGE("descriptor not found %s %s %s %s", [uuid UTF8String], [serviceUuid UTF8String], [characteristicUuid UTF8String], [descriptorUuid UTF8String]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForDescriptor:(CBDescriptor *)descriptor error:(NSError *)error { + std::string uuid = getUuid(peripheral); + auto serviceUuid = [descriptor.characteristic.service.UUID.UUIDString UTF8String]; + auto characteristicUuid = [descriptor.characteristic.UUID.UUIDString UTF8String]; + auto descriptorUuid = [descriptor.UUID.UUIDString UTF8String]; + if(error) { + LOGE("error %s %s %s %s %s", uuid.c_str(), serviceUuid, characteristicUuid, descriptorUuid, description(error)); + } + IF(NSNumber*, handle, [self getDescriptorHandle:descriptor]) { + emit.WriteHandle(uuid, [handle intValue]); + } + emit.WriteValue(uuid, serviceUuid, characteristicUuid, descriptorUuid); +} + +- (BOOL)readHandle:(NSString*) uuid handle:(NSNumber*) handle { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBDescriptor*, descriptor, [self getDescriptor:peripheral ByHandle:handle]) { + [peripheral readValueForDescriptor:descriptor]; + return YES; + } + LOGE("descriptor not found %s handle %d", [uuid UTF8String], [handle intValue]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +- (BOOL)writeHandle:(NSString*) uuid handle:(NSNumber*) handle data:(NSData*) data { + IF(CBPeripheral *, peripheral, [self.peripherals objectForKey:uuid]) { + IF(CBDescriptor*, descriptor, [self getDescriptor:peripheral ByHandle:handle]) { + [peripheral writeValue:data forDescriptor:descriptor]; + return YES; + } + LOGE("descriptor not found %s handle %d", [uuid UTF8String], [handle intValue]); + return NO; + } + LOGE("peripheral not found %s", [uuid UTF8String]); + return NO; +} + +#pragma mark - Accessor + +-(CBService*)getService:(CBPeripheral*) peripheral service:(NSString*) serviceUuid { + if(peripheral && peripheral.services) { + for(CBService* service in peripheral.services) { + if([service.UUID isEqualTo:[CBUUID UUIDWithString:serviceUuid]]) { + return service; + } + } + } + return nil; +} + +-(CBCharacteristic*)getCharacteristic:(CBPeripheral*) peripheral service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid { + CBService* service = [self getService:peripheral service:serviceUuid]; + if(service && service.characteristics) { + for(CBCharacteristic* characteristic in service.characteristics) { + if([characteristic.UUID isEqualTo:[CBUUID UUIDWithString:characteristicUuid]]) { + return characteristic; + } + } + } + return nil; +} + +-(CBDescriptor*)getDescriptor:(CBPeripheral*) peripheral service:(NSString*) serviceUuid characteristic:(NSString*) characteristicUuid descriptor:(NSString*) descriptorUuid { + CBCharacteristic* characteristic = [self getCharacteristic:peripheral service:serviceUuid characteristic:characteristicUuid]; + if(characteristic && characteristic.descriptors) { + for(CBDescriptor* descriptor in characteristic.descriptors) { + if([descriptor.UUID isEqualTo:[CBUUID UUIDWithString:descriptorUuid]]) { + return descriptor; + } + } + } + return nil; +} + +-(NSNumber*)getDescriptorHandle:(CBDescriptor*) descriptor { + // use KVC to get the private handle property + id handle = [descriptor valueForKey:@"handle"]; + if([handle isKindOfClass:[NSNumber class]]) { + return handle; + } + return nil; +} + +-(CBDescriptor*)getDescriptor:(CBPeripheral*) peripheral ByHandle:(NSNumber*) handle { + if(peripheral && peripheral.services) { + for(CBService* service in peripheral.services) { + if(service.characteristics) { + for(CBCharacteristic* characteristic in service.characteristics) { + if(characteristic.descriptors) { + for(CBDescriptor* descriptor in characteristic.descriptors) { + if([handle isEqualTo:[self getDescriptorHandle:descriptor]]) { + return descriptor; + } + } + } + } + } + } + } + return nil; +} + +@end diff --git a/lib/mac/src/callbacks.cc b/lib/mac/src/callbacks.cc new file mode 100644 index 000000000..fd407135f --- /dev/null +++ b/lib/mac/src/callbacks.cc @@ -0,0 +1,231 @@ +// +// callbacks.cc +// noble-mac-native +// +// Created by Georg Vienna on 30.08.18. +// +#include "callbacks.h" + +#include + +#define _s(val) Napi::String::New(env, val) +#define _b(val) Napi::Boolean::New(env, val) +#define _n(val) Napi::Number::New(env, val) +#define _u(str) toUuid(env, str) + +Napi::String toUuid(Napi::Env& env, const std::string& uuid) { + std::string str(uuid); + str.erase(std::remove(str.begin(), str.end(), '-'), str.end()); + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + return _s(str); +} + +Napi::String toAddressType(Napi::Env& env, const AddressType& type) { + if(type == PUBLIC) { + return _s("public"); + } else if (type == RANDOM) { + return _s("random"); + } + return _s("unknown"); +} + +Napi::Buffer toBuffer(Napi::Env& env, const Data& data) { + if (data.empty()) { + return Napi::Buffer::New(env, 0); + } + return Napi::Buffer::Copy(env, &data[0], data.size()); +} + +Napi::Array toUuidArray(Napi::Env& env, const std::vector& data) { + if (data.empty()) { + return Napi::Array::New(env); + } + auto arr = Napi::Array::New(env, data.size()); + for (size_t i = 0; i < data.size(); i++) { + arr.Set(i, _u(data[i])); + } + return arr; +} + +Napi::Array toArray(Napi::Env& env, const std::vector& data) { + if (data.empty()) { + return Napi::Array::New(env); + } + auto arr = Napi::Array::New(env, data.size()); + for (size_t i = 0; i < data.size(); i++) { + arr.Set(i, _s(data[i])); + } + return arr; +} + +void Emit::Wrap(const Napi::Value& receiver, const Napi::Function& callback) { + mCallback = std::make_shared(receiver, callback); +} + +void Emit::RadioState(const std::string& state) { + mCallback->call([state](Napi::Env env, std::vector& args) { + // emit('stateChange', state); + args = { _s("stateChange"), _s(state) }; + }); +} + +void Emit::ScanState(bool start) { + mCallback->call([start](Napi::Env env, std::vector& args) { + // emit('scanStart') emit('scanStop') + args = { _s(start ? "scanStart" : "scanStop") }; + }); +} + +void Emit::Scan(const std::string& uuid, int rssi, const Peripheral& peripheral) { + auto address = peripheral.address; + auto addressType = peripheral.addressType; + auto connectable = peripheral.connectable; + auto name = peripheral.name; + auto txPowerLevel = peripheral.txPowerLevel; + auto manufacturerData = peripheral.manufacturerData; + auto serviceData = peripheral.serviceData; + auto serviceUuids = peripheral.serviceUuids; + mCallback->call([uuid, rssi, address, addressType, connectable, name, txPowerLevel, manufacturerData, serviceData, serviceUuids](Napi::Env env, std::vector& args) { + Napi::Object advertisment = Napi::Object::New(env); + if (std::get<1>(name)) { + advertisment.Set(_s("localName"), _s(std::get<0>(name))); + } + + if (std::get<1>(txPowerLevel)) { + advertisment.Set(_s("txPowerLevel"), std::get<0>(txPowerLevel)); + } + + if (std::get<1>(manufacturerData)) { + advertisment.Set(_s("manufacturerData"), toBuffer(env, std::get<0>(manufacturerData))); + } + + if (std::get<1>(serviceData)) { + auto array = std::get<0>(serviceData).empty() ? Napi::Array::New(env) : Napi::Array::New(env, std::get<0>(serviceData).size()); + for (size_t i = 0; i < std::get<0>(serviceData).size(); i++) { + Napi::Object data = Napi::Object::New(env); + data.Set(_s("uuid"), _u(std::get<0>(serviceData)[i].first)); + data.Set(_s("data"), toBuffer(env, std::get<0>(serviceData)[i].second)); + array.Set(i, data); + } + advertisment.Set(_s("serviceData"), array); + } + + if (std::get<1>(serviceUuids)) { + advertisment.Set(_s("serviceUuids"), toUuidArray(env, std::get<0>(serviceUuids))); + } + // emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi); + args = { _s("discover"), _u(uuid), _s(address), toAddressType(env, addressType), _b(connectable), advertisment, _n(rssi) }; + }); +} + +void Emit::Connected(const std::string& uuid, const std::string& error) { + mCallback->call([uuid, error](Napi::Env env, std::vector& args) { + // emit('connect', deviceUuid) error added here + args = { _s("connect"), _u(uuid), error.empty() ? env.Null() : _s(error) }; + }); +} + +void Emit::Disconnected(const std::string& uuid) { + mCallback->call([uuid](Napi::Env env, std::vector& args) { + // emit('disconnect', deviceUuid); + args = { _s("disconnect"), _u(uuid) }; + }); +} + +void Emit::RSSI(const std::string & uuid, int rssi) { + mCallback->call([uuid, rssi](Napi::Env env, std::vector& args) { + // emit('rssiUpdate', deviceUuid, rssi); + args = { _s("rssiUpdate"), _u(uuid), _n(rssi) }; + }); +} + +void Emit::ServicesDiscovered(const std::string & uuid, const std::vector& serviceUuids) { + mCallback->call([uuid, serviceUuids](Napi::Env env, std::vector& args) { + // emit('servicesDiscover', deviceUuid, serviceUuids) + args = { _s("servicesDiscover"), _u(uuid), toUuidArray(env, serviceUuids) }; + }); +} + +void Emit::IncludedServicesDiscovered(const std::string & uuid, const std::string & serviceUuid, const std::vector& serviceUuids) { + mCallback->call([uuid, serviceUuid, serviceUuids](Napi::Env env, std::vector& args) { + // emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids) + args = { _s("includedServicesDiscover"), _u(uuid), _u(serviceUuid), toUuidArray(env, serviceUuids) }; + }); +} + +void Emit::CharacteristicsDiscovered(const std::string & uuid, const std::string & serviceUuid, const std::vector>>& characteristics) { + mCallback->call([uuid, serviceUuid, characteristics](Napi::Env env, std::vector& args) { + auto arr = characteristics.empty() ? Napi::Array::New(env) : Napi::Array::New(env, characteristics.size()); + for (size_t i = 0; i < characteristics.size(); i++) { + Napi::Object characteristic = Napi::Object::New(env); + characteristic.Set(_s("uuid"), _u(characteristics[i].first)); + characteristic.Set(_s("properties"), toArray(env, characteristics[i].second)); + arr.Set(i, characteristic); + } + // emit('characteristicsDiscover', deviceUuid, serviceUuid, { uuid, properties: ['broadcast', 'read', ...]}) + args = { _s("characteristicsDiscover"), _u(uuid), _u(serviceUuid), arr }; + }); +} + +void Emit::Read(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid, const Data& data, bool isNotification) { + mCallback->call([uuid, serviceUuid, characteristicUuid, data, isNotification](Napi::Env env, std::vector& args) { + // emit('read', deviceUuid, serviceUuid, characteristicsUuid, data, isNotification); + args = { _s("read"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), toBuffer(env, data), _b(isNotification) }; + }); +} + +void Emit::Write(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid) { + mCallback->call([uuid, serviceUuid, characteristicUuid](Napi::Env env, std::vector& args) { + // emit('write', deviceUuid, servicesUuid, characteristicsUuid) + args = { _s("write"), _u(uuid), _u(serviceUuid), _u(characteristicUuid) }; + }); +} + +void Emit::Notify(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid, bool state) { + mCallback->call([uuid, serviceUuid, characteristicUuid, state](Napi::Env env, std::vector& args) { + // emit('notify', deviceUuid, servicesUuid, characteristicsUuid, state) + args = { _s("notify"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), _b(state) }; + }); +} + +void Emit::DescriptorsDiscovered(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid, const std::vector& descriptorUuids) { + mCallback->call([uuid, serviceUuid, characteristicUuid, descriptorUuids](Napi::Env env, std::vector& args) { + // emit('descriptorsDiscover', deviceUuid, servicesUuid, characteristicsUuid, descriptors: [uuids]) + args = { _s("descriptorsDiscover"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), toUuidArray(env, descriptorUuids) }; + }); +} + +void Emit::ReadValue(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid, const std::string& descriptorUuid, const Data& data) { + mCallback->call([uuid, serviceUuid, characteristicUuid, descriptorUuid, data](Napi::Env env, std::vector& args) { + // emit('valueRead', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) + args = { _s("valueRead"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), _u(descriptorUuid), toBuffer(env, data) }; + }); +} + +void Emit::WriteValue(const std::string & uuid, const std::string & serviceUuid, const std::string & characteristicUuid, const std::string& descriptorUuid) { + mCallback->call([uuid, serviceUuid, characteristicUuid, descriptorUuid](Napi::Env env, std::vector& args) { + // emit('valueWrite', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid); + args = { _s("valueWrite"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), _u(descriptorUuid) }; + }); +} + +void Emit::ReadHandle(const std::string & uuid, int descriptorHandle, const Data& data) { + mCallback->call([uuid, descriptorHandle, data](Napi::Env env, std::vector& args) { + // emit('handleRead', deviceUuid, descriptorHandle, data); + args = { _s("handleRead"), _u(uuid), _n(descriptorHandle), toBuffer(env, data) }; + }); +} + +void Emit::WriteHandle(const std::string & uuid, int descriptorHandle) { + mCallback->call([uuid, descriptorHandle](Napi::Env env, std::vector& args) { + // emit('handleWrite', deviceUuid, descriptorHandle); + args = { _s("handleWrite"), _u(uuid), _n(descriptorHandle) }; + }); +} + +void Emit::Log(const std::string& log) { + mCallback->call([log](Napi::Env env, std::vector& args) { + // emit('log', log); + args = { _s("log"), _s(log) }; + }); +} diff --git a/lib/mac/src/callbacks.h b/lib/mac/src/callbacks.h new file mode 100644 index 000000000..49a5d9299 --- /dev/null +++ b/lib/mac/src/callbacks.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include "peripheral.h" + +class ThreadSafeCallback; + +class Emit { +public: + void Wrap(const Napi::Value& receiver, const Napi::Function& callback); + void RadioState(const std::string& status); + void ScanState(bool start); + void Scan(const std::string& uuid, int rssi, const Peripheral& peripheral); + void Connected(const std::string& uuid, const std::string& error = ""); + void Disconnected(const std::string& uuid); + void RSSI(const std::string& uuid, int rssi); + void ServicesDiscovered(const std::string& uuid, const std::vector& serviceUuids); + void IncludedServicesDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::vector& serviceUuids); + void CharacteristicsDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::vector>>& characteristics); + void Read(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const Data& data, bool isNotification); + void Write(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid); + void Notify(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, bool state); + void DescriptorsDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::vector& descriptorUuids); + void ReadValue(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::string& descriptorUuid, const Data& data); + void WriteValue(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::string& descriptorUuid); + void ReadHandle(const std::string& uuid, int descriptorHandle, const std::vector& data); + void WriteHandle(const std::string& uuid, int descriptorHandle); + void Log(const std::string& log); +protected: + std::shared_ptr mCallback; +}; diff --git a/lib/mac/src/napi_objc.h b/lib/mac/src/napi_objc.h new file mode 100644 index 000000000..62fcb5e86 --- /dev/null +++ b/lib/mac/src/napi_objc.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#import + +NSArray* getUuidArray(const Napi::Value& value); +BOOL getBool(const Napi::Value& value, BOOL def); + +NSString* napiToUuidString(Napi::String string); +NSArray* napiToUuidArray(Napi::Array array); +NSData* napiToData(Napi::Buffer buffer); +NSNumber* napiToNumber(Napi::Number number); diff --git a/lib/mac/src/napi_objc.mm b/lib/mac/src/napi_objc.mm new file mode 100644 index 000000000..cf1ee3695 --- /dev/null +++ b/lib/mac/src/napi_objc.mm @@ -0,0 +1,50 @@ +// +// napi_objc.mm +// noble-mac-native +// +// Created by Georg Vienna on 30.08.18. +// +#include "napi_objc.h" + +NSString* napiToUuidString(Napi::String string) { + std::string str = string.Utf8Value(); + NSMutableString * uuid = [[NSMutableString alloc] initWithCString:str.c_str() encoding:NSASCIIStringEncoding]; + if([uuid length] == 32) { + [uuid insertString: @"-" atIndex: 8]; + [uuid insertString: @"-" atIndex: 13]; + [uuid insertString: @"-" atIndex: 18]; + [uuid insertString: @"-" atIndex: 23]; + } + return [uuid uppercaseString]; +} + +NSArray* napiToUuidArray(Napi::Array array) { + NSMutableArray* serviceUuids = [NSMutableArray arrayWithCapacity:array.Length()]; + for(size_t i = 0; i < array.Length(); i++) { + Napi::Value val = array[i]; + [serviceUuids addObject:napiToUuidString(val.As())]; + } + return serviceUuids; +} + +NSData* napiToData(Napi::Buffer buffer) { + return [NSData dataWithBytes:buffer.Data() length:buffer.Length()]; +} + +NSNumber* napiToNumber(Napi::Number number) { + return [NSNumber numberWithInt:number.Int64Value()]; +} + +NSArray* getUuidArray(const Napi::Value& value) { + if (value.IsArray()) { + return napiToUuidArray(value.As()); + } + return nil; +} + +BOOL getBool(const Napi::Value& value, BOOL def) { + if (value.IsBoolean()) { + return value.As().Value(); + } + return def; +} diff --git a/lib/mac/src/noble_mac.h b/lib/mac/src/noble_mac.h new file mode 100644 index 000000000..8ec1076a8 --- /dev/null +++ b/lib/mac/src/noble_mac.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "ble_manager.h" + +class NobleMac : public Napi::ObjectWrap +{ +public: + NobleMac(const Napi::CallbackInfo&); + Napi::Value Init(const Napi::CallbackInfo&); + Napi::Value Scan(const Napi::CallbackInfo&); + Napi::Value StopScan(const Napi::CallbackInfo&); + Napi::Value Connect(const Napi::CallbackInfo&); + Napi::Value Disconnect(const Napi::CallbackInfo&); + Napi::Value UpdateRSSI(const Napi::CallbackInfo&); + Napi::Value DiscoverServices(const Napi::CallbackInfo&); + Napi::Value DiscoverIncludedServices(const Napi::CallbackInfo& info); + Napi::Value DiscoverCharacteristics(const Napi::CallbackInfo& info); + Napi::Value Read(const Napi::CallbackInfo& info); + Napi::Value Write(const Napi::CallbackInfo& info); + Napi::Value Notify(const Napi::CallbackInfo& info); + Napi::Value DiscoverDescriptors(const Napi::CallbackInfo& info); + Napi::Value ReadValue(const Napi::CallbackInfo& info); + Napi::Value WriteValue(const Napi::CallbackInfo& info); + Napi::Value ReadHandle(const Napi::CallbackInfo& info); + Napi::Value WriteHandle(const Napi::CallbackInfo& info); + Napi::Value Stop(const Napi::CallbackInfo&); + + static Napi::Function GetClass(Napi::Env); + +private: + BLEManager* manager; +}; diff --git a/lib/mac/src/noble_mac.mm b/lib/mac/src/noble_mac.mm new file mode 100644 index 000000000..52025759a --- /dev/null +++ b/lib/mac/src/noble_mac.mm @@ -0,0 +1,259 @@ +// +// noble_mac.mm +// noble-mac-native +// +// Created by Georg Vienna on 28.08.18. +// +#include "noble_mac.h" + +#include "napi_objc.h" + +#define THROW(msg) \ +Napi::TypeError::New(info.Env(), msg).ThrowAsJavaScriptException(); \ +return Napi::Value(); + +#define ARG1(type1) \ +if (!info[0].Is##type1()) { \ + THROW("There should be one argument: (" #type1 ")") \ +} + +#define ARG2(type1, type2) \ +if (!info[0].Is##type1() || !info[1].Is##type2()) { \ + THROW("There should be 2 arguments: (" #type1 ", " #type2 ")"); \ +} + +#define ARG3(type1, type2, type3) \ +if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3()) { \ + THROW("There should be 3 arguments: (" #type1 ", " #type2 ", " #type3 ")"); \ +} + +#define ARG4(type1, type2, type3, type4) \ +if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3() || !info[3].Is##type4()) { \ + THROW("There should be 4 arguments: (" #type1 ", " #type2 ", " #type3 ", " #type4 ")"); \ +} + +#define ARG5(type1, type2, type3, type4, type5) \ +if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3() || !info[3].Is##type4() || !info[4].Is##type5()) { \ + THROW("There should be 5 arguments: (" #type1 ", " #type2 ", " #type3 ", " #type4 ", " #type5 ")"); \ +} + +#define CHECK_MANAGER() \ +if(!manager) { \ + THROW("BLEManager has already been cleaned up"); \ +} + +NobleMac::NobleMac(const Napi::CallbackInfo& info) : ObjectWrap(info) { +} + +Napi::Value NobleMac::Init(const Napi::CallbackInfo& info) { + Napi::Function emit = info.This().As().Get("emit").As(); + manager = [[BLEManager alloc] init:info.This() with:emit]; + return Napi::Value(); +} + +// startScanning(serviceUuids, allowDuplicates) +Napi::Value NobleMac::Scan(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + NSArray* array = getUuidArray(info[0]); + // default value NO + auto duplicates = getBool(info[1], NO); + [manager scan:array allowDuplicates:duplicates]; + return Napi::Value(); +} + +// stopScanning() +Napi::Value NobleMac::StopScan(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + [manager stopScan]; + return Napi::Value(); +} + +// connect(deviceUuid) +Napi::Value NobleMac::Connect(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG1(String) + auto uuid = napiToUuidString(info[0].As()); + [manager connect:uuid]; + return Napi::Value(); +} + +// disconnect(deviceUuid) +Napi::Value NobleMac::Disconnect(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG1(String) + auto uuid = napiToUuidString(info[0].As()); + [manager disconnect:uuid]; + return Napi::Value(); +} + +// updateRssi(deviceUuid) +Napi::Value NobleMac::UpdateRSSI(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG1(String) + auto uuid = napiToUuidString(info[0].As()); + [manager updateRSSI:uuid]; + return Napi::Value(); +} + +// discoverServices(deviceUuid, uuids) +Napi::Value NobleMac::DiscoverServices(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG1(String) + auto uuid = napiToUuidString(info[0].As()); + NSArray* array = getUuidArray(info[1]); + [manager discoverServices:uuid serviceUuids:array]; + return Napi::Value(); +} + +// discoverIncludedServices(deviceUuid, serviceUuid, serviceUuids) +Napi::Value NobleMac::DiscoverIncludedServices(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG2(String, String) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + NSArray* serviceUuids = getUuidArray(info[2]); + [manager discoverIncludedServices:uuid forService:service services:serviceUuids]; + return Napi::Value(); +} + +// discoverCharacteristics(deviceUuid, serviceUuid, characteristicUuids) +Napi::Value NobleMac::DiscoverCharacteristics(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG2(String, String) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + NSArray* characteristics = getUuidArray(info[2]); + [manager discoverCharacteristics:uuid forService:service characteristics:characteristics]; + return Napi::Value(); +} + +// read(deviceUuid, serviceUuid, characteristicUuid) +Napi::Value NobleMac::Read(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG3(String, String, String) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + [manager read:uuid service:service characteristic:characteristic]; + return Napi::Value(); +} + +// write(deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) +Napi::Value NobleMac::Write(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG5(String, String, String, Buffer, Boolean) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + auto data = napiToData(info[3].As>()); + auto withoutResponse = info[4].As().Value(); + [manager write:uuid service:service characteristic:characteristic data:data withoutResponse:withoutResponse]; + return Napi::Value(); +} + +// notify(deviceUuid, serviceUuid, characteristicUuid, notify) +Napi::Value NobleMac::Notify(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG4(String, String, String, Boolean) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + auto on = info[3].As().Value(); + [manager notify:uuid service:service characteristic:characteristic on:on]; + return Napi::Value(); +} + +// discoverDescriptors(deviceUuid, serviceUuid, characteristicUuid) +Napi::Value NobleMac::DiscoverDescriptors(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG3(String, String, String) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + [manager discoverDescriptors:uuid service:service characteristic:characteristic]; + return Napi::Value(); +} + +// readValue(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) +Napi::Value NobleMac::ReadValue(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG4(String, String, String, String) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + auto descriptor = napiToUuidString(info[3].As()); + [manager readValue:uuid service:service characteristic:characteristic descriptor:descriptor]; + return Napi::Value(); +} + +// writeValue(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) +Napi::Value NobleMac::WriteValue(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG5(String, String, String, String, Buffer) + auto uuid = napiToUuidString(info[0].As()); + auto service = napiToUuidString(info[1].As()); + auto characteristic = napiToUuidString(info[2].As()); + auto descriptor = napiToUuidString(info[3].As()); + auto data = napiToData(info[4].As>()); + [manager writeValue:uuid service:service characteristic:characteristic descriptor:descriptor data: data]; + return Napi::Value(); +} + +// readHandle(deviceUuid, handle) +Napi::Value NobleMac::ReadHandle(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG2(String, Number) + auto uuid = napiToUuidString(info[0].As()); + auto handle = napiToNumber(info[1].As()); + [manager readHandle:uuid handle:handle]; + return Napi::Value(); +} + +// writeHandle(deviceUuid, handle, data, (unused)withoutResponse) +Napi::Value NobleMac::WriteHandle(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + ARG3(String, Number, Buffer) + auto uuid = napiToUuidString(info[0].As()); + auto handle = napiToNumber(info[1].As()); + auto data = napiToData(info[2].As>()); + [manager writeHandle:uuid handle:handle data: data]; + return Napi::Value(); +} + +Napi::Value NobleMac::Stop(const Napi::CallbackInfo& info) { + CHECK_MANAGER() + CFRelease((__bridge CFTypeRef)manager); + manager = nil; + return Napi::Value(); +} + +Napi::Function NobleMac::GetClass(Napi::Env env) { + return DefineClass(env, "NobleMac", { + NobleMac::InstanceMethod("init", &NobleMac::Init), + NobleMac::InstanceMethod("startScanning", &NobleMac::Scan), + NobleMac::InstanceMethod("stopScanning", &NobleMac::StopScan), + NobleMac::InstanceMethod("connect", &NobleMac::Connect), + NobleMac::InstanceMethod("disconnect", &NobleMac::Disconnect), + NobleMac::InstanceMethod("updateRssi", &NobleMac::UpdateRSSI), + NobleMac::InstanceMethod("discoverServices", &NobleMac::DiscoverServices), + NobleMac::InstanceMethod("discoverIncludedServices", &NobleMac::DiscoverIncludedServices), + NobleMac::InstanceMethod("discoverCharacteristics", &NobleMac::DiscoverCharacteristics), + NobleMac::InstanceMethod("read", &NobleMac::Read), + NobleMac::InstanceMethod("write", &NobleMac::Write), + NobleMac::InstanceMethod("notify", &NobleMac::Notify), + NobleMac::InstanceMethod("discoverDescriptors", &NobleMac::DiscoverDescriptors), + NobleMac::InstanceMethod("readValue", &NobleMac::ReadValue), + NobleMac::InstanceMethod("writeValue", &NobleMac::WriteValue), + NobleMac::InstanceMethod("readHandle", &NobleMac::ReadHandle), + NobleMac::InstanceMethod("writeHandle", &NobleMac::WriteHandle), + NobleMac::InstanceMethod("stop", &NobleMac::Stop), + }); +} + +Napi::Object Init(Napi::Env env, Napi::Object exports) { + Napi::String name = Napi::String::New(env, "NobleMac"); + exports.Set(name, NobleMac::GetClass(env)); + return exports; +} + +NODE_API_MODULE(addon, Init) diff --git a/lib/mac/src/objc_cpp.h b/lib/mac/src/objc_cpp.h new file mode 100644 index 000000000..0a58cd7d9 --- /dev/null +++ b/lib/mac/src/objc_cpp.h @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#import +#import +#include "peripheral.h" + +#define IF(type, var, code) type var = code; if(var) + +#if defined(MAC_OS_X_VERSION_10_13) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" + std::string stateToString(CBManagerState state); +#pragma clang diagnostic pop +#else + std::string stateToString(CBCentralManagerState state); +#endif + + +std::string getUuid(CBPeripheral* peripheral); +std::string getAddress(std::string uuid, AddressType* addressType); +std::vector getServices(NSArray* services); +std::vector>> getCharacteristics(NSArray* characteristics); +std::vector getDescriptors(NSArray* descriptors); diff --git a/lib/mac/src/objc_cpp.mm b/lib/mac/src/objc_cpp.mm new file mode 100644 index 000000000..eab289fa5 --- /dev/null +++ b/lib/mac/src/objc_cpp.mm @@ -0,0 +1,122 @@ +// +// objc_cpp.mm +// noble-mac-native +// +// Created by Georg Vienna on 30.08.18. +// +#include "objc_cpp.h" + +#if defined(MAC_OS_X_VERSION_10_13) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" +std::string stateToString(CBManagerState state) +{ + switch(state) { + case CBManagerStatePoweredOff: + return "poweredOff"; + case CBManagerStatePoweredOn: + return "poweredOn"; + case CBManagerStateResetting: + return "resetting"; + case CBManagerStateUnauthorized: + return "unauthorized"; + case CBManagerStateUnknown: + return "unknown"; + case CBManagerStateUnsupported: + return "unsupported"; + } + return "unknown"; +} +#pragma clang diagnostic pop + +// In the 10.13 SDK, CBPeripheral became a subclass of CBPeer, which defines +// -[CBPeer identifier] as partially available. Pretend it still exists on +// CBPeripheral. At runtime the implementation on CBPeer will be invoked. +@interface CBPeripheral (HighSierraSDK) +@property(readonly, nonatomic) NSUUID* identifier; +@end +#else +std::string stateToString(CBCentralManagerState state) +{ + switch(state) { + case CBCentralManagerStatePoweredOff: + return "poweredOff"; + case CBCentralManagerStatePoweredOn: + return "poweredOn"; + case CBCentralManagerStateResetting: + return "resetting"; + case CBCentralManagerStateUnauthorized: + return "unauthorized"; + case CBCentralManagerStateUnknown: + return "unknown"; + case CBCentralManagerStateUnsupported: + return "unsupported"; + } + return "unknown"; +} +#endif + +std::string getUuid(CBPeripheral* peripheral) { + return std::string([peripheral.identifier.UUIDString UTF8String]); +} + +std::string getAddress(std::string uuid, AddressType* addressType) { + NSString* deviceUuid = [[NSString alloc] initWithCString:uuid.c_str() encoding:NSASCIIStringEncoding]; + IF(NSDictionary*, plist, [NSDictionary dictionaryWithContentsOfFile:@"/Library/Preferences/com.apple.Bluetooth.plist"]) { + IF(NSDictionary*, cache, [plist objectForKey:@"CoreBluetoothCache"]) { + IF(NSDictionary*, entry, [cache objectForKey:deviceUuid]) { + IF(NSNumber*, type, [entry objectForKey:@"DeviceAddressType"]) { + *addressType = [type boolValue] ? RANDOM : PUBLIC; + } + IF(NSString*, address, [entry objectForKey:@"DeviceAddress"]) { + return [address UTF8String]; + } + } + } + } + return ""; +} + +std::vector getServices(NSArray* services) { + std::vector result; + if(services) { + for (CBService* service in services) { + result.push_back([[service.UUID UUIDString] UTF8String]); + } + } + return result; +} + +#define TEST_PROP(type, str) if((characteristic.properties & type) == type) { properties.push_back(str); } + +std::vector>> getCharacteristics(NSArray* characteristics) { + std::vector>> result; + if(characteristics) { + for (CBCharacteristic* characteristic in characteristics) { + auto uuid = [[characteristic.UUID UUIDString] UTF8String]; + auto properties = std::vector(); + TEST_PROP(CBCharacteristicPropertyBroadcast, "broadcast"); + TEST_PROP(CBCharacteristicPropertyRead, "read"); + TEST_PROP(CBCharacteristicPropertyWriteWithoutResponse, "writeWithoutResponse"); + TEST_PROP(CBCharacteristicPropertyWrite, "write"); + TEST_PROP(CBCharacteristicPropertyNotify, "notify"); + TEST_PROP(CBCharacteristicPropertyIndicate, "indicate"); + TEST_PROP(CBCharacteristicPropertyAuthenticatedSignedWrites, "authenticatedSignedWrites"); + TEST_PROP(CBCharacteristicPropertyExtendedProperties, "extendedProperties"); + TEST_PROP(CBCharacteristicPropertyNotifyEncryptionRequired, "notifyEncryptionRequired"); + TEST_PROP(CBCharacteristicPropertyIndicateEncryptionRequired, "indicateEncryptionRequired"); + result.push_back(std::make_pair(uuid, properties)); + } + } + return result; +} + +std::vector getDescriptors(NSArray* descriptors) { + std::vector result; + if(descriptors) { + for (CBDescriptor* descriptor in descriptors) { + result.push_back([[descriptor.UUID UUIDString] UTF8String]); + } + } + return result; +} diff --git a/lib/mac/src/peripheral.h b/lib/mac/src/peripheral.h new file mode 100644 index 000000000..1b0dd069c --- /dev/null +++ b/lib/mac/src/peripheral.h @@ -0,0 +1,23 @@ +#pragma once + +using Data = std::vector; + +enum AddressType { + PUBLIC, + RANDOM, + UNKNOWN, +}; + +class Peripheral { +public: + Peripheral() : address("unknown"), addressType(UNKNOWN), connectable(false) { + } + std::string address; + AddressType addressType; + bool connectable; + std::pair name; + std::pair txPowerLevel; + std::pair manufacturerData; + std::pair>, bool> serviceData; + std::pair, bool> serviceUuids; +}; diff --git a/lib/mac/uuid-to-address.js b/lib/mac/uuid-to-address.js deleted file mode 100644 index fe4c4867a..000000000 --- a/lib/mac/uuid-to-address.js +++ /dev/null @@ -1,24 +0,0 @@ -var bplist = require('bplist-parser'); - -module.exports = function(uuid, callback) { - bplist.parseFile('/Library/Preferences/com.apple.Bluetooth.plist', function (err, obj) { - if (err) { - return callback(err); - } else if (obj[0].CoreBluetoothCache === undefined) { - return callback(new Error('Empty CoreBluetoothCache entry!')); - } - - uuid = uuid.toUpperCase(); - - var formattedUuid = uuid.substring(0, 8) + '-' + - uuid.substring(8, 12) + '-' + - uuid.substring(12, 16) + '-' + - uuid.substring(16, 20) + '-' + - uuid.substring(20); - - var coreBluetoothCacheEntry = obj[0].CoreBluetoothCache[formattedUuid]; - var address = coreBluetoothCacheEntry ? coreBluetoothCacheEntry.DeviceAddress.replace(/-/g, ':') : undefined; - - callback(null, address); - }); -}; diff --git a/lib/mac/yosemite.js b/lib/mac/yosemite.js deleted file mode 100644 index 79a4d39a0..000000000 --- a/lib/mac/yosemite.js +++ /dev/null @@ -1,865 +0,0 @@ -var events = require('events'); -var os = require('os'); -var util = require('util'); - -var debug = require('debug')('yosemite-bindings'); - -var XpcConnection = require('xpc-connection'); - -var localAddress = require('./local-address'); -var uuidToAddress = require('./uuid-to-address'); - -/** - * NobleBindings for mac - */ -var NobleBindings = function() { - this._peripherals = {}; - - this._xpcConnection = new XpcConnection('com.apple.blued'); - this._xpcConnection.on('error', function(message) {this.emit('xpcError', message);}.bind(this)); - this._xpcConnection.on('event', function(event) {this.emit('xpcEvent', event); }.bind(this)); -}; - -util.inherits(NobleBindings, events.EventEmitter); - -NobleBindings.prototype.sendXpcMessage = function(message) { - this._xpcConnection.sendMessage(message); -}; - -var nobleBindings = new NobleBindings(); - -// General xpc message handling -nobleBindings.on('xpcEvent', function(event) { - debug('xpcEvent: ' + JSON.stringify(event, undefined, 2)); - - var kCBMsgId = event.kCBMsgId; - var kCBMsgArgs = event.kCBMsgArgs; - this.emit('kCBMsgId' + kCBMsgId, kCBMsgArgs); -}); - -nobleBindings.on('xpcError', function(message) { - console.error('xpcError: ' + message); -}); - -nobleBindings.sendCBMsg = function(id, args) { - debug('sendCBMsg: ' + id + ', ' + JSON.stringify(args, undefined, 2)); - this.sendXpcMessage({kCBMsgId: id,kCBMsgArgs: args}); -}; - - - - -/** - * Init xpc connection to blued - * - * @discussion tested - */ -nobleBindings.init = function() { - this._xpcConnection.setup(); - - localAddress(function(address) { - if (address) { - this.emit('addressChange', address); - } - - this.sendCBMsg(1, { - kCBMsgArgName: 'node-' + (new Date()).getTime(), - kCBMsgArgOptions: { - kCBInitOptionShowPowerAlert: 0 - }, - kCBMsgArgType: 0 - }); - }.bind(this)); -}; - -nobleBindings.on('kCBMsgId6', function(args) { - var state = ['unknown', 'resetting', 'unsupported', 'unauthorized', 'poweredOff', 'poweredOn'][args.kCBMsgArgState]; - debug('state change ' + state); - this.emit('stateChange', state); -}); - - - -/** - * Start scanning - * @param {Array} serviceUuids Scan for these UUIDs, if undefined then scan for all - * @param {Bool} allowDuplicates Scan can return duplicates - * - * @discussion tested - */ -nobleBindings.startScanning = function(serviceUuids, allowDuplicates) { - var args = { - kCBMsgArgOptions: {}, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - if (allowDuplicates) { - args.kCBMsgArgOptions.kCBScanOptionAllowDuplicates = 1; - } - - this.sendCBMsg(29, args); - this.emit('scanStart'); -}; - -/** - * Response message to start scanning - * - * @example - * // For `TI Sensortag` the message lookes like this: - * handleMsg: 37, { - * kCBMsgArgAdvertisementData = { - * kCBAdvDataIsConnectable = 1; - * kCBAdvDataLocalName = SensorTag; - * kCBAdvDataTxPowerLevel = 0; - * }; - * kCBMsgArgDeviceUUID = "<__NSConcreteUUID 0x6180000208e0> 53486C7A-DED2-4AA6-8913-387CD22F25D8"; - * kCBMsgArgName = SensorTag; - * kCBMsgArgRssi = "-68"; - * } - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId37', function(args) { - if (Object.keys(args.kCBMsgArgAdvertisementData).length === 0 || - (args.kCBMsgArgAdvertisementData.kCBAdvDataIsConnectable !== undefined && - Object.keys(args.kCBMsgArgAdvertisementData).length === 1)) { - return; - } - - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var advertisement = { - localName: args.kCBMsgArgAdvertisementData.kCBAdvDataLocalName || args.kCBMsgArgName, - txPowerLevel: args.kCBMsgArgAdvertisementData.kCBAdvDataTxPowerLevel, - manufacturerData: args.kCBMsgArgAdvertisementData.kCBAdvDataManufacturerData, - serviceData: [], - serviceUuids: [] - }; - var connectable = args.kCBMsgArgAdvertisementData.kCBAdvDataIsConnectable ? true : false; - var rssi = args.kCBMsgArgRssi; - var i; - - if (args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs) { - for(i = 0; i < args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs.length; i++) { - advertisement.serviceUuids.push(args.kCBMsgArgAdvertisementData.kCBAdvDataServiceUUIDs[i].toString('hex')); - } - } - - var serviceData = args.kCBMsgArgAdvertisementData.kCBAdvDataServiceData; - if (serviceData) { - for (i = 0; i < serviceData.length; i += 2) { - var serviceDataUuid = serviceData[i].toString('hex'); - var data = serviceData[i + 1]; - - advertisement.serviceData.push({ - uuid: serviceDataUuid, - data: data - }); - } - } - - debug('peripheral ' + deviceUuid + ' discovered'); - - var uuid = new Buffer(deviceUuid, 'hex'); - uuid.isUuid = true; - - if (!this._peripherals[deviceUuid]) { - this._peripherals[deviceUuid] = {}; - } - - this._peripherals[deviceUuid].uuid = uuid; - this._peripherals[deviceUuid].connectable = connectable; - this._peripherals[deviceUuid].advertisement = advertisement; - this._peripherals[deviceUuid].rssi = rssi; - - (function(deviceUuid, advertisement, rssi) { - uuidToAddress(deviceUuid, function(error, address, addressType) { - address = address || 'unknown'; - addressType = addressType || 'unknown'; - - this._peripherals[deviceUuid].address = address; - this._peripherals[deviceUuid].addressType = addressType; - - this.emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi); - }.bind(this)); - }.bind(this))(deviceUuid, advertisement, rssi); -}); - - -/** - * Stop scanning - * - * @discussion tested - */ -nobleBindings.stopScanning = function() { - this.sendCBMsg(30, null); - this.emit('scanStop'); -}; - - - -/** - * Connect to peripheral - * @param {String} deviceUuid Peripheral uuid to connect to - * - * @discussion tested - */ -nobleBindings.connect = function(deviceUuid) { - this.sendCBMsg(31, { - kCBMsgArgOptions: { - kCBConnectOptionNotifyOnDisconnection: 1 - }, - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -nobleBindings.on('kCBMsgId38', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' connected'); - - this.emit('connect', deviceUuid); -}); - - - -/** - * Disconnect - * - * @param {String} deviceUuid Peripheral uuid to disconnect - * - * @discussion tested - */ -nobleBindings.disconnect = function(deviceUuid) { - this.sendCBMsg(32, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -/** - * Response to disconnect - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId40', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - - debug('peripheral ' + deviceUuid + ' disconnected'); - - this.emit('disconnect', deviceUuid); -}); - - - -/** - * Update RSSI - * - * @discussion tested - */ -nobleBindings.updateRssi = function(deviceUuid) { - this.sendCBMsg(44, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid - }); -}; - -/** - * Response to RSSI update - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId55', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var rssi = args.kCBMsgArgData; - - this._peripherals[deviceUuid].rssi = rssi; - - debug('peripheral ' + deviceUuid + ' RSSI update ' + rssi); - - this.emit('rssiUpdate', deviceUuid, rssi); -}); - - - -/** - * Discover services - * - * @param {String} deviceUuid Device UUID - * @param {Array} uuids Services to discover, if undefined then all - * - * @discussion tested - */ -nobleBindings.discoverServices = function(deviceUuid, uuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgUUIDs: [] - }; - - if (uuids) { - for(var i = 0; i < uuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(uuids[i], 'hex'); - } - } - - this.sendCBMsg(45, args); -}; - -/** - * Response to discover service - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId56', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceUuids = []; - - this._peripherals[deviceUuid].services = this._peripherals[deviceUuid].services || {}; - - if (args.kCBMsgArgServices) { - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var service = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - if (typeof this._peripherals[deviceUuid].services[service.uuid] == 'undefined') { - this._peripherals[deviceUuid].services[service.uuid] = this._peripherals[deviceUuid].services[service.startHandle] = service; - } - - serviceUuids.push(service.uuid); - } - } - // TODO: result 24 => device not connected - - this.emit('servicesDiscover', deviceUuid, serviceUuids); -}); - - - -/** - * [discoverIncludedServices description] - * - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} serviceUuids - * - * @dicussion tested - */ -nobleBindings.discoverIncludedServices = function(deviceUuid, serviceUuid, serviceUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (serviceUuids) { - for(var i = 0; i < serviceUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(serviceUuids[i], 'hex'); - } - } - - this.sendCBMsg(61, args); -}; - -/** - * Response to dicover included services - * - * @dicussion tested - */ -nobleBindings.on('kCBMsgId63', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var includedServiceUuids = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices || {}; - - for(var i = 0; i < args.kCBMsgArgServices.length; i++) { - var includedService = { - uuid: args.kCBMsgArgServices[i].kCBMsgArgUUID.toString('hex'), - startHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceStartHandle, - endHandle: args.kCBMsgArgServices[i].kCBMsgArgServiceEndHandle - }; - - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].includedServices[includedServices.startHandle] = includedService; - - includedServiceUuids.push(includedService.uuid); - } - - this.emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids); -}); - - - -/** - * Discover characteristic - * - * @param {String} deviceUuid Peripheral UUID - * @param {String} serviceUuid Service UUID - * @param {Array} characteristicUuids Characteristics to discover, all if empty - * - * @discussion tested - */ -nobleBindings.discoverCharacteristics = function(deviceUuid, serviceUuid, characteristicUuids) { - var args = { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgServiceStartHandle: this._peripherals[deviceUuid].services[serviceUuid].startHandle, - kCBMsgArgServiceEndHandle: this._peripherals[deviceUuid].services[serviceUuid].endHandle, - kCBMsgArgUUIDs: [] - }; - - if (characteristicUuids) { - for(var i = 0; i < characteristicUuids.length; i++) { - args.kCBMsgArgUUIDs[i] = new Buffer(characteristicUuids[i], 'hex'); - } - } - - this.sendCBMsg(62, args); -}; - -/** - * Response to characteristic discovery - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId64', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var serviceStartHandle = args.kCBMsgArgServiceStartHandle; - var serviceUuid = this._peripherals[deviceUuid].services[serviceStartHandle].uuid; - var result = args.kCBMsgArgResult; - var characteristics = []; - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics || {}; - - for(var i = 0; i < args.kCBMsgArgCharacteristics.length; i++) { - var properties = args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicProperties; - - var characteristic = { - uuid: args.kCBMsgArgCharacteristics[i].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicHandle, - valueHandle: args.kCBMsgArgCharacteristics[i].kCBMsgArgCharacteristicValueHandle, - properties: [] - }; - - if (properties & 0x01) { - characteristic.properties.push('broadcast'); - } - - if (properties & 0x02) { - characteristic.properties.push('read'); - } - - if (properties & 0x04) { - characteristic.properties.push('writeWithoutResponse'); - } - - if (properties & 0x08) { - characteristic.properties.push('write'); - } - - if (properties & 0x10) { - characteristic.properties.push('notify'); - } - - if (properties & 0x20) { - characteristic.properties.push('indicate'); - } - - if (properties & 0x40) { - characteristic.properties.push('authenticatedSignedWrites'); - } - - if (properties & 0x80) { - characteristic.properties.push('extendedProperties'); - } - - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.uuid] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.handle] = - this._peripherals[deviceUuid].services[serviceStartHandle].characteristics[characteristic.valueHandle] = characteristic; - - characteristics.push({ - uuid: characteristic.uuid, - properties: characteristic.properties - }); - } - - this.emit('characteristicsDiscover', deviceUuid, serviceUuid, characteristics); -}); - - - -/** - * Read value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * - * @discussion tested - */ -nobleBindings.read = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(65 , { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -/** - * Response to read value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId71', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var isNotification = args.kCBMsgArgIsNotification ? true : false; - var data = args.kCBMsgArgData; - - var peripheral = this._peripherals[deviceUuid]; - - if (peripheral) { - for(var i in peripheral.services) { - if (peripheral.services[i].characteristics && - peripheral.services[i].characteristics[characteristicHandle]) { - - this.emit('read', deviceUuid, peripheral.services[i].uuid, - peripheral.services[i].characteristics[characteristicHandle].uuid, data, isNotification); - break; - } - } - } else { - console.warn('noble (mac yosemite): received read event from unknown peripheral: ' + deviceUuid + ' !'); - } -}); - - - -/** - * Write value - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} characteristicUuid - * @param {[Type]} data - * @param {Bool} withoutResponse - * - * @discussion tested - */ -nobleBindings.write = function(deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) { - this.sendCBMsg(66, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgData: data, - kCBMsgArgType: (withoutResponse ? 1 : 0) - }); - - if (withoutResponse) { - this.emit('write', deviceUuid, serviceUuid, characteristicUuid); - } -}; - -/** - * Response to write - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId72', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('write', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid); - break; - } - } -}); - - - -/** - * Broadcast - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} broadcast [description] - * @return {[type]} [description] - * - * @discussion The ids were incemented but there seems to be no CoreBluetooth function to call/verify this. - */ -nobleBindings.broadcast = function(deviceUuid, serviceUuid, characteristicUuid, broadcast) { - this.sendCBMsg(67, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (broadcast ? 1 : 0) - }); -}; - -/** - * Response to broadcast - * - * @discussion The ids were incemented but there seems to be no CoreBluetooth function to call/verify this. - */ -nobleBindings.on('kCBMsgId73', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('broadcast', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - - - -/** - * Register notification hanlder - * - * @param {String} deviceUuid Peripheral UUID - * @param {String} serviceUuid Service UUID - * @param {String} characteristicUuid Charactereistic UUID - * @param {Bool} notify If want to get notification - * - * @discussion tested - */ -nobleBindings.notify = function(deviceUuid, serviceUuid, characteristicUuid, notify) { - this.sendCBMsg(68, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle, - kCBMsgArgState: (notify ? 1 : 0) - }); -}; - -/** - * Response notification - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId74', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var state = args.kCBMsgArgState ? true : false; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - this.emit('notify', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, state); - break; - } - } -}); - - - -/** - * Discover service descriptors - * - * @param {String} deviceUuid - * @param {String} serviceUuid - * @param {String} characteristicUuid - * - * @discussion tested - */ -nobleBindings.discoverDescriptors = function(deviceUuid, serviceUuid, characteristicUuid) { - this.sendCBMsg(70, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgCharacteristicHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].handle, - kCBMsgArgCharacteristicValueHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].valueHandle - }); -}; - -/** - * Response to descriptor discovery - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId76', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var characteristicHandle = args.kCBMsgArgCharacteristicHandle; - var result = args.kCBMsgArgResult; - var descriptors = []; //args.kCBMsgArgDescriptors; - - for(var i in this._peripherals[deviceUuid].services) { - if (this._peripherals[deviceUuid].services[i].characteristics && - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle]) { - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors = {}; - - for(var j = 0; j < args.kCBMsgArgDescriptors.length; j++) { - var descriptor = { - uuid: args.kCBMsgArgDescriptors[j].kCBMsgArgUUID.toString('hex'), - handle: args.kCBMsgArgDescriptors[j].kCBMsgArgDescriptorHandle - }; - - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.uuid] = - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].descriptors[descriptor.handle] = descriptor; - - descriptors.push(descriptor.uuid); - } - - this.emit('descriptorsDiscover', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[characteristicHandle].uuid, descriptors); - break; - } - } -}); - - - -/** - * Read value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} descriptorUuid [description] - * - * @discussion tested - */ -nobleBindings.readValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) { - this.sendCBMsg(77, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle - }); -}; - -/** - * Response to read value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId79', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - var data = args.kCBMsgArgData; - - this.emit('handleRead', deviceUuid, descriptorHandle, data); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueRead', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid, data); - return; // break; - } - } - } -}); - - - -/** - * Write value - * - * @param {[type]} deviceUuid [description] - * @param {[type]} serviceUuid [description] - * @param {[type]} characteristicUuid [description] - * @param {[type]} descriptorUuid [description] - * @param {[type]} data [description] - * - * @discussion tested - */ -nobleBindings.writeValue = function(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { - this.sendCBMsg(78, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: this._peripherals[deviceUuid].services[serviceUuid].characteristics[characteristicUuid].descriptors[descriptorUuid].handle, - kCBMsgArgData: data - }); -}; - -/** - * Response to write value - * - * @discussion tested - */ -nobleBindings.on('kCBMsgId80', function(args) { - var deviceUuid = args.kCBMsgArgDeviceUUID.toString('hex'); - var descriptorHandle = args.kCBMsgArgDescriptorHandle; - var result = args.kCBMsgArgResult; - - this.emit('handleWrite', deviceUuid, descriptorHandle); - - for(var i in this._peripherals[deviceUuid].services) { - for(var j in this._peripherals[deviceUuid].services[i].characteristics) { - if (this._peripherals[deviceUuid].services[i].characteristics[j].descriptors && - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle]) { - - this.emit('valueWrite', deviceUuid, this._peripherals[deviceUuid].services[i].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].uuid, - this._peripherals[deviceUuid].services[i].characteristics[j].descriptors[descriptorHandle].uuid); - return; // break; - } - } - } -}); - - - -/** - * Reade value directly from handle - * - * @param {[type]} deviceUuid [description] - * @param {[type]} handle [description] - * - * @discussion tested - */ -nobleBindings.readHandle = function(deviceUuid, handle) { - this.sendCBMsg(77, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle - }); -}; - - - -/** - * Write value directly to handle - * - * @param {[type]} deviceUuid [description] - * @param {[type]} handle [description] - * @param {[type]} data [description] - * @param {[type]} withoutResponse [description] - * - * @discussion tested - */ -nobleBindings.writeHandle = function(deviceUuid, handle, data, withoutResponse) { - // TODO: use without response - this.sendCBMsg(78, { - kCBMsgArgDeviceUUID: this._peripherals[deviceUuid].uuid, - kCBMsgArgDescriptorHandle: handle, - kCBMsgArgData: data - }); -}; - - -// Exports -module.exports = nobleBindings; diff --git a/lib/noble.js b/lib/noble.js index 5a45e2c8e..9f18f8510 100644 --- a/lib/noble.js +++ b/lib/noble.js @@ -41,6 +41,7 @@ function Noble(bindings) { this._bindings.on('handleRead', this.onHandleRead.bind(this)); this._bindings.on('handleWrite', this.onHandleWrite.bind(this)); this._bindings.on('handleNotify', this.onHandleNotify.bind(this)); + this._bindings.on('log', this.onLog.bind(this)); this.on('warning', function(message) { if (this.listeners('warning').length === 1) { @@ -463,4 +464,8 @@ Noble.prototype.onHandleNotify = function(peripheralUuid, handle, data) { } }; +Noble.prototype.onLog = function(log) { + this.emit('log', log); +} + module.exports = Noble; diff --git a/lib/resolve-bindings.js b/lib/resolve-bindings.js index 36752fa85..bedadb51f 100644 --- a/lib/resolve-bindings.js +++ b/lib/resolve-bindings.js @@ -1,5 +1,12 @@ var os = require('os'); +function recentEnoughVersion() { + const ver = os.release().split('.').map(Number); + return (ver[0] > 10 || + (ver[0] === 10 && ver[1] > 0) || + (ver[0] === 10 && ver[1] === 0 && ver[2] >= 17134)) +} + module.exports = function() { var platform = os.platform(); @@ -9,7 +16,13 @@ module.exports = function() { return require('./distributed/bindings'); } else if (platform === 'darwin') { return require('./mac/bindings'); - } else if (platform === 'linux' || platform === 'freebsd' || platform === 'win32') { + } else if (platform === 'win32') { + if(!process.env.NOBLE_FORCE_DONGLE && recentEnoughVersion()) { + return require('./winrt/bindings'); + } else { + return require('./win32/bindings'); + } + } else if (platform === 'linux' || platform === 'freebsd') { return require('./hci-socket/bindings'); } else { throw new Error('Unsupported platform'); diff --git a/lib/win32/Readme.txt b/lib/win32/Readme.txt new file mode 100644 index 000000000..c3e911713 --- /dev/null +++ b/lib/win32/Readme.txt @@ -0,0 +1 @@ +this is the same as the hci-socket directory, but uses a adapted bluetooth-hci-socket (hci-socket.js) diff --git a/lib/win32/bindings.js b/lib/win32/bindings.js new file mode 100644 index 000000000..478e218ba --- /dev/null +++ b/lib/win32/bindings.js @@ -0,0 +1,511 @@ +var debug = require('debug')('bindings'); + +var events = require('events'); +var util = require('util'); + +var AclStream = require('../hci-socket/acl-stream'); +var Gatt = require('../hci-socket/gatt'); +var Gap = require('../hci-socket/gap'); +var Hci = require('./hci'); +var Signaling = require('../hci-socket/signaling'); + + +var NobleBindings = function() { + this._state = null; + + this._addresses = {}; + this._addresseTypes = {}; + this._connectable = {}; + + this._pendingConnectionUuid = null; + this._connectionQueue = []; + + this._handles = {}; + this._gatts = {}; + this._aclStreams = {}; + this._signalings = {}; + + this._hci = new Hci(); + this._gap = new Gap(this._hci); +}; + +util.inherits(NobleBindings, events.EventEmitter); + + +NobleBindings.prototype.startScanning = function(serviceUuids, allowDuplicates) { + this._scanServiceUuids = serviceUuids || []; + + this._gap.startScanning(allowDuplicates); +}; + +NobleBindings.prototype.stopScanning = function() { + this._gap.stopScanning(); +}; + +NobleBindings.prototype.connect = function(peripheralUuid) { + var address = this._addresses[peripheralUuid]; + var addressType = this._addresseTypes[peripheralUuid]; + + if (!this._pendingConnectionUuid) { + this._pendingConnectionUuid = peripheralUuid; + + this._hci.createLeConn(address, addressType); + } else { + this._connectionQueue.push(peripheralUuid); + } +}; + +NobleBindings.prototype.disconnect = function(peripheralUuid) { + this._hci.disconnect(this._handles[peripheralUuid]); +}; + +NobleBindings.prototype.updateRssi = function(peripheralUuid) { + this._hci.readRssi(this._handles[peripheralUuid]); +}; + +NobleBindings.prototype.init = function() { + this.onSigIntBinded = this.onSigInt.bind(this); + + this._gap.on('scanStart', this.onScanStart.bind(this)); + this._gap.on('scanStop', this.onScanStop.bind(this)); + this._gap.on('discover', this.onDiscover.bind(this)); + + this._hci.on('stateChange', this.onStateChange.bind(this)); + this._hci.on('addressChange', this.onAddressChange.bind(this)); + this._hci.on('leConnComplete', this.onLeConnComplete.bind(this)); + this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this)); + this._hci.on('rssiRead', this.onRssiRead.bind(this)); + this._hci.on('disconnComplete', this.onDisconnComplete.bind(this)); + this._hci.on('encryptChange', this.onEncryptChange.bind(this)); + this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this)); + + this._hci.init(); + + /* Add exit handlers after `init()` has completed. If no adaptor + is present it can throw an exception - in which case we don't + want to try and clear up afterwards (issue #502) */ + process.on('SIGINT', this.onSigIntBinded); + process.on('exit', this.onExit.bind(this)); +}; + +NobleBindings.prototype.onSigInt = function() { + var sigIntListeners = process.listeners('SIGINT'); + + if (sigIntListeners[sigIntListeners.length - 1] === this.onSigIntBinded) { + // we are the last listener, so exit + // this will trigger onExit, and clean up + process.exit(1); + } +}; + +NobleBindings.prototype.onExit = function() { + this.stopScanning(); + + for (var handle in this._aclStreams) { + this._hci.disconnect(handle); + } +}; + +NobleBindings.prototype.onStateChange = function(state) { + if (this._state === state) { + return; + } + this._state = state; + + + if (state === 'unauthorized') { + console.log('noble warning: adapter state unauthorized, please run as root or with sudo'); + console.log(' or see README for information on running without root/sudo:'); + console.log(' https://github.com/sandeepmistry/noble#running-on-linux'); + } else if (state === 'unsupported') { + console.log('noble warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).'); + console.log(' Try to run with environment variable:'); + console.log(' [sudo] NOBLE_HCI_DEVICE_ID=x node ...'); + } + + this.emit('stateChange', state); +}; + +NobleBindings.prototype.onAddressChange = function(address) { + this.emit('addressChange', address); +}; + +NobleBindings.prototype.onScanStart = function(filterDuplicates) { + this.emit('scanStart', filterDuplicates); +}; + +NobleBindings.prototype.onScanStop = function() { + this.emit('scanStop'); +}; + +NobleBindings.prototype.onDiscover = function(status, address, addressType, connectable, advertisement, rssi) { + if (this._scanServiceUuids === undefined) { + return; + } + + var serviceUuids = advertisement.serviceUuids || []; + var serviceData = advertisement.serviceData || []; + var hasScanServiceUuids = (this._scanServiceUuids.length === 0); + + if (!hasScanServiceUuids) { + var i; + + serviceUuids = serviceUuids.slice(); + + for (i in serviceData) { + serviceUuids.push(serviceData[i].uuid); + } + + for (i in serviceUuids) { + hasScanServiceUuids = (this._scanServiceUuids.indexOf(serviceUuids[i]) !== -1); + + if (hasScanServiceUuids) { + break; + } + } + } + + if (hasScanServiceUuids) { + var uuid = address.split(':').join(''); + this._addresses[uuid] = address; + this._addresseTypes[uuid] = addressType; + this._connectable[uuid] = connectable; + + this.emit('discover', uuid, address, addressType, connectable, advertisement, rssi); + } +}; + +NobleBindings.prototype.onLeConnComplete = function(status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) { + var uuid = null; + + var error = null; + + if (status === 0) { + uuid = address.split(':').join('').toLowerCase(); + + var aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address); + var gatt = new Gatt(address, aclStream); + var signaling = new Signaling(handle, aclStream); + + this._gatts[uuid] = this._gatts[handle] = gatt; + this._signalings[uuid] = this._signalings[handle] = signaling; + this._aclStreams[handle] = aclStream; + this._handles[uuid] = handle; + this._handles[handle] = uuid; + + this._gatts[handle].on('mtu', this.onMtu.bind(this)); + this._gatts[handle].on('servicesDiscover', this.onServicesDiscovered.bind(this)); + this._gatts[handle].on('includedServicesDiscover', this.onIncludedServicesDiscovered.bind(this)); + this._gatts[handle].on('characteristicsDiscover', this.onCharacteristicsDiscovered.bind(this)); + this._gatts[handle].on('read', this.onRead.bind(this)); + this._gatts[handle].on('write', this.onWrite.bind(this)); + this._gatts[handle].on('broadcast', this.onBroadcast.bind(this)); + this._gatts[handle].on('notify', this.onNotify.bind(this)); + this._gatts[handle].on('notification', this.onNotification.bind(this)); + this._gatts[handle].on('descriptorsDiscover', this.onDescriptorsDiscovered.bind(this)); + this._gatts[handle].on('valueRead', this.onValueRead.bind(this)); + this._gatts[handle].on('valueWrite', this.onValueWrite.bind(this)); + this._gatts[handle].on('handleRead', this.onHandleRead.bind(this)); + this._gatts[handle].on('handleWrite', this.onHandleWrite.bind(this)); + this._gatts[handle].on('handleNotify', this.onHandleNotify.bind(this)); + + this._signalings[handle].on('connectionParameterUpdateRequest', this.onConnectionParameterUpdateRequest.bind(this)); + + this._gatts[handle].exchangeMtu(256); + } else { + uuid = this._pendingConnectionUuid; + var statusMessage = Hci.STATUS_MAPPER[status] || 'HCI Error: Unknown'; + var errorCode = ' (0x' + status.toString(16) + ')'; + statusMessage = statusMessage + errorCode; + error = new Error(statusMessage); + } + + this.emit('connect', uuid, error); + + if (this._connectionQueue.length > 0) { + var peripheralUuid = this._connectionQueue.shift(); + + address = this._addresses[peripheralUuid]; + addressType = this._addresseTypes[peripheralUuid]; + + this._pendingConnectionUuid = peripheralUuid; + + this._hci.createLeConn(address, addressType); + } else { + this._pendingConnectionUuid = null; + } +}; + +NobleBindings.prototype.onLeConnUpdateComplete = function(handle, interval, latency, supervisionTimeout) { + // no-op +}; + +NobleBindings.prototype.onDisconnComplete = function(handle, reason) { + var uuid = this._handles[handle]; + + if (uuid) { + this._aclStreams[handle].push(null, null); + this._gatts[handle].removeAllListeners(); + this._signalings[handle].removeAllListeners(); + + delete this._gatts[uuid]; + delete this._gatts[handle]; + delete this._signalings[uuid]; + delete this._signalings[handle]; + delete this._aclStreams[handle]; + delete this._handles[uuid]; + delete this._handles[handle]; + + this.emit('disconnect', uuid); // TODO: handle reason? + } else { + console.warn('noble warning: unknown handle ' + handle + ' disconnected!'); + } +}; + +NobleBindings.prototype.onEncryptChange = function(handle, encrypt) { + var aclStream = this._aclStreams[handle]; + + if (aclStream) { + aclStream.pushEncrypt(encrypt); + } +}; + +NobleBindings.prototype.onMtu = function(address, mtu) { + +}; + +NobleBindings.prototype.onRssiRead = function(handle, rssi) { + this.emit('rssiUpdate', this._handles[handle], rssi); +}; + + +NobleBindings.prototype.onAclDataPkt = function(handle, cid, data) { + var aclStream = this._aclStreams[handle]; + + if (aclStream) { + aclStream.push(cid, data); + } +}; + + +NobleBindings.prototype.discoverServices = function(peripheralUuid, uuids) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.discoverServices(uuids || []); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onServicesDiscovered = function(address, serviceUuids) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('servicesDiscover', uuid, serviceUuids); +}; + +NobleBindings.prototype.discoverIncludedServices = function(peripheralUuid, serviceUuid, serviceUuids) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.discoverIncludedServices(serviceUuid, serviceUuids || []); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onIncludedServicesDiscovered = function(address, serviceUuid, includedServiceUuids) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('includedServicesDiscover', uuid, serviceUuid, includedServiceUuids); +}; + +NobleBindings.prototype.discoverCharacteristics = function(peripheralUuid, serviceUuid, characteristicUuids) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.discoverCharacteristics(serviceUuid, characteristicUuids || []); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onCharacteristicsDiscovered = function(address, serviceUuid, characteristics) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('characteristicsDiscover', uuid, serviceUuid, characteristics); +}; + +NobleBindings.prototype.read = function(peripheralUuid, serviceUuid, characteristicUuid) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.read(serviceUuid, characteristicUuid); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onRead = function(address, serviceUuid, characteristicUuid, data) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('read', uuid, serviceUuid, characteristicUuid, data, false); +}; + +NobleBindings.prototype.write = function(peripheralUuid, serviceUuid, characteristicUuid, data, withoutResponse) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.write(serviceUuid, characteristicUuid, data, withoutResponse); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onWrite = function(address, serviceUuid, characteristicUuid) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('write', uuid, serviceUuid, characteristicUuid); +}; + +NobleBindings.prototype.broadcast = function(peripheralUuid, serviceUuid, characteristicUuid, broadcast) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.broadcast(serviceUuid, characteristicUuid, broadcast); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onBroadcast = function(address, serviceUuid, characteristicUuid, state) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('broadcast', uuid, serviceUuid, characteristicUuid, state); +}; + +NobleBindings.prototype.notify = function(peripheralUuid, serviceUuid, characteristicUuid, notify) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.notify(serviceUuid, characteristicUuid, notify); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onNotify = function(address, serviceUuid, characteristicUuid, state) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('notify', uuid, serviceUuid, characteristicUuid, state); +}; + +NobleBindings.prototype.onNotification = function(address, serviceUuid, characteristicUuid, data) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('read', uuid, serviceUuid, characteristicUuid, data, true); +}; + +NobleBindings.prototype.discoverDescriptors = function(peripheralUuid, serviceUuid, characteristicUuid) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.discoverDescriptors(serviceUuid, characteristicUuid); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onDescriptorsDiscovered = function(address, serviceUuid, characteristicUuid, descriptorUuids) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('descriptorsDiscover', uuid, serviceUuid, characteristicUuid, descriptorUuids); +}; + +NobleBindings.prototype.readValue = function(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.readValue(serviceUuid, characteristicUuid, descriptorUuid); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onValueRead = function(address, serviceUuid, characteristicUuid, descriptorUuid, data) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('valueRead', uuid, serviceUuid, characteristicUuid, descriptorUuid, data); +}; + +NobleBindings.prototype.writeValue = function(peripheralUuid, serviceUuid, characteristicUuid, descriptorUuid, data) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.writeValue(serviceUuid, characteristicUuid, descriptorUuid, data); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onValueWrite = function(address, serviceUuid, characteristicUuid, descriptorUuid) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('valueWrite', uuid, serviceUuid, characteristicUuid, descriptorUuid); +}; + +NobleBindings.prototype.readHandle = function(peripheralUuid, attHandle) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.readHandle(attHandle); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onHandleRead = function(address, handle, data) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('handleRead', uuid, handle, data); +}; + +NobleBindings.prototype.writeHandle = function(peripheralUuid, attHandle, data, withoutResponse) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.writeHandle(attHandle, data, withoutResponse); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + +NobleBindings.prototype.onHandleWrite = function(address, handle) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('handleWrite', uuid, handle); +}; + +NobleBindings.prototype.onHandleNotify = function(address, handle, data) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('handleNotify', uuid, handle, data); +}; + +NobleBindings.prototype.onConnectionParameterUpdateRequest = function(handle, minInterval, maxInterval, latency, supervisionTimeout) { + this._hci.connUpdateLe(handle, minInterval, maxInterval, latency, supervisionTimeout); +}; + +module.exports = new NobleBindings(); diff --git a/lib/win32/hci-socket.js b/lib/win32/hci-socket.js new file mode 100644 index 000000000..4d9297373 --- /dev/null +++ b/lib/win32/hci-socket.js @@ -0,0 +1,241 @@ +var events = require('events'); +var util = require('util'); + +var debug = require('debug')('hci-usb'); +var usb = require('usb'); + +var HCI_COMMAND_PKT = 0x01; +var HCI_ACLDATA_PKT = 0x02; +var HCI_EVENT_PKT = 0x04; + +var OGF_HOST_CTL = 0x03; +var OCF_RESET = 0x0003; + +var VENDOR_DEVICE_LIST = [ + {vid: 0x0CF3, pid: 0xE300 }, // Qualcomm Atheros QCA61x4 + {vid: 0x0a5c, pid: 0x21e8 }, // Broadcom BCM20702A0 + {vid: 0x19ff, pid: 0x0239 }, // Broadcom BCM20702A0 + {vid: 0x0a12, pid: 0x0001 }, // CSR + {vid: 0x0b05, pid: 0x17cb }, // ASUS BT400 + {vid: 0x8087, pid: 0x07da }, // Intel 6235 + {vid: 0x8087, pid: 0x07dc }, // Intel 7260 + {vid: 0x8087, pid: 0x0a2a }, // Intel 7265 + {vid: 0x8087, pid: 0x0a2b }, // Intel 8265 + {vid: 0x0489, pid: 0xe07a }, // Broadcom BCM20702A1 + {vid: 0x0a5c, pid: 0x6412 }, // Broadcom BCM2045A0 + {vid: 0x050D, pid: 0x065A }, // Belkin BCM20702A0 +]; + +function BluetoothHciSocket() { + this._hciEventEndpointBuffer = new Buffer(0); + this._aclDataInEndpointBuffer = new Buffer(0); +} + +util.inherits(BluetoothHciSocket, events.EventEmitter); + +BluetoothHciSocket.prototype.setFilter = function(filter) { + // no-op +}; + +BluetoothHciSocket.prototype.bindRaw = function() { + this._mode = 'raw'; + + usb.on('attach', this.onDeviceAttached.bind(this)) + usb.on('detach', this.onDeviceDetached.bind(this)) + + this._usbDevice = VENDOR_DEVICE_LIST + .map(d => usb.findByIds(d.vid, d.pid)) + .find(d => d != null); + + if (!this._usbDevice) { + this._usbDevice = null + this.emit('up', false) + return + } + this.start() +} + +BluetoothHciSocket.prototype.onDeviceAttached = function(device) { + if(!device || !device.deviceDescriptor) { + return + } + let found = VENDOR_DEVICE_LIST.find(function(a) { + return a.vid === device.deviceDescriptor.idVendor && a.pid === device.deviceDescriptor.idProduct + }) + if (found && !this._usbDevice) { + this._usbDevice = device + this.start() + } else { + // attached unknown device + } +} + +BluetoothHciSocket.prototype.onDeviceDetached = function(device) { + if (device === this._usbDevice) { + this._usbDevice = null + this.emit('up', false) + } +} + +BluetoothHciSocket.prototype.start = function() { + try { + this._usbDevice.open(); + } catch (error) { + // error opening device e.g. use by another process + this._usbDevice = null + this.emit('up', false) + return + } + + this._usbDeviceInterface = this._usbDevice.interfaces[0]; + + this._aclDataOutEndpoint = this._usbDeviceInterface.endpoint(0x02); + + this._hciEventEndpoint = this._usbDeviceInterface.endpoint(0x81); + this._aclDataInEndpoint = this._usbDeviceInterface.endpoint(0x82); + + this._usbDeviceInterface.claim(); + + this.reset(); + + // we have to register for the errors else they will be thrown + this._hciEventEndpoint.on('data', this.onHciEventEndpointData.bind(this)); + this._hciEventEndpoint.on('error', this.onHciEventEndpointError.bind(this)); + this._hciEventEndpoint.startPoll(); + + this._aclDataInEndpoint.on('data', this.onAclDataInEndpointData.bind(this)); + this._aclDataInEndpoint.on('error', this.onAclDataInEndpointError.bind(this)); + this._aclDataInEndpoint.startPoll(); +}; + +BluetoothHciSocket.prototype.bindControl = function() { + this._mode = 'control'; +}; + +BluetoothHciSocket.prototype.stop = function() { + if (this._mode === 'raw' || this._mode === 'user') { + this._hciEventEndpoint.stopPoll(); + this._hciEventEndpoint.removeAllListeners(); + + this._aclDataInEndpoint.stopPoll(); + this._aclDataInEndpoint.removeAllListeners(); + } +}; + +BluetoothHciSocket.prototype.write = function(data) { + debug('write: ' + data.toString('hex')); + if(!this._usbDevice) { + return + } + + if (this._mode === 'raw' || this._mode === 'user') { + var type = data.readUInt8(0); + + try { + if (HCI_COMMAND_PKT === type) { + this._usbDevice.controlTransfer(usb.LIBUSB_REQUEST_TYPE_CLASS | usb.LIBUSB_RECIPIENT_INTERFACE, 0, 0, 0, data.slice(1), function() {}); + } else if(HCI_ACLDATA_PKT === type) { + this._aclDataOutEndpoint.transfer(data.slice(1)); + } + } catch (error) { + console.log('write catched:', error) + } + } +}; + +BluetoothHciSocket.prototype.onHciEventEndpointError = function(error) { + debug('onHciEventEndpointError caught: ' + error) +} + +BluetoothHciSocket.prototype.onHciEventEndpointData = function(data) { + debug('HCI event: ' + data.toString('hex')); + + if (data.length === 0) { + return; + } + + // add to buffer + this._hciEventEndpointBuffer = Buffer.concat([ + this._hciEventEndpointBuffer, + data + ]); + + if (this._hciEventEndpointBuffer.length < 2) { + return; + } + + // check if desired length + var pktLen = this._hciEventEndpointBuffer.readUInt8(1); + if (pktLen <= (this._hciEventEndpointBuffer.length - 2)) { + + var buf = this._hciEventEndpointBuffer.slice(0, pktLen + 2); + + if (this._mode === 'raw' && buf.length === 6 && ('0e0401030c00' === buf.toString('hex') || '0e0402030c00' === buf.toString('hex'))) { + debug('reset complete'); + this.emit('up', true); + } + + // fire event + this.emit('data', Buffer.concat([ + new Buffer([HCI_EVENT_PKT]), + buf + ])); + + // reset buffer + this._hciEventEndpointBuffer = this._hciEventEndpointBuffer.slice(pktLen + 2); + } +}; + +BluetoothHciSocket.prototype.onAclDataInEndpointError = function(error) { + debug('onAclDataInEndpointError caught: ' + error) +} + +BluetoothHciSocket.prototype.onAclDataInEndpointData = function(data) { + debug('ACL Data In: ' + data.toString('hex')); + + if (data.length === 0) { + return; + } + + // add to buffer + this._aclDataInEndpointBuffer = Buffer.concat([ + this._aclDataInEndpointBuffer, + data + ]); + + if (this._aclDataInEndpointBuffer.length < 4) { + return; + } + + // check if desired length + var pktLen = this._aclDataInEndpointBuffer.readUInt16LE(2); + if (pktLen <= (this._aclDataInEndpointBuffer.length - 4)) { + + var buf = this._aclDataInEndpointBuffer.slice(0, pktLen + 4); + + // fire event + this.emit('data', Buffer.concat([ + new Buffer([HCI_ACLDATA_PKT]), + buf + ])); + + // reset buffer + this._aclDataInEndpointBuffer = this._aclDataInEndpointBuffer.slice(pktLen + 4); + } +}; + +BluetoothHciSocket.prototype.reset = function() { + var cmd = new Buffer(4); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(OCF_RESET | OGF_HOST_CTL << 10, 1); + + // length + cmd.writeUInt8(0x00, 3); + + debug('reset'); + this.write(cmd); +}; + +module.exports = BluetoothHciSocket; diff --git a/lib/win32/hci.js b/lib/win32/hci.js new file mode 100644 index 000000000..38c14ee61 --- /dev/null +++ b/lib/win32/hci.js @@ -0,0 +1,668 @@ +var debug = require('debug')('hci'); + +var events = require('events'); +var util = require('util'); + +var BluetoothHciSocket = require('./hci-socket'); + +var HCI_COMMAND_PKT = 0x01; +var HCI_ACLDATA_PKT = 0x02; +var HCI_EVENT_PKT = 0x04; + +var ACL_START_NO_FLUSH = 0x00; +var ACL_CONT = 0x01; +var ACL_START = 0x02; + +var EVT_DISCONN_COMPLETE = 0x05; +var EVT_ENCRYPT_CHANGE = 0x08; +var EVT_CMD_COMPLETE = 0x0e; +var EVT_CMD_STATUS = 0x0f; +var EVT_LE_META_EVENT = 0x3e; + +var EVT_LE_CONN_COMPLETE = 0x01; +var EVT_LE_ADVERTISING_REPORT = 0x02; +var EVT_LE_CONN_UPDATE_COMPLETE = 0x03; + +var OGF_LINK_CTL = 0x01; +var OCF_DISCONNECT = 0x0006; + +var OGF_HOST_CTL = 0x03; +var OCF_SET_EVENT_MASK = 0x0001; +var OCF_RESET = 0x0003; +var OCF_READ_LE_HOST_SUPPORTED = 0x006C; +var OCF_WRITE_LE_HOST_SUPPORTED = 0x006D; + + +var OGF_INFO_PARAM = 0x04; +var OCF_READ_LOCAL_VERSION = 0x0001; +var OCF_READ_BD_ADDR = 0x0009; + +var OGF_STATUS_PARAM = 0x05; +var OCF_READ_RSSI = 0x0005; + +var OGF_LE_CTL = 0x08; +var OCF_LE_SET_EVENT_MASK = 0x0001; +var OCF_LE_SET_SCAN_PARAMETERS = 0x000b; +var OCF_LE_SET_SCAN_ENABLE = 0x000c; +var OCF_LE_CREATE_CONN = 0x000d; +var OCF_LE_CONN_UPDATE = 0x0013; +var OCF_LE_START_ENCRYPTION = 0x0019; + +var DISCONNECT_CMD = OCF_DISCONNECT | OGF_LINK_CTL << 10; + +var SET_EVENT_MASK_CMD = OCF_SET_EVENT_MASK | OGF_HOST_CTL << 10; +var RESET_CMD = OCF_RESET | OGF_HOST_CTL << 10; +var READ_LE_HOST_SUPPORTED_CMD = OCF_READ_LE_HOST_SUPPORTED | OGF_HOST_CTL << 10; +var WRITE_LE_HOST_SUPPORTED_CMD = OCF_WRITE_LE_HOST_SUPPORTED | OGF_HOST_CTL << 10; + +var READ_LOCAL_VERSION_CMD = OCF_READ_LOCAL_VERSION | (OGF_INFO_PARAM << 10); +var READ_BD_ADDR_CMD = OCF_READ_BD_ADDR | (OGF_INFO_PARAM << 10); + +var READ_RSSI_CMD = OCF_READ_RSSI | OGF_STATUS_PARAM << 10; + +var LE_SET_EVENT_MASK_CMD = OCF_LE_SET_EVENT_MASK | OGF_LE_CTL << 10; +var LE_SET_SCAN_PARAMETERS_CMD = OCF_LE_SET_SCAN_PARAMETERS | OGF_LE_CTL << 10; +var LE_SET_SCAN_ENABLE_CMD = OCF_LE_SET_SCAN_ENABLE | OGF_LE_CTL << 10; +var LE_CREATE_CONN_CMD = OCF_LE_CREATE_CONN | OGF_LE_CTL << 10; +var LE_CONN_UPDATE_CMD = OCF_LE_CONN_UPDATE | OGF_LE_CTL << 10; +var LE_START_ENCRYPTION_CMD = OCF_LE_START_ENCRYPTION | OGF_LE_CTL << 10; + +var HCI_OE_USER_ENDED_CONNECTION = 0x13; + +var STATUS_MAPPER = require('../hci-socket/hci-status'); + +var Hci = function() { + this._socket = new BluetoothHciSocket(); + this._isDevUp = null; + this._state = null; + this._deviceId = null; + + this._handleBuffers = {}; + + this.on('stateChange', this.onStateChange.bind(this)); +}; + +util.inherits(Hci, events.EventEmitter); + +Hci.STATUS_MAPPER = STATUS_MAPPER; + +Hci.prototype.init = function() { + this._socket.on('data', this.onSocketData.bind(this)); + this._socket.on('error', this.onSocketError.bind(this)); + + this._socket.on('up', this.onDevUp.bind(this)); + this._deviceId = this._socket.bindRaw(); +}; + +Hci.prototype.onDevUp = function(isDevUp) { + if (this._isDevUp !== isDevUp) { + if (isDevUp) { + this.setSocketFilter(); + this.setEventMask(); + this.setLeEventMask(); + this.readLocalVersion(); + this.writeLeHostSupported(); + this.readLeHostSupported(); + this.readBdAddr(); + } else { + this.emit('stateChange', 'poweredOff'); + } + + this._isDevUp = isDevUp; + } +}; + +Hci.prototype.setSocketFilter = function() { + var filter = new Buffer(14); + var typeMask = (1 << HCI_COMMAND_PKT) | (1 << HCI_EVENT_PKT) | (1 << HCI_ACLDATA_PKT); + var eventMask1 = (1 << EVT_DISCONN_COMPLETE) | (1 << EVT_ENCRYPT_CHANGE) | (1 << EVT_CMD_COMPLETE) | (1 << EVT_CMD_STATUS); + var eventMask2 = (1 << (EVT_LE_META_EVENT - 32)); + var opcode = 0; + + filter.writeUInt32LE(typeMask, 0); + filter.writeUInt32LE(eventMask1, 4); + filter.writeUInt32LE(eventMask2, 8); + filter.writeUInt16LE(opcode, 12); + + debug('setting filter to: ' + filter.toString('hex')); + this._socket.setFilter(filter); +}; + +Hci.prototype.setEventMask = function() { + var cmd = new Buffer(12); + var eventMask = new Buffer('fffffbff07f8bf3d', 'hex'); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(SET_EVENT_MASK_CMD, 1); + + // length + cmd.writeUInt8(eventMask.length, 3); + + eventMask.copy(cmd, 4); + + debug('set event mask - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.reset = function() { + var cmd = new Buffer(4); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(OCF_RESET | OGF_HOST_CTL << 10, 1); + + // length + cmd.writeUInt8(0x00, 3); + + debug('reset - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + + +Hci.prototype.readLocalVersion = function() { + var cmd = new Buffer(4); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(READ_LOCAL_VERSION_CMD, 1); + + // length + cmd.writeUInt8(0x0, 3); + + debug('read local version - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.readBdAddr = function() { + var cmd = new Buffer(4); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(READ_BD_ADDR_CMD, 1); + + // length + cmd.writeUInt8(0x0, 3); + + debug('read bd addr - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.setLeEventMask = function() { + var cmd = new Buffer(12); + var leEventMask = new Buffer('1f00000000000000', 'hex'); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_SET_EVENT_MASK_CMD, 1); + + // length + cmd.writeUInt8(leEventMask.length, 3); + + leEventMask.copy(cmd, 4); + + debug('set le event mask - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.readLeHostSupported = function() { + var cmd = new Buffer(4); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(READ_LE_HOST_SUPPORTED_CMD, 1); + + // length + cmd.writeUInt8(0x00, 3); + + debug('read LE host supported - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.writeLeHostSupported = function() { + var cmd = new Buffer(6); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(WRITE_LE_HOST_SUPPORTED_CMD, 1); + + // length + cmd.writeUInt8(0x02, 3); + + // data + cmd.writeUInt8(0x01, 4); // le + cmd.writeUInt8(0x00, 5); // simul + + debug('write LE host supported - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.setScanParameters = function() { + var cmd = new Buffer(11); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_SET_SCAN_PARAMETERS_CMD, 1); + + // length + cmd.writeUInt8(0x07, 3); + + // data + cmd.writeUInt8(0x01, 4); // type: 0 -> passive, 1 -> active + cmd.writeUInt16LE(0x0010, 5); // internal, ms * 1.6 + cmd.writeUInt16LE(0x0010, 7); // window, ms * 1.6 + cmd.writeUInt8(0x00, 9); // own address type: 0 -> public, 1 -> random + cmd.writeUInt8(0x00, 10); // filter: 0 -> all event types + + debug('set scan parameters - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.setScanEnabled = function(enabled, filterDuplicates) { + var cmd = new Buffer(6); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_SET_SCAN_ENABLE_CMD, 1); + + // length + cmd.writeUInt8(0x02, 3); + + // data + cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled + cmd.writeUInt8(filterDuplicates ? 0x01 : 0x00, 5); // duplicates: 0 -> duplicates, 0 -> duplicates + + debug('set scan enabled - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.createLeConn = function(address, addressType) { + var cmd = new Buffer(29); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_CREATE_CONN_CMD, 1); + + // length + cmd.writeUInt8(0x19, 3); + + // data + cmd.writeUInt16LE(0x0060, 4); // interval + cmd.writeUInt16LE(0x0030, 6); // window + cmd.writeUInt8(0x00, 8); // initiator filter + + cmd.writeUInt8(addressType === 'random' ? 0x01 : 0x00, 9); // peer address type + (new Buffer(address.split(':').reverse().join(''), 'hex')).copy(cmd, 10); // peer address + + cmd.writeUInt8(0x00, 16); // own address type + + cmd.writeUInt16LE(0x0006, 17); // min interval + cmd.writeUInt16LE(0x000c, 19); // max interval + cmd.writeUInt16LE(0x0000, 21); // latency + cmd.writeUInt16LE(0x00c8, 23); // supervision timeout + cmd.writeUInt16LE(0x0004, 25); // min ce length + cmd.writeUInt16LE(0x0006, 27); // max ce length + + debug('create le conn - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.connUpdateLe = function(handle, minInterval, maxInterval, latency, supervisionTimeout) { + var cmd = new Buffer(18); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_CONN_UPDATE_CMD, 1); + + // length + cmd.writeUInt8(0x0e, 3); + + // data + cmd.writeUInt16LE(handle, 4); + cmd.writeUInt16LE(Math.floor(minInterval / 1.25), 6); // min interval + cmd.writeUInt16LE(Math.floor(maxInterval / 1.25), 8); // max interval + cmd.writeUInt16LE(latency, 10); // latency + cmd.writeUInt16LE(Math.floor(supervisionTimeout / 10), 12); // supervision timeout + cmd.writeUInt16LE(0x0000, 14); // min ce length + cmd.writeUInt16LE(0x0000, 16); // max ce length + + debug('conn update le - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.startLeEncryption = function(handle, random, diversifier, key) { + var cmd = new Buffer(32); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(LE_START_ENCRYPTION_CMD, 1); + + // length + cmd.writeUInt8(0x1c, 3); + + // data + cmd.writeUInt16LE(handle, 4); // handle + random.copy(cmd, 6); + diversifier.copy(cmd, 14); + key.copy(cmd, 16); + + debug('start le encryption - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.disconnect = function(handle, reason) { + var cmd = new Buffer(7); + + reason = reason || HCI_OE_USER_ENDED_CONNECTION; + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(DISCONNECT_CMD, 1); + + // length + cmd.writeUInt8(0x03, 3); + + // data + cmd.writeUInt16LE(handle, 4); // handle + cmd.writeUInt8(reason, 6); // reason + + debug('disconnect - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.readRssi = function(handle) { + var cmd = new Buffer(6); + + // header + cmd.writeUInt8(HCI_COMMAND_PKT, 0); + cmd.writeUInt16LE(READ_RSSI_CMD, 1); + + // length + cmd.writeUInt8(0x02, 3); + + // data + cmd.writeUInt16LE(handle, 4); // handle + + debug('read rssi - writing: ' + cmd.toString('hex')); + this._socket.write(cmd); +}; + +Hci.prototype.writeAclDataPkt = function(handle, cid, data) { + var pkt = new Buffer(9 + data.length); + + // header + pkt.writeUInt8(HCI_ACLDATA_PKT, 0); + pkt.writeUInt16LE(handle | ACL_START_NO_FLUSH << 12, 1); + pkt.writeUInt16LE(data.length + 4, 3); // data length 1 + pkt.writeUInt16LE(data.length, 5); // data length 2 + pkt.writeUInt16LE(cid, 7); + + data.copy(pkt, 9); + + debug('write acl data pkt - writing: ' + pkt.toString('hex')); + this._socket.write(pkt); +}; + +Hci.prototype.onSocketData = function(data) { + debug('onSocketData: ' + data.toString('hex')); + + var eventType = data.readUInt8(0); + var handle; + var cmd; + var status; + + debug('\tevent type = ' + eventType); + + if (HCI_EVENT_PKT === eventType) { + var subEventType = data.readUInt8(1); + + debug('\tsub event type = ' + subEventType); + + if (subEventType === EVT_DISCONN_COMPLETE) { + handle = data.readUInt16LE(4); + var reason = data.readUInt8(6); + + debug('\t\thandle = ' + handle); + debug('\t\treason = ' + reason); + + this.emit('disconnComplete', handle, reason); + } else if (subEventType === EVT_ENCRYPT_CHANGE) { + handle = data.readUInt16LE(4); + var encrypt = data.readUInt8(6); + + debug('\t\thandle = ' + handle); + debug('\t\tencrypt = ' + encrypt); + + this.emit('encryptChange', handle, encrypt); + } else if (subEventType === EVT_CMD_COMPLETE) { + cmd = data.readUInt16LE(4); + status = data.readUInt8(6); + var result = data.slice(7); + + debug('\t\tcmd = ' + cmd); + debug('\t\tstatus = ' + status); + debug('\t\tresult = ' + result.toString('hex')); + + this.processCmdCompleteEvent(cmd, status, result); + } else if (subEventType === EVT_CMD_STATUS) { + status = data.readUInt8(3); + cmd = data.readUInt16LE(5); + + debug('\t\tstatus = ' + status); + debug('\t\tcmd = ' + cmd); + + this.processCmdStatusEvent(cmd, status); + } else if (subEventType === EVT_LE_META_EVENT) { + var leMetaEventType = data.readUInt8(3); + var leMetaEventStatus = data.readUInt8(4); + var leMetaEventData = data.slice(5); + + debug('\t\tLE meta event type = ' + leMetaEventType); + debug('\t\tLE meta event status = ' + leMetaEventStatus); + debug('\t\tLE meta event data = ' + leMetaEventData.toString('hex')); + + this.processLeMetaEvent(leMetaEventType, leMetaEventStatus, leMetaEventData); + } + } else if (HCI_ACLDATA_PKT === eventType) { + var flags = data.readUInt16LE(1) >> 12; + handle = data.readUInt16LE(1) & 0x0fff; + + if (ACL_START === flags) { + var cid = data.readUInt16LE(7); + + var length = data.readUInt16LE(5); + var pktData = data.slice(9); + + debug('\t\tcid = ' + cid); + + if (length === pktData.length) { + debug('\t\thandle = ' + handle); + debug('\t\tdata = ' + pktData.toString('hex')); + + this.emit('aclDataPkt', handle, cid, pktData); + } else { + this._handleBuffers[handle] = { + length: length, + cid: cid, + data: pktData + }; + } + } else if (ACL_CONT === flags) { + if (!this._handleBuffers[handle] || !this._handleBuffers[handle].data) { + return; + } + + this._handleBuffers[handle].data = Buffer.concat([ + this._handleBuffers[handle].data, + data.slice(5) + ]); + + if (this._handleBuffers[handle].data.length === this._handleBuffers[handle].length) { + this.emit('aclDataPkt', handle, this._handleBuffers[handle].cid, this._handleBuffers[handle].data); + + delete this._handleBuffers[handle]; + } + } + } else if (HCI_COMMAND_PKT === eventType) { + cmd = data.readUInt16LE(1); + var len = data.readUInt8(3); + + debug('\t\tcmd = ' + cmd); + debug('\t\tdata len = ' + len); + + if (cmd === LE_SET_SCAN_ENABLE_CMD) { + var enable = (data.readUInt8(4) === 0x1); + var filterDuplicates = (data.readUInt8(5) === 0x1); + + debug('\t\t\tLE enable scan command'); + debug('\t\t\tenable scanning = ' + enable); + debug('\t\t\tfilter duplicates = ' + filterDuplicates); + + this.emit('leScanEnableSetCmd', enable, filterDuplicates); + } + } +}; + +Hci.prototype.onSocketError = function(error) { + debug('onSocketError: ' + error.message); + + if (error.message === 'Operation not permitted') { + this.emit('stateChange', 'unauthorized'); + } else if (error.message === 'Network is down') { + // no-op + } +}; + +Hci.prototype.processCmdCompleteEvent = function(cmd, status, result) { + if (cmd === RESET_CMD) { + this.setEventMask(); + this.setLeEventMask(); + this.readLocalVersion(); + this.readBdAddr(); + } else if (cmd === READ_LE_HOST_SUPPORTED_CMD) { + if (status === 0) { + var le = result.readUInt8(0); + var simul = result.readUInt8(1); + + debug('\t\t\tle = ' + le); + debug('\t\t\tsimul = ' + simul); + } + } else if (cmd === READ_LOCAL_VERSION_CMD) { + var hciVer = result.readUInt8(0); + var hciRev = result.readUInt16LE(1); + var lmpVer = result.readInt8(3); + var manufacturer = result.readUInt16LE(4); + var lmpSubVer = result.readUInt16LE(6); + + if (hciVer < 0x06) { + this.emit('stateChange', 'unsupported'); + } else if (this._state !== 'poweredOn') { + this.setScanEnabled(false, true); + this.setScanParameters(); + } + + this.emit('readLocalVersion', hciVer, hciRev, lmpVer, manufacturer, lmpSubVer); + } else if (cmd === READ_BD_ADDR_CMD) { + this.addressType = 'public'; + this.address = result.toString('hex').match(/.{1,2}/g).reverse().join(':'); + + debug('address = ' + this.address); + + this.emit('addressChange', this.address); + } else if (cmd === LE_SET_SCAN_PARAMETERS_CMD) { + this.emit('stateChange', 'poweredOn'); + + this.emit('leScanParametersSet'); + } else if (cmd === LE_SET_SCAN_ENABLE_CMD) { + this.emit('leScanEnableSet', status); + } else if (cmd === READ_RSSI_CMD) { + var handle = result.readUInt16LE(0); + var rssi = result.readInt8(2); + + debug('\t\t\thandle = ' + handle); + debug('\t\t\trssi = ' + rssi); + + this.emit('rssiRead', handle, rssi); + } +}; + +Hci.prototype.processLeMetaEvent = function(eventType, status, data) { + if (eventType === EVT_LE_CONN_COMPLETE) { + this.processLeConnComplete(status, data); + } else if (eventType === EVT_LE_ADVERTISING_REPORT) { + this.processLeAdvertisingReport(status, data); + } else if (eventType === EVT_LE_CONN_UPDATE_COMPLETE) { + this.processLeConnUpdateComplete(status, data); + } +}; + +Hci.prototype.processLeConnComplete = function(status, data) { + var handle = data.readUInt16LE(0); + var role = data.readUInt8(2); + var addressType = data.readUInt8(3) === 0x01 ? 'random': 'public'; + var address = data.slice(4, 10).toString('hex').match(/.{1,2}/g).reverse().join(':'); + var interval = data.readUInt16LE(10) * 1.25; + var latency = data.readUInt16LE(12); // TODO: multiplier? + var supervisionTimeout = data.readUInt16LE(14) * 10; + var masterClockAccuracy = data.readUInt8(16); // TODO: multiplier? + + debug('\t\t\thandle = ' + handle); + debug('\t\t\trole = ' + role); + debug('\t\t\taddress type = ' + addressType); + debug('\t\t\taddress = ' + address); + debug('\t\t\tinterval = ' + interval); + debug('\t\t\tlatency = ' + latency); + debug('\t\t\tsupervision timeout = ' + supervisionTimeout); + debug('\t\t\tmaster clock accuracy = ' + masterClockAccuracy); + + this.emit('leConnComplete', status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy); +}; + +Hci.prototype.processLeAdvertisingReport = function(count, data) { + for (var i = 0; i < count; i++) { + var type = data.readUInt8(0); + var addressType = data.readUInt8(1) === 0x01 ? 'random' : 'public'; + var address = data.slice(2, 8).toString('hex').match(/.{1,2}/g).reverse().join(':'); + var eirLength = data.readUInt8(8); + var eir = data.slice(9, eirLength + 9); + var rssi = data.readInt8(eirLength + 9); + + debug('\t\t\ttype = ' + type); + debug('\t\t\taddress = ' + address); + debug('\t\t\taddress type = ' + addressType); + debug('\t\t\teir = ' + eir.toString('hex')); + debug('\t\t\trssi = ' + rssi); + + this.emit('leAdvertisingReport', 0, type, address, addressType, eir, rssi); + + data = data.slice(eirLength + 10); + } +}; + +Hci.prototype.processLeConnUpdateComplete = function(status, data) { + var handle = data.readUInt16LE(0); + var interval = data.readUInt16LE(2) * 1.25; + var latency = data.readUInt16LE(4); // TODO: multiplier? + var supervisionTimeout = data.readUInt16LE(6) * 10; + + debug('\t\t\thandle = ' + handle); + debug('\t\t\tinterval = ' + interval); + debug('\t\t\tlatency = ' + latency); + debug('\t\t\tsupervision timeout = ' + supervisionTimeout); + + this.emit('leConnUpdateComplete', status, handle, interval, latency, supervisionTimeout); +}; + +Hci.prototype.processCmdStatusEvent = function(cmd, status) { + if (cmd === LE_CREATE_CONN_CMD) { + if (status !== 0) { + this.emit('leConnComplete', status); + } + } +}; + +Hci.prototype.onStateChange = function(state) { + this._state = state; +}; + +module.exports = Hci; diff --git a/lib/winrt/bindings.js b/lib/winrt/bindings.js new file mode 100644 index 000000000..f0e19715f --- /dev/null +++ b/lib/winrt/bindings.js @@ -0,0 +1,8 @@ +const events = require('events'); +const util = require('util'); + +const NobleWinrt = require('bindings')('noble').NobleWinrt; + +util.inherits(NobleWinrt, events.EventEmitter); + +module.exports = new NobleWinrt(); diff --git a/lib/winrt/src/ble_manager.cc b/lib/winrt/src/ble_manager.cc new file mode 100644 index 000000000..bf94ed7d2 --- /dev/null +++ b/lib/winrt/src/ble_manager.cc @@ -0,0 +1,813 @@ +// +// ble_manager.cc +// noble-winrt-native +// +// Created by Georg Vienna on 03.09.18. +// + +#include "ble_manager.h" +#include "winrt_cpp.h" + +#include +using winrt::Windows::Devices::Bluetooth::BluetoothCacheMode; +using winrt::Windows::Devices::Bluetooth::BluetoothError; +using winrt::Windows::Devices::Bluetooth::BluetoothConnectionStatus; +using winrt::Windows::Storage::Streams::DataReader; +using winrt::Windows::Storage::Streams::DataWriter; + +template auto inFilter(std::vector filter, T object) +{ + return filter.empty() || std::find(filter.begin(), filter.end(), object) != filter.end(); +} + +template auto bind2(O* object, M method, Types&... args) +{ + return std::bind(method, object, std::placeholders::_1, std::placeholders::_2, args...); +} + +#define LOGE(message, ...) \ + {\ + char buff[255];\ + snprintf(buff, sizeof(buff), __FUNCTION__ ": " message, __VA_ARGS__);\ + std::string buffAsStdStr = buff;\ + mEmit.Log(buffAsStdStr);\ + } + +#define CHECK_DEVICE() \ + LOGE(""); \ + if (mDeviceMap.find(uuid) == mDeviceMap.end()) \ + { \ + LOGE("device with id %s not found", uuid.c_str()); \ + return false; \ + } + +#define IFDEVICE(_device, _uuid) \ + PeripheralWinrt& peripheral = mDeviceMap[_uuid]; \ + if (!peripheral.device.has_value()) \ + { \ + LOGE("device not connected"); \ + return false; \ + } \ + BluetoothLEDevice& _device = *peripheral.device; + +#define CHECK_RESULT(_result) \ + if (!_result) \ + { \ + LOGE("result is null"); \ + return; \ + } \ + auto _commStatus = _result.Status(); \ + if (_commStatus != GattCommunicationStatus::Success) \ + { \ + LOGE("communication status: %d", _commStatus); \ + return; \ + } + +#define FOR(object, vector) \ + auto& _vector = vector; \ + if (!_vector) \ + { \ + LOGE(#vector " is null"); \ + return; \ + } \ + else \ + for (auto&& object : _vector) + +BLEManager::BLEManager(const Napi::Value& receiver, const Napi::Function& callback) +{ + mRadioState = AdapterState::Initial; + mEmit.Wrap(receiver, callback); + auto onRadio = std::bind(&BLEManager::OnRadio, this, std::placeholders::_1); + mWatcher.Start(onRadio); + mAdvertismentWatcher.ScanningMode(BluetoothLEScanningMode::Active); + auto onReceived = bind2(this, &BLEManager::OnScanResult); + mReceivedRevoker = mAdvertismentWatcher.Received(winrt::auto_revoke, onReceived); + auto onStopped = bind2(this, &BLEManager::OnScanStopped); + mStoppedRevoker = mAdvertismentWatcher.Stopped(winrt::auto_revoke, onStopped); +} + +const char* adapterStateToString(AdapterState state) +{ + switch (state) + { + case AdapterState::Unsupported: + return "unsupported"; + case AdapterState::On: + return "poweredOn"; + break; + case AdapterState::Off: + return "poweredOff"; + break; + case AdapterState::Disabled: + return "poweredOff"; + break; + default: + return "unknown"; + } +} + +void BLEManager::OnRadio(Radio& radio) +{ + auto state = AdapterState::Unsupported; + if (radio) + { + state = (AdapterState)radio.State(); + } + if (state != mRadioState) + { + mRadioState = state; + mEmit.RadioState(adapterStateToString(state)); + } +} + +void BLEManager::Scan(const std::vector& serviceUUIDs, bool allowDuplicates) +{ + mAdvertismentMap.clear(); + mAllowDuplicates = allowDuplicates; + BluetoothLEAdvertisementFilter filter = BluetoothLEAdvertisementFilter(); + BluetoothLEAdvertisement advertisment = BluetoothLEAdvertisement(); + auto& services = advertisment.ServiceUuids(); + for (auto& uuid : serviceUUIDs) + { + services.Append(uuid); + } + filter.Advertisement(advertisment); + mAdvertismentWatcher.AdvertisementFilter(filter); + mAdvertismentWatcher.Start(); + mEmit.ScanState(true); +} + +void BLEManager::OnScanResult(BluetoothLEAdvertisementWatcher watcher, + const BluetoothLEAdvertisementReceivedEventArgs& args) +{ + uint64_t bluetoothAddress = args.BluetoothAddress(); + std::string uuid = formatBluetoothUuid(bluetoothAddress); + int16_t rssi = args.RawSignalStrengthInDBm(); + auto advertismentType = args.AdvertisementType(); + + if (mDeviceMap.find(uuid) == mDeviceMap.end()) + { + mAdvertismentMap.insert(uuid); + auto peripheral = + PeripheralWinrt(bluetoothAddress, advertismentType, rssi, args.Advertisement()); + mEmit.Scan(uuid, rssi, peripheral); + mDeviceMap.emplace(std::make_pair(uuid, std::move(peripheral))); + } + else + { + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + peripheral.Update(rssi, args.Advertisement()); + if (mAllowDuplicates || mAdvertismentMap.find(uuid) == mAdvertismentMap.end()) + { + mAdvertismentMap.insert(uuid); + mEmit.Scan(uuid, rssi, peripheral); + } + } +} + +void BLEManager::StopScan() +{ + mAdvertismentWatcher.Stop(); +} + +void BLEManager::OnScanStopped(BluetoothLEAdvertisementWatcher watcher, + const BluetoothLEAdvertisementWatcherStoppedEventArgs& args) +{ + if (mRadioState == AdapterState::On && args.Error() == BluetoothError::RadioNotAvailable) { + mAdvertismentWatcher.Start(); + return; + + } + mEmit.ScanState(false); +} + +bool BLEManager::Connect(const std::string& uuid) +{ + if (mDeviceMap.find(uuid) == mDeviceMap.end()) + { + mEmit.Connected(uuid, "device not found"); + return false; + } + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + if (!peripheral.device.has_value()) + { + auto completed = bind2(this, &BLEManager::OnConnected, uuid); + BluetoothLEDevice::FromBluetoothAddressAsync(peripheral.bluetoothAddress).Completed(completed); + } + else + { + mEmit.Connected(uuid); + } + return true; +} + +void BLEManager::OnConnected(IAsyncOperation asyncOp, AsyncStatus& status, + const std::string uuid) +{ + if (status == AsyncStatus::Completed) + { + BluetoothLEDevice& device = asyncOp.GetResults(); + // device can be null if the connection failed + if (device) + { + auto onChanged = bind2(this, &BLEManager::OnConnectionStatusChanged); + auto token = device.ConnectionStatusChanged(onChanged); + auto uuid = formatBluetoothUuid(device.BluetoothAddress()); + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + peripheral.device = device; + peripheral.connectionToken = token; + mEmit.Connected(uuid); + } + else + { + mEmit.Connected(uuid, "could not connect to device: result is null"); + } + } + else + { + mEmit.Connected(uuid, "could not connect to device"); + } +} + +bool BLEManager::Disconnect(const std::string& uuid) +{ + CHECK_DEVICE(); + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + peripheral.Disconnect(); + mNotifyMap.Remove(uuid); + mEmit.Disconnected(uuid); + return true; +} + +void BLEManager::OnConnectionStatusChanged(BluetoothLEDevice device, + winrt::Windows::Foundation::IInspectable inspectable) +{ + if (device.ConnectionStatus() == BluetoothConnectionStatus::Disconnected) + { + auto uuid = formatBluetoothUuid(device.BluetoothAddress()); + if (mDeviceMap.find(uuid) == mDeviceMap.end()) + { + LOGE("device with id %s not found", uuid.c_str()); + return; + } + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + peripheral.Disconnect(); + mNotifyMap.Remove(uuid); + mEmit.Disconnected(uuid); + } +} + +bool BLEManager::UpdateRSSI(const std::string& uuid) +{ + CHECK_DEVICE(); + + PeripheralWinrt& peripheral = mDeviceMap[uuid]; + // no way to get the rssi while we are connected, return the last value of advertisement + mEmit.RSSI(uuid, peripheral.rssi); + return true; +} + +bool BLEManager::DiscoverServices(const std::string& uuid, + const std::vector& serviceUUIDs) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + auto completed = bind2(this, &BLEManager::OnServicesDiscovered, uuid, serviceUUIDs); + device.GetGattServicesAsync(BluetoothCacheMode::Uncached).Completed(completed); + return true; + } +} + +void BLEManager::OnServicesDiscovered(IAsyncOperation asyncOp, + AsyncStatus status, const std::string uuid, + const std::vector serviceUUIDs) +{ + if (status == AsyncStatus::Completed) + { + GattDeviceServicesResult& result = asyncOp.GetResults(); + CHECK_RESULT(result); + std::vector serviceUuids; + FOR(service, result.Services()) + { + auto id = service.Uuid(); + if (inFilter(serviceUUIDs, id)) + { + serviceUuids.push_back(toStr(id)); + } + } + mEmit.ServicesDiscovered(uuid, serviceUuids); + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::DiscoverIncludedServices(const std::string& uuid, const winrt::guid& serviceUuid, + const std::vector& serviceUUIDs) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetService(serviceUuid, [=](std::optional service, std::string error) { + if (service) + { + std::string serviceId = toStr(serviceUuid); + service->GetIncludedServicesAsync(BluetoothCacheMode::Uncached) + .Completed(bind2(this, &BLEManager::OnIncludedServicesDiscovered, uuid, + serviceId, serviceUUIDs)); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnIncludedServicesDiscovered(IAsyncOperation asyncOp, + AsyncStatus status, const std::string uuid, + const std::string serviceId, + const std::vector serviceUUIDs) +{ + if (status == AsyncStatus::Completed) + { + auto& result = asyncOp.GetResults(); + CHECK_RESULT(result); + std::vector servicesUuids; + FOR(service, result.Services()) + { + auto id = service.Uuid(); + if (inFilter(serviceUUIDs, id)) + { + servicesUuids.push_back(toStr(id)); + } + } + mEmit.IncludedServicesDiscovered(uuid, serviceId, servicesUuids); + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::DiscoverCharacteristics(const std::string& uuid, const winrt::guid& serviceUuid, + const std::vector& characteristicUUIDs) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetService(serviceUuid, [=](std::optional service, std::string error) { + if (service) + { + std::string serviceId = toStr(serviceUuid); + service->GetCharacteristicsAsync(BluetoothCacheMode::Uncached) + .Completed(bind2(this, &BLEManager::OnCharacteristicsDiscovered, uuid, + serviceId, characteristicUUIDs)); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnCharacteristicsDiscovered(IAsyncOperation asyncOp, + AsyncStatus status, const std::string uuid, + const std::string serviceId, + const std::vector characteristicUUIDs) +{ + if (status == AsyncStatus::Completed) + { + auto& result = asyncOp.GetResults(); + CHECK_RESULT(result); + std::vector>> characteristicsUuids; + FOR(characteristic, result.Characteristics()) + { + auto id = characteristic.Uuid(); + if (inFilter(characteristicUUIDs, id)) + { + auto props = characteristic.CharacteristicProperties(); + characteristicsUuids.push_back({ toStr(id), toPropertyArray(props) }); + } + } + mEmit.CharacteristicsDiscovered(uuid, serviceId, characteristicsUuids); + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::Read(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetCharacteristic( + serviceUuid, characteristicUuid, [=](std::optional characteristic, std::string error) { + if (characteristic) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + characteristic->ReadValueAsync(BluetoothCacheMode::Uncached) + .Completed( + bind2(this, &BLEManager::OnRead, uuid, serviceId, characteristicId)); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnRead(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const std::string serviceId, + const std::string characteristicId) +{ + if (status == AsyncStatus::Completed) + { + GattReadResult& result = asyncOp.GetResults(); + CHECK_RESULT(result); + auto& value = result.Value(); + if (value) + { + auto& reader = DataReader::FromBuffer(value); + Data data(reader.UnconsumedBufferLength()); + reader.ReadBytes(data); + mEmit.Read(uuid, serviceId, characteristicId, data, false); + } + else + { + LOGE("value is null"); + } + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::Write(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid, const Data& data, + bool withoutResponse) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetCharacteristic( + serviceUuid, characteristicUuid, [=](std::optional characteristic, std::string error) { + if (characteristic) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + auto writer = DataWriter(); + writer.WriteBytes(data); + auto& value = writer.DetachBuffer(); + GattWriteOption option = withoutResponse ? GattWriteOption::WriteWithoutResponse + : GattWriteOption::WriteWithResponse; + characteristic->WriteValueWithResultAsync(value, option) + .Completed( + bind2(this, &BLEManager::OnWrite, uuid, serviceId, characteristicId)); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnWrite(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const std::string serviceId, + const std::string characteristicId) +{ + if (status == AsyncStatus::Completed) + { + mEmit.Write(uuid, serviceId, characteristicId); + } + else + { + LOGE("status: %d", status); + } +} + +GattClientCharacteristicConfigurationDescriptorValue +GetDescriptorValue(GattCharacteristicProperties properties) +{ + if ((properties & GattCharacteristicProperties::Indicate) == + GattCharacteristicProperties::Indicate) + { + return GattClientCharacteristicConfigurationDescriptorValue::Indicate; + } + else + { + return GattClientCharacteristicConfigurationDescriptorValue::Notify; + } +} + +bool BLEManager::Notify(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid, bool on) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + auto onCharacteristic = [=](std::optional characteristic, std::string error) { + if (characteristic) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + bool subscribed = mNotifyMap.IsSubscribed(uuid, *characteristic); + + if (on) + { + if (subscribed) + { + // already listening + mEmit.Notify(uuid, serviceId, characteristicId, true); + return; + } + auto descriptorValue = + GetDescriptorValue(characteristic->CharacteristicProperties()); + + auto completed = bind2(this, &BLEManager::OnNotify, *characteristic, uuid, + serviceId, characteristicId, on); + characteristic + ->WriteClientCharacteristicConfigurationDescriptorWithResultAsync( + descriptorValue) + .Completed(completed); + } + else + { + if (!subscribed) + { + // already not listening + mEmit.Notify(uuid, serviceId, characteristicId, false); + return; + } + + mNotifyMap.Unsubscribe(uuid, *characteristic); + auto descriptorValue = + GattClientCharacteristicConfigurationDescriptorValue::None; + auto completed = bind2(this, &BLEManager::OnNotify, *characteristic, uuid, + serviceId, characteristicId, on); + characteristic + ->WriteClientCharacteristicConfigurationDescriptorWithResultAsync( + descriptorValue) + .Completed(completed); + } + } + else + { + LOGE("%s", error.c_str()); + } + }; + peripheral.GetCharacteristic(serviceUuid, characteristicUuid, onCharacteristic); + return true; + } +} + +void BLEManager::OnNotify(IAsyncOperation asyncOp, AsyncStatus status, + const GattCharacteristic characteristic, const std::string uuid, + const std::string serviceId, const std::string characteristicId, + const bool state) +{ + if (status == AsyncStatus::Completed) + { + if (state == true) + { + auto onChanged = bind2(this, &BLEManager::OnValueChanged, uuid); + auto token = characteristic.ValueChanged(onChanged); + mNotifyMap.Add(uuid, characteristic, token); + } + mEmit.Notify(uuid, serviceId, characteristicId, state); + } + else + { + LOGE("status: %d", status); + } +} + +void BLEManager::OnValueChanged(GattCharacteristic characteristic, + const GattValueChangedEventArgs& args, std::string deviceUuid) +{ + auto& reader = DataReader::FromBuffer(args.CharacteristicValue()); + Data data(reader.UnconsumedBufferLength()); + reader.ReadBytes(data); + auto characteristicUuid = toStr(characteristic.Uuid()); + auto serviceUuid = toStr(characteristic.Service().Uuid()); + mEmit.Read(deviceUuid, serviceUuid, characteristicUuid, data, true); +} + +bool BLEManager::DiscoverDescriptors(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetCharacteristic( + serviceUuid, characteristicUuid, [=](std::optional characteristic, std::string error) { + if (characteristic) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + auto completed = bind2(this, &BLEManager::OnDescriptorsDiscovered, uuid, + serviceId, characteristicId); + characteristic->GetDescriptorsAsync(BluetoothCacheMode::Uncached) + .Completed(completed); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnDescriptorsDiscovered(IAsyncOperation asyncOp, + AsyncStatus status, const std::string uuid, + const std::string serviceId, + const std::string characteristicId) +{ + if (status == AsyncStatus::Completed) + { + auto& result = asyncOp.GetResults(); + CHECK_RESULT(result); + std::vector descriptorUuids; + FOR(descriptor, result.Descriptors()) + { + descriptorUuids.push_back(toStr(descriptor.Uuid())); + } + mEmit.DescriptorsDiscovered(uuid, serviceId, characteristicId, descriptorUuids); + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::ReadValue(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid, const winrt::guid& descriptorUuid) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + peripheral.GetDescriptor( + serviceUuid, characteristicUuid, descriptorUuid, + [=](std::optional descriptor, std::string error) { + if (descriptor) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + std::string descriptorId = toStr(descriptorUuid); + auto completed = bind2(this, &BLEManager::OnReadValue, uuid, serviceId, + characteristicId, descriptorId); + descriptor->ReadValueAsync(BluetoothCacheMode::Uncached).Completed(completed); + } + else + { + LOGE("%s", error.c_str()); + } + }); + return true; + } +} + +void BLEManager::OnReadValue(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const std::string serviceId, + const std::string characteristicId, const std::string descriptorId) +{ + if (status == AsyncStatus::Completed) + { + GattReadResult& result = asyncOp.GetResults(); + CHECK_RESULT(result); + auto& value = result.Value(); + if (value) + { + auto& reader = DataReader::FromBuffer(value); + Data data(reader.UnconsumedBufferLength()); + reader.ReadBytes(data); + mEmit.ReadValue(uuid, serviceId, characteristicId, descriptorId, data); + } + else + { + LOGE("value is null"); + } + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::WriteValue(const std::string& uuid, const winrt::guid& serviceUuid, + const winrt::guid& characteristicUuid, + const winrt::guid& descriptorUuid, const Data& data) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + auto onDescriptor = [=](std::optional descriptor, std::string error) { + if (descriptor) + { + std::string serviceId = toStr(serviceUuid); + std::string characteristicId = toStr(characteristicUuid); + std::string descriptorId = toStr(descriptorUuid); + auto writer = DataWriter(); + writer.WriteBytes(data); + auto& value = writer.DetachBuffer(); + auto& asyncOp = descriptor->WriteValueWithResultAsync(value); + asyncOp.Completed(bind2(this, &BLEManager::OnWriteValue, uuid, serviceId, + characteristicId, descriptorId)); + } + else + { + LOGE("%s", error.c_str()); + } + }; + peripheral.GetDescriptor(serviceUuid, characteristicUuid, descriptorUuid, onDescriptor); + return true; + } +} + +void BLEManager::OnWriteValue(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const std::string serviceId, + const std::string characteristicId, const std::string descriptorId) +{ + if (status == AsyncStatus::Completed) + { + mEmit.WriteValue(uuid, serviceId, characteristicId, descriptorId); + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::ReadHandle(const std::string& uuid, int handle) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + LOGE("not available"); + return true; + } +} + +void BLEManager::OnReadHandle(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const int handle) +{ + if (status == AsyncStatus::Completed) + { + GattReadResult& result = asyncOp.GetResults(); + CHECK_RESULT(result); + auto& value = result.Value(); + if (value) + { + auto& reader = DataReader::FromBuffer(value); + Data data(reader.UnconsumedBufferLength()); + reader.ReadBytes(data); + mEmit.ReadHandle(uuid, handle, data); + } + else + { + LOGE("value is null"); + } + } + else + { + LOGE("status: %d", status); + } +} + +bool BLEManager::WriteHandle(const std::string& uuid, int handle, Data data) +{ + CHECK_DEVICE(); + IFDEVICE(device, uuid) + { + LOGE("not available"); + return true; + } +} + +void BLEManager::OnWriteHandle(IAsyncOperation asyncOp, AsyncStatus status, + const std::string uuid, const int handle) +{ + if (status == AsyncStatus::Completed) + { + mEmit.WriteHandle(uuid, handle); + } + else + { + LOGE("status %d", status); + } +} diff --git a/lib/winrt/src/ble_manager.h b/lib/winrt/src/ble_manager.h new file mode 100644 index 000000000..09150cdb5 --- /dev/null +++ b/lib/winrt/src/ble_manager.h @@ -0,0 +1,77 @@ +// +// ble_manager.h +// noble-winrt-native +// +// Created by Georg Vienna on 03.09.18. +// + +#pragma once + +#include +#include + +#include "callbacks.h" +#include "peripheral_winrt.h" +#include "radio_watcher.h" +#include "notify_map.h" + +using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; +using namespace winrt::Windows::Devices::Bluetooth::Advertisement; +using winrt::Windows::Foundation::AsyncStatus; + +class BLEManager +{ +public: + // clang-format off + BLEManager(const Napi::Value& receiver, const Napi::Function& callback); + void Scan(const std::vector& serviceUUIDs, bool allowDuplicates); + void StopScan(); + bool Connect(const std::string& uuid); + bool Disconnect(const std::string& uuid); + bool UpdateRSSI(const std::string& uuid); + bool DiscoverServices(const std::string& uuid, const std::vector& serviceUUIDs); + bool DiscoverIncludedServices(const std::string& uuid, const winrt::guid& serviceUuid, const std::vector& serviceUUIDs); + bool DiscoverCharacteristics(const std::string& uuid, const winrt::guid& service, const std::vector& characteristicUUIDs); + bool Read(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid); + bool Write(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid, const Data& data, bool withoutResponse); + bool Notify(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid, bool on); + bool DiscoverDescriptors(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid); + bool ReadValue(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid, const winrt::guid& descriptorUuid); + bool WriteValue(const std::string& uuid, const winrt::guid& serviceUuid, const winrt::guid& characteristicUuid, const winrt::guid& descriptorUuid, const Data& data); + bool ReadHandle(const std::string& uuid, int handle); + bool WriteHandle(const std::string& uuid, int handle, Data data); + // clang-format on + +private: + // clang-format off + void OnRadio(Radio& radio); + void OnScanResult(BluetoothLEAdvertisementWatcher watcher, const BluetoothLEAdvertisementReceivedEventArgs& args); + void OnScanStopped(BluetoothLEAdvertisementWatcher watcher, const BluetoothLEAdvertisementWatcherStoppedEventArgs& args); + void OnConnected(IAsyncOperation asyncOp, AsyncStatus& status, std::string uuid); + void OnConnectionStatusChanged(BluetoothLEDevice device, winrt::Windows::Foundation::IInspectable inspectable); + void OnServicesDiscovered(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::vector serviceUUIDs); + void OnIncludedServicesDiscovered(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::vector serviceUUIDs); + void OnCharacteristicsDiscovered(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::vector characteristicUUIDs); + void OnRead(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::string characteristicId); + void OnWrite(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::string characteristicId); + void OnNotify(IAsyncOperation asyncOp, AsyncStatus status, GattCharacteristic characteristic, std::string uuid, std::string serviceId, std::string characteristicId, bool state); + void OnValueChanged(GattCharacteristic chracteristic, const GattValueChangedEventArgs& args, std::string uuid); + void OnDescriptorsDiscovered(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::string characteristicId); + void OnReadValue(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::string characteristicId, std::string descriptorId); + void OnWriteValue(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, std::string serviceId, std::string characteristicId, std::string descriptorId); + void OnReadHandle(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, int handle); + void OnWriteHandle(IAsyncOperation asyncOp, AsyncStatus status, std::string uuid, int handle); + // clang-format on + + Emit mEmit; + RadioWatcher mWatcher; + AdapterState mRadioState; + BluetoothLEAdvertisementWatcher mAdvertismentWatcher; + winrt::event_revoker mReceivedRevoker; + winrt::event_revoker mStoppedRevoker; + bool mAllowDuplicates; + + std::unordered_map mDeviceMap; + std::set mAdvertismentMap; + NotifyMap mNotifyMap; +}; diff --git a/lib/winrt/src/callbacks.cc b/lib/winrt/src/callbacks.cc new file mode 100644 index 000000000..9243146db --- /dev/null +++ b/lib/winrt/src/callbacks.cc @@ -0,0 +1,282 @@ +// +// callbacks.cc +// noble-mac-native +// +// Created by Georg Vienna on 30.08.18. +// +#include "callbacks.h" + +#include + +#define _s(val) Napi::String::New(env, val) +#define _b(val) Napi::Boolean::New(env, val) +#define _n(val) Napi::Number::New(env, val) +#define _u(str) toUuid(env, str) + +Napi::String toUuid(Napi::Env& env, const std::string& uuid) +{ + std::string str(uuid); + str.erase(std::remove(str.begin(), str.end(), '-'), str.end()); + std::transform(str.begin(), str.end(), str.begin(), ::tolower); + return _s(str); +} + +Napi::String toAddressType(Napi::Env& env, const AddressType& type) +{ + if (type == PUBLIC) + { + return _s("public"); + } + else if (type == RANDOM) + { + return _s("random"); + } + return _s("unknown"); +} + +Napi::Buffer toBuffer(Napi::Env& env, const Data& data) +{ + if (data.empty()) + { + return Napi::Buffer::New(env, 0); + } + return Napi::Buffer::Copy(env, &data[0], data.size()); +} + +Napi::Array toUuidArray(Napi::Env& env, const std::vector& data) +{ + if (data.empty()) + { + return Napi::Array::New(env); + } + auto arr = Napi::Array::New(env, data.size()); + for (size_t i = 0; i < data.size(); i++) + { + arr.Set(i, _u(data[i])); + } + return arr; +} + +Napi::Array toArray(Napi::Env& env, const std::vector& data) +{ + if (data.empty()) + { + return Napi::Array::New(env); + } + auto arr = Napi::Array::New(env, data.size()); + for (size_t i = 0; i < data.size(); i++) + { + arr.Set(i, _s(data[i])); + } + return arr; +} + +void Emit::Wrap(const Napi::Value& receiver, const Napi::Function& callback) +{ + mCallback = std::make_shared(receiver, callback); +} + +void Emit::RadioState(const std::string& state) +{ + mCallback->call([state](Napi::Env env, std::vector& args) { + // emit('stateChange', state); + args = { _s("stateChange"), _s(state) }; + }); +} + +void Emit::ScanState(bool start) +{ + mCallback->call([start](Napi::Env env, std::vector& args) { + // emit('scanStart') emit('scanStop') + args = { _s(start ? "scanStart" : "scanStop") }; + }); +} + +void Emit::Scan(const std::string& uuid, int rssi, const Peripheral& peripheral) +{ + auto address = peripheral.address; + auto addressType = peripheral.addressType; + auto connectable = peripheral.connectable; + auto name = peripheral.name; + auto txPowerLevel = peripheral.txPowerLevel; + auto manufacturerData = peripheral.manufacturerData; + auto serviceData = peripheral.serviceData; + auto serviceUuids = peripheral.serviceUuids; + mCallback->call([uuid, rssi, address, addressType, connectable, name, txPowerLevel, + manufacturerData, serviceData, + serviceUuids](Napi::Env env, std::vector& args) { + Napi::Object advertisment = Napi::Object::New(env); + advertisment.Set(_s("localName"), _s(name)); + advertisment.Set(_s("txPowerLevel"), txPowerLevel); + advertisment.Set(_s("manufacturerData"), toBuffer(env, manufacturerData)); + auto array = + serviceData.empty() ? Napi::Array::New(env) : Napi::Array::New(env, serviceData.size()); + for (size_t i = 0; i < serviceData.size(); i++) + { + Napi::Object data = Napi::Object::New(env); + data.Set(_s("uuid"), _u(serviceData[i].first)); + data.Set(_s("data"), toBuffer(env, serviceData[i].second)); + array.Set(i, data); + } + advertisment.Set(_s("serviceData"), array); + advertisment.Set(_s("serviceUuids"), toUuidArray(env, serviceUuids)); + // emit('discover', deviceUuid, address, addressType, connectable, advertisement, rssi); + args = { _s("discover"), _u(uuid), _s(address), toAddressType(env, addressType), + _b(connectable), advertisment, _n(rssi) }; + }); +} + +void Emit::Connected(const std::string& uuid, const std::string& error) +{ + mCallback->call([uuid, error](Napi::Env env, std::vector& args) { + // emit('connect', deviceUuid) error added here + args = { _s("connect"), _u(uuid), error.empty() ? env.Null() : _s(error) }; + }); +} + +void Emit::Disconnected(const std::string& uuid) +{ + mCallback->call([uuid](Napi::Env env, std::vector& args) { + // emit('disconnect', deviceUuid); + args = { _s("disconnect"), _u(uuid) }; + }); +} + +void Emit::RSSI(const std::string& uuid, int rssi) +{ + mCallback->call([uuid, rssi](Napi::Env env, std::vector& args) { + // emit('rssiUpdate', deviceUuid, rssi); + args = { _s("rssiUpdate"), _u(uuid), _n(rssi) }; + }); +} + +void Emit::ServicesDiscovered(const std::string& uuid, const std::vector& serviceUuids) +{ + mCallback->call([uuid, serviceUuids](Napi::Env env, std::vector& args) { + // emit('servicesDiscover', deviceUuid, serviceUuids) + args = { _s("servicesDiscover"), _u(uuid), toUuidArray(env, serviceUuids) }; + }); +} + +void Emit::IncludedServicesDiscovered(const std::string& uuid, const std::string& serviceUuid, + const std::vector& serviceUuids) +{ + mCallback->call( + [uuid, serviceUuid, serviceUuids](Napi::Env env, std::vector& args) { + // emit('includedServicesDiscover', deviceUuid, serviceUuid, includedServiceUuids) + args = { _s("includedServicesDiscover"), _u(uuid), _u(serviceUuid), + toUuidArray(env, serviceUuids) }; + }); +} + +void Emit::CharacteristicsDiscovered( + const std::string& uuid, const std::string& serviceUuid, + const std::vector>>& characteristics) +{ + mCallback->call( + [uuid, serviceUuid, characteristics](Napi::Env env, std::vector& args) { + auto arr = characteristics.empty() ? Napi::Array::New(env) + : Napi::Array::New(env, characteristics.size()); + for (size_t i = 0; i < characteristics.size(); i++) + { + Napi::Object characteristic = Napi::Object::New(env); + characteristic.Set(_s("uuid"), _u(characteristics[i].first)); + characteristic.Set(_s("properties"), toArray(env, characteristics[i].second)); + arr.Set(i, characteristic); + } + // emit('characteristicsDiscover', deviceUuid, serviceUuid, { uuid, properties: + // ['broadcast', 'read', ...]}) + args = { _s("characteristicsDiscover"), _u(uuid), _u(serviceUuid), arr }; + }); +} + +void Emit::Read(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid, const Data& data, bool isNotification) +{ + mCallback->call([uuid, serviceUuid, characteristicUuid, data, + isNotification](Napi::Env env, std::vector& args) { + // emit('read', deviceUuid, serviceUuid, characteristicsUuid, data, isNotification); + args = { _s("read"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), + toBuffer(env, data), _b(isNotification) }; + }); +} + +void Emit::Write(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid) +{ + mCallback->call( + [uuid, serviceUuid, characteristicUuid](Napi::Env env, std::vector& args) { + // emit('write', deviceUuid, servicesUuid, characteristicsUuid) + args = { _s("write"), _u(uuid), _u(serviceUuid), _u(characteristicUuid) }; + }); +} + +void Emit::Notify(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid, bool state) +{ + mCallback->call([uuid, serviceUuid, characteristicUuid, state](Napi::Env env, + std::vector& args) { + // emit('notify', deviceUuid, servicesUuid, characteristicsUuid, state) + args = { _s("notify"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), _b(state) }; + }); +} + +void Emit::DescriptorsDiscovered(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid, + const std::vector& descriptorUuids) +{ + mCallback->call([uuid, serviceUuid, characteristicUuid, + descriptorUuids](Napi::Env env, std::vector& args) { + // emit('descriptorsDiscover', deviceUuid, servicesUuid, characteristicsUuid, descriptors: + // [uuids]) + args = { _s("descriptorsDiscover"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), + toUuidArray(env, descriptorUuids) }; + }); +} + +void Emit::ReadValue(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid, const std::string& descriptorUuid, + const Data& data) +{ + mCallback->call([uuid, serviceUuid, characteristicUuid, descriptorUuid, + data](Napi::Env env, std::vector& args) { + // emit('valueRead', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) + args = { _s("valueRead"), _u(uuid), _u(serviceUuid), + _u(characteristicUuid), _u(descriptorUuid), toBuffer(env, data) }; + }); +} + +void Emit::WriteValue(const std::string& uuid, const std::string& serviceUuid, + const std::string& characteristicUuid, const std::string& descriptorUuid) +{ + mCallback->call([uuid, serviceUuid, characteristicUuid, + descriptorUuid](Napi::Env env, std::vector& args) { + // emit('valueWrite', deviceUuid, serviceUuid, characteristicUuid, descriptorUuid); + args = { _s("valueWrite"), _u(uuid), _u(serviceUuid), _u(characteristicUuid), + _u(descriptorUuid) }; + }); +} + +void Emit::ReadHandle(const std::string& uuid, int descriptorHandle, const Data& data) +{ + mCallback->call([uuid, descriptorHandle, data](Napi::Env env, std::vector& args) { + // emit('handleRead', deviceUuid, descriptorHandle, data); + args = { _s("handleRead"), _u(uuid), _n(descriptorHandle), toBuffer(env, data) }; + }); +} + +void Emit::WriteHandle(const std::string& uuid, int descriptorHandle) +{ + mCallback->call([uuid, descriptorHandle](Napi::Env env, std::vector& args) { + // emit('handleWrite', deviceUuid, descriptorHandle); + args = { _s("handleWrite"), _u(uuid), _n(descriptorHandle) }; + }); +} + +void Emit::Log(const std::string& log) +{ + mCallback->call([log](Napi::Env env, std::vector& args) { + // emit('log', log); + args = { _s("log"), _s(log) }; + }); +} diff --git a/lib/winrt/src/callbacks.h b/lib/winrt/src/callbacks.h new file mode 100644 index 000000000..2612c7412 --- /dev/null +++ b/lib/winrt/src/callbacks.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "peripheral.h" + +class ThreadSafeCallback; + +class Emit +{ +public: + // clang-format off + void Wrap(const Napi::Value& receiver, const Napi::Function& callback); + void RadioState(const std::string& status); + void ScanState(bool start); + void Scan(const std::string& uuid, int rssi, const Peripheral& peripheral); + void Connected(const std::string& uuid, const std::string& error = ""); + void Disconnected(const std::string& uuid); + void RSSI(const std::string& uuid, int rssi); + void ServicesDiscovered(const std::string& uuid, const std::vector& serviceUuids); + void IncludedServicesDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::vector& serviceUuids); + void CharacteristicsDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::vector>>& characteristics); + void Read(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const Data& data, bool isNotification); + void Write(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid); + void Notify(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, bool state); + void DescriptorsDiscovered(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::vector& descriptorUuids); + void ReadValue(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::string& descriptorUuid, const Data& data); + void WriteValue(const std::string& uuid, const std::string& serviceUuid, const std::string& characteristicUuid, const std::string& descriptorUuid); + void ReadHandle(const std::string& uuid, int descriptorHandle, const Data& data); + void WriteHandle(const std::string& uuid, int descriptorHandle); + void Log(const std::string& log); + // clang-format on +protected: + std::shared_ptr mCallback; +}; diff --git a/lib/winrt/src/napi_winrt.cc b/lib/winrt/src/napi_winrt.cc new file mode 100644 index 000000000..b48943b82 --- /dev/null +++ b/lib/winrt/src/napi_winrt.cc @@ -0,0 +1,76 @@ +// +// napi_objc.mm +// noble-mac-native +// +// Created by Georg Vienna on 30.08.18. +// +#include "napi_winrt.h" + +#include +#include + +using namespace winrt::Windows::Devices::Bluetooth; + +winrt::guid napiToUuid(Napi::String string) +{ + std::string str = string.Utf8Value(); + if (str.size() == 32) + { + str.insert(8, "-"); + str.insert(13, "-"); + str.insert(18, "-"); + str.insert(23, "-"); + } + if (str.size() == 4) + { + int id = std::stoi(str, 0, 16); + return BluetoothUuidHelper::FromShortId(id); + } + UUID uuid; + UuidFromString((RPC_CSTR)str.c_str(), &uuid); + std::array data4; + std::copy_n(uuid.Data4, data4.size(), data4.begin()); + return winrt::guid(uuid.Data1, uuid.Data2, uuid.Data3, data4); +} + +std::vector napiToUuidArray(Napi::Array array) +{ + std::vector uuids; + for (size_t i = 0; i < array.Length(); i++) + { + Napi::Value val = array[i]; + uuids.push_back(napiToUuid(val.As())); + } + return uuids; +} + +Data napiToData(Napi::Buffer buffer) +{ + Data data; + auto bytes = buffer.Data(); + data.assign(bytes, bytes + buffer.Length()); + return data; +} + +int napiToNumber(Napi::Number number) +{ + return number.Int32Value(); +} + +std::vector getUuidArray(const Napi::Value& value) +{ + if (value.IsArray()) + { + return napiToUuidArray(value.As()); + } + return std::vector(); +} + +bool getBool(const Napi::Value& value, bool def) +{ + if (value.IsBoolean()) + { + return value.As().Value(); + } + return def; +} diff --git a/lib/winrt/src/napi_winrt.h b/lib/winrt/src/napi_winrt.h new file mode 100644 index 000000000..1718c7214 --- /dev/null +++ b/lib/winrt/src/napi_winrt.h @@ -0,0 +1,12 @@ +#pragma once + +#include +#include "winrt/base.h" +#include "peripheral.h" + +std::vector getUuidArray(const Napi::Value& value); +bool getBool(const Napi::Value& value, bool def); + +winrt::guid napiToUuid(Napi::String string); +Data napiToData(Napi::Buffer buffer); +int napiToNumber(Napi::Number number); diff --git a/lib/winrt/src/noble_winrt.cc b/lib/winrt/src/noble_winrt.cc new file mode 100644 index 000000000..506226317 --- /dev/null +++ b/lib/winrt/src/noble_winrt.cc @@ -0,0 +1,308 @@ +// +// noble_winrt.cc +// noble-winrt-native +// +// Created by Georg Vienna on 03.09.18. +// +#include "noble_winrt.h" + +#include "napi_winrt.h" + +#define THROW(msg) \ + Napi::TypeError::New(info.Env(), msg).ThrowAsJavaScriptException(); \ + return Napi::Value(); + +#define ARG1(type1) \ + if (!info[0].Is##type1()) \ + { \ + THROW("There should be one argument: (" #type1 ")") \ + } + +#define ARG2(type1, type2) \ + if (!info[0].Is##type1() || !info[1].Is##type2()) \ + { \ + THROW("There should be 2 arguments: (" #type1 ", " #type2 ")"); \ + } + +#define ARG3(type1, type2, type3) \ + if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3()) \ + { \ + THROW("There should be 3 arguments: (" #type1 ", " #type2 ", " #type3 ")"); \ + } + +#define ARG4(type1, type2, type3, type4) \ + if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3() || \ + !info[3].Is##type4()) \ + { \ + THROW("There should be 4 arguments: (" #type1 ", " #type2 ", " #type3 ", " #type4 ")"); \ + } + +#define ARG5(type1, type2, type3, type4, type5) \ + if (!info[0].Is##type1() || !info[1].Is##type2() || !info[2].Is##type3() || \ + !info[3].Is##type4() || !info[4].Is##type5()) \ + { \ + THROW("There should be 5 arguments: (" #type1 ", " #type2 ", " #type3 ", " #type4 \ + ", " #type5 ")"); \ + } + +#define CHECK_MANAGER() \ + if (!manager) \ + { \ + THROW("BLEManager has already been cleaned up"); \ + } + +NobleWinrt::NobleWinrt(const Napi::CallbackInfo& info) : ObjectWrap(info) +{ +} + +Napi::Value NobleWinrt::Init(const Napi::CallbackInfo& info) +{ + Napi::Function emit = info.This().As().Get("emit").As(); + manager = new BLEManager(info.This(), emit); + return Napi::Value(); +} + +// startScanning(serviceUuids, allowDuplicates) +Napi::Value NobleWinrt::Scan(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + auto vector = getUuidArray(info[0]); + // default value false + auto duplicates = getBool(info[1], false); + manager->Scan(vector, duplicates); + return Napi::Value(); +} + +// stopScanning() +Napi::Value NobleWinrt::StopScan(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + manager->StopScan(); + return Napi::Value(); +} + +// connect(deviceUuid) +Napi::Value NobleWinrt::Connect(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG1(String) + auto uuid = info[0].As().Utf8Value(); + manager->Connect(uuid); + return Napi::Value(); +} + +// disconnect(deviceUuid) +Napi::Value NobleWinrt::Disconnect(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG1(String) + auto uuid = info[0].As().Utf8Value(); + manager->Disconnect(uuid); + return Napi::Value(); +} + +// updateRssi(deviceUuid) +Napi::Value NobleWinrt::UpdateRSSI(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG1(String) + auto uuid = info[0].As().Utf8Value(); + manager->UpdateRSSI(uuid); + return Napi::Value(); +} + +// discoverServices(deviceUuid, uuids) +Napi::Value NobleWinrt::DiscoverServices(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG1(String) + auto uuid = info[0].As().Utf8Value(); + std::vector uuids = getUuidArray(info[1]); + manager->DiscoverServices(uuid, uuids); + return Napi::Value(); +} + +// discoverIncludedServices(deviceUuid, serviceUuid, serviceUuids) +Napi::Value NobleWinrt::DiscoverIncludedServices(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG2(String, String) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + std::vector uuids = getUuidArray(info[2]); + manager->DiscoverIncludedServices(uuid, service, uuids); + return Napi::Value(); +} + +// discoverCharacteristics(deviceUuid, serviceUuid, characteristicUuids) +Napi::Value NobleWinrt::DiscoverCharacteristics(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG2(String, String) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + std::vector characteristics = getUuidArray(info[2]); + manager->DiscoverCharacteristics(uuid, service, characteristics); + return Napi::Value(); +} + +// read(deviceUuid, serviceUuid, characteristicUuid) +Napi::Value NobleWinrt::Read(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG3(String, String, String) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + manager->Read(uuid, service, characteristic); + return Napi::Value(); +} + +// write(deviceUuid, serviceUuid, characteristicUuid, data, withoutResponse) +Napi::Value NobleWinrt::Write(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG5(String, String, String, Buffer, Boolean) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + auto data = napiToData(info[3].As>()); + auto withoutResponse = info[4].As().Value(); + manager->Write(uuid, service, characteristic, data, withoutResponse); + return Napi::Value(); +} + +// notify(deviceUuid, serviceUuid, characteristicUuid, notify) +Napi::Value NobleWinrt::Notify(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG4(String, String, String, Boolean) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + auto on = info[3].As().Value(); + manager->Notify(uuid, service, characteristic, on); + return Napi::Value(); +} + +// discoverDescriptors(deviceUuid, serviceUuid, characteristicUuid) +Napi::Value NobleWinrt::DiscoverDescriptors(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG3(String, String, String) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + manager->DiscoverDescriptors(uuid, service, characteristic); + return Napi::Value(); +} + +// readValue(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid) +Napi::Value NobleWinrt::ReadValue(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG4(String, String, String, String) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + auto descriptor = napiToUuid(info[3].As()); + manager->ReadValue(uuid, service, characteristic, descriptor); + return Napi::Value(); +} + +// writeValue(deviceUuid, serviceUuid, characteristicUuid, descriptorUuid, data) +Napi::Value NobleWinrt::WriteValue(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG5(String, String, String, String, Buffer) + auto uuid = info[0].As().Utf8Value(); + auto service = napiToUuid(info[1].As()); + auto characteristic = napiToUuid(info[2].As()); + auto descriptor = napiToUuid(info[3].As()); + auto data = napiToData(info[4].As>()); + manager->WriteValue(uuid, service, characteristic, descriptor, data); + return Napi::Value(); +} + +// readHandle(deviceUuid, handle) +Napi::Value NobleWinrt::ReadHandle(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG2(String, Number) + auto uuid = info[0].As().Utf8Value(); + auto handle = napiToNumber(info[1].As()); + manager->ReadHandle(uuid, handle); + return Napi::Value(); +} + +// writeHandle(deviceUuid, handle, data, (unused)withoutResponse) +Napi::Value NobleWinrt::WriteHandle(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + ARG3(String, Number, Buffer) + auto uuid = info[0].As().Utf8Value(); + auto handle = napiToNumber(info[1].As()); + auto data = napiToData(info[2].As>()); + manager->WriteHandle(uuid, handle, data); + return Napi::Value(); +} + +Napi::Value NobleWinrt::CleanUp(const Napi::CallbackInfo& info) +{ + CHECK_MANAGER() + delete manager; + manager = nullptr; + return Napi::Value(); +} + +Napi::Function NobleWinrt::GetClass(Napi::Env env) +{ + // clang-format off + return DefineClass(env, "NobleWinrt", { + NobleWinrt::InstanceMethod("init", &NobleWinrt::Init), + NobleWinrt::InstanceMethod("startScanning", &NobleWinrt::Scan), + NobleWinrt::InstanceMethod("stopScanning", &NobleWinrt::StopScan), + NobleWinrt::InstanceMethod("connect", &NobleWinrt::Connect), + NobleWinrt::InstanceMethod("disconnect", &NobleWinrt::Disconnect), + NobleWinrt::InstanceMethod("updateRssi", &NobleWinrt::UpdateRSSI), + NobleWinrt::InstanceMethod("discoverServices", &NobleWinrt::DiscoverServices), + NobleWinrt::InstanceMethod("discoverIncludedServices", &NobleWinrt::DiscoverIncludedServices), + NobleWinrt::InstanceMethod("discoverCharacteristics", &NobleWinrt::DiscoverCharacteristics), + NobleWinrt::InstanceMethod("read", &NobleWinrt::Read), + NobleWinrt::InstanceMethod("write", &NobleWinrt::Write), + NobleWinrt::InstanceMethod("notify", &NobleWinrt::Notify), + NobleWinrt::InstanceMethod("discoverDescriptors", &NobleWinrt::DiscoverDescriptors), + NobleWinrt::InstanceMethod("readValue", &NobleWinrt::ReadValue), + NobleWinrt::InstanceMethod("writeValue", &NobleWinrt::WriteValue), + NobleWinrt::InstanceMethod("readHandle", &NobleWinrt::ReadHandle), + NobleWinrt::InstanceMethod("writeHandle", &NobleWinrt::WriteHandle), + NobleWinrt::InstanceMethod("cleanUp", &NobleWinrt::CleanUp), + }); + // clang-format on +} + +#pragma comment(lib, "windowsapp") + +Napi::Object Init(Napi::Env env, Napi::Object exports) +{ + try + { + winrt::init_apartment(); + } + catch (winrt::hresult_error hresult) + { + // electron already initialized the COM library + if (hresult.code() != RPC_E_CHANGED_MODE) + { + wprintf(L"Failed initializing apartment: %d %s", hresult.code().value, + hresult.message().c_str()); + Napi::TypeError::New(env, "Failed initializing apartment").ThrowAsJavaScriptException(); + return exports; + } + } + Napi::String name = Napi::String::New(env, "NobleWinrt"); + exports.Set(name, NobleWinrt::GetClass(env)); + return exports; +} + +NODE_API_MODULE(addon, Init) diff --git a/lib/winrt/src/noble_winrt.h b/lib/winrt/src/noble_winrt.h new file mode 100644 index 000000000..519bab7a2 --- /dev/null +++ b/lib/winrt/src/noble_winrt.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +#include "ble_manager.h" + +class NobleWinrt : public Napi::ObjectWrap +{ +public: + NobleWinrt(const Napi::CallbackInfo&); + Napi::Value Init(const Napi::CallbackInfo&); + Napi::Value CleanUp(const Napi::CallbackInfo&); + Napi::Value Scan(const Napi::CallbackInfo&); + Napi::Value StopScan(const Napi::CallbackInfo&); + Napi::Value Connect(const Napi::CallbackInfo&); + Napi::Value Disconnect(const Napi::CallbackInfo&); + Napi::Value UpdateRSSI(const Napi::CallbackInfo&); + Napi::Value DiscoverServices(const Napi::CallbackInfo&); + Napi::Value DiscoverIncludedServices(const Napi::CallbackInfo& info); + Napi::Value DiscoverCharacteristics(const Napi::CallbackInfo& info); + Napi::Value Read(const Napi::CallbackInfo& info); + Napi::Value Write(const Napi::CallbackInfo& info); + Napi::Value Notify(const Napi::CallbackInfo& info); + Napi::Value DiscoverDescriptors(const Napi::CallbackInfo& info); + Napi::Value ReadValue(const Napi::CallbackInfo& info); + Napi::Value WriteValue(const Napi::CallbackInfo& info); + Napi::Value ReadHandle(const Napi::CallbackInfo& info); + Napi::Value WriteHandle(const Napi::CallbackInfo& info); + + static Napi::Function GetClass(Napi::Env); + +private: + BLEManager* manager; +}; diff --git a/lib/winrt/src/notify_map.cc b/lib/winrt/src/notify_map.cc new file mode 100644 index 000000000..285044a9f --- /dev/null +++ b/lib/winrt/src/notify_map.cc @@ -0,0 +1,62 @@ +// +// notify_map.cc +// noble-winrt-native +// +// Created by Georg Vienna on 07.09.18. +// + +#include "notify_map.h" + +bool Key::operator==(const Key& other) const +{ + return (uuid == other.uuid && serviceUuid == other.serviceUuid && + characteristicUuid == other.characteristicUuid); +} + +void NotifyMap::Add(std::string uuid, GattCharacteristic characteristic, winrt::event_token token) +{ + Key key = { uuid, characteristic.Service().Uuid(), characteristic.Uuid() }; + mNotifyMap.insert(std::make_pair(key, token)); +} + +bool NotifyMap::IsSubscribed(std::string uuid, GattCharacteristic characteristic) +{ + try + { + Key key = { uuid, characteristic.Service().Uuid(), characteristic.Uuid() }; + return mNotifyMap.find(key) != mNotifyMap.end(); + } + catch (...) + { + return false; + } +} + +void NotifyMap::Unsubscribe(std::string uuid, GattCharacteristic characteristic) +{ + Key key = { uuid, characteristic.Service().Uuid(), characteristic.Uuid() }; + auto& it = mNotifyMap.find(key); + if (it == mNotifyMap.end()) + { + return; + } + auto& token = it->second; + characteristic.ValueChanged(token); + mNotifyMap.erase(key); +} + +void NotifyMap::Remove(std::string uuid) +{ + for (auto it = mNotifyMap.begin(); it != mNotifyMap.end();) + { + auto& key = it->first; + if (key.uuid == uuid) + { + it = mNotifyMap.erase(it); + } + else + { + it++; + } + } +} diff --git a/lib/winrt/src/notify_map.h b/lib/winrt/src/notify_map.h new file mode 100644 index 000000000..70261ecfe --- /dev/null +++ b/lib/winrt/src/notify_map.h @@ -0,0 +1,50 @@ +// +// notify_map.h +// noble-winrt-native +// +// Created by Georg Vienna on 07.09.18. +// + +#pragma once + +#include +#include "winrt_guid.h" + +using namespace winrt::Windows::Devices::Bluetooth::GenericAttributeProfile; + +struct Key +{ +public: + std::string uuid; + winrt::guid serviceUuid; + winrt::guid characteristicUuid; + + bool operator==(const Key& other) const; +}; + +namespace std +{ + template <> struct hash + { + std::size_t operator()(const Key& k) const + { + auto serviceHash = std::hash()(k.serviceUuid); + auto characteristicHash = std::hash()(k.characteristicUuid); + return ((std::hash()(k.uuid) ^ (serviceHash << 1)) >> 1) ^ + (characteristicHash << 1); + } + }; +} // namespace std + +class NotifyMap +{ +public: + void Add(std::string uuid, GattCharacteristic characteristic, winrt::event_token token); + bool IsSubscribed(std::string uuid, GattCharacteristic characteristic); + void Unsubscribe(std::string uuid, GattCharacteristic characteristic); + + void Remove(std::string uuid); + +private: + std::unordered_map mNotifyMap; +}; diff --git a/lib/winrt/src/peripheral.h b/lib/winrt/src/peripheral.h new file mode 100644 index 000000000..2c67cdb41 --- /dev/null +++ b/lib/winrt/src/peripheral.h @@ -0,0 +1,23 @@ +#pragma once + +using Data = std::vector; + +enum AddressType +{ + PUBLIC, + RANDOM, + UNKNOWN, +}; + +class Peripheral +{ +public: + std::string address = "unknown"; + AddressType addressType = UNKNOWN; + bool connectable = false; + std::string name; + int txPowerLevel; + Data manufacturerData; + std::vector> serviceData; + std::vector serviceUuids; +}; diff --git a/lib/winrt/src/peripheral_winrt.cc b/lib/winrt/src/peripheral_winrt.cc new file mode 100644 index 000000000..d2c9f23cf --- /dev/null +++ b/lib/winrt/src/peripheral_winrt.cc @@ -0,0 +1,293 @@ +#include "peripheral_winrt.h" +#include "winrt_cpp.h" + +#include +using namespace winrt::Windows::Storage::Streams; + +using winrt::Windows::Devices::Bluetooth::BluetoothCacheMode; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristicsResult; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptorsResult; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceServicesResult; +using winrt::Windows::Foundation::AsyncStatus; +using winrt::Windows::Foundation::IAsyncOperation; + +#include + +PeripheralWinrt::PeripheralWinrt(uint64_t bluetoothAddress, + BluetoothLEAdvertisementType advertismentType, const int rssiValue, + const BluetoothLEAdvertisement& advertisment) +{ + this->bluetoothAddress = bluetoothAddress; + address = formatBluetoothAddress(bluetoothAddress); + // Random addresses have the two most-significant bits set of the 48-bit address. + addressType = (bluetoothAddress >= 211106232532992) ? RANDOM : PUBLIC; + connectable = advertismentType == BluetoothLEAdvertisementType::ConnectableUndirected || + advertismentType == BluetoothLEAdvertisementType::ConnectableDirected; + Update(rssiValue, advertisment); +} + +PeripheralWinrt::~PeripheralWinrt() +{ + if (device.has_value() && connectionToken) + { + device->ConnectionStatusChanged(connectionToken); + } +} + +void PeripheralWinrt::Update(const int rssiValue, const BluetoothLEAdvertisement& advertisment) +{ + std::string localName = ws2s(advertisment.LocalName().c_str()); + if (!localName.empty()) + { + name = localName; + } + + manufacturerData.clear(); + for (auto& ds : advertisment.DataSections()) + { + if (ds.DataType() == BluetoothLEAdvertisementDataTypes::TxPowerLevel()) + { + auto d = ds.Data(); + auto dr = DataReader::FromBuffer(d); + txPowerLevel = dr.ReadByte(); + if (txPowerLevel >= 128) + txPowerLevel -= 256; + dr.Close(); + } + if (ds.DataType() == BluetoothLEAdvertisementDataTypes::ManufacturerSpecificData()) + { + auto d = ds.Data(); + auto dr = DataReader::FromBuffer(d); + manufacturerData.resize(d.Length()); + dr.ReadBytes(manufacturerData); + dr.Close(); + } + } + + serviceUuids.clear(); + for (auto& uuid : advertisment.ServiceUuids()) + { + serviceUuids.push_back(toStr(uuid)); + } + + rssi = rssiValue; +} + +void PeripheralWinrt::Disconnect() +{ + cachedServices.clear(); + if (device.has_value() && connectionToken) + { + device->ConnectionStatusChanged(connectionToken); + } + device = std::nullopt; +} + +void PeripheralWinrt::GetServiceFromDevice( + winrt::guid serviceUuid, std::function, std::string)> callback) +{ + if (device.has_value()) + { + device->GetGattServicesForUuidAsync(serviceUuid, BluetoothCacheMode::Cached) + .Completed([=](IAsyncOperation result, auto& status) { + if (status == AsyncStatus::Completed) + { + auto& services = result.GetResults(); + auto& service = services.Services().First(); + if (service.HasCurrent()) + { + GattDeviceService& s = service.Current(); + cachedServices.insert(std::make_pair(serviceUuid, CachedService(s))); + callback(s, ""); + } + else + { + callback(std::nullopt, "GetServiceFromDevice: no service with given id"); + } + } + else + { + std::stringstream ss; + ss << "GetServiceFromDevice: failed with status: "; + ss << int(status); + callback(std::nullopt, ss.str()); + } + }); + } + else + { + callback(std::nullopt, "GetServiceFromDevice: no device currently connected"); + } +} + +void PeripheralWinrt::GetService(winrt::guid serviceUuid, + std::function, std::string)> callback) +{ + auto it = cachedServices.find(serviceUuid); + if (it != cachedServices.end()) + { + callback(it->second.service, ""); + } + else + { + GetServiceFromDevice(serviceUuid, callback); + } +} + +void PeripheralWinrt::GetCharacteristicFromService( + GattDeviceService service, winrt::guid characteristicUuid, + std::function, std::string)> callback) +{ + service.GetCharacteristicsForUuidAsync(characteristicUuid, BluetoothCacheMode::Cached) + .Completed([=](IAsyncOperation result, auto& status) { + if (status == AsyncStatus::Completed) + { + auto& characteristics = result.GetResults(); + auto& characteristic = characteristics.Characteristics().First(); + if (characteristic.HasCurrent()) + { + winrt::guid serviceUuid = service.Uuid(); + CachedService& cachedService = cachedServices[serviceUuid]; + GattCharacteristic& c = characteristic.Current(); + cachedService.characterisitics.insert( + std::make_pair(c.Uuid(), CachedCharacteristic(c))); + callback(c, ""); + } + else + { + callback(std::nullopt, "GetCharacteristicFromService: no characteristic with given id"); + } + } + else + { + std::stringstream ss; + ss << "GetCharacteristicsForUuidAsync: failed with status: "; + ss << int(status); + callback(std::nullopt, ss.str()); + } + }); +} + +void PeripheralWinrt::GetCharacteristic( + winrt::guid serviceUuid, winrt::guid characteristicUuid, + std::function, std::string)> callback) +{ + auto it = cachedServices.find(serviceUuid); + if (it != cachedServices.end()) + { + auto& cachedService = it->second; + auto cit = cachedService.characterisitics.find(characteristicUuid); + if (cit != cachedService.characterisitics.end()) + { + callback(cit->second.characteristic, ""); + } + else + { + GetCharacteristicFromService(cachedService.service, characteristicUuid, callback); + } + } + else + { + GetServiceFromDevice(serviceUuid, [=](std::optional service, std::string error) { + if (service) + { + GetCharacteristicFromService(*service, characteristicUuid, callback); + } + else + { + callback(std::nullopt, error); + } + }); + } +} + +void PeripheralWinrt::GetDescriptorFromCharacteristic( + GattCharacteristic characteristic, winrt::guid descriptorUuid, + std::function, std::string)> callback) +{ + characteristic.GetDescriptorsForUuidAsync(descriptorUuid, BluetoothCacheMode::Cached) + .Completed([=](IAsyncOperation result, auto& status) { + if (status == AsyncStatus::Completed) + { + auto& descriptors = result.GetResults(); + auto& descriptor = descriptors.Descriptors().First(); + if (descriptor.HasCurrent()) + { + GattDescriptor d = descriptor.Current(); + winrt::guid characteristicUuid = characteristic.Uuid(); + winrt::guid descriptorUuid = d.Uuid(); + winrt::guid serviceUuid = characteristic.Service().Uuid(); + CachedService& cachedService = cachedServices[serviceUuid]; + CachedCharacteristic& c = cachedService.characterisitics[characteristicUuid]; + c.descriptors.insert(std::make_pair(descriptorUuid, d)); + callback(d, ""); + } + else + { + callback(std::nullopt, "GetDescriptorFromCharacteristic: no characteristic with given id"); + } + } + else + { + std::stringstream ss; + ss << "GetDescriptorFromCharacteristic: failed with status: "; + ss << int(status); + callback(std::nullopt, ss.str()); + } + }); +} + +void PeripheralWinrt::GetDescriptor(winrt::guid serviceUuid, winrt::guid characteristicUuid, winrt::guid descriptorUuid, + std::function, std::string)> callback) +{ + auto it = cachedServices.find(serviceUuid); + if (it != cachedServices.end()) + { + auto& cachedService = it->second; + auto cit = cachedService.characterisitics.find(characteristicUuid); + if (cit != cachedService.characterisitics.end()) + { + GetDescriptorFromCharacteristic(cit->second.characteristic, descriptorUuid, callback); + } + else + { + GetCharacteristicFromService( + cachedService.service, characteristicUuid, + [=](std::optional characteristic, std::string error) { + if (characteristic) + { + GetDescriptorFromCharacteristic(*characteristic, descriptorUuid, callback); + } + else + { + callback(std::nullopt, error); + } + }); + } + } + else + { + GetServiceFromDevice(serviceUuid, [=](std::optional service, std::string error) { + if (service) + { + GetCharacteristicFromService( + *service, characteristicUuid, + [=](std::optional characteristic, std::string charError) { + if (characteristic) + { + GetDescriptorFromCharacteristic(*characteristic, descriptorUuid, + callback); + } + else + { + callback(std::nullopt, charError); + } + }); + } + else + { + callback(std::nullopt, error); + } + }); + } +} diff --git a/lib/winrt/src/peripheral_winrt.h b/lib/winrt/src/peripheral_winrt.h new file mode 100644 index 000000000..0e4b20492 --- /dev/null +++ b/lib/winrt/src/peripheral_winrt.h @@ -0,0 +1,80 @@ +#pragma once + +#include +#define WIN32_LEAN_AND_MEAN +#include +#include + +using namespace winrt::Windows::Devices::Bluetooth::Advertisement; +using winrt::Windows::Devices::Bluetooth::BluetoothLEDevice; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristic; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDescriptor; +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattDeviceService; + +#include "winrt/Windows.Devices.Bluetooth.h" + +#include +#include + +#include "peripheral.h" +#include "winrt_guid.h" + +class CachedCharacteristic +{ +public: + CachedCharacteristic() = default; + CachedCharacteristic(GattCharacteristic& c) : characteristic(c) + { + } + + GattCharacteristic characteristic = nullptr; + std::unordered_map descriptors; +}; + +class CachedService +{ +public: + CachedService() = default; + CachedService(GattDeviceService& s) : service(s) + { + } + + GattDeviceService service = nullptr; + std::unordered_map characterisitics; +}; + +class PeripheralWinrt : public Peripheral +{ +public: + PeripheralWinrt() = default; + PeripheralWinrt(uint64_t bluetoothAddress, BluetoothLEAdvertisementType advertismentType, + int rssiValue, const BluetoothLEAdvertisement& advertisment); + ~PeripheralWinrt(); + + void Update(int rssiValue, const BluetoothLEAdvertisement& advertisment); + + void Disconnect(); + + void GetService(winrt::guid serviceUuid, + std::function, std::string error)> callback); + void GetCharacteristic(winrt::guid serviceUuid, winrt::guid characteristicUuid, + std::function, std::string error)> callback); + void GetDescriptor(winrt::guid serviceUuid, winrt::guid characteristicUuid, winrt::guid descriptorUuid, + std::function, std::string error)> callback); + + int rssi; + uint64_t bluetoothAddress; + std::optional device; + winrt::event_token connectionToken; + +private: + void GetServiceFromDevice(winrt::guid serviceUuid, + std::function, std::string error)> callback); + void + GetCharacteristicFromService(GattDeviceService service, winrt::guid characteristicUuid, + std::function, std::string error)> callback); + void + GetDescriptorFromCharacteristic(GattCharacteristic characteristic, winrt::guid descriptorUuid, + std::function, std::string error)> callback); + std::unordered_map cachedServices; +}; diff --git a/lib/winrt/src/radio_watcher.cc b/lib/winrt/src/radio_watcher.cc new file mode 100644 index 000000000..de6c059e9 --- /dev/null +++ b/lib/winrt/src/radio_watcher.cc @@ -0,0 +1,123 @@ +// +// radio_watcher.cc +// noble-winrt-native +// +// Created by Georg Vienna on 07.09.18. +// + +#pragma once + +#include "radio_watcher.h" +#include "winrt_cpp.h" + +using winrt::Windows::Devices::Radios::RadioKind; +using winrt::Windows::Foundation::AsyncStatus; + +template auto bind2(O* object, M method, Types&... args) +{ + return std::bind(method, object, std::placeholders::_1, std::placeholders::_2, args...); +} + +#define RADIO_INTERFACE_CLASS_GUID \ + L"System.Devices.InterfaceClassGuid:=\"{A8804298-2D5F-42E3-9531-9C8C39EB29CE}\"" + +RadioWatcher::RadioWatcher() + : mRadio(nullptr), watcher(DeviceInformation::CreateWatcher(RADIO_INTERFACE_CLASS_GUID)) +{ + mAddedRevoker = watcher.Added(winrt::auto_revoke, bind2(this, &RadioWatcher::OnAdded)); + mUpdatedRevoker = watcher.Updated(winrt::auto_revoke, bind2(this, &RadioWatcher::OnUpdated)); + mRemovedRevoker = watcher.Removed(winrt::auto_revoke, bind2(this, &RadioWatcher::OnRemoved)); + auto completed = bind2(this, &RadioWatcher::OnCompleted); + mCompletedRevoker = watcher.EnumerationCompleted(winrt::auto_revoke, completed); +} + +void RadioWatcher::Start(std::function on) +{ + radioStateChanged = on; + inEnumeration = true; + initialDone = false; + initialCount = 0; + watcher.Start(); +} + +IAsyncOperation RadioWatcher::GetRadios(std::set ids) +{ + Radio bluetooth = nullptr; + for (auto id : ids) + { + try + { + auto radio = co_await Radio::FromIdAsync(id); + if (radio && radio.Kind() == RadioKind::Bluetooth) + { + auto state = radio.State(); + // we only get state changes for turned on/off adapter but not for disabled adapter + if (state == RadioState::On || state == RadioState::Off) + { + bluetooth = radio; + } + } + } + catch (...) + { + // Radio::RadioFromAsync throws if the device is not available (unplugged) + } + } + return bluetooth; +} + +void RadioWatcher::OnRadioChanged() +{ + GetRadios(radioIds).Completed([=](auto&& asyncOp, auto&& status) { + if (status == AsyncStatus::Completed) + { + Radio radio = asyncOp.GetResults(); + // !radio: to handle if there is no radio + if (!radio || radio != mRadio) + { + if (radio) + { + mRadioStateChangedRevoker.revoke(); + mRadioStateChangedRevoker = radio.StateChanged(winrt::auto_revoke, [=](Radio radio, auto&&) { radioStateChanged(radio); }); + } + else + { + mRadioStateChangedRevoker.revoke(); + } + radioStateChanged(radio); + mRadio = radio; + } + } + else { + mRadio = nullptr; + mRadioStateChangedRevoker.revoke(); + radioStateChanged(mRadio); + } + }); +} + +void RadioWatcher::OnAdded(DeviceWatcher watcher, DeviceInformation info) +{ + radioIds.insert(info.Id()); + if (!inEnumeration) + { + OnRadioChanged(); + } +} + +void RadioWatcher::OnUpdated(DeviceWatcher watcher, DeviceInformationUpdate info) +{ + OnRadioChanged(); +} + +void RadioWatcher::OnRemoved(DeviceWatcher watcher, DeviceInformationUpdate info) +{ + radioIds.erase(info.Id()); + OnRadioChanged(); +} + +void RadioWatcher::OnCompleted(DeviceWatcher watcher, IInspectable info) +{ + inEnumeration = false; + OnRadioChanged(); +} diff --git a/lib/winrt/src/radio_watcher.h b/lib/winrt/src/radio_watcher.h new file mode 100644 index 000000000..39a55329c --- /dev/null +++ b/lib/winrt/src/radio_watcher.h @@ -0,0 +1,60 @@ +// +// radio_watcher.h +// noble-winrt-native +// +// Created by Georg Vienna on 07.09.18. +// + +#pragma once + +#include +#include +#include + +using namespace winrt::Windows::Devices::Enumeration; + +using winrt::Windows::Devices::Radios::Radio; +using winrt::Windows::Devices::Radios::IRadio; +using winrt::Windows::Devices::Radios::RadioState; +using winrt::Windows::Foundation::IAsyncOperation; +using winrt::Windows::Foundation::IInspectable; + +enum class AdapterState : int32_t +{ + Initial = -2, + Unsupported = -1, + Unknown = (int32_t)RadioState::Unknown, + On = (int32_t)RadioState::On, + Off = (int32_t)RadioState::Off, + Disabled = (int32_t)RadioState::Disabled, +}; + +class RadioWatcher +{ +public: + RadioWatcher(); + + void Start(std::function on); + +private: + IAsyncOperation GetRadios(std::set ids); + + void OnRadioChanged(); + void OnAdded(DeviceWatcher watcher, DeviceInformation info); + void OnUpdated(DeviceWatcher watcher, DeviceInformationUpdate info); + void OnRemoved(DeviceWatcher watcher, DeviceInformationUpdate info); + void OnCompleted(DeviceWatcher watcher, IInspectable info); + + DeviceWatcher watcher; + winrt::event_revoker mAddedRevoker; + winrt::event_revoker mUpdatedRevoker; + winrt::event_revoker mRemovedRevoker; + winrt::event_revoker mCompletedRevoker; + bool inEnumeration; + bool initialDone; + int initialCount; + std::set radioIds; + Radio mRadio; + winrt::event_revoker mRadioStateChangedRevoker; + std::function radioStateChanged; +}; diff --git a/lib/winrt/src/winrt_cpp.cc b/lib/winrt/src/winrt_cpp.cc new file mode 100644 index 000000000..505110caf --- /dev/null +++ b/lib/winrt/src/winrt_cpp.cc @@ -0,0 +1,82 @@ +#include "winrt_cpp.h" + +#include +#include + +#include + +std::string ws2s(const wchar_t* wstr) +{ + return winrt::to_string(wstr); +} + +std::string formatBluetoothAddress(unsigned long long BluetoothAddress) +{ + std::ostringstream ret; + ret << std::hex << std::setfill('0') << std::setw(2) << ((BluetoothAddress >> (5 * 8)) & 0xff) + << ":" << std::setw(2) << ((BluetoothAddress >> (4 * 8)) & 0xff) << ":" << std::setw(2) + << ((BluetoothAddress >> (3 * 8)) & 0xff) << ":" << std::setw(2) + << ((BluetoothAddress >> (2 * 8)) & 0xff) << ":" << std::setw(2) + << ((BluetoothAddress >> (1 * 8)) & 0xff) << ":" << std::setw(2) + << ((BluetoothAddress >> (0 * 8)) & 0xff); + return ret.str(); +} + +std::string formatBluetoothUuid(unsigned long long BluetoothAddress) +{ + std::ostringstream ret; + ret << std::hex << std::setfill('0') << std::setw(2) << ((BluetoothAddress >> (5 * 8)) & 0xff) + << std::setw(2) << ((BluetoothAddress >> (4 * 8)) & 0xff) << std::setw(2) + << ((BluetoothAddress >> (3 * 8)) & 0xff) << std::setw(2) + << ((BluetoothAddress >> (2 * 8)) & 0xff) << std::setw(2) + << ((BluetoothAddress >> (1 * 8)) & 0xff) << std::setw(2) + << ((BluetoothAddress >> (0 * 8)) & 0xff); + return ret.str(); +} + +std::string toStr(winrt::guid uuid) +{ + try + { + auto ref = winrt::Windows::Devices::Bluetooth::BluetoothUuidHelper::TryGetShortId(uuid); + if (ref) + { + auto i = ref.Value(); + std::ostringstream ret; + ret << std::hex << i; + return ret.str(); + } + } + catch (...) + { + } + + // taken from winrt/base.h + char buffer[38]; + // 00000000-0000-0000-0000-000000000000 + sprintf_s(buffer, "%08x-%04hx-%04hx-%02hhx%02hhx-%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx", + uuid.Data1, uuid.Data2, uuid.Data3, uuid.Data4[0], uuid.Data4[1], + uuid.Data4[2], uuid.Data4[3], uuid.Data4[4], uuid.Data4[5], uuid.Data4[6], uuid.Data4[7]); + return std::string(buffer); +} + +#define SET_VAL(prop, val, str) \ + if ((prop & val) == val) \ + { \ + arr.push_back(str); \ + } + +std::vector toPropertyArray(GattCharacteristicProperties& properties) +{ + std::vector arr; + SET_VAL(properties, GattCharacteristicProperties::Broadcast, "broadcast") + SET_VAL(properties, GattCharacteristicProperties::Read, "read") + SET_VAL(properties, GattCharacteristicProperties::WriteWithoutResponse, "writeWithoutResponse") + SET_VAL(properties, GattCharacteristicProperties::Write, "write") + SET_VAL(properties, GattCharacteristicProperties::Notify, "notify") + SET_VAL(properties, GattCharacteristicProperties::Indicate, "indicate") + SET_VAL(properties, GattCharacteristicProperties::AuthenticatedSignedWrites, + "authenticatedSignedWrites") + SET_VAL(properties, GattCharacteristicProperties::ExtendedProperties, "extendedProperties") + return arr; +} diff --git a/lib/winrt/src/winrt_cpp.h b/lib/winrt/src/winrt_cpp.h new file mode 100644 index 000000000..d62dcadc5 --- /dev/null +++ b/lib/winrt/src/winrt_cpp.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +using winrt::Windows::Devices::Bluetooth::GenericAttributeProfile::GattCharacteristicProperties; + +std::string ws2s(const wchar_t* wstr); +std::string formatBluetoothAddress(unsigned long long BluetoothAddress); +std::string formatBluetoothUuid(unsigned long long BluetoothAddress); +std::string toStr(winrt::guid uuid); +std::vector toPropertyArray(GattCharacteristicProperties& properties); diff --git a/lib/winrt/src/winrt_guid.cc b/lib/winrt/src/winrt_guid.cc new file mode 100644 index 000000000..41e7b644b --- /dev/null +++ b/lib/winrt/src/winrt_guid.cc @@ -0,0 +1,10 @@ +#include "winrt_guid.h" + +namespace std +{ + std::size_t hash::operator()(const winrt::guid& k) const + { + auto str = winrt::to_hstring(k).c_str(); + return std::hash()(str); + } +} diff --git a/lib/winrt/src/winrt_guid.h b/lib/winrt/src/winrt_guid.h new file mode 100644 index 000000000..4a3b76aef --- /dev/null +++ b/lib/winrt/src/winrt_guid.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +namespace std +{ + template <> struct hash + { + std::size_t operator()(const winrt::guid& k) const; + }; +} diff --git a/package.json b/package.json index 905bc5fbf..162b4b7c6 100644 --- a/package.json +++ b/package.json @@ -7,13 +7,13 @@ "license": "MIT", "name": "noble", "description": "A Node.js BLE (Bluetooth Low Energy) central library.", - "version": "1.9.1", + "version": "2.0.8", "repository": { "type": "git", - "url": "https://github.com/sandeepmistry/noble.git" + "url": "https://github.com/Timeular/noble.git" }, "bugs": { - "url": "https://github.com/sandeepmistry/noble/issues" + "url": "https://github.com/Timeular/noble/issues" }, "keywords": [ "bluetooth", @@ -33,12 +33,18 @@ "win32" ], "dependencies": { - "debug": "~2.2.0" + "debug": "~2.2.0", + "napi-thread-safe-callback": "0.0.6", + "node-addon-api": "^1.1.0", + "prebuild-install": "^5.0.0", + "bindings": "~1.3.0" }, "optionalDependencies": { "bluetooth-hci-socket": "^0.5.1", - "bplist-parser": "0.0.6", - "xpc-connection": "~0.1.4" + "usb": "git+https://github.com/Timeular/nobe-usb#61cbd0ec54b0d1572428609d319034912c329a23" + }, + "resolutions": { + "bluetooth-hci-socket/usb": "git+https://github.com/Timeular/nobe-usb#61cbd0ec54b0d1572428609d319034912c329a23" }, "devDependencies": { "jshint": "latest", @@ -50,7 +56,10 @@ }, "scripts": { "pretest": "jshint *.js lib/. test/.", - "test": "mocha -R spec test/*.js" + "test": "mocha -R spec test/*.js", + "install": "prebuild-install --force || node-gyp rebuild", + "ci": "node --napi-modules ./test/test-ci.js", + "build:source": "node-gyp rebuild" }, "browser": { "./lib/resolve-bindings.js": "./lib/resolve-bindings-web.js" diff --git a/test/test-ci.js b/test/test-ci.js new file mode 100644 index 000000000..edf844528 --- /dev/null +++ b/test/test-ci.js @@ -0,0 +1,2 @@ +// require bindings they should be build +const noble = require('../index'); diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 000000000..bdd867a21 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1128 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ajv@^5.3.0: + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" + dependencies: + co "^4.6.0" + fast-deep-equal "^1.0.0" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.3.0" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + +async@~0.2.9: + version "0.2.10" + resolved "http://registry.npmjs.org/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + +async@~1.0.0: + version "1.0.0" + resolved "http://registry.npmjs.org/async/-/async-1.0.0.tgz#f8fc04ca3a13784ade9e1641af98578cfbd647a9" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + +aws4@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + dependencies: + tweetnacl "^0.14.3" + +bindings@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7" + +bl@^1.0.0: + version "1.2.2" + resolved "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +bluetooth-hci-socket@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.1.tgz#efbe21524fc1cf5d3fae5d51365d561d4abbed0b" + dependencies: + debug "^2.2.0" + nan "^2.0.5" + optionalDependencies: + usb "^1.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + +buster-core@=0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/buster-core/-/buster-core-0.6.4.tgz#27bf6bad674244ea720f311d900a0ca1cb786050" + +buster-format@~0.5: + version "0.5.6" + resolved "https://registry.yarnpkg.com/buster-format/-/buster-format-0.5.6.tgz#2b86c322ecf5e1b0ae6e6e7905ebfcf387d2ab95" + dependencies: + buster-core "=0.6.4" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + +chownr@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" + +cli@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14" + dependencies: + exit "0.1.2" + glob "^7.1.1" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +colors@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.7" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" + dependencies: + delayed-stream "~1.0.0" + +commander@0.6.1: + version "0.6.1" + resolved "http://registry.npmjs.org/commander/-/commander-0.6.1.tgz#fa68a14f6a945d54dbbe50d8cdb3320e9e3b1a06" + +commander@~2.1.0: + version "2.1.0" + resolved "http://registry.npmjs.org/commander/-/commander-2.1.0.tgz#d121bbae860d9992a3d517ba96f56588e47c6781" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +concat-stream@1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +console-browserify@1.1.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10" + dependencies: + date-now "^0.1.4" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +cycle@1.0.x: + version "1.0.3" + resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + dependencies: + assert-plus "^1.0.0" + +date-now@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" + +debug@*: + version "4.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.0.tgz#373687bffa678b38b1cd91f861b63850035ddc87" + dependencies: + ms "^2.1.1" + +debug@2.6.9, debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +debug@~2.2.0: + version "2.2.0" + resolved "http://registry.npmjs.org/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" + dependencies: + ms "0.7.1" + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-3.3.0.tgz#80a4dd323748384bfa248083622aedec982adff3" + dependencies: + mimic-response "^1.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +diff@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-1.0.2.tgz#4ae73f1aee8d6fcf484f1a1ce77ce651d9b7f0c9" + +dom-serializer@0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" + dependencies: + domelementtype "~1.1.1" + entities "~1.1.1" + +domelementtype@1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.2.1.tgz#578558ef23befac043a1abb0db07635509393479" + +domelementtype@~1.1.1: + version "1.1.3" + resolved "http://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" + +domhandler@2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738" + dependencies: + domelementtype "1" + +domutils@1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + dependencies: + dom-serializer "0" + domelementtype "1" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +end-of-stream@^1.0.0, end-of-stream@^1.1.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" + dependencies: + once "^1.4.0" + +entities@1.0: + version "1.0.0" + resolved "http://registry.npmjs.org/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26" + +entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + +es6-promise@^4.0.3: + version "4.2.5" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" + +exit@0.1.2, exit@0.1.x: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + +expand-template@^1.0.2: + version "1.1.1" + resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-1.1.1.tgz#981f188c0c3a87d2e28f559bc541426ff94f21dd" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + +extract-zip@^1.6.5: + version "1.6.7" + resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9" + dependencies: + concat-stream "1.6.2" + debug "2.6.9" + mkdirp "0.5.1" + yauzl "2.4.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + +eyes@0.1.x: + version "0.1.8" + resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0" + +fast-deep-equal@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + +fd-slicer@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65" + dependencies: + pend "~1.2.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + +fs-extra@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-1.0.0.tgz#cd3ce5f7e7cb6145883fcae3191e9877f8587950" + dependencies: + graceful-fs "^4.1.2" + jsonfile "^2.1.0" + klaw "^1.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + dependencies: + assert-plus "^1.0.0" + +github-from-package@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce" + +glob@^7.1.1: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +growl@1.7.x: + version "1.7.0" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.7.0.tgz#de2d66136d002e112ba70f3f10c31cf7c350b2da" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + +har-validator@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.0.tgz#44657f5688a22cfd4b72486e81b3a3fb11742c29" + dependencies: + ajv "^5.3.0" + har-schema "^2.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +hasha@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hasha/-/hasha-2.2.0.tgz#78d7cbfc1e6d66303fe79837365984517b2f6ee1" + dependencies: + is-stream "^1.0.1" + pinkie-promise "^2.0.0" + +htmlparser2@3.8.x: + version "3.8.3" + resolved "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068" + dependencies: + domelementtype "1" + domhandler "2.3" + domutils "1.5" + entities "1.0" + readable-stream "1.1" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-stream@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + +isstream@0.1.x, isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + +jade@0.26.3: + version "0.26.3" + resolved "https://registry.yarnpkg.com/jade/-/jade-0.26.3.tgz#8f10d7977d8d79f2f6ff862a81b0513ccb25686c" + dependencies: + commander "0.6.1" + mkdirp "0.3.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + +jshint@latest: + version "2.9.6" + resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.6.tgz#19b34e578095a34928fe006135a6cb70137b9c08" + dependencies: + cli "~1.0.0" + console-browserify "1.1.x" + exit "0.1.x" + htmlparser2 "3.8.x" + lodash "~4.17.10" + minimatch "~3.0.2" + shelljs "0.3.x" + strip-json-comments "1.0.x" + unicode-5.2.0 "^0.7.5" + optionalDependencies: + phantom "~4.0.1" + phantomjs-prebuilt "~2.1.7" + +json-schema-traverse@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz#349a6d44c53a51de89b40805c5d5e59b417d3340" + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + +jsonfile@^2.1.0: + version "2.4.0" + resolved "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" + optionalDependencies: + graceful-fs "^4.1.6" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kew@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" + +klaw@^1.0.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/klaw/-/klaw-1.3.1.tgz#4088433b46b3b1ba259d78785d8e96f73ba02439" + optionalDependencies: + graceful-fs "^4.1.9" + +lodash@~4.17.10: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + +mime-db@~1.37.0: + version "1.37.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.21" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" + dependencies: + mime-db "~1.37.0" + +mimic-response@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" + +minimatch@^3.0.4, minimatch@~3.0.2: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +mkdirp@0.3.0: + version "0.3.0" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" + +mkdirp@0.3.3: + version "0.3.3" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.3.3.tgz#595e251c1370c3a68bab2136d0e348b8105adf13" + +mkdirp@0.5.1, mkdirp@^0.5.1: + version "0.5.1" + resolved "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +mocha@~1.8.2: + version "1.8.2" + resolved "http://registry.npmjs.org/mocha/-/mocha-1.8.2.tgz#fb6b1d07d98f2eba418546844c3be0731f75e390" + dependencies: + commander "0.6.1" + debug "*" + diff "1.0.2" + growl "1.7.x" + jade "0.26.3" + mkdirp "0.3.3" + ms "0.3.0" + +ms@0.3.0: + version "0.3.0" + resolved "http://registry.npmjs.org/ms/-/ms-0.3.0.tgz#03edc348d613e66a56486cfdac53bcbe899cbd61" + +ms@0.7.1: + version "0.7.1" + resolved "http://registry.npmjs.org/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +ms@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + +nan@^2.0.5, nan@^2.4.0: + version "2.11.1" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766" + +nan@~1.0.0: + version "1.0.0" + resolved "http://registry.npmjs.org/nan/-/nan-1.0.0.tgz#ae24f8850818d662fcab5acf7f3b95bfaa2ccf38" + +napi-build-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.1.tgz#1381a0f92c39d66bf19852e7873432fc2123e508" + +napi-thread-safe-callback@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/napi-thread-safe-callback/-/napi-thread-safe-callback-0.0.6.tgz#ef86a149b5312e480f74e89a614e6d9e3b17b456" + +node-abi@^2.2.0: + version "2.4.5" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-2.4.5.tgz#1fd1fb66641bf3c4dcf55a5490ba10c467ead80c" + dependencies: + semver "^5.4.1" + +node-addon-api@^1.1.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.5.0.tgz#55be6b3da36e746f4b1f2af16c2adf67647d1ff8" + +noop-logger@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/noop-logger/-/noop-logger-0.1.1.tgz#94a2b1633c4f1317553007d8966fd0e841b6a4c2" + +npmlog@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +options@>=0.0.5: + version "0.0.6" + resolved "https://registry.yarnpkg.com/options/-/options-0.0.6.tgz#ec22d312806bb53e731773e7cdaefcf1c643128f" + +os-homedir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + +phantom@~4.0.1: + version "4.0.12" + resolved "https://registry.yarnpkg.com/phantom/-/phantom-4.0.12.tgz#78d18cf3f2a76fea4909f6160fcabf2742d7dbf0" + dependencies: + phantomjs-prebuilt "^2.1.16" + split "^1.0.1" + winston "^2.4.0" + +phantomjs-prebuilt@^2.1.16, phantomjs-prebuilt@~2.1.7: + version "2.1.16" + resolved "https://registry.yarnpkg.com/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz#efd212a4a3966d3647684ea8ba788549be2aefef" + dependencies: + es6-promise "^4.0.3" + extract-zip "^1.6.5" + fs-extra "^1.0.0" + hasha "^2.2.0" + kew "^0.7.0" + progress "^1.1.8" + request "^2.81.0" + request-progress "^2.0.1" + which "^1.2.10" + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + +prebuild-install@^5.0.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-5.2.1.tgz#87ba8cf17c65360a75eefeb3519e87973bf9791d" + dependencies: + detect-libc "^1.0.3" + expand-template "^1.0.2" + github-from-package "0.0.0" + minimist "^1.2.0" + mkdirp "^0.5.1" + napi-build-utils "^1.0.1" + node-abi "^2.2.0" + noop-logger "^0.1.1" + npmlog "^4.0.1" + os-homedir "^1.0.1" + pump "^2.0.1" + rc "^1.2.7" + simple-get "^2.7.0" + tar-fs "^1.13.0" + tunnel-agent "^0.6.0" + which-pm-runs "^1.0.0" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +progress@^1.1.8: + version "1.1.8" + resolved "http://registry.npmjs.org/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" + +psl@^1.1.24: + version "1.1.29" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67" + +pump@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pump/-/pump-1.0.3.tgz#5dfe8311c33bbf6fc18261f9f34702c47c08a954" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +pump@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@1.1: + version "1.1.13" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.5: + version "2.3.6" + resolved "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +request-progress@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/request-progress/-/request-progress-2.0.1.tgz#5d36bb57961c673aa5b788dbc8141fdf23b44e08" + dependencies: + throttleit "^1.0.0" + +request@^2.81.0: + version "2.88.0" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.0" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.4.3" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +semver@^5.4.1: + version "5.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +shelljs@0.3.x: + version "0.3.0" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1" + +should@~1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/should/-/should-1.2.2.tgz#0f03f775066d9ea2632690c917b12824fcc1d582" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +simple-concat@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.0.tgz#7344cbb8b6e26fb27d66b2fc86f9f6d5997521c6" + +simple-get@^2.7.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-2.8.1.tgz#0e22e91d4575d87620620bc91308d57a77f44b5d" + dependencies: + decompress-response "^3.3.0" + once "^1.3.1" + simple-concat "^1.0.0" + +sinon@~1.6.0: + version "1.6.0" + resolved "http://registry.npmjs.org/sinon/-/sinon-1.6.0.tgz#577b017d86943b8c42537d5ac2e863730f58cc9b" + dependencies: + buster-format "~0.5" + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + dependencies: + through "2" + +sshpk@^1.7.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.15.1.tgz#b79a089a732e346c6e0714830f36285cd38191a2" + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "http://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +tar-fs@^1.13.0: + version "1.16.3" + resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-1.16.3.tgz#966a628841da2c4010406a82167cbd5e0c72d509" + dependencies: + chownr "^1.0.1" + mkdirp "^0.5.1" + pump "^1.0.0" + tar-stream "^1.1.2" + +tar-stream@^1.1.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + dependencies: + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" + +throttleit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" + +through@2: + version "2.3.8" + resolved "http://registry.npmjs.org/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + +tinycolor@0.x: + version "0.0.1" + resolved "https://registry.yarnpkg.com/tinycolor/-/tinycolor-0.0.1.tgz#320b5a52d83abb5978d81a3e887d4aefb15a6164" + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + +tough-cookie@~2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" + dependencies: + psl "^1.1.24" + punycode "^1.4.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + +unicode-5.2.0@^0.7.5: + version "0.7.5" + resolved "https://registry.yarnpkg.com/unicode-5.2.0/-/unicode-5.2.0-0.7.5.tgz#e0df129431a28a95263d8c480fb5e9ab2b0973f0" + +usb@^1.1.0, "usb@git+https://github.com/Timeular/nobe-usb#61cbd0ec54b0d1572428609d319034912c329a23": + version "1.2.2" + uid "61cbd0ec54b0d1572428609d319034912c329a23" + resolved "git+https://github.com/Timeular/nobe-usb#61cbd0ec54b0d1572428609d319034912c329a23" + dependencies: + bindings "~1.3.0" + nan "^2.4.0" + prebuild-install "^5.0.0" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +uuid@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which-pm-runs@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/which-pm-runs/-/which-pm-runs-1.0.0.tgz#670b3afbc552e0b55df6b7780ca74615f23ad1cb" + +which@^1.2.10: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + dependencies: + isexe "^2.0.0" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + dependencies: + string-width "^1.0.2 || 2" + +winston@^2.4.0: + version "2.4.4" + resolved "https://registry.yarnpkg.com/winston/-/winston-2.4.4.tgz#a01e4d1d0a103cf4eada6fc1f886b3110d71c34b" + dependencies: + async "~1.0.0" + colors "1.0.x" + cycle "1.0.x" + eyes "0.1.x" + isstream "0.1.x" + stack-trace "0.0.x" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +ws@~0.4.31: + version "0.4.32" + resolved "http://registry.npmjs.org/ws/-/ws-0.4.32.tgz#787a6154414f3c99ed83c5772153b20feb0cec32" + dependencies: + commander "~2.1.0" + nan "~1.0.0" + options ">=0.0.5" + tinycolor "0.x" + +xtend@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + +yauzl@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" + dependencies: + fd-slicer "~1.0.1"