UNPKG

gattacker

Version:
1,081 lines (899 loc) 34 kB
require('env2')('config.env'); var events = require('events'); var debug = require('debug')('ws-slave'); var WebSocket = require('ws'); var async = require('async'); var util = require('util'); var colors = require('colors'); var noble = require('./lib/noble'); var utils = require('./lib/utils'); var port = 0xB1e; var ws; var wss; var peripherals = {}; var servicesCache=[]; var localMacAddress=''; var targetPeripheralId=''; var keepTargetConnected=false; var initialized=false; var connected=false; var isClientConnected=false; var eventEmitter = new events.EventEmitter(); console.log('GATTacker ws-slave'); wss = new WebSocket.Server({ port: port, perMessageDeflate: false }); wss.on('connection', function(ws_) { console.log('ws -> connection'); ws = ws_; sendEvent({ type: 'stateChange', state: noble.state }); ws.on('message', onMessage); ws.on('close', function() { console.log('ws -> close'); //stop keeping connection to target device if (targetPeripheralId) { deinitialize(targetPeripheralId, function(peripheralId){ debug('deinitialized: ' + peripheralId); }); } noble.stopScanning(); }); }); //handle raw notifications noble._bindings.on('handleNotify', onRawNotify); function sendEvent(event) { var message = JSON.stringify(event); console.log('ws -> send: '.blue + message); var clients = wss.clients; for (var i = 0; i < clients.length; i++) { clients[i].send(message); } } noble.on('scanStop', function(){ sendEvent({ type: "stopScanning" }) }); noble.on('scanStart', function(){ sendEvent({ type: "startScanning" }) }); //check local MAC address noble._bindings._hci.on('addressChange', function(address){ debug('Local MAC: ' + address); localMacAddress = address; }) var onMessage = function(message) { console.log('ws -> message: '.green + message); var command = JSON.parse(message); var action = command.action; var peripheralId = command.peripheralId; var serviceUuids = command.serviceUuids; var serviceUuid = command.serviceUuid; var characteristicUuids = command.characteristicUuids; var characteristicUuid = command.characteristicUuid; var data = command.data ? new Buffer(command.data, 'hex') : null; var withoutResponse = command.withoutResponse; var broadcast = command.broadcast; var notify = command.notify; var descriptorUuid = command.descriptorUuid; var handle = handle; var peripheral = peripherals[peripheralId]; var service = null; var characteristic = null; var descriptor = null; if (peripheral && serviceUuid) { var service = servicesCache[peripheralId].services[serviceUuid]; if (!service) { debug('service not found!'.red); } else { var characteristic = servicesCache[peripheralId].services[serviceUuid].characteristics[characteristicUuid]; if (!characteristic) { debug('-- Characteristic not found : '.red + characteristicUuid + '\nservice object:\n\n' + util.inspect( service, {showHidden: false, depth: 2, colorize: true})); } } } if (action === 'read') { readRaw(peripheralId, serviceUuid, characteristicUuid) } else if (action === 'write') { writeRaw(peripheralId, serviceUuid, characteristicUuid, data, withoutResponse); } else if (action === 'notify') { subscribeRaw(peripheralId, serviceUuid, characteristicUuid, notify); } else if (action === 'explore') { explore(peripheralId, command.readValues); } else if (action === 'initialize'){ if (command.servicesJsonData) { initialize(peripheralId, command.servicesJsonData, command.keepConnected); } else { initializeWithoutServices(peripheralId); } } else if (action === 'clientConnection') { clientConnection(command.clientAddress, command.state); } else if (action === 'macAddress') { sendEvent({ type: 'macAddress', macAddress: localMacAddress }); } else if (action === 'startScanning') { noble.startScanning(serviceUuids, command.allowDuplicates); } else if (action === 'stopScanning') { noble.stopScanning(); } else if (action === 'connect') { peripheral.connect(); } else if (action === 'disconnect') { peripheral.disconnect(); } else if (action === 'updateRssi') { peripheral.updateRssi(); } else if (action === 'discoverServices') { peripheral.discoverServices(command.uuids); } else if (action === 'discoverIncludedServices') { service.discoverIncludedServices(serviceUuids); } else if (action === 'discoverCharacteristics') { service.discoverCharacteristics(characteristicUuids); } else if (action === 'broadcast') { characteristic.broadcast(broadcast); } else if (action === 'discoverDescriptors') { characteristic.discoverDescriptors(); } else if (action === 'readValue') { descriptor.readValue(); } else if (action === 'writeValue') { descriptor.writeValue(data); } else if (action === 'readHandle') { peripheral.readHandle(handle); } else if (action === 'writeHandle') { peripheral.writeHandle(handle, data, withoutResponse); } }; function initializeWithoutServices(peripheralId) { //was previously initialized for other device if (targetPeripheralId && targetPeripheralId != peripheralId) { deinitialize(targetPeripheralId, function(){ console.log('re-initialize for other device '); }) } targetPeripheralId = peripheralId; sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'about to explore services' }) explore(peripheralId, false, function(servicesJson){ //give it a second to settle for reconnect after exploring, otherwise "unknown connection identifier" setTimeout(function() { peripherals[peripheralId].connect(function(error){ if (error) { debug('Connect error! '.red + error) sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'connect error' + error }) } else { sendEvent({ type: 'initialized', peripheralId: peripheralId }) } }) }, 1000); }) } function initialize(peripheralId, servicesJson, keepConnected) { debug('Initialize JSON services for ' + peripheralId) sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'JSON services received' }) //was previously initialized for other device if (targetPeripheralId && targetPeripheralId != peripheralId) { deinitialize(targetPeripheralId, function(){ console.log('re-initialize for other device '); }) } keepTargetConnected=keepConnected; targetPeripheralId = peripheralId; if (!servicesCache[peripheralId]) { servicesCache[peripheralId] = { services : [], handles : [] } } //parse JSON to servicesCache array, indexed by uuids for (sindex = 0; sindex < servicesJson.length; ++sindex) { service = servicesJson[sindex]; if (!servicesCache[peripheralId].services[service.uuid]) { servicesCache[peripheralId].services[service.uuid] = { characteristics : []}; } debug("Setting up service: " + service.uuid); for (cindex = 0; cindex<service.characteristics.length; ++cindex) { characteristic = service.characteristics[cindex]; debug(" Setting up Characteristic: " + characteristic.uuid); debug(" Handle: " + characteristic.valueHandle); debug(" Properties: " + characteristic.properties); servicesCache[peripheralId].services[service.uuid].characteristics[characteristic.uuid]= { handle: characteristic.valueHandle, properties : characteristic.properties, descriptors : [] }; //store handle association for quick raw notifications lookup servicesCache[peripheralId].handles[characteristic.valueHandle] = { serviceUuid : service.uuid, uuid : characteristic.uuid } if (characteristic.descriptors.length > 0) { debug(" Descriptors: "); for (dindex = 0; dindex < characteristic.descriptors.length; ++dindex) { descriptor = characteristic.descriptors[dindex]; debug(" " + descriptor.uuid + ' handle ' + descriptor.handle) servicesCache[peripheralId].services[service.uuid].characteristics[characteristic.uuid].descriptors[descriptor.uuid]=descriptor.handle; } } } } if (!peripherals[peripheralId]) { debug('Peripheral not yet discovered, start scanning...') sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'start scanning for target peripheral' }) noble.startScanning(); eventEmitter.once('targetDiscovered', function(){ noble.stopScanning(); sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'target peripheral discovered, trying to connect...' }) //connect to target peripheral peripherals[peripheralId].connect(function(error){ if (error) { sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'connect error' }); } else { sendEvent({ type: 'initialized', peripheralId: peripheralId }); } }) }) } else { //previously discovered sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'target peripheral previously discovered, trying to connect...' }) //connect to target peripheral peripherals[peripheralId].connect(function(error){ if (error) { sendEvent({ type: 'initializeStatus', peripheralId: peripheralId, status: 'connect error' }); } else { sendEvent({ type: 'initialized', peripheralId: peripheralId }); } }) } } //read by handle function readRaw(peripheralId, serviceUuid, uuid, callback){ //todo catch exceptions var handle = servicesCache[peripheralId].services[serviceUuid].characteristics[uuid].handle; var peripheral = peripherals[peripheralId]; //if not connected, connect checkConnected(peripheral, function(){ peripheral.readHandle(handle, function(error, data){ if (error) { debug('readHandle error : '.red + error) } debug('read handle data :' + data.toString('hex')); sendEvent({ type: 'read', peripheralId: peripheralId, serviceUuid: serviceUuid, characteristicUuid: uuid, data: data.toString('hex'), isNotification: false }); if (callback) { callback(error, data); } }); }) } //write by handle function writeRaw(peripheralId, serviceUuid, uuid, data, withoutResponse, callback){ //todo catch exceptions var handle = servicesCache[peripheralId].services[serviceUuid].characteristics[uuid].handle; var peripheral = peripherals[peripheralId]; //if not connected, connect checkConnected(peripheral, function(){ peripheral.writeHandle(handle, new Buffer(data,'hex'), withoutResponse, function(error){ if (error) { debug('Write handle error! '. red + error); } debug('write handle sent ' + peripheralId + ' : ' + serviceUuid + ' : ' + uuid ) sendEvent({ type: 'write', peripheralId: peripheralId, serviceUuid: serviceUuid, characteristicUuid: uuid }); }); }) } //raw subscription to indication/notification function subscribeRaw(peripheralId, serviceUuid, uuid, state, callback) { //todo catch exceptions var peripheral = peripherals[peripheralId]; var characteristic = servicesCache[peripheralId].services[serviceUuid].characteristics[uuid]; var handle = characteristic.descriptors['2902']; if (state == false) { value = '0000'; } else { if (characteristic.properties == 'indicate'){ value = '0200'; } else {// notify value = '0100'; } } //if not connected, connect checkConnected(peripheral, function(){ peripheral.writeHandle(handle, new Buffer(value, 'hex'), false, function(error){ if (error){ debug('Subscribe error '.red + peripheralId + ' : ' + serviceUuid + ' : ' + uuid ) } sendEvent({ type: 'notify', peripheralId: peripheralId, serviceUuid: serviceUuid, characteristicUuid: uuid, state: state }); }); }) checkRawNotificationListeners(); } //Debug function checkRawNotificationListeners() { listenersCount=noble._bindings.listeners('handleNotify').length; // debug('Raw notification listeners: ' + util.inspect(noble._bindings.listeners('handleNotify'))); if (!listenersCount) { noble._bindings.on('handleNotify', onRawNotify); } } function onRawNotify(peripheralId, handle, data){ //todo catch exceptions var serviceUuid = servicesCache[peripheralId].handles[handle].serviceUuid; var uuid = servicesCache[peripheralId].handles[handle].uuid; debug('Raw notify: ' + peripheralId + ' : ' + handle + ' : ' + data.toString('hex')); sendEvent({ type: 'read', peripheralId: peripheralId, serviceUuid: serviceUuid, characteristicUuid: uuid, data: data.toString('hex'), isNotification: true }); } function deinitialize(peripheralId, callback){ targetPeripheralId=''; initialized=false; if (peripherals[peripheralId]) { if (peripherals[peripheralId].connected) { debug('disconnecting peripheral'); peripherals[peripheralId].disconnect(); } } if (callback) { callback(peripheralId); } } function clientConnection(clientAddress, state) { var dis = ''; if (state != true) { dis='dis'; } console.log('client ' + dis + 'connected : ' + clientAddress); isClientConnected = state; // send confirmation event for reconnect eventEmitter.emit('clientConnection', clientAddress, state); } //check whether connected to peripheral, if not - reconnect, and then invoke peripheral function function checkConnected(peripheral, callback){ if (peripheral.state === 'connected') { debug(' - connected'); if (callback) { callback(); } } else if (peripheral.state === 'connecting'){ debug(' - connecting....'); //wait until the connection completes, invoke callback peripheral.once('connect', function(){ if (callback){ callback(); } }) } else { //not connected debug(' - not connected'); //if peripheral is lost by noble if (! noble._peripherals[peripheral.id]) { console.log('No such peripheral! This should not happen, restart manually...'); return } peripheral.connect( function(error) { if (error) { debug('checkconnected -> reconnect error !'.red + error) } else { debug('checkconnected -> reconnect'); } if (callback) { callback(error); }; }); } } noble.on('discover', function(peripheral) { var alreadyDiscovered=false; if (peripherals[peripheral.id]) { alreadyDiscovered=true; } peripherals[peripheral.id] = peripheral; debug('DISCOVER: '+ peripheral.id + ', target: ' + targetPeripheralId) if ((peripheral.id === targetPeripheralId)) { eventEmitter.emit('targetDiscovered'); } sendEvent({ type: 'discover', peripheralId: peripheral.id, address: peripheral.address, addressType: peripheral.addressType, connectable: peripheral.connectable, advertisement: { localName: peripheral.advertisement.localName, txPowerLevel: peripheral.advertisement.txPowerLevel, serviceUuids: peripheral.advertisement.serviceUuids, manufacturerData: (peripheral.advertisement.manufacturerData ? peripheral.advertisement.manufacturerData.toString('hex') : null), //todo convert object to array serviceData: (peripheral.advertisement.serviceData ? peripheral.advertisement.serviceData.toString('hex') : null), eir: (peripheral.advertisement.eir ? peripheral.advertisement.eir.toString('hex') : null), scanResponse: (peripheral.advertisement.scanResponse ? peripheral.advertisement.scanResponse.toString('hex') : null) }, rssi: peripheral.rssi }); if (alreadyDiscovered) { //return - we do not need to setup the event listeners again return } peripheral.on('connect', function() { sendEvent({ type: 'connect', peripheralId: this.uuid }); }); peripheral.on('disconnect', function() { sendEvent({ type: 'disconnect', peripheralId: this.uuid }); for (var i in this.services) { for (var j in this.services[i].characteristics) { for (var k in this.services[i].characteristics[j].descriptors) { this.services[i].characteristics[j].descriptors[k].removeAllListeners(); } this.services[i].characteristics[j].removeAllListeners(); } this.services[i].removeAllListeners(); } this.removeAllListeners(); if (keepTargetConnected) { debug('Keep connected ') if (!isClientConnected) { // we wait here for the targeted mobile phone to connect to bleno-cloned device // otherwise we will most probably connect to ourselves // TODO: as an option, it will not be a problem if remote or different MAC sendEvent({ type: 'initializeStatus', peripheralId: peripheral.id, status: 'waiting with reconnect to target device for client connection to bleno...' }); if (! eventEmitter.listeners('clientConnected').length) { eventEmitter.once('clientConnected', function(){ sendEvent({ type: 'initializeStatus', peripheralId: peripheral.id, status: 'client connection to bleno, reconnecting to target device' }); checkConnected(peripheral); }) } } else { debug('reconnect'); checkConnected(peripheral); } } }); //on disconnect peripheral.on('rssiUpdate', function(rssi) { sendEvent({ type: 'rssiUpdate', peripheralId: this.uuid, rssi: rssi }); }); peripheral.on('servicesDiscover', function(services) { debug('--- on servicesDiscover'); var peripheral = this; var exportServices = []; var includedServicesDiscover = function(includedServiceUuids) { sendEvent({ type: 'includedServicesDiscover', peripheralId: peripheral.id, serviceUuid: this.uuid, includedServiceUuids: includedServiceUuids }); }; var characteristicsDiscover = function(characteristics) { var service = this; var discoveredCharacteristics = []; var read = function(data, isNotification) { var characteristic = this; sendEvent({ type: 'read', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid, data: data.toString('hex'), isNotification: isNotification }); }; var write = function() { var characteristic = this; sendEvent({ type: 'write', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid }); }; var broadcast = function(state) { var characteristic = this; sendEvent({ type: 'broadcast', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid, state: state }); }; var notify = function(state) { var characteristic = this; sendEvent({ type: 'notify', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid, state: state }); }; var descriptorsDiscover = function(descriptors) { var characteristic = this; var discoveredDescriptors = []; var valueRead = function(data) { var descriptor = this; sendEvent({ type: 'valueRead', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid, descriptorUuid: descriptor.uuid, data: data.toString('hex') }); }; var valueWrite = function(data) { var descriptor = this; sendEvent({ type: 'valueWrite', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: characteristic.uuid, descriptorUuid: descriptor.uuid }); }; //have to get it from _gatts, only there is handle number stored var rawDescriptors = peripheral._noble._bindings._gatts[peripheral.id]._descriptors[service.uuid][characteristic.uuid]; for (var k in descriptors) { var descriptorValue=''; var descriptorUuid = descriptors[k].uuid; descriptors[k].on('valueRead', valueRead); descriptors[k].on('valueWrite', valueWrite); var exportDescriptor = { handle: rawDescriptors[descriptorUuid].handle, uuid: descriptorUuid, } discoveredDescriptors.push(exportDescriptor); } sendEvent({ type: 'descriptorsDiscover', peripheralId: peripheral.id, serviceUuid: service.uuid, characteristicUuid: this.uuid, descriptors: discoveredDescriptors }); }; for (var j = 0; j < characteristics.length; j++) { var characteristic = characteristics[j]; characteristic.on('read', read); characteristic.on('write', write); characteristic.on('broadcast', broadcast); characteristic.on('notify', notify); characteristic.on('descriptorsDiscover', descriptorsDiscover); discoveredCharacteristics.push({ uuid: characteristic.uuid, name: characteristic.name, properties: characteristic.properties, //we have to get it from internal noble _gatts objects startHandle : peripheral._noble._bindings._gatts[peripheral.id]._characteristics[service.uuid][characteristic.uuid].startHandle, valueHandle : peripheral._noble._bindings._gatts[peripheral.id]._characteristics[service.uuid][characteristic.uuid].valueHandle, endHandle : peripheral._noble._bindings._gatts[peripheral.id]._characteristics[service.uuid][characteristic.uuid].endHandle }); } sendEvent({ type: 'characteristicsDiscover', peripheralId: peripheral.id, serviceUuid: this.uuid, characteristics: discoveredCharacteristics }); }; for (var i in services) { service=services[i]; service.on('includedServicesDiscover', includedServicesDiscover); service.on('characteristicsDiscover', characteristicsDiscover); var exportService = { uuid: service.uuid, name: service.name, type: service.type, //we have to get handle numbers from _gatts startHandle: peripheral._noble._bindings._gatts[peripheral.id]._services[service.uuid].startHandle, endHandle: peripheral._noble._bindings._gatts[peripheral.id]._services[service.uuid].endHandle, }; exportServices.push(exportService); } sendEvent({ type: 'servicesDiscover', peripheralId: this.uuid, services: exportServices }); }); peripheral.on('handleRead', function(handle, data) { sendEvent({ type: 'handleRead', peripheralId: this.uuid, handle: handle, data: data.toString('hex') }); }); peripheral.on('handleWrite', function(handle) { sendEvent({ type: 'handleWrite', peripheralId: this.uuid, handle: handle }); }); }); function explore(peripheralId, readValues, callback) { var peripheral=peripherals[peripheralId]; /* //was previously initialized for other device if (targetPeripheralId && targetPeripheralId != peripheralId) { deinitialize(targetPeripheralId, function(){ console.log('re-initialize for other device '); }) } targetPeripheralId = peripheralId; */ if (!peripheral) { sendEvent({ type:'explore', peripheralId: peripheralId, state: 'startScan' }) targetPeripheralId = peripheralId; noble.startScanning(); eventEmitter.once('targetDiscovered', function(){ noble.stopScanning(); sendEvent({ type: 'explore', peripheralId: peripheralId, state: 'discovered' }) exploreServices(peripherals[peripheralId], readValues, callback); }) } else { exploreServices(peripherals[peripheralId], readValues, callback); } } function exploreServices(peripheral, readValues, callback) { var exportServices=[]; var peripheralId = peripheral.id; sendEvent({ type:'explore', peripheralId: peripheral.id, state: 'start' }) peripheral.connect( function(error) { if (error){ console.log('Explore: connect error: '.red + error + ' - try hciconfig reset...'); } peripheral.discoverServices([], function(error, services) { debug('Explore: discoverServices'); var serviceIndex = 0; if (!servicesCache[peripheralId]) { servicesCache[peripheralId] = { services : [], handles : [] } } async.whilst( function () { return (serviceIndex < services.length); }, function(callback) { var service = services[serviceIndex]; var serviceInfo = service.uuid; var exportService = { uuid: service.uuid, name: service.name, type: service.type, startHandle: peripheral._noble._bindings._gatts[peripheral.id]._services[service.uuid].startHandle, endHandle: peripheral._noble._bindings._gatts[peripheral.id]._services[service.uuid].endHandle, characteristics: [] }; if (service.name) { serviceInfo += ' (' + service.name + ')'; } debug(serviceInfo); if (!servicesCache[peripheralId].services[service.uuid]) { servicesCache[peripheralId].services[service.uuid] = { characteristics : []}; } service.discoverCharacteristics([], function(error, characteristics) { var characteristicIndex = 0; var userDescriptorValue=''; async.whilst( function () { return (characteristicIndex < characteristics.length); }, function(callback) { var characteristic = characteristics[characteristicIndex]; var exportCharacteristic = {uuid: characteristic.uuid, name: characteristic.name, properties: characteristic.properties, value: "", descriptors: []}; var characteristicInfo = ' ' + characteristic.uuid; servicesCache[peripheralId].services[service.uuid].characteristics[characteristic.uuid]= { properties : characteristic.properties, descriptors : [] }; if (characteristic.name) { characteristicInfo += ' (' + characteristic.name + ')'; } async.series([ //get the descriptors, fill 2901 (user descriptor) value function(callback) { characteristic.discoverDescriptors(function(error, descriptors) { async.detect( descriptors, function(descriptor, callback) { return callback(null, descriptor.uuid === '2901'); }, function(error, userDescriptionDescriptor){ if (userDescriptionDescriptor && readValues) { userDescriptionDescriptor.readValue(function(error, data) { if (data) { userDescriptorValue=data.toString(); // debug(' Descriptor value: '+ userDescriptorValue); characteristicInfo += ' (' + userDescriptorValue + ')'; } else userDescriptorValue=''; callback(); }); } else { callback(); } } ); }); }, function(callback){ var descriptorIndex = 0; var exportDescriptor; //have to get it from _gatts, only there is handle # stored var rawDescriptors = peripheral._noble._bindings._gatts[peripheral.id]._descriptors[service.uuid][characteristic.uuid]; var descriptorValue = ''; for (var descriptorUuid in rawDescriptors) { servicesCache[peripheralId].services[service.uuid].characteristics[characteristic.uuid].descriptors[descriptorUuid]=rawDescriptors[descriptorUuid].handle; //get value of user description if (descriptorUuid === '2901') { //from previous async series function descriptorValue = userDescriptorValue; } // else if (descriptorUuid === '2902') { // 2902 - bit 0 = notifications: enabled/disabled, bit 1 = indications enabled/disabled // it may be helpful after reconnect // descriptorValue = ''; // } else descriptorValue=''; exportDescriptor = { handle: rawDescriptors[descriptorUuid].handle, uuid: rawDescriptors[descriptorUuid].uuid, value: descriptorValue } //debug('DESCRIPT : ' + util.inspect(exportDescriptor, {showHidden: false, depth: null})); characteristicInfo += '\n Descriptor: ' + exportDescriptor; exportCharacteristic.descriptors.push(exportDescriptor); } callback(); }, function(callback) { characteristicInfo += '\n properties ' + characteristic.properties.join(', '); exportCharacteristic.startHandle = peripheral._noble._bindings._gatts[peripheral.id]._characteristics[service.uuid][characteristic.uuid].startHandle; exportCharacteristic.valueHandle = peripheral._noble._bindings._gatts[peripheral.id]._characteristics[service.uuid][characteristic.uuid].valueHandle; characteristicInfo += '\n HANDLE: ' + exportCharacteristic.valueHandle; servicesCache[peripheralId].services[service.uuid].characteristics[characteristic.uuid].handle = exportCharacteristic.valueHandle; servicesCache[peripheralId].handles[exportCharacteristic.valueHandle] = { serviceUuid : service.uuid, uuid : characteristic.uuid } if ( (characteristic.properties.indexOf('read') !== -1) && readValues ) { characteristic.read(function(error, data) { if (data) { var hexValue = data.toString('hex'); var asciiValue = utils.hex2a(hexValue); exportCharacteristic.value=hexValue; exportCharacteristic.asciiValue=asciiValue; characteristicInfo += '\n value ' + hexValue + ' | \'' + asciiValue + '\''; } callback(); }); } else { callback(); } }, function() { debug(characteristicInfo); exportService.characteristics.push(exportCharacteristic); characteristicIndex++; callback(); } ]); }, function(error) { exportServices.push(exportService); serviceIndex++; callback(); } ); }); }, function (err) { sendEvent({ type:'explore', peripheralId: peripheral.id, state: 'finished', servicesJsonData : exportServices }) //todo - as an option, not always desirable peripheral.disconnect(); if (callback){ callback(exportServices); } } ); }); }); }