diff --git a/examples/cache-gatt-discovery.js b/examples/cache-gatt-discovery.js new file mode 100644 index 000000000..72a300b17 --- /dev/null +++ b/examples/cache-gatt-discovery.js @@ -0,0 +1,162 @@ +/** discover a device (here, the first one where the name was resolved), + * for the first device discover all services and characteristics, + * store the collected GATT information into a meta-data object and write to disk. + * Finds a temperature characteristic and registers for data. + * Prints timing information from discovered to connected to reading states. + */ + +var noble = require('../index'); +const fs = require('fs'); + +// the sensor value to scan for, number of bits and factor for displaying it +const CHANNEL = process.env['CHANNEL'] ? process.env['CHANNEL'] : 'Temperature' +const BITS = process.env['BITS'] ? 1 * process.env['BITS'] : 16 +const FACTOR = process.env['FACTOR'] ? 1. * process.env['FACTOR'] : .1 + +const EXT='.dump' + +noble.on('stateChange', function(state) { + if (state === 'poweredOn') { + noble.startScanning(); + } else { + noble.stopScanning(); + } +}); + +let tDisco=0; // time when device was discovered +let tConn =0; // time when connection to device was established +let tRead =0; // time when reading data starts. + +// collect device meta-data into this object: +let meta = { + services: [], // stores an array of GATT service data objects + characteristics: {} // a map with key service-UUID, stores the array of characteristics +} + +noble.on('discover', function(peripheral) { + console.log('peripheral discovered (' + peripheral.id + + ' with address <' + peripheral.address + ', ' + peripheral.addressType + '>,' + + ' connectable ' + peripheral.connectable + ',' + + ' RSSI ' + peripheral.rssi + ':'); + console.log('\thello my local name is:'); + console.log('\t\t' + peripheral.advertisement.localName); + console.log(); + + // connect to the first device with a valid name + if (peripheral.advertisement.localName) { + console.log('Connecting to ' + peripheral.address + ' ' + peripheral.advertisement.localName) + + tDisco = Date.now() + + connectToDevice(peripheral) + } +}); + +let connectToDevice = function (peripheral) { + // BLE cannot scan and connect in parallel, so we stop scanning here: + noble.stopScanning() + + peripheral.connect((error) => { + // noble.startScanning([], true) + if (error) { + console.log('Connect error: ' + error) + noble.startScanning([], true) + return + } + tConn = Date.now() + console.log('Connected!') + + findServices(noble, peripheral) + }) +} + + +let servicesToRead = 0; + +let findServices = function (noble, peripheral) { + meta.uuid = peripheral.uuid + meta.address = peripheral.address + meta.name = peripheral.advertisement.localName // not needed but nice to have + + meta.characteristics = {} + + // callback triggers with GATT-relevant data + peripheral.on('servicesDiscovered', (peripheral, services) => { + + console.log('servicesDiscovered: Found '+ services.length + ' services! ') + meta.services = services + for (let i in services) { + const service = services[i] + console.log('\tservice ' + i + ' : ' + JSON.stringify(service)) + //meta.services[ service.uuid ] = service + } + }) + + peripheral.discoverServices([], (error, services) => { + + let sensorCharacteristic + + servicesToRead = services.length + // we found the list of services, now trigger characteristics lookup for each of them: + + for (let i = 0; i < services.length; i++) { + let service = services[i] + + service.on('characteristicsDiscovered', (characteristics) => { + // store the list of characteristics per service + meta.characteristics[service.uuid] = characteristics + + console.log('SRV\t' + service.uuid + ' characteristic GATT data: ') + for (let i = 0; i < characteristics.length; i++) { + console.log('\t' + service.uuid + ' chara.\t ' + ' ' + i + ' ' + JSON.stringify(characteristics[i])) + } + }) + + service.discoverCharacteristics([], function (error, characteristics) { + console.log('SRV\t' + service.uuid + ' characteristic decoded data: ' ) + for (let j = 0; j< characteristics.length; j++) { + let ch = characteristics[j] + console.log('\t' + service.uuid + ' chara.\t ' + ' ' + j + ' ' + ch) + + if ( ch.name === CHANNEL) { + console.log('found ' + CHANNEL + ' characteristic!') + sensorCharacteristic = ch + } + } + + servicesToRead-- + if (!servicesToRead) { + console.log('----------------- FINISHED') + console.log(JSON.stringify(meta, null, 4)) + // write to file + fs.writeFile(meta.uuid + EXT, JSON.stringify(meta,null,2), function(err) { + if(err) { + return console.log(err); + } + console.log("The data was saved to " , meta.uuid + EXT); + }); + + if (sensorCharacteristic) { + console.log('Listening for temperature data...') + + tRead = Date.now() + + sensorCharacteristic.on('data', (data) => { + if (BITS === 16 ) { + console.log(' new ' + CHANNEL + ' ' + (data.readUInt16LE() * FACTOR) ) + } else if (BITS === 32) { + console.log(' new ' + CHANNEL + ' ' + (data.readUInt32LE() * FACTOR) ) + } else { + console.log(' Cannot cope with BITS value '+ BITS) + } + }) + sensorCharacteristic.read() + } + + console.log('Timespan from discovery to connected: ' + (tConn -tDisco) + ' ms') + console.log('Timespan from connected to reading : ' + (tRead -tConn) + ' ms') + } + }) + } + }) +} diff --git a/examples/cache-gatt-reconnect.js b/examples/cache-gatt-reconnect.js new file mode 100644 index 000000000..7ca8d0736 --- /dev/null +++ b/examples/cache-gatt-reconnect.js @@ -0,0 +1,146 @@ +/** reconnect to a device that has been discovered earlier on using cache-gatt-discovery: + * If a device is discovered and a dump file exists, load it and connect to it, re-initializing service + * and characteristic objects in the noble stack. + * Finds a temperature characteristic and registers for data. + * Prints timing information from discovered to connected to reading states. + */ + +var noble = require('../index'); +const fs = require('fs'); + +// the sensor value to scan for, number of bits and factor for displaying it +const CHANNEL = process.env['CHANNEL'] ? process.env['CHANNEL'] : 'Temperature' +const BITS = process.env['BITS'] ? 1 * process.env['BITS'] : 16 +const FACTOR = process.env['FACTOR'] ? 1. * process.env['FACTOR'] : .1 + +const EXT='.dump' + +noble.on('stateChange', function(state) { + if (state === 'poweredOn') { + noble.startScanning(); + } else { + noble.stopScanning(); + } +}); + +let tDisco=0; // time when device was discovered +let tConn =0; // time when connection to device was established +let tRead =0; // time when reading data starts. + +// collect device meta-data into this object: +let meta = { + services: {}, // a map indexted by service-UUID -> contains service data + characteristics: {} // an map with key service-UUID, stores the array of characteristics +} + +noble.on('discover', function(peripheral) { + console.log('peripheral discovered (' + peripheral.id + + ' with address <' + peripheral.address + ', ' + peripheral.addressType + '>,' + + ' connectable ' + peripheral.connectable + ',' + + ' RSSI ' + peripheral.rssi + ':'); + console.log('\thello my local name is:'); + console.log('\t\t' + peripheral.advertisement.localName); + console.log(); + + + // Check if a dump exists in the current directory. + fs.access(peripheral.uuid + EXT, fs.constants.F_OK, (err) => { + if (!err) { + console.log('found dump file for ' + peripheral.uuid ) + + tDisco=Date.now() + + quickConnect(peripheral) + } + }); +}); + + +let quickConnect = function (peripheral) { + // BLE cannot scan and connect in parallel, so we stop scanning here: + noble.stopScanning() + + + peripheral.connect((error) => { + if (error) { + console.log('Connect error: ' + error) + noble.startScanning([], true) + return + } + tConn = Date.now() + console.log('Connected!') + + // load stored data. This needs to be done when connected, as we need a handle at GATT level + meta = loadData(peripheral) + + // initialize the service and charateristics objects in Noble; return a temperature characteristic, if found + let sensorCharacteristic = setData(peripheral, meta) + + if (!sensorCharacteristic) { + console.log('Warning - no temperature characteristic found.') + } else { + console.log('Listening for temperature data...') + + tRead = Date.now() + + sensorCharacteristic.on('data', (data) => { + if (BITS === 16 ) { + console.log(' new ' + CHANNEL + ' ' + (data.readUInt16LE() * FACTOR) ) + } else if (BITS === 32) { + console.log(' new ' + CHANNEL + ' ' + (data.readUInt32LE() * FACTOR) ) + } else { + console.log(' Cannot cope with BITS value '+ BITS) + } + }) + sensorCharacteristic.read() + + console.log('Timespan from discovery to connected: ' + (tConn -tDisco) + ' ms') + console.log('Timespan from connected to reading : ' + (tRead -tConn) + ' ms') + } + }) +} + +let loadData = function(peripheral) { + const dump = fs.readFileSync(peripheral.uuid + EXT) + const data = JSON.parse(dump) + + // verify data: console.log(JSON.stringify(data,null,2)) + return data +} + +let setData = function(peripheral, meta) { + // first, create the service objects: + console.log('initializing services... ') + + // addServices returns an array of initialized service objects + let services = noble.addServices(peripheral.uuid, meta.services) + + console.log('initialized services: ') + for (let i in services) { + const service = services[i] + console.log('\tservice ' + i + ' ' + service) + } + console.log() + + let sensorCharacteristic + + console.log('initializing characteristics... ') + // now, for each service, set the characteristics: + for (let i in services) { + const service = services[i] + const charas = meta.characteristics[service.uuid] + console.log('\tservice ' + i + ' ' + service + ' ' + JSON.stringify(charas)) + + let characteristics = noble.addCharacteristics(peripheral.uuid, service.uuid, charas) + + for (let j in characteristics) { + let characteristic = characteristics[j] + console.log('\t\tcharac ' + service.uuid + ' ' + j + ' ' + characteristic + ' ' + characteristic.rawProps) + if (characteristic.name === CHANNEL) { + console.log('\t\t\t-->found ' + CHANNEL + ' characteristic!') + sensorCharacteristic = characteristic + } + } + } + return sensorCharacteristic +} diff --git a/lib/hci-socket/bindings.js b/lib/hci-socket/bindings.js index c4436a440..94e5cca30 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -195,8 +195,10 @@ NobleBindings.prototype.onLeConnComplete = function(status, handle, role, addres this._gatts[handle].on('mtu', this.onMtu.bind(this)); this._gatts[handle].on('servicesDiscover', this.onServicesDiscovered.bind(this)); + this._gatts[handle].on('servicesDiscovered', this.onServicesDiscoveredEX.bind(this)); this._gatts[handle].on('includedServicesDiscover', this.onIncludedServicesDiscovered.bind(this)); this._gatts[handle].on('characteristicsDiscover', this.onCharacteristicsDiscovered.bind(this)); + this._gatts[handle].on('characteristicsDiscovered', this.onCharacteristicsDiscoveredEX.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)); @@ -287,6 +289,16 @@ NobleBindings.prototype.onAclDataPkt = function(handle, cid, data) { } }; +NobleBindings.prototype.addService = function(peripheralUuid, service) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.addService(service); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; NobleBindings.prototype.discoverServices = function(peripheralUuid, uuids) { var handle = this._handles[peripheralUuid]; @@ -305,6 +317,12 @@ NobleBindings.prototype.onServicesDiscovered = function(address, serviceUuids) { this.emit('servicesDiscover', uuid, serviceUuids); }; +NobleBindings.prototype.onServicesDiscoveredEX = function(address, services) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('servicesDiscovered', uuid, services); +}; + NobleBindings.prototype.discoverIncludedServices = function(peripheralUuid, serviceUuid, serviceUuids) { var handle = this._handles[peripheralUuid]; var gatt = this._gatts[handle]; @@ -322,6 +340,17 @@ NobleBindings.prototype.onIncludedServicesDiscovered = function(address, service this.emit('includedServicesDiscover', uuid, serviceUuid, includedServiceUuids); }; +NobleBindings.prototype.addCharacteristics = function(peripheralUuid, serviceUuid, characteristics) { + var handle = this._handles[peripheralUuid]; + var gatt = this._gatts[handle]; + + if (gatt) { + gatt.addCharacteristics(serviceUuid, characteristics); + } else { + console.warn('noble warning: unknown peripheral ' + peripheralUuid); + } +}; + NobleBindings.prototype.discoverCharacteristics = function(peripheralUuid, serviceUuid, characteristicUuids) { var handle = this._handles[peripheralUuid]; var gatt = this._gatts[handle]; @@ -339,6 +368,12 @@ NobleBindings.prototype.onCharacteristicsDiscovered = function(address, serviceU this.emit('characteristicsDiscover', uuid, serviceUuid, characteristics); }; +NobleBindings.prototype.onCharacteristicsDiscoveredEX = function(address, serviceUuid, characteristics) { + var uuid = address.split(':').join('').toLowerCase(); + + this.emit('characteristicsDiscovered', uuid, serviceUuid, characteristics); +}; + NobleBindings.prototype.read = function(peripheralUuid, serviceUuid, characteristicUuid) { var handle = this._handles[peripheralUuid]; var gatt = this._gatts[handle]; diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index d04e53aed..c13cdb2f9 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -329,6 +329,10 @@ Gatt.prototype.exchangeMtu = function(mtu) { }.bind(this)); }; +Gatt.prototype.addService = function(service) { + this._services[service.uuid] = service; +}; + Gatt.prototype.discoverServices = function(uuids) { var services = []; @@ -358,6 +362,7 @@ Gatt.prototype.discoverServices = function(uuids) { this._services[services[i].uuid] = services[i]; } + this.emit('servicesDiscovered', this._address, JSON.parse(JSON.stringify(services)) /*services*/); this.emit('servicesDiscover', this._address, serviceUuids); } else { this._queueCommand(this.readByGroupRequest(services[services.length - 1].endHandle + 1, 0xffff, GATT_PRIM_SVC_UUID), callback); @@ -406,6 +411,17 @@ Gatt.prototype.discoverIncludedServices = function(serviceUuid, uuids) { this._queueCommand(this.readByTypeRequest(service.startHandle, service.endHandle, GATT_INCLUDE_UUID), callback); }; +Gatt.prototype.addCharacteristics = function (serviceUuid, characteristics) { + var service = this._services[serviceUuid]; + this._characteristics[serviceUuid] = this._characteristics[serviceUuid] || {}; + this._descriptors[serviceUuid] = this._descriptors[serviceUuid] || {}; + + + for (i = 0; i < characteristics.length; i++) { + this._characteristics[serviceUuid][characteristics[i].uuid] = characteristics[i]; + } +}; + Gatt.prototype.discoverCharacteristics = function(serviceUuid, characteristicUuids) { var service = this._services[serviceUuid]; var characteristics = []; @@ -442,6 +458,10 @@ Gatt.prototype.discoverCharacteristics = function(serviceUuid, characteristicUui uuid: characteristics[i].uuid }; + // work around name-clash of numeric vs. string-array properties field: + characteristics[i].propsDecoded = characteristic.properties; + characteristics[i].rawProps = properties; + if (i !== 0) { characteristics[i - 1].endHandle = characteristics[i].startHandle - 1; } @@ -489,6 +509,7 @@ Gatt.prototype.discoverCharacteristics = function(serviceUuid, characteristicUui } } + this.emit('characteristicsDiscovered', this._address, serviceUuid, characteristics); this.emit('characteristicsDiscover', this._address, serviceUuid, characteristicsDiscovered); } else { this._queueCommand(this.readByTypeRequest(characteristics[characteristics.length - 1].valueHandle + 1, service.endHandle, GATT_CHARAC_UUID), callback); diff --git a/lib/noble.js b/lib/noble.js index 5a45e2c8e..8d83efdf7 100644 --- a/lib/noble.js +++ b/lib/noble.js @@ -29,8 +29,10 @@ function Noble(bindings) { this._bindings.on('disconnect', this.onDisconnect.bind(this)); this._bindings.on('rssiUpdate', this.onRssiUpdate.bind(this)); this._bindings.on('servicesDiscover', this.onServicesDiscover.bind(this)); + this._bindings.on('servicesDiscovered', this.onServicesDiscovered.bind(this)); this._bindings.on('includedServicesDiscover', this.onIncludedServicesDiscover.bind(this)); this._bindings.on('characteristicsDiscover', this.onCharacteristicsDiscover.bind(this)); + this._bindings.on('characteristicsDiscovered', this.onCharacteristicsDiscovered.bind(this)); this._bindings.on('read', this.onRead.bind(this)); this._bindings.on('write', this.onWrite.bind(this)); this._bindings.on('broadcast', this.onBroadcast.bind(this)); @@ -225,6 +227,47 @@ Noble.prototype.onRssiUpdate = function(peripheralUuid, rssi) { } }; +/// add an array of service objects (as retrieved via the servicesDiscovered event) +Noble.prototype.addServices = function (peripheralUuid, services) { + var servObjs = []; + + for (var i = 0; i < services.length; i++) { + var o = this.addService(peripheralUuid, services[i]); + servObjs.push(o); + } + return servObjs; +}; + +/// service is a ServiceObject { uuid, startHandle, endHandle,..} +Noble.prototype.addService = function (peripheralUuid, service) { + var peripheral = this._peripherals[peripheralUuid]; + + // pass on to lower layers (gatt) + this._bindings.addService(peripheralUuid, service); + + if (!peripheral.services) { + peripheral.services = []; + } + // allocate internal service object and return + var serv = new Service(this, peripheralUuid, service.uuid); + + this._services[peripheralUuid][service.uuid] = serv; + this._characteristics[peripheralUuid][service.uuid] = {}; + this._descriptors[peripheralUuid][service.uuid] = {}; + + peripheral.services.push(serv); + + return serv; +}; + +/// callback receiving a list of service objects from the gatt layer +Noble.prototype.onServicesDiscovered = function (peripheralUuid, services) { + var peripheral = this._peripherals[peripheralUuid]; + + if (peripheral) + peripheral.emit('servicesDiscovered', peripheral, services); // pass on to higher layers +}; + Noble.prototype.discoverServices = function(peripheralUuid, uuids) { this._bindings.discoverServices(peripheralUuid, uuids); }; @@ -270,6 +313,44 @@ Noble.prototype.onIncludedServicesDiscover = function(peripheralUuid, serviceUui } }; +/// add characteristics to the peripheral; returns an array of initialized Characteristics objects +Noble.prototype.addCharacteristics = function (peripheralUuid, serviceUuid, characteristics) { + // first, initialize gatt layer: + this._bindings.addCharacteristics(peripheralUuid, serviceUuid, characteristics); + + var service = this._services[peripheralUuid][serviceUuid]; + if (!service) { + this.emit('warning', 'unknown service ' + peripheralUuid + ', ' + serviceUuid + ' characteristics discover!'); + return; + } + + var characteristics_ = []; + for (var i = 0; i < characteristics.length; i++) { + var characteristicUuid = characteristics[i].uuid; + + var characteristic = new Characteristic( + this, + peripheralUuid, + serviceUuid, + characteristicUuid, + characteristics[i].properties + ); + + this._characteristics[peripheralUuid][serviceUuid][characteristicUuid] = characteristic; + this._descriptors[peripheralUuid][serviceUuid][characteristicUuid] = {}; + + characteristics_.push(characteristic); + } + service.characteristics = characteristics_; + return characteristics_; +}; + +Noble.prototype.onCharacteristicsDiscovered = function (peripheralUuid, serviceUuid, characteristics) { + var service = this._services[peripheralUuid][serviceUuid]; + + service.emit('characteristicsDiscovered', characteristics); +}; + Noble.prototype.discoverCharacteristics = function(peripheralUuid, serviceUuid, characteristicUuids) { this._bindings.discoverCharacteristics(peripheralUuid, serviceUuid, characteristicUuids); };