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

981 lines (790 loc) 32 kB
var debug = require('debug')('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 ATT_CID = 0x0004; var Gatt = function(address, aclStream) { this._address = address; this._aclStream = aclStream; this._services = {}; this._characteristics = {}; this._descriptors = {}; this._currentCommand = null; this._commandQueue = []; this._mtu = 23; this._security = 'low'; this.onAclStreamDataBinded = this.onAclStreamData.bind(this); this.onAclStreamEncryptBinded = this.onAclStreamEncrypt.bind(this); this.onAclStreamEncryptFailBinded = this.onAclStreamEncryptFail.bind(this); this.onAclStreamEndBinded = this.onAclStreamEnd.bind(this); this._aclStream.on('data', this.onAclStreamDataBinded); this._aclStream.on('encrypt', this.onAclStreamEncryptBinded); this._aclStream.on('encryptFail', this.onAclStreamEncryptFailBinded); this._aclStream.on('end', this.onAclStreamEndBinded); }; util.inherits(Gatt, events.EventEmitter); Gatt.prototype.fromJson = function(json) { this._address = json.address; this._services = json.services; this._characteristics = json.characteristics; this._descriptors = json.descriptors; this._mtu = json.mtu; this._security = json.security; } Gatt.prototype.toJson = function() { var output; output.address = this._address; output.services = this._services; output.characteristics = this._characteristics; output.descriptors = this._descriptors; output.mtu = this._mtu; output.security = this._security; return output; } Gatt.prototype.onAclStreamData = function(cid, data) { if (cid !== ATT_CID) { return; } var request = data; var requestType = request[0]; var response = null; switch(requestType) { case ATT_OP_ERROR: debug('ATT_OP_ERROR: ' + request.toString('hex')); this.handleOpError(request); break; case ATT_OP_READ_BY_TYPE_RESP: debug('ATT_OP_READ_BY_TYPE_RESP: ' + request.toString('hex')); break; case ATT_OP_READ_BY_GROUP_RESP: debug('ATT_OP_READ_BY_GROUP_RESP: ' + request.toString('hex')); break; case ATT_OP_HANDLE_NOTIFY: case ATT_OP_HANDLE_IND: debug('ATT_OP_HANDLE_NOTIFY: ' + request.toString('hex')); var valueHandle = data.readUInt16LE(1); var valueData = data.slice(3); this.emit('handleNotify', this._address, valueHandle, valueData); if (data[0] === ATT_OP_HANDLE_IND) { this._queueCommand(this.handleConfirmation(), null, function() { this.emit('handleConfirmation', this._address, valueHandle); }.bind(this)); } for (var serviceUuid in this._services) { for (var characteristicUuid in this._characteristics[serviceUuid]) { if (this._characteristics[serviceUuid][characteristicUuid].valueHandle === valueHandle) { this.emit('notification', this._address, serviceUuid, characteristicUuid, valueData); } } } 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); } if (this._currentCommand && data.toString('hex') === this._currentCommand.buffer.toString('hex')) { debug(this._address + ': echo ... echo ... echo ...'); } else if (data[0] % 2 === 0) { //debug(this._address + ': ignoring request/command: ' + requestType); } else if (!this._currentCommand) { debug(this._address + ': uh oh, no current command'); } else { debug(this._address + ': There is a current command'); if (data[0] === ATT_OP_ERROR && (data[4] === ATT_ECODE_AUTHENTICATION || data[4] === ATT_ECODE_AUTHORIZATION || data[4] === ATT_ECODE_INSUFF_ENC) && this._security !== 'medium') { debug('\t!! Need to encrypt!'); this._aclStream.encrypt(); return; } this._currentCommand.callback(data); this._currentCommand = null; while(this._commandQueue.length) { this._currentCommand = this._commandQueue.shift(); debug('\tUnqueueing and sending command'); this.writeAtt(this._currentCommand.buffer); if (this._currentCommand.callback) { break; } else if (this._currentCommand.writeCallback) { this._currentCommand.writeCallback(); this._currentCommand = null; } } } }; Gatt.prototype.onAclStreamEncrypt = function(encrypt) { if (encrypt) { this._security = 'medium'; debug("Trying to Encrypt"); //this.writeAtt(this._currentCommand.buffer); } }; Gatt.prototype.onAclStreamEncryptFail = function(aclStream) { debug("onAclStreamEncryptFail: "+ aclStream); this._security = 'low'; this.emit('encryptFail',aclStream); }; Gatt.prototype.onAclStreamEnd = function() { this._aclStream.removeListener('data', this.onAclStreamDataBinded); this._aclStream.removeListener('encrypt', this.onAclStreamEncryptBinded); this._aclStream.removeListener('encryptFail', this.onAclStreamEncryptFailBinded); this._aclStream.removeListener('end', this.onAclStreamEndBinded); }; Gatt.prototype.errorResponse = function(opcode, handle, status) { var buf = new Buffer(5); debug('\tSending ATT_OP_ERROR'); debug('\t\topcode: 0x' + opcode.toString(16)); debug('\t\thandle 0x' + handle.toString(16)); debug('\t\tstatus: 0x' + status.toString(16)); buf.writeUInt8(ATT_OP_ERROR, 0); buf.writeUInt8(opcode, 1); buf.writeUInt16LE(handle, 2); buf.writeUInt8(status, 4); return buf; }; Gatt.prototype.writeAtt = function(data) { debug(this._address + ': write: ' + data.toString('hex')); this._aclStream.write(ATT_CID, data); }; Gatt.prototype._queueCommand = function(buffer, callback, writeCallback) { this._commandQueue.push({ buffer: buffer, callback: callback, writeCallback: writeCallback }); debug(this._commandQueue.length + ' Commands in Queue'); if (this._currentCommand === null) { while (this._commandQueue.length) { this._currentCommand = this._commandQueue.shift(); this.writeAtt(this._currentCommand.buffer); if (this._currentCommand.callback) { break; } else if (this._currentCommand.writeCallback) { this._currentCommand.writeCallback(); this._currentCommand = null; } } } }; Gatt.prototype.mtuRequest = function(mtu) { var buf = new Buffer(3); debug("Sending ATT_OP_MTU_REQ: " + mtu); buf.writeUInt8(ATT_OP_MTU_REQ, 0); buf.writeUInt16LE(mtu, 1); return buf; }; Gatt.prototype.readByGroupRequest = function(startHandle, endHandle, groupUuid) { var buf = new Buffer(7); var printBuf = new Buffer(2); debug('\tSending ATT_OP_READ_BY_GROUP_REQ ID'); debug('\t\tstartHandle: 0x' + startHandle.toString(16)); debug('\t\tendHandle: 0x' + endHandle.toString(16)); debug('\t\tgroupUuid: 0x' + groupUuid.toString(16)); buf.writeUInt8(ATT_OP_READ_BY_GROUP_REQ, 0); buf.writeUInt16LE(startHandle, 1); buf.writeUInt16LE(endHandle, 3); buf.writeUInt16LE(groupUuid, 5); return buf; }; /* Gatt.prototype.readByTypeResponse = function(startHandle, endHandle, groupUuid) { var buf = new Buffer(7); buf.writeUInt8(ATT_OP_READ_BY_TYPE_REQ, 0); buf.writeUInt16LE(startHandle, 1); buf.writeUInt16LE(endHandle, 3); buf.writeUInt16LE(groupUuid, 5); return buf; }*/ Gatt.prototype.readByTypeRequest = function(startHandle, endHandle, groupUuid) { var buf = new Buffer(7); debug('\tSending ATT_OP_READ_BY_TYPE_REQ'); debug('\t\tstartHandle: 0x' + startHandle.toString(16)); debug('\t\tendHandle: 0x' + endHandle.toString(16)); debug('\t\tgroupUuid: 0x' + groupUuid.toString(16)); buf.writeUInt8(ATT_OP_READ_BY_TYPE_REQ, 0); buf.writeUInt16LE(startHandle, 1); buf.writeUInt16LE(endHandle, 3); buf.writeUInt16LE(groupUuid, 5); return buf; }; Gatt.prototype.readRequest = function(handle) { var buf = new Buffer(3); debug('\tSending ATT_OP_READ_REQ'); debug('\t\thandle: 0x' + handle.toString(16)); buf.writeUInt8(ATT_OP_READ_REQ, 0); buf.writeUInt16LE(handle, 1); return buf; }; Gatt.prototype.readBlobRequest = function(handle, offset) { var buf = new Buffer(5); debug('\tSending ATT_OP_READ_BLOB_REQ'); debug('\t\thandle: 0x' + handle.toString(16)); debug('\t\toffset: 0x' + offset.toString(16)); buf.writeUInt8(ATT_OP_READ_BLOB_REQ, 0); buf.writeUInt16LE(handle, 1); buf.writeUInt16LE(offset, 3); return buf; }; Gatt.prototype.findInfoRequest = function(startHandle, endHandle) { var buf = new Buffer(5); debug('\tSending ATT_OP_FIND_INFO_REQ'); debug('\t\tstartHandle: 0x' + startHandle.toString(16)); debug('\t\tendHandle: 0x' + endHandle.toString(16)); buf.writeUInt8(ATT_OP_FIND_INFO_REQ, 0); buf.writeUInt16LE(startHandle, 1); buf.writeUInt16LE(endHandle, 3); return buf; }; Gatt.prototype.findByTypeRequest = function(startHandle, endHandle, uuid, value) { var buf = new Buffer(7 + value.length); debug('\tSending ATT_OP_READ_BY_TYPE_REQ'); debug('\t\tstartHandle: 0x' + startHandle.toString(16)); debug('\t\tendHandle: 0x' + endHandle.toString(16)); debug('\t\tuuid: 0x' + uuid.toString(16)); buf.writeUInt8(ATT_OP_FIND_BY_TYPE_REQ , 0); buf.writeUInt16LE(startHandle, 1); buf.writeUInt16LE(endHandle, 3); buf.writeUInt16LE(uuid, 5); for (var i = 0; i < value.length; i++) { buf.writeUInt8(value.readUInt8(i), (value.length - i - 1) + 7); } var callback = function(data) { var opcode = data[0]; var i = 0; debug("\tATT_OP_FIND_BY_TYPE_RESP Callback"); if (opcode === ATT_OP_FIND_BY_TYPE_RESP) { var serviceUuids = []; var services = []; var start = data.readUInt16LE(1); var end = data.readUInt16LE(3); services.push({ startHandle: start, endHandle: end, uuid: uuid }); serviceUuids.push(uuid); this._services[services[0].uuid] = services[0]; debug('\tATT_OP_FIND_BY_TYPE_RESP Found Service UUID: 0x' + uuid.toString(16) + " start- 0x" + start.toString(16) + " end - 0x" + end.toString(16)); this.emit('servicesDiscover', this._address, serviceUuids); //this.emit('descriptorsDiscover', this._address, start, end); } }.bind(this); this._queueCommand(buf, callback ); return buf; }; Gatt.prototype.writeRequest = function(handle, data, withoutResponse) { var buf = new Buffer(3 + data.length); if (withoutResponse) { debug('\tSending ATT_OP_WRITE_CMD'); } else { debug('\tSending ATT_OP_WRITE_REQ'); } debug('\t\thandle: 0x' + handle.toString(16)); buf.writeUInt8(withoutResponse ? ATT_OP_WRITE_CMD : ATT_OP_WRITE_REQ , 0); buf.writeUInt16LE(handle, 1); for (var i = 0; i < data.length; i++) { buf.writeUInt8(data.readUInt8(i), i + 3); } return buf; }; Gatt.prototype.handleOpError = function(data) { var opcode = data[0]; if (opcode === ATT_OP_ERROR) { 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: 0x' + data.readUInt16LE(2).toString(16)); var errorCode = data[4]; debug('\tError Code: 0x'+errorCode.toString(16)); switch(errorCode) { case ATT_ECODE_SUCCESS: debug('\tError Code: ATT_ECODE_SUCCESS: '); break; case ATT_ECODE_INVALID_HANDLE: debug('\tError Code: ATT_ECODE_INVALID_HANDLE: '); break; case ATT_ECODE_READ_NOT_PERM: debug('\tError Code: ATT_ECODE_READ_NOT_PERM: '); break; case ATT_ECODE_WRITE_NOT_PERM: debug('\tError Code: ATT_ECODE_WRITE_NOT_PERM: '); break; case ATT_ECODE_INVALID_PDU: debug('\tError Code: ATT_ECODE_INVALID_PDU: '); break; case ATT_ECODE_AUTHENTICATION: debug('\tError Code: ATT_ECODE_AUTHENTICATION: '); break; case ATT_ECODE_REQ_NOT_SUPP: debug('\tError Code: ATT_ECODE_REQ_NOT_SUPP: '); break; case ATT_ECODE_INVALID_OFFSET: debug('\tError Code: ATT_ECODE_INVALID_OFFSET: '); break; case ATT_ECODE_AUTHORIZATION: debug('\tError Code: ATT_ECODE_AUTHORIZATION: '); break; case ATT_ECODE_PREP_QUEUE_FULL: debug('\tError Code: ATT_ECODE_PREP_QUEUE_FULL: '); break; case ATT_ECODE_ATTR_NOT_FOUND: debug('\tError Code: ATT_ECODE_ATTR_NOT_FOUND: '); break; case ATT_ECODE_ATTR_NOT_LONG: debug('\tError Code: ATT_ECODE_ATTR_NOT_LONG: '); break; case ATT_ECODE_INSUFF_ENCR_KEY_SIZE: debug('\tError Code: ATT_ECODE_INSUFF_ENCR_KEY_SIZE: '); break; case ATT_ECODE_INVAL_ATTR_VALUE_LEN: debug('\tError Code: ATT_ECODE_INVAL_ATTR_VALUE_LEN: '); break; case ATT_ECODE_UNLIKELY: debug('\tError Code: ATT_ECODE_UNLIKELY: '); break; case ATT_ECODE_INSUFF_ENC: debug('\tError Code: ATT_ECODE_INSUFF_ENC: '); break; case ATT_ECODE_UNSUPP_GRP_TYPE: debug('\tError Code: ATT_ECODE_UNSUPP_GRP_TYPE: '); break; case ATT_ECODE_INSUFF_RESOURCES: debug('\tError Code: ATT_ECODE_INSUFF_RESOURCES: '); break; default: debug('\tError Code: ' + errorCode); break; } } }; Gatt.prototype.handleConfirmation = function(request) { if (this._lastIndicatedAttribute) { if (this._lastIndicatedAttribute.emit) { this._lastIndicatedAttribute.emit('indicate'); } this._lastIndicatedAttribute = null; } }; Gatt.prototype.exchangeMtu = function(mtu) { this._queueCommand(this.mtuRequest(mtu), function(data) { var opcode = data[0]; if (opcode === ATT_OP_MTU_RESP) { var newMtu = data.readUInt16LE(1); debug("Echange Mtu: " + newMtu) debug(this._address + ': new MTU is ' + newMtu); this._mtu = newMtu; this.emit('mtu', this._address, newMtu); } else { this.emit('mtu', this._address, 23); } }.bind(this)); }; Gatt.prototype.discoverServices = function(uuids) { var services = []; debug("Discover Services: " + uuids); var callback = function(data) { var opcode = data[0]; var i = 0; if (opcode === ATT_OP_READ_BY_GROUP_RESP) { var type = data[1]; var num = (data.length - 2) / type; debug("ATT_OP_READ_BY_GROUP_RESP: "); for (i = 0; i < num; i++) { debug("\tService - Start: " + data.readUInt16LE(2 + i * type + 0) + " End: " + data.readUInt16LE(2 + i * type + 2) + " UUID: " + (type == 6) ? data.readUInt16LE(2 + i * type + 4).toString(16) : data.slice(2 + i * type + 4).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('')); services.push({ startHandle: data.readUInt16LE(2 + i * type + 0), endHandle: data.readUInt16LE(2 + i * type + 2), uuid: (type == 6) ? data.readUInt16LE(2 + i * type + 4).toString(16) : data.slice(2 + i * type + 4).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') }); } } if (opcode !== ATT_OP_READ_BY_GROUP_RESP || services[services.length - 1].endHandle === 0xffff) { var serviceUuids = []; for (i = 0; i < services.length; i++) { if (uuids.length === 0 || uuids.indexOf(services[i].uuid) !== -1) { serviceUuids.push(services[i].uuid); } this._services[services[i].uuid] = services[i]; } this.emit('servicesDiscover', this._address, serviceUuids); } else { this._queueCommand(this.readByGroupRequest(services[services.length - 1].endHandle + 1, 0xffff, GATT_PRIM_SVC_UUID), callback); } }.bind(this); this._queueCommand(this.readByGroupRequest(0x0001, 0xffff, GATT_PRIM_SVC_UUID), callback); }; Gatt.prototype.discoverIncludedServices = function(serviceUuid, uuids) { var service = this._services[serviceUuid]; var includedServices = []; var callback = function(data) { var opcode = data[0]; var i = 0; if (opcode === ATT_OP_READ_BY_TYPE_RESP) { var type = data[1]; var num = (data.length - 2) / type; for (i = 0; i < num; i++) { includedServices.push({ endHandle: data.readUInt16LE(2 + i * type + 0), startHandle: data.readUInt16LE(2 + i * type + 2), uuid: (type == 8) ? data.readUInt16LE(2 + i * type + 6).toString(16) : data.slice(2 + i * type + 6).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') }); } } if (opcode !== ATT_OP_READ_BY_TYPE_RESP || includedServices[includedServices.length - 1].endHandle === service.endHandle) { var includedServiceUuids = []; for (i = 0; i < includedServices.length; i++) { if (uuids.length === 0 || uuids.indexOf(includedServices[i].uuid) !== -1) { includedServiceUuids.push(includedServices[i].uuid); } } this.emit('includedServicesDiscover', this._address, service.uuid, includedServiceUuids); } else { this._queueCommand(this.readByTypeRequest(includedServices[includedServices.length - 1].endHandle + 1, service.endHandle, GATT_INCLUDE_UUID), callback); } }.bind(this); this._queueCommand(this.readByTypeRequest(service.startHandle, service.endHandle, GATT_INCLUDE_UUID), callback); }; Gatt.prototype.discoverCharacteristics = function(serviceUuid, characteristicUuids) { var service = this._services[serviceUuid]; var characteristics = []; this._characteristics[serviceUuid] = {}; this._descriptors[serviceUuid] = {}; debug('Discovering Characteristics for Service: ' + serviceUuid.toString(16)); var callback = function(data) { var opcode = data[0]; var i = 0; debug("Recieved response for Read By Type Resp"); if (opcode === ATT_OP_READ_BY_TYPE_RESP) { debug('\tATT_OP_READ_BY_TYPE_RESP: 0x' + opcode.toString(16)); var type = data[1]; var num = (data.length - 2) / type; for (i = 0; i < num; i++) { characteristics.push({ startHandle: data.readUInt16LE(2 + i * type + 0), properties: data.readUInt8(2 + i * type + 2), valueHandle: data.readUInt16LE(2 + i * type + 3), uuid: (type == 7) ? data.readUInt16LE(2 + i * type + 5).toString(16) : data.slice(2 + i * type + 5).slice(0, 16).toString('hex').match(/.{1,2}/g).reverse().join('') }); debug('\tuuid: 0x' + characteristics[characteristics.length - 1].uuid); debug('\t\tstartHandle: 0x' + data.readUInt16LE(2 + i * type + 0).toString(16)); debug('\t\tproperties: 0x' + data.readUInt8(2 + i * type + 2)); debug('\t\tvalueHandle: 0x' + data.readUInt16LE(2 + i * type + 3)); } } if (opcode !== ATT_OP_READ_BY_TYPE_RESP || characteristics[characteristics.length - 1].valueHandle === service.endHandle) { debug('\tNot ATT_OP_READ_BY_TYPE_RESP: 0x' + opcode.toString(16)); var characteristicsDiscovered = []; for (i = 0; i < characteristics.length; i++) { var properties = characteristics[i].properties; var characteristic = { properties: [], uuid: characteristics[i].uuid }; if (i !== 0) { characteristics[i - 1].endHandle = characteristics[i].startHandle - 1; } if (i === (characteristics.length - 1)) { characteristics[i].endHandle = service.endHandle; } this._characteristics[serviceUuid][characteristics[i].uuid] = characteristics[i]; if (properties & 0x01) { characteristic.properties.push('broadcast'); } if (properties & 0x02) { characteristic.properties.push('read'); } if (properties & 0x04) { characteristic.properties.push('writeWithoutResponse'); } if (properties & 0x08) { characteristic.properties.push('write'); } if (properties & 0x10) { characteristic.properties.push('notify'); } if (properties & 0x20) { characteristic.properties.push('indicate'); } if (properties & 0x40) { characteristic.properties.push('authenticatedSignedWrites'); } if (properties & 0x80) { characteristic.properties.push('extendedProperties'); } if (characteristicUuids.length === 0 || characteristicUuids.indexOf(characteristic.uuid) !== -1) { characteristicsDiscovered.push(characteristic); } } this.emit('characteristicsDiscover', this._address, serviceUuid, characteristicsDiscovered); } else { this._queueCommand(this.readByTypeRequest(characteristics[characteristics.length - 1].valueHandle + 1, service.endHandle, GATT_CHARAC_UUID), callback); } }.bind(this); this._queueCommand(this.readByTypeRequest(service.startHandle, service.endHandle, GATT_CHARAC_UUID), callback); }; Gatt.prototype.read = function(serviceUuid, characteristicUuid) { var characteristic = this._characteristics[serviceUuid][characteristicUuid]; var readData = new Buffer(0); var callback = function(data) { var opcode = data[0]; if (opcode === ATT_OP_READ_RESP || opcode === ATT_OP_READ_BLOB_RESP) { readData = new Buffer(readData.toString('hex') + data.slice(1).toString('hex'), 'hex'); if (data.length === this._mtu) { this._queueCommand(this.readBlobRequest(characteristic.valueHandle, readData.length), callback); } else { this.emit('read', this._address, serviceUuid, characteristicUuid, readData); } } else { this.emit('read', this._address, serviceUuid, characteristicUuid, readData); } }.bind(this); this._queueCommand(this.readRequest(characteristic.valueHandle), callback); }; Gatt.prototype.write = function(serviceUuid, characteristicUuid, data, withoutResponse) { var characteristic = this._characteristics[serviceUuid][characteristicUuid]; if (withoutResponse) { this._queueCommand(this.writeRequest(characteristic.valueHandle, data, true), null, function() { this.emit('write', this._address, serviceUuid, characteristicUuid); }.bind(this)); } else { this._queueCommand(this.writeRequest(characteristic.valueHandle, data, false), function(data) { var opcode = data[0]; if (opcode === ATT_OP_WRITE_RESP) { this.emit('write', this._address, serviceUuid, characteristicUuid); } }.bind(this)); } }; Gatt.prototype.broadcast = function(serviceUuid, characteristicUuid, broadcast) { var characteristic = this._characteristics[serviceUuid][characteristicUuid]; this._queueCommand(this.readByTypeRequest(characteristic.startHandle, characteristic.endHandle, GATT_SERVER_CHARAC_CFG_UUID), function(data) { var opcode = data[0]; if (opcode === ATT_OP_READ_BY_TYPE_RESP) { var type = data[1]; var handle = data.readUInt16LE(2); var value = data.readUInt16LE(4); if (broadcast) { value |= 0x0001; } else { value &= 0xfffe; } var valueBuffer = new Buffer(2); valueBuffer.writeUInt16LE(value, 0); this._queueCommand(this.writeRequest(handle, valueBuffer, false), function(data) { var opcode = data[0]; if (opcode === ATT_OP_WRITE_RESP) { this.emit('broadcast', this._address, serviceUuid, characteristicUuid, broadcast); } }.bind(this)); } }.bind(this)); }; Gatt.prototype.notify = function(serviceUuid, characteristicUuid, notify) { var characteristic = this._characteristics[serviceUuid][characteristicUuid]; this._queueCommand(this.readByTypeRequest(characteristic.startHandle, characteristic.endHandle, GATT_CLIENT_CHARAC_CFG_UUID), function(data) { var opcode = data[0]; if (opcode === ATT_OP_READ_BY_TYPE_RESP) { var type = data[1]; var handle = data.readUInt16LE(2); var value = data.readUInt16LE(4); var useNotify = characteristic.properties & 0x10; var useIndicate = characteristic.properties & 0x20; if (notify) { if (useNotify) { value |= 0x0001; } else if (useIndicate) { value |= 0x0002; } } else { if (useNotify) { value &= 0xfffe; } else if (useIndicate) { value &= 0xfffd; } } var valueBuffer = new Buffer(2); valueBuffer.writeUInt16LE(value, 0); this._queueCommand(this.writeRequest(handle, valueBuffer, false), function(data) { var opcode = data[0]; if (opcode === ATT_OP_WRITE_RESP) { this.emit('notify', this._address, serviceUuid, characteristicUuid, notify); } }.bind(this)); } }.bind(this)); }; Gatt.prototype.discoverDescriptors = function(serviceUuid, characteristicUuid) { var characteristic = this._characteristics[serviceUuid][characteristicUuid]; var descriptors = []; this._descriptors[serviceUuid][characteristicUuid] = {}; var callback = function(data) { var opcode = data[0]; var i = 0; if (opcode === ATT_OP_FIND_INFO_RESP) { var num = data[1]; for (i = 0; i < num; i++) { descriptors.push({ handle: data.readUInt16LE(2 + i * 4 + 0), uuid: data.readUInt16LE(2 + i * 4 + 2).toString(16) }); } } if (opcode !== ATT_OP_FIND_INFO_RESP || descriptors[descriptors.length - 1].handle === characteristic.endHandle) { var descriptorUuids = []; for (i = 0; i < descriptors.length; i++) { descriptorUuids.push(descriptors[i].uuid); this._descriptors[serviceUuid][characteristicUuid][descriptors[i].uuid] = descriptors[i]; } this.emit('descriptorsDiscover', this._address, serviceUuid, characteristicUuid, descriptorUuids); } else { this._queueCommand(this.findInfoRequest(descriptors[descriptors.length - 1].handle + 1, characteristic.endHandle), callback); } }.bind(this); this._queueCommand(this.findInfoRequest(characteristic.valueHandle + 1, characteristic.endHandle), callback); }; Gatt.prototype.readValue = function(serviceUuid, characteristicUuid, descriptorUuid) { var descriptor = this._descriptors[serviceUuid][characteristicUuid][descriptorUuid]; this._queueCommand(this.readRequest(descriptor.handle), function(data) { var opcode = data[0]; if (opcode === ATT_OP_READ_RESP) { this.emit('valueRead', this._address, serviceUuid, characteristicUuid, descriptorUuid, data.slice(1)); } }.bind(this)); }; Gatt.prototype.writeValue = function(serviceUuid, characteristicUuid, descriptorUuid, data) { var descriptor = this._descriptors[serviceUuid][characteristicUuid][descriptorUuid]; this._queueCommand(this.writeRequest(descriptor.handle, data, false), function(data) { var opcode = data[0]; if (opcode === ATT_OP_WRITE_RESP) { this.emit('valueWrite', this._address, serviceUuid, characteristicUuid, descriptorUuid); } }.bind(this)); }; Gatt.prototype.readHandle = function(handle) { this._queueCommand(this.readRequest(handle), function(data) { var opcode = data[0]; if (opcode === ATT_OP_READ_RESP) { this.emit('handleRead', this._address, handle, data.slice(1)); } }.bind(this)); }; Gatt.prototype.writeHandle = function(handle, data, withoutResponse) { if (withoutResponse) { this._queueCommand(this.writeRequest(handle, data, true), null, function() { this.emit('handleWrite', this._address, handle); }.bind(this)); } else { this._queueCommand(this.writeRequest(handle, data, false), function(data) { var opcode = data[0]; if (opcode === ATT_OP_WRITE_RESP) { this.emit('handleWrite', this._address, handle); } }.bind(this)); } }; module.exports = Gatt;