From 13ce04e1c4199ed0579c2c11032b4f32b4ae3ffd Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Fri, 11 Sep 2015 16:15:40 -0500 Subject: [PATCH 1/8] gatt - enable add / remove /removeAll services for attribute caching --- examples/pizza/peripheral.js | 12 + lib/bleno.js | 12 + lib/hci-socket/bindings.js | 16 +- lib/hci-socket/gatt.js | 399 ++++++++++++++++++++------------- lib/hci-socket/hci-status.json | 2 +- lib/mac/bindings.js | 200 +++++++++-------- package.json | 3 +- 7 files changed, 400 insertions(+), 244 deletions(-) diff --git a/examples/pizza/peripheral.js b/examples/pizza/peripheral.js index b186a6a8..6e1f61d2 100644 --- a/examples/pizza/peripheral.js +++ b/examples/pizza/peripheral.js @@ -24,6 +24,7 @@ var PizzaService = require('./pizza-service'); // var name = 'PizzaSquat'; var pizzaService = new PizzaService(new pizza.Pizza()); +var pizzaService2 = new PizzaService(new pizza.Pizza()); // // Wait until the BLE radio powers on before attempting to advertise. @@ -56,5 +57,16 @@ bleno.on('advertisingStart', function(err) { bleno.setServices([ pizzaService ]); + + var flip = false; + setInterval(function (){ + if(flip){ + bleno.removeService(pizzaService2); + } else { + bleno.addService(pizzaService2); + } + flip = !flip; + }, 1500); + } }); diff --git a/lib/bleno.js b/lib/bleno.js index fd1b0a03..43740cce 100644 --- a/lib/bleno.js +++ b/lib/bleno.js @@ -199,6 +199,18 @@ Bleno.prototype.onAdvertisingStop = function() { this.emit('advertisingStop'); }; +Bleno.prototype.addService = function(service) { + this._bindings.addService(service); +}; + +Bleno.prototype.removeAllServices = function() { + this._bindings.removeAllServices(); +} + +Bleno.prototype.removeService = function(service) { + this._bindings.removeService(service); +}; + Bleno.prototype.setServices = function(services, callback) { if (callback) { this.once('servicesSet', callback); diff --git a/lib/hci-socket/bindings.js b/lib/hci-socket/bindings.js index 6b068a52..2c13b0f9 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -48,7 +48,21 @@ BlenoBindings.prototype.stopAdvertising = function() { this._gap.stopAdvertising(); }; -BlenoBindings.prototype.setServices = function(services) { +BlenoBindings.prototype.addService = function(service) { + this._gatt.addService(service); + + this.emit('servicesChanged'); +}; + +BlenoBindings.prototype.removeAllServices = function() { + this._gatt.removeAllServices(); +} + +BlenoBindings.prototype.removeService = function(service){ + this._gatt.removeService(service); +} + +BlenoBindings.prototype.setServices = function(services, deviceName) { this._gatt.setServices(services); this.emit('servicesSet'); diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index f7aac3af..d809c3f0 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -64,6 +64,39 @@ var ATT_ECODE_INSUFF_RESOURCES = 0x11; var ATT_CID = 0x0004; var Gatt = function() { + this._services = [ + { + uuid: '1800', + characteristics: [ + { + uuid: '2a00', + properties: ['read'], + secure: [], + value: null, + descriptors: [] + }, + { + uuid: '2a01', + properties: ['read'], + secure: [], + value: new Buffer([0x80, 0x00]), + descriptors: [] + } + ] + }, + { + uuid: '1801', + characteristics: [ + { + uuid: '2a05', + properties: ['indicate'], + secure: [], + value: new Buffer([0x00, 0x00, 0x00, 0x00]), + descriptors: [] + } + ] + } + ]; this._mtu = 23; this.onAclStreamDataBinded = this.onAclStreamData.bind(this); @@ -72,175 +105,254 @@ var Gatt = function() { util.inherits(Gatt, events.EventEmitter); -Gatt.prototype.setServices = function(services) { - var deviceName = process.env.BLENO_DEVICE_NAME || os.hostname(); +function createATTMessage(op, mtu, valueHandle, data){ + var dataLength = Math.min(data.length, mtu - 3); + var message = new Buffer(3 + dataLength); - // base services and characteristics - var allServices = [ - { - uuid: '1800', - characteristics: [ - { - uuid: '2a00', - properties: ['read'], - secure: [], - value: new Buffer(deviceName), - descriptors: [] - }, - { - uuid: '2a01', - properties: ['read'], - secure: [], - value: new Buffer([0x80, 0x00]), - descriptors: [] - } - ] - }, - { - uuid: '1801', - characteristics: [ - { - uuid: '2a05', - properties: ['indicate'], - secure: [], - value: new Buffer([0x00, 0x00, 0x00, 0x00]), - descriptors: [] - } - ] + message.writeUInt8(parseInt(op, 16), 0); + message.writeUInt16LE(parseInt(valueHandle, 16), 1); + + for (i = 0; i < dataLength; i++) { + message[3 + i] = data[i]; } - ].concat(services); - this._handles = []; + return message; +} - var handle = 0; - var i; - var j; +function createServiceHandles(service, start){ + var handle = start; + var handles = []; - for (i = 0; i < allServices.length; i++) { - var service = allServices[i]; + handle++; + var serviceHandle = handle; - handle++; - var serviceHandle = handle; + handles.push({ + type: 'service', + uuid: service.uuid, + attribute: service, + startHandle: serviceHandle + // endHandle filled in below + }); + service.handle = serviceHandle; + for (var j = 0; j < service.characteristics.length; j++) { + var characteristic = service.characteristics[j]; - this._handles[serviceHandle] = { - type: 'service', - uuid: service.uuid, - attribute: service, - startHandle: serviceHandle - // endHandle filled in below - }; + var properties = 0; + var secure = 0; - for (j = 0; j < service.characteristics.length; j++) { - var characteristic = service.characteristics[j]; + if (characteristic.properties.indexOf('read') !== -1) { + properties |= 0x02; - var properties = 0; - var secure = 0; + if (characteristic.secure.indexOf('read') !== -1) { + secure |= 0x02; + } + } - if (characteristic.properties.indexOf('read') !== -1) { - properties |= 0x02; + if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { + properties |= 0x04; - if (characteristic.secure.indexOf('read') !== -1) { - secure |= 0x02; - } + if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { + secure |= 0x04; } + } - if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { - properties |= 0x04; + if (characteristic.properties.indexOf('write') !== -1) { + properties |= 0x08; - if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { - secure |= 0x04; - } + if (characteristic.secure.indexOf('write') !== -1) { + secure |= 0x08; } + } - if (characteristic.properties.indexOf('write') !== -1) { - properties |= 0x08; + if (characteristic.properties.indexOf('notify') !== -1) { + properties |= 0x10; - if (characteristic.secure.indexOf('write') !== -1) { - secure |= 0x08; - } + if (characteristic.secure.indexOf('notify') !== -1) { + secure |= 0x10; } + } - if (characteristic.properties.indexOf('notify') !== -1) { - properties |= 0x10; + if (characteristic.properties.indexOf('indicate') !== -1) { + properties |= 0x20; - if (characteristic.secure.indexOf('notify') !== -1) { - secure |= 0x10; - } + if (characteristic.secure.indexOf('indicate') !== -1) { + secure |= 0x20; } + } - if (characteristic.properties.indexOf('indicate') !== -1) { - properties |= 0x20; + handle++; + var characteristicHandle = handle; - if (characteristic.secure.indexOf('indicate') !== -1) { - secure |= 0x20; - } - } + handle++; + var characteristicValueHandle = handle; + + handles.push({ + type: 'characteristic', + uuid: characteristic.uuid, + properties: properties, + secure: secure, + attribute: characteristic, + startHandle: characteristicHandle, + valueHandle: characteristicValueHandle + }); + + handles.push({ + type: 'characteristicValue', + handle: characteristicValueHandle, + value: characteristic.value + }); + + if (properties & 0x30) { // notify or indicate + // add client characteristic configuration descriptor handle++; - var characteristicHandle = handle; + var clientCharacteristicConfigurationDescriptorHandle = handle; + handles.push({ + type: 'descriptor', + handle: clientCharacteristicConfigurationDescriptorHandle, + uuid: '2902', + attribute: characteristic, + properties: (0x02 | 0x04 | 0x08), // read/write + secure: (secure & 0x10) ? (0x02 | 0x04 | 0x08) : 0, + value: new Buffer([0x00, 0x00]) + }); + } + + var descriptors = characteristic.descriptors; + for (var k = 0, kk = descriptors.length; k < kk; k++) { + var descriptor = descriptors[k]; handle++; - var characteristicValueHandle = handle; + var descriptorHandle = handle; - this._handles[characteristicHandle] = { - type: 'characteristic', - uuid: characteristic.uuid, - properties: properties, - secure: secure, - attribute: characteristic, - startHandle: characteristicHandle, - valueHandle: characteristicValueHandle - }; - - this._handles[characteristicValueHandle] = { - type: 'characteristicValue', - handle: characteristicValueHandle, - value: characteristic.value - }; - - if (properties & 0x30) { // notify or indicate - // add client characteristic configuration descriptor - - handle++; - var clientCharacteristicConfigurationDescriptorHandle = handle; - this._handles[clientCharacteristicConfigurationDescriptorHandle] = { - type: 'descriptor', - handle: clientCharacteristicConfigurationDescriptorHandle, - uuid: '2902', - attribute: characteristic, - properties: (0x02 | 0x04 | 0x08), // read/write - secure: (secure & 0x10) ? (0x02 | 0x04 | 0x08) : 0, - value: new Buffer([0x00, 0x00]) - }; + handles.push({ + type: 'descriptor', + handle: descriptorHandle, + uuid: descriptor.uuid, + attribute: descriptor, + properties: 0x02, // read only + secure: 0x00, + value: descriptor.value + }); + } + } + + handles[0].endHandle = handle; + + return handles; +} + +Gatt.prototype.addService = function AddGATTService(service){ + this._services.push(service); + var handles = createServiceHandles(service, this._handles.length - 1); + for(var i = 0, ii = handles.length; i < ii; i++){ + var handle = handles[i].startHandle || handles[i].handle; + this._handles[handle] = handles[i]; + } + + // Indicate that services changed. +var serviceHandleObject = handles[0]; + + var serviceChangedValue = new Buffer(4); + serviceChangedValue.writeUInt16LE(parseInt(serviceHandleObject.startHandle, 16), 0); + serviceChangedValue.writeUInt16LE(parseInt(serviceHandleObject.endHandle, 16), 2); + this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; + + var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, serviceChangedValue); + this._lastIndicatedAttribute = this._handles[this._serviceChangedValueHandle - 1].attribute; + + debug('addService: indicate message: ' + indicateMessage.toString('hex')); + this.send(indicateMessage); +}; + +Gatt.prototype.removeAllServices = function RemoveAllGATTServices(){ + var startHandle = null; + var endHandle = null; + for (var i = 0; i < this._handles.length; i++) { + var handle = this._handles[i]; + if(handle.uuid === '1800' || handle.uuid === '1801' || handle.uuid === '2a00' || handle.uuid === '2a01' || + handle.uuid === '2a05'){ + continue; + } else { + if (!startHandle && handle.uuid) { + startHandle = i; } - for (var k = 0; k < characteristic.descriptors.length; k++) { - var descriptor = characteristic.descriptors[k]; - - handle++; - var descriptorHandle = handle; - - this._handles[descriptorHandle] = { - type: 'descriptor', - handle: descriptorHandle, - uuid: descriptor.uuid, - attribute: descriptor, - properties: 0x02, // read only - secure: 0x00, - value: descriptor.value - }; + if(handle.uuid){ + endHandle = i; } } + } + + var serviceChangedValue = new Buffer(4); + serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); + serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); + this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; + + var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, + serviceChangedValue); +}; + +Gatt.prototype.removeService = function RemoveGattService(service){ + var handle = service.handle; + + var startHandle = this._handles[handle].startHandle; + var endHandle = this._handles[handle].endHandle; + for(var handle = startHandle; handle <= endHandle; handle++){ + // Don't slice, indexing will be corrupted. + this._handles[handle] = {}; + } + debug('removeService - removed handles: ' + startHandle + '-' + endHandle); + + var serviceChangedValue = new Buffer(4); + serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); + serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); + this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; - this._handles[serviceHandle].endHandle = handle; + var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, serviceChangedValue); + this._lastIndicatedAttribute = this._handles[this._serviceChangedValueHandle - 1].attribute; + + debug('removeService: indicate message: ' + indicateMessage.toString('hex')); + this.send(indicateMessage); +}; + +/** + * Set services for the GATT server for discovery. Do not call this function if you are connected to a client, it can have + * negative consequences if the services are in a different order from the time it was initially called. + * @param {Array} services The services that will be available for Primary Service Discovery Ref. Vol 3 GATT 4.13 + * @returns {undefined} No payload data provided for return. + */ +Gatt.prototype.setServices = function SetGATTServices(services) { + var deviceName = process.env.BLENO_DEVICE_NAME || os.hostname(); + this._services[0].characteristics[0].value = new Buffer(deviceName); + services = services || []; + + // base services and characteristics + this._services = this._services.concat(services); + + this._handles = []; + + for (var i = 0, ii = this._services.length; i < ii; i++) { + var service = this._services[i]; + + // A bit hacky, but keeps indexing proper so access can be this._handles[index], concat will leave empty properties + var serviceHandles = createServiceHandles(service, this._handles.length - 1); + for(var j = 0, jj = serviceHandles.length; j < jj; j++){ + var handle = serviceHandles[j].startHandle || serviceHandles[j].handle; + this._handles[handle] = serviceHandles[j]; + + if (this._handles[handle].uuid === '2a05'){ + this._serviceChangedValueHandle = handle; + } + } } var debugHandles = []; - for (i = 0; i < this._handles.length; i++) { + for (var i = 0; i < this._handles.length; i++) { handle = this._handles[i]; debugHandles[i] = {}; - for(j in handle) { + for(var j in handle) { if (Buffer.isBuffer(handle[j])) { debugHandles[i][j] = handle[j] ? 'Buffer(\'' + handle[j].toString('hex') + '\', \'hex\')' : null; } else if (j !== 'attribute') { @@ -273,8 +385,10 @@ Gatt.prototype.onAclStreamEnd = function() { }; Gatt.prototype.send = function(data) { - debug('send: ' + data.toString('hex')); - this._aclStream.write(ATT_CID, data); + if (this._aclStream) { + debug('send: ' + data.toString('hex')); + this._aclStream.write(ATT_CID, data); + } }; Gatt.prototype.errorResponse = function(opcode, handle, status) { @@ -764,7 +878,7 @@ Gatt.prototype.handleReadOrReadBlobRequest = function(request) { return response; }; -Gatt.prototype.handleWriteRequestOrCommand = function(request) { +Gatt.prototype.handleWriteRequestOrCommand = function handleWriteRequestOrCommand(request) { var response = null; var requestType = request[0]; @@ -819,34 +933,19 @@ Gatt.prototype.handleWriteRequestOrCommand = function(request) { if (value & 0x0003) { var updateValueCallback = (function(valueHandle, attribute) { return function(data) { - var dataLength = Math.min(data.length, this._mtu - 3); var useNotify = attribute.properties.indexOf('notify') !== -1; var useIndicate = attribute.properties.indexOf('indicate') !== -1; var i; if (useNotify) { - var notifyMessage = new Buffer(3 + dataLength); - - notifyMessage.writeUInt8(ATT_OP_HANDLE_NOTIFY, 0); - notifyMessage.writeUInt16LE(valueHandle, 1); - - for (i = 0; i < dataLength; i++) { - notifyMessage[3 + i] = data[i]; - } + var notifyMessage = createATTMessage(ATT_OP_HANDLE_NOTIFY, this._mtu, valueHandle, data); debug('notify message: ' + notifyMessage.toString('hex')); this.send(notifyMessage); attribute.emit('notify'); } else if (useIndicate) { - var indicateMessage = new Buffer(3 + dataLength); - - indicateMessage.writeUInt8(ATT_OP_HANDLE_IND, 0); - indicateMessage.writeUInt16LE(valueHandle, 1); - - for (i = 0; i < dataLength; i++) { - indicateMessage[3 + i] = data[i]; - } + var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, valueHandle, data); this._lastIndicatedAttribute = attribute; diff --git a/lib/hci-socket/hci-status.json b/lib/hci-socket/hci-status.json index 3cef0e76..db4f5945 100644 --- a/lib/hci-socket/hci-status.json +++ b/lib/hci-socket/hci-status.json @@ -64,4 +64,4 @@ "Connection Failed to be Established", "MAC Connection Failed", "Coarse Clock Adjustment Rejected but Will Try to Adjust Using Clock Dragging" -] \ No newline at end of file +] diff --git a/lib/mac/bindings.js b/lib/mac/bindings.js index da83508a..8304bd73 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -149,110 +149,123 @@ blenoBindings.on('kCBMsgId17', function(args) { this.emit('advertisingStop'); }); -blenoBindings.setServices = function(services) { - this.sendCBMsg(12, null); // remove all services - - services = services || []; - var attributeId = 1; - - this._attributes = []; - this._setServicesError = undefined; - - if (services.length) { - for (var i = 0; i < services.length; i++) { - var service = services[i]; - - var arg = { - kCBMsgArgAttributeID: attributeId, - kCBMsgArgAttributeIDs: [], - kCBMsgArgCharacteristics: [], - kCBMsgArgType: 1, // 1 => primary, 0 => included - kCBMsgArgUUID: new Buffer(service.uuid, 'hex') - }; - - this._attributes[attributeId] = service; +blenoBindings.addService = function AddGATTService(service) { + var attributeId = this._attributes.length ? this._attributes.length - 1 : 1; + var arg = { + kCBMsgArgAttributeID: attributeId, + kCBMsgArgAttributeIDs: [], + kCBMsgArgCharacteristics: [], + kCBMsgArgType: 1, // 1 => primary, 0 => included + kCBMsgArgUUID: new Buffer(service.uuid, 'hex') + }; + this._attributes[attributeId] = service; + service.attributeId = attributeId; - this._lastServiceAttributeId = attributeId; - attributeId++; + this._lastServiceAttributeId = attributeId; + attributeId++; - for (var j = 0; j < service.characteristics.length; j++) { - var characteristic = service.characteristics[j]; + for (var j = 0; j < service.characteristics.length; j++) { + var characteristic = service.characteristics[j]; - var properties = 0; - var permissions = 0; + var properties = 0; + var permissions = 0; - if (characteristic.properties.indexOf('read') !== -1) { - properties |= 0x02; + if (characteristic.properties.indexOf('read') !== -1) { + properties |= 0x02; - if (characteristic.secure.indexOf('read') !== -1) { - permissions |= 0x04; - } else { - permissions |= 0x01; - } - } + if (characteristic.secure.indexOf('read') !== -1) { + permissions |= 0x04; + } else { + permissions |= 0x01; + } + } - if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { - properties |= 0x04; + if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { + properties |= 0x04; - if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { - permissions |= 0x08; - } else { - permissions |= 0x02; - } - } + if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { + permissions |= 0x08; + } else { + permissions |= 0x02; + } + } - if (characteristic.properties.indexOf('write') !== -1) { - properties |= 0x08; + if (characteristic.properties.indexOf('write') !== -1) { + properties |= 0x08; - if (characteristic.secure.indexOf('write') !== -1) { - permissions |= 0x08; - } else { - permissions |= 0x02; - } - } - - if (characteristic.properties.indexOf('notify') !== -1) { - if (characteristic.secure.indexOf('notify') !== -1) { - properties |= 0x100; - } else { - properties |= 0x10; - } - } - - if (characteristic.properties.indexOf('indicate') !== -1) { - if (characteristic.secure.indexOf('indicate') !== -1) { - properties |= 0x200; - } else { - properties |= 0x20; - } - } + if (characteristic.secure.indexOf('write') !== -1) { + permissions |= 0x08; + } else { + permissions |= 0x02; + } + } - var characteristicArg = { - kCBMsgArgAttributeID: attributeId, - kCBMsgArgAttributePermissions: permissions, - kCBMsgArgCharacteristicProperties: properties, - kCBMsgArgData: characteristic.value, - kCBMsgArgDescriptors: [], - kCBMsgArgUUID: new Buffer(characteristic.uuid, 'hex') - }; + if (characteristic.properties.indexOf('notify') !== -1) { + if (characteristic.secure.indexOf('notify') !== -1) { + properties |= 0x100; + } else { + properties |= 0x10; + } + } - this._attributes[attributeId] = characteristic; + if (characteristic.properties.indexOf('indicate') !== -1) { + if (characteristic.secure.indexOf('indicate') !== -1) { + properties |= 0x200; + } else { + properties |= 0x20; + } + } - for (var k = 0; k < characteristic.descriptors.length; k++) { - var descriptor = characteristic.descriptors[k]; + var characteristicArg = { + kCBMsgArgAttributeID: attributeId, + kCBMsgArgAttributePermissions: permissions, + kCBMsgArgCharacteristicProperties: properties, + kCBMsgArgData: characteristic.value, + kCBMsgArgDescriptors: [], + kCBMsgArgUUID: new Buffer(characteristic.uuid, 'hex') + }; + this._attributes[attributeId] = characteristic; + + for (var k = 0; k < characteristic.descriptors.length; k++) { + var descriptor = characteristic.descriptors[k]; + + characteristicArg.kCBMsgArgDescriptors.push({ + kCBMsgArgData: descriptor.value, + kCBMsgArgUUID: new Buffer(descriptor.uuid, 'hex') + }); + } + arg.kCBMsgArgCharacteristics.push(characteristicArg); + attributeId++; + } + this.sendCBMsg(10, arg); +}; - characteristicArg.kCBMsgArgDescriptors.push({ - kCBMsgArgData: descriptor.value, - kCBMsgArgUUID: new Buffer(descriptor.uuid, 'hex') - }); - } +blenoBindings.removeService = function RemoveGATTService(service){ + var attributeId = this._attributes[service.attributeId]; + var messageBody = { + kCBMsgArgAttributeID: attributeId + }; + this._attributes[service.attributeId] = {}; + this.sendCBMsg(11, attributeId); +} + +blenoBindings.removeAllServices = function RemoveAllGATTServices(){ + this.sendCBMsg(12, null); +} + +blenoBindings.setServices = function SetGATTServices(services) { + this.sendCBMsg(12, null); + services = services || []; - arg.kCBMsgArgCharacteristics.push(characteristicArg); + this._attributes = []; + this._setServicesError = undefined; - attributeId++; - } + if (services.length) { + for (var i = 0; i < services.length; i++) { + var service = services[i]; - this.sendCBMsg(10, arg); + // Add service, which will send message via XPC + this.addService(service); } } else { this.emit('servicesSet'); @@ -269,12 +282,19 @@ blenoBindings.updateRssi = function() { } }; -blenoBindings.on('kCBMsgId18', function(args) { +/** + * Callback for adding / removing services + * @param {Object} 'kCBMsgId18' Callback string for handle message type 18 + * @param {function} AddOrRemoveServiceCallback callback function with provided arguments for handle message + * @returns {undefined} No payload data provided + */ +blenoBindings.on('kCBMsgId18', function AddOrRemoveServiceCallback(args) { var attributeId = args.kCBMsgArgAttributeID; var result = args.kCBMsgArgResult; if (result) { - var errorMessage = 'failed to set service ' + this._attributes[attributeId].uuid; + var message = this._attributes[attributeId].uuid ? this._attributes[attributeId].uuid : ('attributeId: ' + attributeId); + var errorMessage = 'failed to set / add / remove service ' + message; if (result === 27) { errorMessage += ', UUID not allowed!'; diff --git a/package.json b/package.json index 18c4a0e4..14726d1b 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,7 @@ "devDependencies": { "jshint": "~2.3.0", "should": "~2.0.2", - "mocha": "~1.14.0", - "node-blink1": "~0.1.1" + "mocha": "~1.14.0" }, "dependencies": { "debug": "^2.2.0" From 54323ccd752a539bcb60c269d971a326705e568b Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Sun, 20 Sep 2015 15:35:55 -0500 Subject: [PATCH 2/8] mac - add strategy for adding / removing attribute ID's --- examples/pizza/peripheral.js | 25 ++++++------ lib/mac/bindings.js | 77 +++++++++++++++++++++++++++++++++--- 2 files changed, 82 insertions(+), 20 deletions(-) diff --git a/examples/pizza/peripheral.js b/examples/pizza/peripheral.js index 6e1f61d2..e7d561ef 100644 --- a/examples/pizza/peripheral.js +++ b/examples/pizza/peripheral.js @@ -54,19 +54,16 @@ bleno.on('advertisingStart', function(err) { // Once we are advertising, it's time to set up our services, // along with our characteristics. // - bleno.setServices([ - pizzaService - ]); - - var flip = false; - setInterval(function (){ - if(flip){ - bleno.removeService(pizzaService2); - } else { - bleno.addService(pizzaService2); - } - flip = !flip; - }, 1500); - + bleno.setServices([ pizzaService ], function (){ + var flip = false; + setInterval(function (){ + if(flip){ + bleno.removeService(pizzaService2); + } else { + bleno.addService(pizzaService2); + } + flip = !flip; + }, 1500); + }); } }); diff --git a/lib/mac/bindings.js b/lib/mac/bindings.js index 8304bd73..7805671f 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -150,7 +150,64 @@ blenoBindings.on('kCBMsgId17', function(args) { }); blenoBindings.addService = function AddGATTService(service) { - var attributeId = this._attributes.length ? this._attributes.length - 1 : 1; + var attributeId; + var attributeCount = 1 + service.characteristics.length; + var attributes = this._attributes; + + /** + * The goal is to find an attribute range that can host the full service, which in most cases will be the next available push + * position. In the edge case, where our attribute consumes more Id's than the standard permits (0xFFFF), we must find + * a place within our attributes array that has been vacated or throw an error. + */ + if(!attributes.length){ + attributeId = 1; + } else if((attributes.length - 1 + attributeCount) >= 0xFFFF){ + // perform search to find somewhere within our attributes array that contains a hole that can host the attributeCount + var consecutiveCount = 0; + var groupedOpenings = {}; + for(var i = 0, ii = attributes.length; i < ii; i++){ + var attribute = attributes[i]; + if(!attribute.kCBMsgArgAttributeID){ + consecutiveCount++; + } else if(attribute.kCBMsgArgAttributeID && consecutiveCount){ + + // If we have found this amount of openings before, don't mark in our group so we use the first available. + if(!groupedOpenings[consecutiveCount].start){ + var previousIndex = i - 1; + + // PreviousIndex - Attribute Openings + var start = previousIndex - consecutiveCount; + + // Previous Index + var end = previousIndex; + groupedOpenings[consecutiveCount] = { start: start, end: end}; + + if(consecutiveCount === attributeCount){ + break; + } + } + consecutiveCount = 0; + } + } + + if(groupedOpenings[attributeCount].start){ + attributeId = groupedOpenings[attributeCount].start; + } else { + for(var openingCount in groupedOpenings){ + if(parseInt(openingCount, 10) > attributeCount){ + attributeId = groupedOpenings[openingCount].start; + break; + } + } + } + + if(!attributeId){ + throw new Error('Not enough space to add service.'); + } + } else { + attributeId = attributes.length; + } + var arg = { kCBMsgArgAttributeID: attributeId, kCBMsgArgAttributeIDs: [], @@ -225,6 +282,7 @@ blenoBindings.addService = function AddGATTService(service) { kCBMsgArgUUID: new Buffer(characteristic.uuid, 'hex') }; this._attributes[attributeId] = characteristic; + characteristic.attributeId = attributeId; for (var k = 0; k < characteristic.descriptors.length; k++) { var descriptor = characteristic.descriptors[k]; @@ -241,17 +299,24 @@ blenoBindings.addService = function AddGATTService(service) { }; blenoBindings.removeService = function RemoveGATTService(service){ - var attributeId = this._attributes[service.attributeId]; + var attributes = this._attributes; + var attributeId = service.attributeId; + var attribute = attributes[attributeId]; + for(var i = 0, ii = attribute.characteristics.length; i < ii; i++){ + var characteristic = attribute.characteristics[i]; + attributes[characteristic.attributeId] = {}; + } + attributes[attributeId] = {}; var messageBody = { kCBMsgArgAttributeID: attributeId }; - this._attributes[service.attributeId] = {}; - this.sendCBMsg(11, attributeId); -} + this.sendCBMsg(11, messageBody); +}; blenoBindings.removeAllServices = function RemoveAllGATTServices(){ + this._attributes = []; this.sendCBMsg(12, null); -} +}; blenoBindings.setServices = function SetGATTServices(services) { this.sendCBMsg(12, null); From 2785721fbb96a204ece5ebfdb4bd5777139c6e0f Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Sun, 20 Sep 2015 18:42:25 -0500 Subject: [PATCH 3/8] linux - add strategy for handle management --- lib/hci-socket/gatt.js | 319 +++++++++++++++++++++-------------------- package.json | 3 +- 2 files changed, 167 insertions(+), 155 deletions(-) diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index d809c3f0..d1ae20fb 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -64,39 +64,6 @@ var ATT_ECODE_INSUFF_RESOURCES = 0x11; var ATT_CID = 0x0004; var Gatt = function() { - this._services = [ - { - uuid: '1800', - characteristics: [ - { - uuid: '2a00', - properties: ['read'], - secure: [], - value: null, - descriptors: [] - }, - { - uuid: '2a01', - properties: ['read'], - secure: [], - value: new Buffer([0x80, 0x00]), - descriptors: [] - } - ] - }, - { - uuid: '1801', - characteristics: [ - { - uuid: '2a05', - properties: ['indicate'], - secure: [], - value: new Buffer([0x00, 0x00, 0x00, 0x00]), - descriptors: [] - } - ] - } - ]; this._mtu = 23; this.onAclStreamDataBinded = this.onAclStreamData.bind(this); @@ -105,35 +72,19 @@ var Gatt = function() { util.inherits(Gatt, events.EventEmitter); -function createATTMessage(op, mtu, valueHandle, data){ - var dataLength = Math.min(data.length, mtu - 3); - var message = new Buffer(3 + dataLength); - - message.writeUInt8(parseInt(op, 16), 0); - message.writeUInt16LE(parseInt(valueHandle, 16), 1); - - for (i = 0; i < dataLength; i++) { - message[3 + i] = data[i]; - } - - return message; -} - -function createServiceHandles(service, start){ - var handle = start; - var handles = []; +Gatt.prototype.addService = function AddGATTService(service, indicate){ + var handles = this._handles; + indicate = indicate === undefined ? true : indicate; - handle++; - var serviceHandle = handle; - - handles.push({ + var serviceHandles = []; + serviceHandles.push({ type: 'service', uuid: service.uuid, attribute: service, - startHandle: serviceHandle + startHandle: null // endHandle filled in below }); - service.handle = serviceHandle; + for (var j = 0; j < service.characteristics.length; j++) { var characteristic = service.characteristics[j]; @@ -180,54 +131,41 @@ function createServiceHandles(service, start){ } } - handle++; - var characteristicHandle = handle; - - handle++; - var characteristicValueHandle = handle; - - handles.push({ + serviceHandles.push({ type: 'characteristic', uuid: characteristic.uuid, properties: properties, secure: secure, attribute: characteristic, - startHandle: characteristicHandle, - valueHandle: characteristicValueHandle + startHandle: null, + valueHandle: null }); - handles.push({ + serviceHandles.push({ type: 'characteristicValue', - handle: characteristicValueHandle, + handle: null, value: characteristic.value }); if (properties & 0x30) { // notify or indicate // add client characteristic configuration descriptor - - handle++; - var clientCharacteristicConfigurationDescriptorHandle = handle; - handles.push({ + serviceHandles.push({ type: 'descriptor', - handle: clientCharacteristicConfigurationDescriptorHandle, + handle: null, uuid: '2902', attribute: characteristic, properties: (0x02 | 0x04 | 0x08), // read/write secure: (secure & 0x10) ? (0x02 | 0x04 | 0x08) : 0, value: new Buffer([0x00, 0x00]) - }); + }); } var descriptors = characteristic.descriptors; for (var k = 0, kk = descriptors.length; k < kk; k++) { var descriptor = descriptors[k]; - - handle++; - var descriptorHandle = handle; - - handles.push({ + serviceHandles.push({ type: 'descriptor', - handle: descriptorHandle, + handle: null, uuid: descriptor.uuid, attribute: descriptor, properties: 0x02, // read only @@ -237,83 +175,105 @@ function createServiceHandles(service, start){ } } - handles[0].endHandle = handle; + var attHandle; + if(!handles.length){ + attHandle = 1; + } else if(parseInt((serviceHandles.length + handles.length), 16) >= 0xFFFF){ + var consecutiveCount = 0; + var groupedOpenings = {}; + for(var i = 0, ii = handles.length; i < ii; i++){ + var handle = handles[i]; + + if(!handle.uuid){ + consecutiveCount++; + } else if(handle.uuid && consecutiveCount) { + var previousIndex = i - 1; + + var start = previousIndex - consecutiveCount; + var end = previousIndex; + groupedOpenings[consecutiveCount] = { start: start, end: end }; + consecutiveCount = 0; + } + } - return handles; -} + var attributeCount = serviceHandles.length; + if(groupedOpenings[attributeCount]){ + attHandle = groupedOpenings[attributeCount].start; + } else { + for(var openingCount in groupedOpenings){ + if(parseInt(openingCount, 10) >= attributeCount){ + attHandle = groupedOpenings[openingCount].start; + } + } + } -Gatt.prototype.addService = function AddGATTService(service){ - this._services.push(service); - var handles = createServiceHandles(service, this._handles.length - 1); - for(var i = 0, ii = handles.length; i < ii; i++){ - var handle = handles[i].startHandle || handles[i].handle; - this._handles[handle] = handles[i]; + if(!attHandle){ + throw new Error('Not enough space to add service.'); + } + } else { + attHandle = handles.length; } - // Indicate that services changed. -var serviceHandleObject = handles[0]; + var startHandle = attHandle; + for(var l = 0, ll = serviceHandles.length; l < ll; l++){ + var serviceHandle = serviceHandles[l]; + + if (serviceHandle.type === 'service'){ + serviceHandle.startHandle = attHandle; + serviceHandle.endHandle = (attHandle + serviceHandles.length); + } else if(serviceHandle.type === 'characteristic'){ + serviceHandle.startHandle = attHandle; + serviceHandle.valueHandle = (attHandle + 1); - var serviceChangedValue = new Buffer(4); - serviceChangedValue.writeUInt16LE(parseInt(serviceHandleObject.startHandle, 16), 0); - serviceChangedValue.writeUInt16LE(parseInt(serviceHandleObject.endHandle, 16), 2); - this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; + if (serviceHandle.uuid === '2a05'){ + this._serviceChangedValueHandle = serviceHandle.valueHandle; + } + } else if(serviceHandle.type === 'characteristicValue' || characteristic.type === 'descriptor'){ + serviceHandle.handle = attHandle; + } + handles[attHandle] = serviceHandle; + attHandle++; + } - var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, serviceChangedValue); - this._lastIndicatedAttribute = this._handles[this._serviceChangedValueHandle - 1].attribute; + var endHandle = startHandle + serviceHandles.length; + if(indicate){ + this.serviceChanged(startHandle, endHandle); + } + service.handle = startHandle; - debug('addService: indicate message: ' + indicateMessage.toString('hex')); - this.send(indicateMessage); + debug('addService: added handles ' + startHandle + '-' + endHandle); }; Gatt.prototype.removeAllServices = function RemoveAllGATTServices(){ - var startHandle = null; - var endHandle = null; - for (var i = 0; i < this._handles.length; i++) { - var handle = this._handles[i]; + var handles = this._handles; + for (var i = 0; i < handles.length; i++) { + var handle = handles[i]; if(handle.uuid === '1800' || handle.uuid === '1801' || handle.uuid === '2a00' || handle.uuid === '2a01' || handle.uuid === '2a05'){ continue; } else { - if (!startHandle && handle.uuid) { - startHandle = i; - } - - if(handle.uuid){ - endHandle = i; - } + handle = {}; } } - var serviceChangedValue = new Buffer(4); - serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); - serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); - this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; - - var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, - serviceChangedValue); + // Make the central rescan all services Reference Bluetooth Spec Vol.3.G 2.5.2 + this.serviceChanged(0x0001, 0xFFFF); + debug('removedAllServices'); }; Gatt.prototype.removeService = function RemoveGattService(service){ var handle = service.handle; + var handles = this._handles; - var startHandle = this._handles[handle].startHandle; - var endHandle = this._handles[handle].endHandle; + var startHandle = handles[handle].startHandle; + var endHandle = handles[handle].endHandle; for(var handle = startHandle; handle <= endHandle; handle++){ // Don't slice, indexing will be corrupted. - this._handles[handle] = {}; + handles[handle] = {}; } - debug('removeService - removed handles: ' + startHandle + '-' + endHandle); - - var serviceChangedValue = new Buffer(4); - serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); - serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); - this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; - - var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, this._serviceChangedValueHandle, serviceChangedValue); - this._lastIndicatedAttribute = this._handles[this._serviceChangedValueHandle - 1].attribute; + this.serviceChanged(startHandle, endHandle); - debug('removeService: indicate message: ' + indicateMessage.toString('hex')); - this.send(indicateMessage); + debug('removeService - removed handles: ' + startHandle + '-' + endHandle); }; /** @@ -324,27 +284,48 @@ Gatt.prototype.removeService = function RemoveGattService(service){ */ Gatt.prototype.setServices = function SetGATTServices(services) { var deviceName = process.env.BLENO_DEVICE_NAME || os.hostname(); - this._services[0].characteristics[0].value = new Buffer(deviceName); services = services || []; // base services and characteristics - this._services = this._services.concat(services); - + var baseServices = [ + { + uuid: '1800', + characteristics: [ + { + uuid: '2a00', + properties: ['read'], + secure: [], + value: new Buffer(deviceName), + descriptors: [] + }, + { + uuid: '2a01', + properties: ['read'], + secure: [], + value: new Buffer([0x80, 0x00]), + descriptors: [] + } + ] + }, + { + uuid: '1801', + characteristics: [ + { + uuid: '2a05', + properties: ['indicate'], + secure: [], + value: new Buffer([0x00, 0x00, 0x00, 0x00]), + descriptors: [] + } + ] + } + ]; + services = baseServices.concat(services); this._handles = []; - for (var i = 0, ii = this._services.length; i < ii; i++) { - var service = this._services[i]; - - // A bit hacky, but keeps indexing proper so access can be this._handles[index], concat will leave empty properties - var serviceHandles = createServiceHandles(service, this._handles.length - 1); - for(var j = 0, jj = serviceHandles.length; j < jj; j++){ - var handle = serviceHandles[j].startHandle || serviceHandles[j].handle; - this._handles[handle] = serviceHandles[j]; - - if (this._handles[handle].uuid === '2a05'){ - this._serviceChangedValueHandle = handle; - } - } + for (var i = 0, ii = services.length; i < ii; i++) { + var service = services[i]; + this.addService(service, false); } var debugHandles = []; @@ -364,6 +345,16 @@ Gatt.prototype.setServices = function SetGATTServices(services) { debug('handles = ' + JSON.stringify(debugHandles, null, 2)); }; +Gatt.prototype.serviceChanged = function ServiceChanged(startHandle, endHandle){ + var serviceChangedValue = new Buffer(4); + serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); + serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); + this._handles[this._serviceChangedValueHandle].value = serviceChangedValue; + + this.sendATTMessage(ATT_OP_HANDLE_IND, this._serviceChangedValueHandle, serviceChangedValue); + this._lastIndicatedAttribute = this._handles[this._serviceChangedValueHandle - 1].attribute; +}; + Gatt.prototype.setAclStream = function(aclStream) { this._aclStream = aclStream; @@ -391,6 +382,32 @@ Gatt.prototype.send = function(data) { } }; +Gatt.prototype.sendATTMessage = function SendATTMessage(op, valueHandle, data){ + var mtu = this._mtu; + var dataLength = Math.min(data.length, mtu - 3); + var message = new Buffer(3 + dataLength); + + message.writeUInt8(parseInt(op, 16), 0); + message.writeUInt16LE(parseInt(valueHandle, 16), 1); + + for (i = 0; i < dataLength; i++) { + message[3 + i] = data[i]; + } + this.send(message); + + switch(op){ + case ATT_OP_HANDLE_IND: + msg = 'indicate'; + break; + case ATT_OP_HANDLE_NOTIFY: + msg = 'notify'; + break; + default: + msg = ' '; + } + debug('sendATTMessage: ' + msg + '-' + message.toString('hex')); +} + Gatt.prototype.errorResponse = function(opcode, handle, status) { var buf = new Buffer(5); @@ -938,19 +955,13 @@ Gatt.prototype.handleWriteRequestOrCommand = function handleWriteRequestOrComman var i; if (useNotify) { - var notifyMessage = createATTMessage(ATT_OP_HANDLE_NOTIFY, this._mtu, valueHandle, data); - - debug('notify message: ' + notifyMessage.toString('hex')); - this.send(notifyMessage); + this.sendATTMessage(ATT_OP_HANDLE_NOTIFY, valueHandle, data); attribute.emit('notify'); } else if (useIndicate) { - var indicateMessage = createATTMessage(ATT_OP_HANDLE_IND, this._mtu, valueHandle, data); + this.sendATTMessage(ATT_OP_HANDLE_IND, valueHandle, data); this._lastIndicatedAttribute = attribute; - - debug('indicate message: ' + indicateMessage.toString('hex')); - this.send(indicateMessage); } }.bind(this); }.bind(this))(valueHandle - 1, handleAttribute); diff --git a/package.json b/package.json index 14726d1b..18c4a0e4 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "devDependencies": { "jshint": "~2.3.0", "should": "~2.0.2", - "mocha": "~1.14.0" + "mocha": "~1.14.0", + "node-blink1": "~0.1.1" }, "dependencies": { "debug": "^2.2.0" From 61dd8e7ce0e5a30bc1acf08ebc37d737260d7851 Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Thu, 8 Oct 2015 18:32:40 -0500 Subject: [PATCH 4/8] linux - have handle managment behave like os x --- lib/hci-socket/gatt.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index d1ae20fb..07df80af 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -211,7 +211,20 @@ Gatt.prototype.addService = function AddGATTService(service, indicate){ throw new Error('Not enough space to add service.'); } } else { - attHandle = handles.length; + /** + * This is put in place so that Linux handle management behaves how OS X handle management does. + * Quick Primer: On OS X, if the last service towards the end of our attributes is taken out, os x will + * use it's handles if another service is added after. Therefore, on linux we need the service that's + * to be added to take the first spot AFTER our last service that's on the array. + */ + var lastServiceIndex = null; + for(var i = 0; i < handles.length; i++){ + var handle = handles[i]; + if(handle && handle.type === 'service'){ + lastServiceIndex = i; + } + } + attHandle = handles[lastServiceIndex].endHandle + 1; } var startHandle = attHandle; From f2cddccec0264a77ff468f1398f72ae10870e987 Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Mon, 12 Oct 2015 02:50:50 -0500 Subject: [PATCH 5/8] gatt - fix linux errors with indicate callback --- lib/hci-socket/gatt.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index 07df80af..4acdcf80 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -228,12 +228,15 @@ Gatt.prototype.addService = function AddGATTService(service, indicate){ } var startHandle = attHandle; + var endHandle = null; for(var l = 0, ll = serviceHandles.length; l < ll; l++){ var serviceHandle = serviceHandles[l]; if (serviceHandle.type === 'service'){ serviceHandle.startHandle = attHandle; - serviceHandle.endHandle = (attHandle + serviceHandles.length); + + endHandle = attHandle + serviceHandles.length -1; + serviceHandle.endHandle = endHandle; } else if(serviceHandle.type === 'characteristic'){ serviceHandle.startHandle = attHandle; serviceHandle.valueHandle = (attHandle + 1); @@ -241,17 +244,18 @@ Gatt.prototype.addService = function AddGATTService(service, indicate){ if (serviceHandle.uuid === '2a05'){ this._serviceChangedValueHandle = serviceHandle.valueHandle; } - } else if(serviceHandle.type === 'characteristicValue' || characteristic.type === 'descriptor'){ + } else if(serviceHandle.type === 'characteristicValue' || serviceHandle.type === 'descriptor'){ serviceHandle.handle = attHandle; } handles[attHandle] = serviceHandle; attHandle++; } - var endHandle = startHandle + serviceHandles.length; if(indicate){ this.serviceChanged(startHandle, endHandle); } + + // The instanced service should store the startHandle for removal later...if necessary. service.handle = startHandle; debug('addService: added handles ' + startHandle + '-' + endHandle); @@ -400,8 +404,8 @@ Gatt.prototype.sendATTMessage = function SendATTMessage(op, valueHandle, data){ var dataLength = Math.min(data.length, mtu - 3); var message = new Buffer(3 + dataLength); - message.writeUInt8(parseInt(op, 16), 0); - message.writeUInt16LE(parseInt(valueHandle, 16), 1); + message.writeUInt8(op, 0); + message.writeUInt16LE(valueHandle, 1); for (i = 0; i < dataLength; i++) { message[3 + i] = data[i]; @@ -418,7 +422,8 @@ Gatt.prototype.sendATTMessage = function SendATTMessage(op, valueHandle, data){ default: msg = ' '; } - debug('sendATTMessage: ' + msg + '-' + message.toString('hex')); + debug('sendATTMessage: \n\t' + msg + ': ' + message.toString('hex') + '\n\tValueHandle: ' + + valueHandle); } Gatt.prototype.errorResponse = function(opcode, handle, status) { From acf3f0faa909ee91bf92439059fc51b5677fe64f Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Tue, 13 Oct 2015 11:53:46 -0500 Subject: [PATCH 6/8] gatt - make sure that service changed is subscribed to before sending ATT message --- lib/hci-socket/gatt.js | 52 +++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/hci-socket/gatt.js b/lib/hci-socket/gatt.js index 4acdcf80..4775d4bd 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -363,6 +363,9 @@ Gatt.prototype.setServices = function SetGATTServices(services) { }; Gatt.prototype.serviceChanged = function ServiceChanged(startHandle, endHandle){ + if (!this._serviceChangedSubscribed){ + return; + } var serviceChangedValue = new Buffer(4); serviceChangedValue.writeUInt16LE(parseInt(startHandle, 16), 0); serviceChangedValue.writeUInt16LE(parseInt(endHandle, 16), 2); @@ -966,31 +969,38 @@ Gatt.prototype.handleWriteRequestOrCommand = function handleWriteRequestOrComman handle.value = data; if (value & 0x0003) { - var updateValueCallback = (function(valueHandle, attribute) { - return function(data) { - var useNotify = attribute.properties.indexOf('notify') !== -1; - var useIndicate = attribute.properties.indexOf('indicate') !== -1; - var i; - - if (useNotify) { - this.sendATTMessage(ATT_OP_HANDLE_NOTIFY, valueHandle, data); - - attribute.emit('notify'); - } else if (useIndicate) { - this.sendATTMessage(ATT_OP_HANDLE_IND, valueHandle, data); - - this._lastIndicatedAttribute = attribute; + // No need to give callback for service changed characteristic + if(handleAttribute.uuid !== '2a05'){ + var updateValueCallback = (function(valueHandle, attribute) { + return function(data) { + var useNotify = attribute.properties.indexOf('notify') !== -1; + var useIndicate = attribute.properties.indexOf('indicate') !== -1; + + if (useNotify) { + this.sendATTMessage(ATT_OP_HANDLE_NOTIFY, valueHandle, data); + + attribute.emit('notify'); + } else if (useIndicate) { + this.sendATTMessage(ATT_OP_HANDLE_IND, valueHandle, data); + + this._lastIndicatedAttribute = attribute; + } + }.bind(this); + }.bind(this))(valueHandle - 1, handleAttribute); + + if (handleAttribute.emit) { + handleAttribute.emit('subscribe', this._mtu - 3, updateValueCallback); } - }.bind(this); - }.bind(this))(valueHandle - 1, handleAttribute); - - if (handleAttribute.emit) { - handleAttribute.emit('subscribe', this._mtu - 3, updateValueCallback); + } else { + this._serviceChangedSubscribed = true; } } else { - handleAttribute.emit('unsubscribe'); + if (handleAttribute.uuid !== '2a05'){ + handleAttribute.emit('unsubscribe'); + } else { + this._serviceChangedSubscribed = false; + } } - result = ATT_ECODE_SUCCESS; } From 22d2b68087aa88e396dff2060d753d6664827244 Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Mon, 16 Nov 2015 11:04:22 -0600 Subject: [PATCH 7/8] bindings - add callback functionality --- lib/bleno.js | 23 ++++++++++++++++++----- lib/hci-socket/bindings.js | 12 ++++++++---- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/bleno.js b/lib/bleno.js index 43740cce..05623254 100644 --- a/lib/bleno.js +++ b/lib/bleno.js @@ -199,22 +199,35 @@ Bleno.prototype.onAdvertisingStop = function() { this.emit('advertisingStop'); }; -Bleno.prototype.addService = function(service) { +Bleno.prototype.addService = function(service, callback) { + if(callback){ + this.once('servicesChanged', callback); + } + this._bindings.addService(service); }; -Bleno.prototype.removeAllServices = function() { - this._bindings.removeAllServices(); +Bleno.prototype.removeAllServices = function(callback) { + if(callback){ + this.once('removedAllServices', callback); + } + + this._bindings.removeAllServices(); } -Bleno.prototype.removeService = function(service) { - this._bindings.removeService(service); +Bleno.prototype.removeService = function(service, callback) { + if(callback){ + this.once('removedService', callback); + } + + this._bindings.removeService(service); }; Bleno.prototype.setServices = function(services, callback) { if (callback) { this.once('servicesSet', callback); } + this._bindings.setServices(services); }; diff --git a/lib/hci-socket/bindings.js b/lib/hci-socket/bindings.js index 2c13b0f9..0e899a19 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -49,17 +49,21 @@ BlenoBindings.prototype.stopAdvertising = function() { }; BlenoBindings.prototype.addService = function(service) { - this._gatt.addService(service); + this._gatt.addService(service); - this.emit('servicesChanged'); + this.emit('servicesChanged'); }; BlenoBindings.prototype.removeAllServices = function() { - this._gatt.removeAllServices(); + this._gatt.removeAllServices(); + + this.emit('removedAllServices'); } BlenoBindings.prototype.removeService = function(service){ - this._gatt.removeService(service); + this._gatt.removeService(service); + + this.emit('removedService'); } BlenoBindings.prototype.setServices = function(services, deviceName) { From c3c65a1edf8a71283349f86a817be6ffbefd02b7 Mon Sep 17 00:00:00 2001 From: Bryce Jacobs Date: Tue, 17 Nov 2015 08:44:22 -0600 Subject: [PATCH 8/8] mac - add emit messages for new add / remove / removeall api --- lib/mac/bindings.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/mac/bindings.js b/lib/mac/bindings.js index 7805671f..a8865e4a 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -296,6 +296,8 @@ blenoBindings.addService = function AddGATTService(service) { attributeId++; } this.sendCBMsg(10, arg); + + this.emit('servicesChanged'); }; blenoBindings.removeService = function RemoveGATTService(service){ @@ -311,11 +313,15 @@ blenoBindings.removeService = function RemoveGATTService(service){ kCBMsgArgAttributeID: attributeId }; this.sendCBMsg(11, messageBody); + + this.emit('removedService'); }; blenoBindings.removeAllServices = function RemoveAllGATTServices(){ this._attributes = []; this.sendCBMsg(12, null); + + this.emit('removedAllServices'); }; blenoBindings.setServices = function SetGATTServices(services) {