UNPKG

@s89/ble-ancs

Version:

An Apple ANCS reciever from Linux. It is a combination of the Bleno, Noble and ANCS projects from Sandeep Mistry

1,072 lines (860 loc) 32.2 kB
var debug = require('debug')('local-gatt'); var gatt = require('./gatt'); var events = require('events'); var os = require('os'); var util = require('util'); var ATT_OP_ERROR = 0x01; var ATT_OP_MTU_REQ = 0x02; var ATT_OP_MTU_RESP = 0x03; var ATT_OP_FIND_INFO_REQ = 0x04; var ATT_OP_FIND_INFO_RESP = 0x05; var ATT_OP_FIND_BY_TYPE_REQ = 0x06; var ATT_OP_FIND_BY_TYPE_RESP = 0x07; var ATT_OP_READ_BY_TYPE_REQ = 0x08; var ATT_OP_READ_BY_TYPE_RESP = 0x09; var ATT_OP_READ_REQ = 0x0a; var ATT_OP_READ_RESP = 0x0b; var ATT_OP_READ_BLOB_REQ = 0x0c; var ATT_OP_READ_BLOB_RESP = 0x0d; var ATT_OP_READ_MULTI_REQ = 0x0e; var ATT_OP_READ_MULTI_RESP = 0x0f; var ATT_OP_READ_BY_GROUP_REQ = 0x10; var ATT_OP_READ_BY_GROUP_RESP = 0x11; var ATT_OP_WRITE_REQ = 0x12; var ATT_OP_WRITE_RESP = 0x13; var ATT_OP_WRITE_CMD = 0x52; var ATT_OP_PREP_WRITE_REQ = 0x16; var ATT_OP_PREP_WRITE_RESP = 0x17; var ATT_OP_EXEC_WRITE_REQ = 0x18; var ATT_OP_EXEC_WRITE_RESP = 0x19; var ATT_OP_HANDLE_NOTIFY = 0x1b; var ATT_OP_HANDLE_IND = 0x1d; var ATT_OP_HANDLE_CNF = 0x1e; var ATT_OP_WRITE_CMD = 0x52; var ATT_OP_SIGNED_WRITE_CMD = 0xd2; var GATT_PRIM_SVC_UUID = 0x2800; var GATT_INCLUDE_UUID = 0x2802; var GATT_CHARAC_UUID = 0x2803; var GATT_CLIENT_CHARAC_CFG_UUID = 0x2902; var GATT_SERVER_CHARAC_CFG_UUID = 0x2903; var ATT_ECODE_SUCCESS = 0x00; var ATT_ECODE_INVALID_HANDLE = 0x01; var ATT_ECODE_READ_NOT_PERM = 0x02; var ATT_ECODE_WRITE_NOT_PERM = 0x03; var ATT_ECODE_INVALID_PDU = 0x04; var ATT_ECODE_AUTHENTICATION = 0x05; var ATT_ECODE_REQ_NOT_SUPP = 0x06; var ATT_ECODE_INVALID_OFFSET = 0x07; var ATT_ECODE_AUTHORIZATION = 0x08; var ATT_ECODE_PREP_QUEUE_FULL = 0x09; var ATT_ECODE_ATTR_NOT_FOUND = 0x0a; var ATT_ECODE_ATTR_NOT_LONG = 0x0b; var ATT_ECODE_INSUFF_ENCR_KEY_SIZE = 0x0c; var ATT_ECODE_INVAL_ATTR_VALUE_LEN = 0x0d; var ATT_ECODE_UNLIKELY = 0x0e; var ATT_ECODE_INSUFF_ENC = 0x0f; var ATT_ECODE_UNSUPP_GRP_TYPE = 0x10; var ATT_ECODE_INSUFF_RESOURCES = 0x11; var GATT_PRIM_SVC_UUID = 0x2800; var GATT_INCLUDE_UUID = 0x2802; var GATT_CHARAC_UUID = 0x2803; var GATT_CLIENT_CHARAC_CFG_UUID = 0x2902; var GATT_SERVER_CHARAC_CFG_UUID = 0x2903; var ATT_CID = 0x0004; var LocalGatt = function(aclStream) { this._mtu = 23; this._aclStream = aclStream; this.onAclStreamDataBinded = this.onAclStreamData.bind(this); this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); }; util.inherits(LocalGatt, events.EventEmitter); /* BLENO */ LocalGatt.prototype.setServices = function(services) { var deviceName = "ble-ancs"; //process.env.BLENO_DEVICE_NAME || os.hostname(); // base services and characteristics var allServices = [ { uuid: '1800', characteristics: [ { uuid: '2a00', properties: ['read'], secure: ['read'], value: new Buffer(deviceName), descriptors: [] }, { uuid: '2a01', properties: ['read'], secure: [], value: new Buffer([0x80, 0x00]), descriptors: [] } ] }, { uuid: '1801', characteristics: [ { uuid: '2a05', properties: ['indicate'], secure: ['indicate'], value: new Buffer([0x00, 0x00, 0x00, 0x00]), descriptors: [] } ] } ].concat(services); this._handles = []; var handle = 0; var i; var j; for (i = 0; i < allServices.length; i++) { var service = allServices[i]; handle++; var serviceHandle = handle; this._handles[serviceHandle] = { type: 'service', uuid: service.uuid, attribute: service, startHandle: serviceHandle // endHandle filled in below }; for (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; } } if (characteristic.properties.indexOf('writeWithoutResponse') !== -1) { properties |= 0x04; if (characteristic.secure.indexOf('writeWithoutResponse') !== -1) { secure |= 0x04; } } if (characteristic.properties.indexOf('write') !== -1) { properties |= 0x08; if (characteristic.secure.indexOf('write') !== -1) { secure |= 0x08; } } if (characteristic.properties.indexOf('notify') !== -1) { properties |= 0x10; if (characteristic.secure.indexOf('notify') !== -1) { secure |= 0x10; } } if (characteristic.properties.indexOf('indicate') !== -1) { properties |= 0x20; if (characteristic.secure.indexOf('indicate') !== -1) { secure |= 0x20; } } handle++; var characteristicHandle = handle; handle++; var characteristicValueHandle = 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]) }; } 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 }; } } this._handles[serviceHandle].endHandle = handle; } var debugHandles = []; for (i = 0; i < this._handles.length; i++) { handle = this._handles[i]; debugHandles[i] = {}; for(j in handle) { if (Buffer.isBuffer(handle[j])) { debugHandles[i][j] = handle[j] ? 'Buffer(\'' + handle[j].toString('hex') + '\', \'hex\')' : null; } else if (j !== 'attribute') { debugHandles[i][j] = handle[j]; } } } debug('handles = ' + JSON.stringify(debugHandles, null, 2)); }; /* BLENO end */ LocalGatt.prototype.onAclStreamData = function(cid, request) { if (cid !== ATT_CID) { return; } //debug('handing request: ' + request.toString('hex')); var requestType = request[0]; var response = null; switch(requestType) { case ATT_OP_ERROR: debug('ATT_OP_ERROR: '); this.handleOpError(request); break; case ATT_OP_MTU_REQ: debug('ATT_OP_MTU_REQ: '); response = this.handleMtuRequest(request); break; case ATT_OP_FIND_INFO_REQ: debug('ATT_OP_FIND_INFO_REQ: '); response = this.handleFindInfoRequest(request); break; case ATT_OP_FIND_BY_TYPE_REQ: debug('ATT_OP_FIND_BY_TYPE_REQ: '); response = this.handleFindByTypeRequest(request); break; case ATT_OP_READ_BY_TYPE_REQ: debug('ATT_OP_READ_BY_TYPE_REQ:'); response = this.handleReadByTypeRequest(request); break; case ATT_OP_READ_REQ: case ATT_OP_READ_BLOB_REQ: debug('ATT_OP_READ_REQ: '); response = this.handleReadOrReadBlobRequest(request); break; case ATT_OP_READ_BY_GROUP_REQ: debug('ATT_OP_READ_BY_GROUP_REQ: '); response = this.handleReadByGroupRequest(request); break; case ATT_OP_WRITE_REQ: case ATT_OP_WRITE_CMD: debug('ATT_OP_WRITE_REQ: '); response = this.handleWriteRequestOrCommand(request); break; case ATT_OP_HANDLE_CNF: debug('ATT_OP_HANDLE_CNF: '); response = this.handleConfirmation(request); break; default: case ATT_OP_READ_MULTI_REQ: case ATT_OP_PREP_WRITE_REQ: case ATT_OP_EXEC_WRITE_REQ: case ATT_OP_SIGNED_WRITE_CMD: //response = this.errorResponse(requestType, 0x0000, ATT_ECODE_REQ_NOT_SUPP); break; } if (response) { debug('response: ' + response.toString('hex')); this.writeAtt(response); } }; LocalGatt.prototype.setAclStream = function(aclStream) { this._aclStream = aclStream; this._aclStream.on('data', this.onAclStreamDataBinded); this._aclStream.on('end', this.onAclStreamEndBinded); }; LocalGatt.prototype.onAclStreamEnd = function() { this._aclStream.removeListener('data', this.onAclStreamDataBinded); this._aclStream.removeListener('end', this.onAclStreamEndBinded); }; LocalGatt.prototype.errorResponse = function(opcode, handle, status) { var buf = new Buffer(5); buf.writeUInt8(ATT_OP_ERROR, 0); buf.writeUInt8(opcode, 1); buf.writeUInt16LE(handle, 2); buf.writeUInt8(status, 4); return buf; }; LocalGatt.prototype.writeAtt = function(data) { this._aclStream.write(ATT_CID, data); }; LocalGatt.prototype.handleConfirmation = function() { var buf = new Buffer(1); buf.writeUInt8(ATT_OP_HANDLE_CNF, 0); return buf; }; LocalGatt.prototype.handleMtuRequest = function(request) { var mtu = request.readUInt16LE(1); if (mtu < 23) { mtu = 23; } else if (mtu > 256) { mtu = 256; } this._mtu = mtu; this.emit('handleMtuRequest', 0, this._mtu); var response = new Buffer(3); debug("Response - Sending MTU: " + mtu); response.writeUInt8(ATT_OP_MTU_RESP, 0); response.writeUInt16LE(mtu, 1); return response; }; LocalGatt.prototype.handleOpError = function(data) { var opcode = data[0]; if (opcode === ATT_OP_MTU_RESP) { var errOpCode = data[1]; switch(errOpCode) { case ATT_OP_ERROR: debug('\tOpCode in Error: ATT_OP_ERROR: '); break; case ATT_OP_READ_BY_TYPE_RESP: debug('\tOpCode in Error: ATT_OP_READ_BY_TYPE_RESP: '); break; case ATT_OP_READ_BY_GROUP_RESP: debug('\tOpCode in Error: ATT_OP_READ_BY_GROUP_RESP: '); break; case ATT_OP_MTU_REQ: debug('\tOpCode in Error: ATT_OP_MTU_REQ: '); break; case ATT_OP_MTU_RESP: debug("\tOpCode in Error: ATT_OP_MTU_RESP"); break; case ATT_OP_FIND_INFO_REQ: debug('\tOpCode in Error: ATT_OP_FIND_INFO_REQ: '); break; case ATT_OP_FIND_BY_TYPE_REQ: debug('\tOpCode in Error: ATT_OP_FIND_BY_TYPE_REQ: '); break; case ATT_OP_READ_BY_TYPE_REQ: debug('\tOpCode in Error: ATT_OP_READ_BY_TYPE_REQ:'); break; case ATT_OP_READ_REQ: case ATT_OP_READ_BLOB_REQ: debug('\tOpCode in Error: ATT_OP_READ_REQ: '); break; case ATT_OP_READ_BY_GROUP_REQ: debug('\tOpCode in Error: ATT_OP_READ_BY_GROUP_REQ: '); break; case ATT_OP_WRITE_REQ: case ATT_OP_WRITE_CMD: debug('\tOpCode in Error: ATT_OP_WRITE_REQ: '); break; case ATT_OP_HANDLE_CNF: debug('\tOpCode in Error: ATT_OP_HANDLE_CNF: '); break; case ATT_OP_HANDLE_NOTIFY: case ATT_OP_HANDLE_IND: debug('\tOpCode in Error: ATT_OP_HANDLE_NOTIFY: '); break; default: debug("\tOpCode in Error: " + errOpCode) } debug("\tHandle: " + request.readUInt16LE(2)); var errorCode = data[5]; switch(errorCode) { case ATT_ECODE_SUCCESS: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INVALID_HANDLE: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_READ_NOT_PERM: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_WRITE_NOT_PERM: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INVALID_PDU: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_AUTHENTICATION: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_REQ_NOT_SUPP: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INVALID_OFFSET: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_AUTHORIZATION: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_PREP_QUEUE_FULL: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_ATTR_NOT_FOUND: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_ATTR_NOT_LONG: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INSUFF_ENCR_KEY_SIZE: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INVAL_ATTR_VALUE_LEN: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_UNLIKELY: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INSUFF_ENC: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_UNSUPP_GRP_TYPE: debug('\tError Code: ATT_OP_READ_REQ: '); break; case ATT_ECODE_INSUFF_RESOURCES: debug('\tError Code: ATT_OP_READ_REQ: '); break; default: debug('\tError Code: ' + errorCode); break; } } }; LocalGatt.prototype.handleFindInfoRequest = function(request) { var response = null; var startHandle = request.readUInt16LE(1); var endHandle = request.readUInt16LE(3); var infos = []; var uuid = null; for (i = startHandle; i <= endHandle; i++) { var handle = this._handles[i]; if (!handle) { break; } uuid = null; if ('service' === handle.type) { uuid = '2800'; } else if ('includedService' === handle.type) { uuid = '2802'; } else if ('characteristic' === handle.type) { uuid = '2803'; } else if ('characteristicValue' === handle.type) { uuid = this._handles[i - 1].uuid; } else if ('descriptor' === handle.type) { uuid = handle.uuid; } if (uuid) { infos.push({ handle: i, uuid: uuid }); } } if (infos.length) { var uuidSize = infos[0].uuid.length / 2; var numInfo = 1; for (i = 1; i < infos.length; i++) { if (infos[0].uuid.length !== infos[i].uuid.length) { break; } numInfo++; } var lengthPerInfo = (uuidSize === 2) ? 4 : 18; var maxInfo = Math.floor((this._mtu - 2) / lengthPerInfo); numInfo = Math.min(numInfo, maxInfo); response = new Buffer(2 + numInfo * lengthPerInfo); response[0] = ATT_OP_FIND_INFO_RESP; response[1] = (uuidSize === 2) ? 0x01 : 0x2; for (i = 0; i < numInfo; i++) { var info = infos[i]; response.writeUInt16LE(info.handle, 2 + i * lengthPerInfo); uuid = new Buffer(info.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (var j = 0; j < uuid.length; j++) { response[2 + i * lengthPerInfo + 2 + j] = uuid[j]; } } } else { debug("No Info Found"); response = this.errorResponse(ATT_OP_FIND_INFO_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } return response; }; LocalGatt.prototype.handleFindByTypeRequest = function(request) { var response = null; var startHandle = request.readUInt16LE(1); var endHandle = request.readUInt16LE(3); var uuid = request.slice(5, 7).toString('hex').match(/.{1,2}/g).reverse().join(''); var value = request.slice(7).toString('hex').match(/.{1,2}/g).reverse().join(''); var handles = []; var handle; for (var i = startHandle; i <= endHandle; i++) { handle = this._handles[i]; if (!handle) { break; } if ('2800' === uuid && handle.type === 'service' && handle.uuid === value) { handles.push({ start: handle.startHandle, end: handle.endHandle }); } } if (handles.length) { var lengthPerHandle = 4; var numHandles = handles.length; var maxHandles = Math.floor((this._mtu - 1) / lengthPerHandle); numHandles = Math.min(numHandles, maxHandles); response = new Buffer(1 + numHandles * lengthPerHandle); response[0] = ATT_OP_FIND_BY_TYPE_RESP; for (i = 0; i < numHandles; i++) { handle = handles[i]; response.writeUInt16LE(handle.start, 1 + i * lengthPerHandle); response.writeUInt16LE(handle.end, 1 + i * lengthPerHandle + 2); } } else { debug("Nothing found for Type Request"); response = this.errorResponse(ATT_OP_FIND_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } return response; }; LocalGatt.prototype.handleReadByGroupRequest = function(request) { var response = null; var startHandle = request.readUInt16LE(1); var endHandle = request.readUInt16LE(3); var uuid = request.slice(5).toString('hex').match(/.{1,2}/g).reverse().join(''); debug('Handle read by group request: startHandle = 0x' + startHandle.toString(16) + ', endHandle = 0x' + endHandle.toString(16) + ', uuid = 0x' + uuid.toString(16)); if ('2800' === uuid || '2802' === uuid) { var services = []; var type = ('2800' === uuid) ? 'service' : 'includedService'; var i; for (i = startHandle; i <= endHandle; i++) { var handle = this._handles[i]; if (!handle) { debug('\t\tNo handle for: ' +i); break; } if (handle.type === type) { services.push(handle); } else { debug('\t\t Handle: ' + handle.startHandle + " UUID: " + handle.uuid + " Service Type: " + handle.type); } } if (services.length) { var uuidSize = services[0].uuid.length / 2; var numServices = 1; for (i = 1; i < services.length; i++) { if (services[0].uuid.length !== services[i].uuid.length) { debug('\t\tUUID length wrong for: ' +i + " UUID: " + services[i].uuid); break; } numServices++; } var lengthPerService = (uuidSize === 2) ? 6 : 20; var maxServices = Math.floor((this._mtu - 2) / lengthPerService); debug('\t\tFound ' + numServices + ' services, max is: ' + maxServices); numServices = Math.min(numServices, maxServices); response = new Buffer(2 + numServices * lengthPerService); response[0] = ATT_OP_READ_BY_GROUP_RESP; response[1] = lengthPerService; for (i = 0; i < numServices; i++) { var service = services[i]; response.writeUInt16LE(service.startHandle, 2 + i * lengthPerService); response.writeUInt16LE(service.endHandle, 2 + i * lengthPerService + 2); var serviceUuid = new Buffer(service.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (var j = 0; j < serviceUuid.length; j++) { response[2 + i * lengthPerService + 4 + j] = serviceUuid[j]; } } } else { debug("Nothing found for group"); response = this.errorResponse(ATT_OP_READ_BY_GROUP_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } } else { debug("UUID Not supported"); response = this.errorResponse(ATT_OP_READ_BY_GROUP_REQ, startHandle, ATT_ECODE_UNSUPP_GRP_TYPE); } return response; }; LocalGatt.prototype.handleReadByTypeRequest = function(request) { var response = null; var startHandle = request.readUInt16LE(1); var endHandle = request.readUInt16LE(3); var uuid = request.slice(5).toString('hex').match(/.{1,2}/g).reverse().join(''); var i; var handle; debug('read by type: startHandle = 0x' + startHandle.toString(16) + ', endHandle = 0x' + endHandle.toString(16) + ', uuid = 0x' + uuid.toString(16)); if ('2803' === uuid) { var characteristics = []; for (i = startHandle; i <= endHandle; i++) { handle = this._handles[i]; if (!handle) { break; } if (handle.type === 'characteristic') { characteristics.push(handle); } } if (characteristics.length) { var uuidSize = characteristics[0].uuid.length / 2; var numCharacteristics = 1; for (i = 1; i < characteristics.length; i++) { if (characteristics[0].uuid.length !== characteristics[i].uuid.length) { break; } numCharacteristics++; } var lengthPerCharacteristic = (uuidSize === 2) ? 7 : 21; var maxCharacteristics = Math.floor((this._mtu - 2) / lengthPerCharacteristic); numCharacteristics = Math.min(numCharacteristics, maxCharacteristics); response = new Buffer(2 + numCharacteristics * lengthPerCharacteristic); response[0] = ATT_OP_READ_BY_TYPE_RESP; response[1] = lengthPerCharacteristic; for (i = 0; i < numCharacteristics; i++) { var characteristic = characteristics[i]; debug('\t\tSending Charecteristic: \t' + i); debug('\t\tStart handle: \t' + characteristic.startHandle.toString(16) ); debug('\t\tProperties: \t' + characteristic.properties.toString(16) ); debug('\t\tValue Handle: \t' + characteristic.valueHandle.toString(16) ); response.writeUInt16LE(characteristic.startHandle, 2 + i * lengthPerCharacteristic); response.writeUInt8(characteristic.properties, 2 + i * lengthPerCharacteristic + 2); response.writeUInt16LE(characteristic.valueHandle, 2 + i * lengthPerCharacteristic + 3); var characteristicUuid = new Buffer(characteristic.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (var j = 0; j < characteristicUuid.length; j++) { response[2 + i * lengthPerCharacteristic + 5 + j] = characteristicUuid[j]; } } } else { debug("No Charecteristics found"); response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } } else { var valueHandle = null; var secure = false; for (i = startHandle; i <= endHandle; i++) { handle = this._handles[i]; if (!handle) { break; } if (handle.type === 'characteristic' && handle.uuid === uuid) { debug('\t\tResponding with Charecteristic'); valueHandle = handle.valueHandle; secure = handle.secure & 0x02; break; } else if (handle.type === 'descriptor' && handle.uuid === uuid) { debug('\t\tResponding with Descriptor'); valueHandle = i; secure = handle.secure & 0x02; break; } } if (secure && !this._aclStream.encrypted) { debug("ATT_ECODE_AUTHENTICATION"); response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_AUTHENTICATION); } else if (valueHandle) { var data = this._handles[valueHandle].value; var dataLength = Math.min(data.length, this._mtu - 4); response = new Buffer(4 + dataLength); response[0] = ATT_OP_READ_BY_TYPE_RESP; response[1] = dataLength + 2; response.writeUInt16LE(valueHandle, 2); debug('\t\tValue handle: \t0x' + valueHandle.toString(16) ); for (i = 0; i < dataLength; i++) { response[i + 4] = data[i]; } } else { debug("ATT_ECODE_ATTR_NOT_FOUND"); response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } } return response; }; LocalGatt.prototype.handleReadOrReadBlobRequest = function(request) { var response = null; var requestType = request[0]; var valueHandle = request.readUInt16LE(1); var offset = (requestType === ATT_OP_READ_BLOB_REQ) ? request.readUInt16LE(3) : 0; var handle = this._handles[valueHandle]; if (handle) { var result = null; var data = null; var handleType = handle.type; var callback = (function(requestType, valueHandle) { return function(result, data) { var callbackResponse = null; if (ATT_ECODE_SUCCESS === result) { var dataLength = Math.min(data.length, this._mtu - 1); callbackResponse = new Buffer(1 + dataLength); callbackResponse[0] = (requestType === ATT_OP_READ_BLOB_REQ) ? ATT_OP_READ_BLOB_RESP : ATT_OP_READ_RESP; for (i = 0; i < dataLength; i++) { callbackResponse[1 + i] = data[i]; } } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('read response: ' + callbackResponse.toString('hex')); this.writeAtt(callbackResponse); }.bind(this); }.bind(this))(requestType, valueHandle); if (handleType === 'service' || handleType === 'includedService') { result = ATT_ECODE_SUCCESS; data = new Buffer(handle.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); } else if (handleType === 'characteristic') { var uuid = new Buffer(handle.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); result = ATT_ECODE_SUCCESS; data = new Buffer(3 + uuid.length); data.writeUInt8(handle.properties, 0); data.writeUInt16LE(handle.valueHandle, 1); for (i = 0; i < uuid.length; i++) { data[i + 3] = uuid[i]; } } else if (handleType === 'characteristicValue' || handleType === 'descriptor') { var handleProperties = handle.properties; var handleSecure = handle.secure; var handleAttribute = handle.attribute; if (handleType === 'characteristicValue') { handleProperties = this._handles[valueHandle - 1].properties; handleSecure = this._handles[valueHandle - 1].secure; handleAttribute = this._handles[valueHandle - 1].attribute; } if (handleProperties & 0x02) { if (handleSecure & 0x02 && !this._aclStream.encrypted) { result = ATT_ECODE_AUTHENTICATION; } else { data = handle.value; if (data) { result = ATT_ECODE_SUCCESS; } else { handleAttribute.emit('readRequest', offset, callback); } } } else { result = ATT_ECODE_READ_NOT_PERM; // non-readable } } if (data && typeof data === 'string') { data = new Buffer(data); } if (result === ATT_ECODE_SUCCESS && data && offset) { if (data.length < offset) { errorCode = ATT_ECODE_INVALID_OFFSET; data = null; } else { data = data.slice(offset); } } if (result !== null) { callback(result, data); } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_INVALID_HANDLE); } return response; }; LocalGatt.prototype.handleWriteRequestOrCommand = function(request) { var response = null; var requestType = request[0]; var withoutResponse = (requestType === ATT_OP_WRITE_CMD); var valueHandle = request.readUInt16LE(1); var data = request.slice(3); var offset = 0; var handle = this._handles[valueHandle]; if (handle) { if (handle.type === 'characteristicValue') { handle = this._handles[valueHandle - 1]; } var handleProperties = handle.properties; var handleSecure = handle.secure; if (handleProperties && (withoutResponse ? (handleProperties & 0x04) : (handleProperties & 0x08))) { var callback = (function(requestType, valueHandle, withoutResponse) { return function(result) { if (!withoutResponse) { var callbackResponse = null; if (ATT_ECODE_SUCCESS === result) { callbackResponse = new Buffer([ATT_OP_WRITE_RESP]); } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('write response: ' + callbackResponse.toString('hex')); this.writeAtt(callbackResponse); } }.bind(this); }.bind(this))(requestType, valueHandle, withoutResponse); if (handleSecure & (withoutResponse ? 0x04 : 0x08) && !this._aclStream.encrypted) { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_AUTHENTICATION); } else if (handle.type === 'descriptor' || handle.uuid === '2902') { var result = null; if (data.length !== 2) { result = ATT_ECODE_INVAL_ATTR_VALUE_LEN; } else { var value = data.readUInt16LE(0); var handleAttribute = handle.attribute; 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.writeAtt(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.writeAtt(indicateMessage); } }.bind(this); }.bind(this))(valueHandle - 1, handleAttribute); if (handleAttribute.emit) { handleAttribute.emit('subscribe', this._mtu - 3, updateValueCallback); } } else { handleAttribute.emit('unsubscribe'); } result = ATT_ECODE_SUCCESS; } callback(result); } else { handle.attribute.emit('writeRequest', data, offset, withoutResponse, callback); } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_WRITE_NOT_PERM); } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_INVALID_HANDLE); } return response; }; LocalGatt.prototype.handleConfirmation = function(request) { if (this._lastIndicatedAttribute) { if (this._lastIndicatedAttribute.emit) { this._lastIndicatedAttribute.emit('indicate'); } this._lastIndicatedAttribute = null; } }; module.exports = LocalGatt;