@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
JavaScript
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;