diff --git a/examples/pizza/peripheral.js b/examples/pizza/peripheral.js index b186a6a8..e7d561ef 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. @@ -53,8 +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 - ]); + 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/bleno.js b/lib/bleno.js index fd1b0a03..05623254 100644 --- a/lib/bleno.js +++ b/lib/bleno.js @@ -199,10 +199,35 @@ Bleno.prototype.onAdvertisingStop = function() { this.emit('advertisingStop'); }; +Bleno.prototype.addService = function(service, callback) { + if(callback){ + this.once('servicesChanged', callback); + } + + this._bindings.addService(service); +}; + +Bleno.prototype.removeAllServices = function(callback) { + if(callback){ + this.once('removedAllServices', callback); + } + + this._bindings.removeAllServices(); +} + +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 6b068a52..0e899a19 100644 --- a/lib/hci-socket/bindings.js +++ b/lib/hci-socket/bindings.js @@ -48,7 +48,25 @@ 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(); + + this.emit('removedAllServices'); +} + +BlenoBindings.prototype.removeService = function(service){ + this._gatt.removeService(service); + + this.emit('removedService'); +} + +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..4775d4bd 100644 --- a/lib/hci-socket/gatt.js +++ b/lib/hci-socket/gatt.js @@ -72,175 +72,285 @@ var Gatt = function() { util.inherits(Gatt, events.EventEmitter); -Gatt.prototype.setServices = function(services) { - var deviceName = process.env.BLENO_DEVICE_NAME || os.hostname(); +Gatt.prototype.addService = function AddGATTService(service, indicate){ + var handles = this._handles; + indicate = indicate === undefined ? true : indicate; + + var serviceHandles = []; + serviceHandles.push({ + type: 'service', + uuid: service.uuid, + attribute: service, + startHandle: null + // endHandle filled in below + }); + + for (var j = 0; j < service.characteristics.length; j++) { + var characteristic = service.characteristics[j]; + + var properties = 0; + var secure = 0; + + if (characteristic.properties.indexOf('read') !== -1) { + properties |= 0x02; + + if (characteristic.secure.indexOf('read') !== -1) { + secure |= 0x02; + } + } - // 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: [] - } - ] + if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { + properties |= 0x04; + + if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { + secure |= 0x04; + } } - ].concat(services); - this._handles = []; + if (characteristic.properties.indexOf('write') !== -1) { + properties |= 0x08; - var handle = 0; - var i; - var j; + if (characteristic.secure.indexOf('write') !== -1) { + secure |= 0x08; + } + } - for (i = 0; i < allServices.length; i++) { - var service = allServices[i]; + if (characteristic.properties.indexOf('notify') !== -1) { + properties |= 0x10; - handle++; - var serviceHandle = handle; + if (characteristic.secure.indexOf('notify') !== -1) { + secure |= 0x10; + } + } - this._handles[serviceHandle] = { - type: 'service', - uuid: service.uuid, - attribute: service, - startHandle: serviceHandle - // endHandle filled in below - }; + if (characteristic.properties.indexOf('indicate') !== -1) { + properties |= 0x20; - for (j = 0; j < service.characteristics.length; j++) { - var characteristic = service.characteristics[j]; + if (characteristic.secure.indexOf('indicate') !== -1) { + secure |= 0x20; + } + } - var properties = 0; - var secure = 0; + serviceHandles.push({ + type: 'characteristic', + uuid: characteristic.uuid, + properties: properties, + secure: secure, + attribute: characteristic, + startHandle: null, + valueHandle: null + }); + + serviceHandles.push({ + type: 'characteristicValue', + handle: null, + value: characteristic.value + }); + + if (properties & 0x30) { // notify or indicate + // add client characteristic configuration descriptor + serviceHandles.push({ + type: 'descriptor', + handle: null, + uuid: '2902', + attribute: characteristic, + properties: (0x02 | 0x04 | 0x08), // read/write + secure: (secure & 0x10) ? (0x02 | 0x04 | 0x08) : 0, + value: new Buffer([0x00, 0x00]) + }); + } - if (characteristic.properties.indexOf('read') !== -1) { - properties |= 0x02; + var descriptors = characteristic.descriptors; + for (var k = 0, kk = descriptors.length; k < kk; k++) { + var descriptor = descriptors[k]; + serviceHandles.push({ + type: 'descriptor', + handle: null, + uuid: descriptor.uuid, + attribute: descriptor, + properties: 0x02, // read only + secure: 0x00, + value: descriptor.value + }); + } + } - if (characteristic.secure.indexOf('read') !== -1) { - secure |= 0x02; + 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; } } - if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { - properties |= 0x04; + 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; + } + } + } - if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { - secure |= 0x04; - } + if(!attHandle){ + throw new Error('Not enough space to add service.'); } + } else { + /** + * 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; + } - if (characteristic.properties.indexOf('write') !== -1) { - properties |= 0x08; + var startHandle = attHandle; + var endHandle = null; + for(var l = 0, ll = serviceHandles.length; l < ll; l++){ + var serviceHandle = serviceHandles[l]; - if (characteristic.secure.indexOf('write') !== -1) { - secure |= 0x08; - } - } + if (serviceHandle.type === 'service'){ + serviceHandle.startHandle = attHandle; - if (characteristic.properties.indexOf('notify') !== -1) { - properties |= 0x10; + endHandle = attHandle + serviceHandles.length -1; + serviceHandle.endHandle = endHandle; + } else if(serviceHandle.type === 'characteristic'){ + serviceHandle.startHandle = attHandle; + serviceHandle.valueHandle = (attHandle + 1); - if (characteristic.secure.indexOf('notify') !== -1) { - secure |= 0x10; + if (serviceHandle.uuid === '2a05'){ + this._serviceChangedValueHandle = serviceHandle.valueHandle; } - } + } else if(serviceHandle.type === 'characteristicValue' || serviceHandle.type === 'descriptor'){ + serviceHandle.handle = attHandle; + } + handles[attHandle] = serviceHandle; + attHandle++; + } - if (characteristic.properties.indexOf('indicate') !== -1) { - properties |= 0x20; + if(indicate){ + this.serviceChanged(startHandle, endHandle); + } - if (characteristic.secure.indexOf('indicate') !== -1) { - secure |= 0x20; - } - } + // The instanced service should store the startHandle for removal later...if necessary. + service.handle = startHandle; + + debug('addService: added handles ' + startHandle + '-' + endHandle); +}; - handle++; - var characteristicHandle = handle; +Gatt.prototype.removeAllServices = function RemoveAllGATTServices(){ + 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 { + handle = {}; + } + } - handle++; - var characteristicValueHandle = handle; + // Make the central rescan all services Reference Bluetooth Spec Vol.3.G 2.5.2 + this.serviceChanged(0x0001, 0xFFFF); + debug('removedAllServices'); +}; - 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]) - }; - } +Gatt.prototype.removeService = function RemoveGattService(service){ + var handle = service.handle; + var handles = this._handles; - 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 - }; + var startHandle = handles[handle].startHandle; + var endHandle = handles[handle].endHandle; + for(var handle = startHandle; handle <= endHandle; handle++){ + // Don't slice, indexing will be corrupted. + handles[handle] = {}; + } + this.serviceChanged(startHandle, endHandle); + + debug('removeService - removed handles: ' + startHandle + '-' + endHandle); +}; + +/** + * 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(); + services = services || []; + + // base services and characteristics + 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 = []; - this._handles[serviceHandle].endHandle = handle; + for (var i = 0, ii = services.length; i < ii; i++) { + var service = services[i]; + this.addService(service, false); } 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') { @@ -252,6 +362,19 @@ Gatt.prototype.setServices = function(services) { debug('handles = ' + JSON.stringify(debugHandles, null, 2)); }; +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); + 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; @@ -273,10 +396,39 @@ 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.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(op, 0); + message.writeUInt16LE(valueHandle, 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: \n\t' + msg + ': ' + message.toString('hex') + '\n\tValueHandle: ' + + valueHandle); +} + Gatt.prototype.errorResponse = function(opcode, handle, status) { var buf = new Buffer(5); @@ -764,7 +916,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]; @@ -817,52 +969,38 @@ Gatt.prototype.handleWriteRequestOrCommand = function(request) { handle.value = data; 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]; - } - - 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]; - } - - this._lastIndicatedAttribute = attribute; - - debug('indicate message: ' + indicateMessage.toString('hex')); - this.send(indicateMessage); + // 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; } 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..a8865e4a 100644 --- a/lib/mac/bindings.js +++ b/lib/mac/bindings.js @@ -149,110 +149,194 @@ blenoBindings.on('kCBMsgId17', function(args) { this.emit('advertisingStop'); }); -blenoBindings.setServices = function(services) { - this.sendCBMsg(12, null); // remove all services +blenoBindings.addService = function AddGATTService(service) { + 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; + } + } - services = services || []; - var attributeId = 1; + if(groupedOpenings[attributeCount].start){ + attributeId = groupedOpenings[attributeCount].start; + } else { + for(var openingCount in groupedOpenings){ + if(parseInt(openingCount, 10) > attributeCount){ + attributeId = groupedOpenings[openingCount].start; + break; + } + } + } - this._attributes = []; - this._setServicesError = undefined; + if(!attributeId){ + throw new Error('Not enough space to add service.'); + } + } else { + attributeId = attributes.length; + } - 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; + service.attributeId = attributeId; - var arg = { - kCBMsgArgAttributeID: attributeId, - kCBMsgArgAttributeIDs: [], - kCBMsgArgCharacteristics: [], - kCBMsgArgType: 1, // 1 => primary, 0 => included - kCBMsgArgUUID: new Buffer(service.uuid, 'hex') - }; + this._lastServiceAttributeId = attributeId; + attributeId++; - this._attributes[attributeId] = service; + for (var j = 0; j < service.characteristics.length; j++) { + var characteristic = service.characteristics[j]; - this._lastServiceAttributeId = attributeId; - attributeId++; + var properties = 0; + var permissions = 0; - for (var j = 0; j < service.characteristics.length; j++) { - var characteristic = service.characteristics[j]; + if (characteristic.properties.indexOf('read') !== -1) { + properties |= 0x02; - var properties = 0; - var permissions = 0; + if (characteristic.secure.indexOf('read') !== -1) { + permissions |= 0x04; + } else { + permissions |= 0x01; + } + } - if (characteristic.properties.indexOf('read') !== -1) { - properties |= 0x02; + if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { + properties |= 0x04; - if (characteristic.secure.indexOf('read') !== -1) { - permissions |= 0x04; - } else { - permissions |= 0x01; - } - } + if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { + permissions |= 0x08; + } else { + permissions |= 0x02; + } + } - if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { - properties |= 0x04; + if (characteristic.properties.indexOf('write') !== -1) { + properties |= 0x08; - if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { - permissions |= 0x08; - } else { - permissions |= 0x02; - } - } + if (characteristic.secure.indexOf('write') !== -1) { + permissions |= 0x08; + } else { + permissions |= 0x02; + } + } - if (characteristic.properties.indexOf('write') !== -1) { - properties |= 0x08; + if (characteristic.properties.indexOf('notify') !== -1) { + if (characteristic.secure.indexOf('notify') !== -1) { + properties |= 0x100; + } else { + properties |= 0x10; + } + } - if (characteristic.secure.indexOf('write') !== -1) { - permissions |= 0x08; - } else { - permissions |= 0x02; - } - } + if (characteristic.properties.indexOf('indicate') !== -1) { + if (characteristic.secure.indexOf('indicate') !== -1) { + properties |= 0x200; + } else { + properties |= 0x20; + } + } - if (characteristic.properties.indexOf('notify') !== -1) { - if (characteristic.secure.indexOf('notify') !== -1) { - properties |= 0x100; - } else { - properties |= 0x10; - } - } + var characteristicArg = { + kCBMsgArgAttributeID: attributeId, + kCBMsgArgAttributePermissions: permissions, + kCBMsgArgCharacteristicProperties: properties, + kCBMsgArgData: characteristic.value, + kCBMsgArgDescriptors: [], + 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]; + + characteristicArg.kCBMsgArgDescriptors.push({ + kCBMsgArgData: descriptor.value, + kCBMsgArgUUID: new Buffer(descriptor.uuid, 'hex') + }); + } + arg.kCBMsgArgCharacteristics.push(characteristicArg); + attributeId++; + } + this.sendCBMsg(10, arg); - if (characteristic.properties.indexOf('indicate') !== -1) { - if (characteristic.secure.indexOf('indicate') !== -1) { - properties |= 0x200; - } else { - properties |= 0x20; - } - } + this.emit('servicesChanged'); +}; + +blenoBindings.removeService = function RemoveGATTService(service){ + 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.sendCBMsg(11, messageBody); - var characteristicArg = { - kCBMsgArgAttributeID: attributeId, - kCBMsgArgAttributePermissions: permissions, - kCBMsgArgCharacteristicProperties: properties, - kCBMsgArgData: characteristic.value, - kCBMsgArgDescriptors: [], - kCBMsgArgUUID: new Buffer(characteristic.uuid, 'hex') - }; + this.emit('removedService'); +}; - this._attributes[attributeId] = characteristic; +blenoBindings.removeAllServices = function RemoveAllGATTServices(){ + this._attributes = []; + this.sendCBMsg(12, null); - for (var k = 0; k < characteristic.descriptors.length; k++) { - var descriptor = characteristic.descriptors[k]; + this.emit('removedAllServices'); +}; - characteristicArg.kCBMsgArgDescriptors.push({ - kCBMsgArgData: descriptor.value, - kCBMsgArgUUID: new Buffer(descriptor.uuid, 'hex') - }); - } +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 +353,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!';