Skip to content

Commit

Permalink
added events for service and characteristics discovery (serviceDiscov…
Browse files Browse the repository at this point in the history
…ered, characteristicsDiscovered) that enables

caching of gatt information.
The cached objects can be used to initialize a device via addServices() and addCharacteristics(), which return an
array of Noble objects ready for connecting in the usual way.

examples/cache-gatt-discovery.js provides an example for collecting GATT data and persisting the information,
examples/cache-gatt-reconnect.js uses the data to connect to the device using the cached data.
  • Loading branch information
svogl authored and rzr committed Jan 15, 2020
1 parent 84967e5 commit 0d15d19
Show file tree
Hide file tree
Showing 6 changed files with 466 additions and 17 deletions.
162 changes: 162 additions & 0 deletions examples/cache-gatt-discovery.js
Original file line number Diff line number Diff line change
@@ -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')
}
})
}
})
}
146 changes: 146 additions & 0 deletions examples/cache-gatt-reconnect.js
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 35 additions & 0 deletions lib/hci-socket/bindings.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down Expand Up @@ -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];
Expand All @@ -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];
Expand All @@ -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];
Expand All @@ -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];
Expand Down
Loading

0 comments on commit 0d15d19

Please sign in to comment.