UNPKG

@stoprocent/bleno

Version:

A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals

1,117 lines (894 loc) 36 kB
/* eslint-disable no-unused-vars */ /* jshint loopfunc: true */ const debug = require('debug')('gatt'); const BlenoEventEmitter = require('../bleno-event-emitter'); const os = require('os'); const Characteristic = require('../characteristic'); const ATT_OP_ERROR = 0x01; const ATT_OP_MTU_REQ = 0x02; const ATT_OP_MTU_RESP = 0x03; const ATT_OP_FIND_INFO_REQ = 0x04; const ATT_OP_FIND_INFO_RESP = 0x05; const ATT_OP_FIND_BY_TYPE_REQ = 0x06; const ATT_OP_FIND_BY_TYPE_RESP = 0x07; const ATT_OP_READ_BY_TYPE_REQ = 0x08; const ATT_OP_READ_BY_TYPE_RESP = 0x09; const ATT_OP_READ_REQ = 0x0a; const ATT_OP_READ_RESP = 0x0b; const ATT_OP_READ_BLOB_REQ = 0x0c; const ATT_OP_READ_BLOB_RESP = 0x0d; const ATT_OP_READ_MULTI_REQ = 0x0e; const ATT_OP_READ_MULTI_RESP = 0x0f; const ATT_OP_READ_BY_GROUP_REQ = 0x10; const ATT_OP_READ_BY_GROUP_RESP = 0x11; const ATT_OP_WRITE_REQ = 0x12; const ATT_OP_WRITE_RESP = 0x13; const ATT_OP_WRITE_CMD = 0x52; const ATT_OP_PREP_WRITE_REQ = 0x16; const ATT_OP_PREP_WRITE_RESP = 0x17; const ATT_OP_EXEC_WRITE_REQ = 0x18; const ATT_OP_EXEC_WRITE_RESP = 0x19; const ATT_OP_HANDLE_NOTIFY = 0x1b; const ATT_OP_HANDLE_IND = 0x1d; const ATT_OP_HANDLE_CNF = 0x1e; const ATT_OP_SIGNED_WRITE_CMD = 0xd2; const GATT_PRIM_SVC_UUID = 0x2800; const GATT_INCLUDE_UUID = 0x2802; const GATT_CHARAC_UUID = 0x2803; const GATT_CLIENT_CHARAC_CFG_UUID = 0x2902; const GATT_SERVER_CHARAC_CFG_UUID = 0x2903; const ATT_ECODE_SUCCESS = 0x00; const ATT_ECODE_INVALID_HANDLE = 0x01; const ATT_ECODE_READ_NOT_PERM = 0x02; const ATT_ECODE_WRITE_NOT_PERM = 0x03; const ATT_ECODE_INVALID_PDU = 0x04; const ATT_ECODE_AUTHENTICATION = 0x05; const ATT_ECODE_REQ_NOT_SUPP = 0x06; const ATT_ECODE_INVALID_OFFSET = 0x07; const ATT_ECODE_AUTHORIZATION = 0x08; const ATT_ECODE_PREP_QUEUE_FULL = 0x09; const ATT_ECODE_ATTR_NOT_FOUND = 0x0a; const ATT_ECODE_ATTR_NOT_LONG = 0x0b; const ATT_ECODE_INSUFF_ENCR_KEY_SIZE = 0x0c; const ATT_ECODE_INVAL_ATTR_VALUE_LEN = 0x0d; const ATT_ECODE_UNLIKELY = 0x0e; const ATT_ECODE_INSUFF_ENC = 0x0f; const ATT_ECODE_UNSUPP_GRP_TYPE = 0x10; const ATT_ECODE_INSUFF_RESOURCES = 0x11; const ATT_CID = 0x0004; class Gatt extends BlenoEventEmitter { constructor () { super(); this.maxMtu = 256; this._name = process.env.BLENO_DEVICE_NAME || os.hostname(); this._connections = new Map(); this._preparedWriteRequests = new Map(); this._aclStreamCallbacks = new Map(); this._lastIndicatedAttributes = new Map(); this._handles = []; this.setServices([]); } updateName (name) { this._name = name; let valueHandle = undefined; for (let i = 0; i < this._handles.length; i++) { // if (typeof this._handles[i] !== 'object') continue; // Update the value of the characteristic if (this._handles[i].type === 'characteristic' && this._handles[i].uuid === '2a00') { this._handles[i].attribute.value = Buffer.from(name); valueHandle = this._handles[i].valueHandle; } // Update the value of the characteristic value if (this._handles[i].type === 'characteristicValue' && this._handles[i].handle === valueHandle) { this._handles[i].value = Buffer.from(name); } } } setServices (services) { // base services and characteristics const allServices = [ { uuid: '1800', characteristics: [ new Characteristic({ uuid: '2a00', properties: ['read'], secure: [], value: Buffer.from(this._name), descriptors: [] }), new Characteristic({ uuid: '2a01', properties: ['read'], secure: [], value: Buffer.from([0x80, 0x00]), descriptors: [] }) ] }, { uuid: '1801', characteristics: [ new Characteristic({ uuid: '2a05', properties: ['indicate'], secure: [], descriptors: [], onIndicate: function (connection) { this.notify(connection, Buffer.from([0x00, 0x00, 0x00, 0x00])); } }) ] } ].concat(services); this._handles = []; let handle = 0; for (let i = 0; i < allServices.length; i++) { const service = allServices[i]; handle++; const serviceHandle = handle; this._handles[serviceHandle] = { type: 'service', uuid: service.uuid, attribute: service, startHandle: serviceHandle // endHandle filled in below }; for (let j = 0; j < service.characteristics.length; j++) { const characteristic = service.characteristics[j]; let properties = 0; let 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++; const characteristicHandle = handle; handle++; const characteristicValueHandle = handle; this._handles[characteristicHandle] = { type: 'characteristic', uuid: characteristic.uuid, properties, 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++; const 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, values: new Map() }; } for (let k = 0; k < characteristic.descriptors.length; k++) { const descriptor = characteristic.descriptors[k]; handle++; const 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; } const debugHandles = []; for (let i = 0; i < this._handles.length; i++) { handle = this._handles[i]; debugHandles[i] = {}; for (const 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)); } registerAclStream (aclStream, connection) { this._connections.set(connection, { aclStream, mtu: 23, }); this._aclStreamCallbacks.set(connection, { onData: function (connection, cid, data) { this.onAclStreamData(connection, cid, data); }.bind(this), onEnd: function (connection) { this.onAclStreamEnd(connection); }.bind(this) }); aclStream.on('data', this._aclStreamCallbacks.get(connection).onData.bind(this)); aclStream.on('end', this._aclStreamCallbacks.get(connection).onEnd.bind(this)); } onAclStreamData (connection, cid, data) { if (cid !== ATT_CID) { return; } this.handleRequest(data, connection); } onAclStreamEnd (connection) { if (this._connections.has(connection) === false) { return; } const { aclStream } = this._connections.get(connection); aclStream.removeListener('data', this._aclStreamCallbacks.get(connection).onData); aclStream.removeListener('end', this._aclStreamCallbacks.get(connection).onEnd); // Clean up subscriptions for this connection for (let i = 0; i < this._handles.length; i++) { if (this._handles[i] && this._handles[i].type === 'descriptor' && this._handles[i].uuid === '2902' && this._handles[i].values && this._handles[i].values.has(connection)) { this._handles[i].values.delete(connection); if (this._handles[i].attribute && this._handles[i].attribute.emit) { this._handles[i].attribute.emit('unsubscribe', connection); } } } this._connections.delete(connection); this._aclStreamCallbacks.delete(connection); this._preparedWriteRequests.delete(connection); } send (data, connection) { debug('send: ' + data.toString('hex')); if (this._connections.has(connection)) { this._connections.get(connection).aclStream.write(ATT_CID, data); } } errorResponse (opcode, handle, status) { const buf = Buffer.alloc(5); buf.writeUInt8(ATT_OP_ERROR, 0); buf.writeUInt8(opcode, 1); buf.writeUInt16LE(handle, 2); buf.writeUInt8(status, 4); return buf; } handleRequest (request, connection) { debug('handling request: ' + request.toString('hex')); const requestType = request[0]; let response = null; switch (requestType) { case ATT_OP_MTU_REQ: response = this.handleMtuRequest(request, connection); break; case ATT_OP_FIND_INFO_REQ: response = this.handleFindInfoRequest(request, connection); break; case ATT_OP_FIND_BY_TYPE_REQ: response = this.handleFindByTypeRequest(request, connection); break; case ATT_OP_READ_BY_TYPE_REQ: response = this.handleReadByTypeRequest(request, connection); break; case ATT_OP_READ_REQ: case ATT_OP_READ_BLOB_REQ: response = this.handleReadOrReadBlobRequest(request, connection); break; case ATT_OP_READ_BY_GROUP_REQ: response = this.handleReadByGroupRequest(request, connection); break; case ATT_OP_WRITE_REQ: case ATT_OP_WRITE_CMD: response = this.handleWriteRequestOrCommand(request, connection); break; case ATT_OP_PREP_WRITE_REQ: response = this.handlePrepareWriteRequest(request, connection); break; case ATT_OP_EXEC_WRITE_REQ: response = this.handleExecuteWriteRequest(request, connection); break; case ATT_OP_HANDLE_CNF: this.handleConfirmation(request, connection); break; default: response = this.errorResponse(requestType, 0x0000, ATT_ECODE_REQ_NOT_SUPP); break; } if (response) { debug('response: ' + response.toString('hex')); this.send(response, connection); } } handleMtuRequest (request, connection) { let mtu = request.readUInt16LE(1); if (mtu < 23) { mtu = 23; } else if (mtu > this.maxMtu) { mtu = this.maxMtu; } if (this._connections.has(connection)) { this._connections.get(connection).mtu = mtu; } this.emit('mtuChange', mtu); const response = Buffer.alloc(3); response.writeUInt8(ATT_OP_MTU_RESP, 0); response.writeUInt16LE(mtu, 1); return response; } handleFindInfoRequest (request, connection) { const { mtu } = this._connections.get(connection); const startHandle = request.readUInt16LE(1); const endHandle = request.readUInt16LE(3); const infos = []; let uuid = null; for (let i = startHandle; i <= endHandle; i++) { const handle = this._handles[i]; if (!handle) { break; } uuid = null; if (handle.type === 'service') { uuid = '2800'; } else if (handle.type === 'includedService') { uuid = '2802'; } else if (handle.type === 'characteristic') { uuid = '2803'; } else if (handle.type === 'characteristicValue') { uuid = this._handles[i - 1].uuid; } else if (handle.type === 'descriptor') { uuid = handle.uuid; } if (uuid) { infos.push({ handle: i, uuid }); } } if (infos.length) { const uuidSize = infos[0].uuid.length / 2; let numInfo = 1; for (let i = 1; i < infos.length; i++) { if (infos[0].uuid.length !== infos[i].uuid.length) { break; } numInfo++; } const lengthPerInfo = (uuidSize === 2) ? 4 : 18; const maxInfo = Math.floor((mtu - 2) / lengthPerInfo); numInfo = Math.min(numInfo, maxInfo); const response = Buffer.alloc(2 + numInfo * lengthPerInfo); response[0] = ATT_OP_FIND_INFO_RESP; response[1] = (uuidSize === 2) ? 0x01 : 0x2; for (let i = 0; i < numInfo; i++) { const info = infos[i]; response.writeUInt16LE(info.handle, 2 + i * lengthPerInfo); uuid = Buffer.from(info.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (let j = 0; j < uuid.length; j++) { response[2 + i * lengthPerInfo + 2 + j] = uuid[j]; } } return response; } return this.errorResponse(ATT_OP_FIND_INFO_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } handleFindByTypeRequest (request, connection) { const { mtu } = this._connections.get(connection); const startHandle = request.readUInt16LE(1); const endHandle = request.readUInt16LE(3); const uuid = request.slice(5, 7).toString('hex').match(/.{1,2}/g).reverse().join(''); const value = request.slice(7).toString('hex').match(/.{1,2}/g).reverse().join(''); const handles = []; let handle; for (let i = startHandle; i <= endHandle; i++) { handle = this._handles[i]; if (!handle) { break; } if (uuid === '2800' && handle.type === 'service' && handle.uuid === value) { handles.push({ start: handle.startHandle, end: handle.endHandle }); } } if (handles.length) { const lengthPerHandle = 4; let numHandles = handles.length; const maxHandles = Math.floor((mtu - 1) / lengthPerHandle); numHandles = Math.min(numHandles, maxHandles); const response = Buffer.alloc(1 + numHandles * lengthPerHandle); response[0] = ATT_OP_FIND_BY_TYPE_RESP; for (let i = 0; i < numHandles; i++) { handle = handles[i]; response.writeUInt16LE(handle.start, 1 + i * lengthPerHandle); response.writeUInt16LE(handle.end, 1 + i * lengthPerHandle + 2); } return response; } return this.errorResponse(ATT_OP_FIND_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } handleReadByGroupRequest (request, connection) { const { mtu } = this._connections.get(connection); const startHandle = request.readUInt16LE(1); const endHandle = request.readUInt16LE(3); const uuid = request.slice(5).toString('hex').match(/.{1,2}/g).reverse().join(''); debug('read by group: startHandle = 0x' + startHandle.toString(16) + ', endHandle = 0x' + endHandle.toString(16) + ', uuid = 0x' + uuid.toString(16)); if (uuid === '2800' || uuid === '2802') { const services = []; const type = (uuid === '2800') ? 'service' : 'includedService'; for (let i = startHandle; i <= endHandle; i++) { const handle = this._handles[i]; if (!handle) { break; } if (handle.type === type) { services.push(handle); } } if (services.length) { const uuidSize = services[0].uuid.length / 2; let numServices = 1; for (let i = 1; i < services.length; i++) { if (services[0].uuid.length !== services[i].uuid.length) { break; } numServices++; } const lengthPerService = (uuidSize === 2) ? 6 : 20; const maxServices = Math.floor((mtu - 2) / lengthPerService); numServices = Math.min(numServices, maxServices); const response = Buffer.alloc(2 + numServices * lengthPerService); response[0] = ATT_OP_READ_BY_GROUP_RESP; response[1] = lengthPerService; for (let i = 0; i < numServices; i++) { const service = services[i]; response.writeUInt16LE(service.startHandle, 2 + i * lengthPerService); response.writeUInt16LE(service.endHandle, 2 + i * lengthPerService + 2); const serviceUuid = Buffer.from(service.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (let j = 0; j < serviceUuid.length; j++) { response[2 + i * lengthPerService + 4 + j] = serviceUuid[j]; } } return response; } return this.errorResponse(ATT_OP_READ_BY_GROUP_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } return this.errorResponse(ATT_OP_READ_BY_GROUP_REQ, startHandle, ATT_ECODE_UNSUPP_GRP_TYPE); } handleReadByTypeRequest (request, connection) { const { mtu } = this._connections.get(connection); let response = null; const requestType = request[0]; const startHandle = request.readUInt16LE(1); const endHandle = request.readUInt16LE(3); const uuid = request.slice(5).toString('hex').match(/.{1,2}/g).reverse().join(''); debug('read by type: startHandle = 0x' + startHandle.toString(16) + ', endHandle = 0x' + endHandle.toString(16) + ', uuid = 0x' + uuid.toString(16)); if (uuid === '2803') { const characteristics = []; for (let i = startHandle; i <= endHandle; i++) { const handle = this._handles[i]; if (!handle) { break; } if (handle.type === 'characteristic') { characteristics.push(handle); } } if (characteristics.length) { const uuidSize = characteristics[0].uuid.length / 2; let numCharacteristics = 1; for (let i = 1; i < characteristics.length; i++) { if (characteristics[0].uuid.length !== characteristics[i].uuid.length) { break; } numCharacteristics++; } const lengthPerCharacteristic = (uuidSize === 2) ? 7 : 21; const maxCharacteristics = Math.floor((mtu - 2) / lengthPerCharacteristic); numCharacteristics = Math.min(numCharacteristics, maxCharacteristics); response = Buffer.alloc(2 + numCharacteristics * lengthPerCharacteristic); response[0] = ATT_OP_READ_BY_TYPE_RESP; response[1] = lengthPerCharacteristic; for (let i = 0; i < numCharacteristics; i++) { const characteristic = characteristics[i]; response.writeUInt16LE(characteristic.startHandle, 2 + i * lengthPerCharacteristic); response.writeUInt8(characteristic.properties, 2 + i * lengthPerCharacteristic + 2); response.writeUInt16LE(characteristic.valueHandle, 2 + i * lengthPerCharacteristic + 3); const characteristicUuid = Buffer.from(characteristic.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); for (let j = 0; j < characteristicUuid.length; j++) { response[2 + i * lengthPerCharacteristic + 5 + j] = characteristicUuid[j]; } } } else { response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } } else { let handleAttribute = null; let valueHandle = null; let secure = false; for (let i = startHandle; i <= endHandle; i++) { const handle = this._handles[i]; if (!handle) { break; } if (handle.type === 'characteristic' && handle.uuid === uuid) { handleAttribute = handle.attribute; valueHandle = handle.valueHandle; secure = handle.secure & 0x02; break; } else if (handle.type === 'descriptor' && handle.uuid === uuid) { valueHandle = i; secure = handle.secure & 0x02; break; } } if (secure && !this._connections.get(connection).aclStream.encrypted) { response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_AUTHENTICATION); } else if (valueHandle) { const callback = (function (requestType, valueHandle, connection) { return function (result, data) { let callbackResponse; if (ATT_ECODE_SUCCESS === result) { const dataLength = Math.min(data.length, mtu - 4); callbackResponse = Buffer.alloc(4 + dataLength); callbackResponse[0] = ATT_OP_READ_BY_TYPE_RESP; callbackResponse[1] = dataLength + 2; callbackResponse.writeUInt16LE(valueHandle, 2); for (let i = 0; i < dataLength; i++) { callbackResponse[4 + i] = data[i]; } } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('read by type response: ' + callbackResponse.toString('hex')); this.send(callbackResponse, connection); }.bind(this); }.bind(this))(requestType, valueHandle, connection); let data = undefined; if (this._handles[valueHandle].values && this._handles[valueHandle].values.has(connection)) { data = this._handles[valueHandle].values.get(connection); } else { data = this._handles[valueHandle].value; } if (!data && this._handles[valueHandle].uuid === '2902' && this._connections.get(connection)) { data = Buffer.from([0x00, 0x00]); } if (data) { callback(ATT_ECODE_SUCCESS, data); } else if (handleAttribute) { handleAttribute.emit('readRequest', connection, 0, callback); } else { callback(ATT_ECODE_UNLIKELY); } } else { response = this.errorResponse(ATT_OP_READ_BY_TYPE_REQ, startHandle, ATT_ECODE_ATTR_NOT_FOUND); } } return response; } handleReadOrReadBlobRequest (request, connection) { let response = null; const requestType = request[0]; const valueHandle = request.readUInt16LE(1); const offset = (requestType === ATT_OP_READ_BLOB_REQ) ? request.readUInt16LE(3) : 0; const handle = this._handles[valueHandle]; if (handle) { let result = null; let data = null; const handleType = handle.type; const callback = (function (requestType, valueHandle, connection) { return function (result, data) { let callbackResponse; const { mtu } = this._connections.get(connection); if (ATT_ECODE_SUCCESS === result) { const dataLength = Math.min(data.length, mtu - 1); callbackResponse = Buffer.alloc(1 + dataLength); callbackResponse[0] = (requestType === ATT_OP_READ_BLOB_REQ) ? ATT_OP_READ_BLOB_RESP : ATT_OP_READ_RESP; for (let i = 0; i < dataLength; i++) { callbackResponse[1 + i] = data[i]; } } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('read response: ' + callbackResponse.toString('hex')); this.send(callbackResponse, connection); }.bind(this); }.bind(this))(requestType, valueHandle, connection); if (handleType === 'service' || handleType === 'includedService') { result = ATT_ECODE_SUCCESS; data = Buffer.from(handle.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); } else if (handleType === 'characteristic') { const uuid = Buffer.from(handle.uuid.match(/.{1,2}/g).reverse().join(''), 'hex'); result = ATT_ECODE_SUCCESS; data = Buffer.alloc(3 + uuid.length); data.writeUInt8(handle.properties, 0); data.writeUInt16LE(handle.valueHandle, 1); for (let i = 0; i < uuid.length; i++) { data[i + 3] = uuid[i]; } } else if (handleType === 'characteristicValue' || handleType === 'descriptor') { let handleProperties = handle.properties; let handleSecure = handle.secure; let 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._connections.get(connection).aclStream.encrypted) { result = ATT_ECODE_AUTHENTICATION; } else { if (handle.values && handle.values.has(connection)) { data = handle.values.get(connection); } else { data = handle.value; } if (!data && handle.uuid === '2902' && this._connections.get(connection)) { data = Buffer.from([0x00, 0x00]); } if (data) { result = ATT_ECODE_SUCCESS; } else { handleAttribute.emit('readRequest', connection, offset, callback); } } } else { result = ATT_ECODE_READ_NOT_PERM; // non-readable } } if (data && typeof data === 'string') { data = Buffer.from(data); } if (result === ATT_ECODE_SUCCESS && data && offset) { if (data.length < offset) { result = 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; } handleWriteRequestOrCommand (request, connection) { let response = null; const requestType = request[0]; const withoutResponse = (requestType === ATT_OP_WRITE_CMD); const valueHandle = request.readUInt16LE(1); const data = request.slice(3); const offset = 0; let handle = this._handles[valueHandle]; if (handle) { if (handle.type === 'characteristicValue') { handle = this._handles[valueHandle - 1]; } const handleProperties = handle.properties; const handleSecure = handle.secure; if (handleProperties && (withoutResponse ? (handleProperties & 0x04) : (handleProperties & 0x08))) { const callback = (function (requestType, valueHandle, withoutResponse, connection) { return function (result) { if (!withoutResponse) { let callbackResponse; if (ATT_ECODE_SUCCESS === result) { callbackResponse = Buffer.from([ATT_OP_WRITE_RESP]); } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('write response: ' + callbackResponse.toString('hex')); this.send(callbackResponse, connection); } }.bind(this); }.bind(this))(requestType, valueHandle, withoutResponse, connection); if (handleSecure & (withoutResponse ? 0x04 : 0x08) && !this._connections.get(connection).aclStream.encrypted) { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_AUTHENTICATION); } else if (handle.type === 'descriptor' || handle.uuid === '2902') { let result; if (data.length !== 2) { result = ATT_ECODE_INVAL_ATTR_VALUE_LEN; } else { const value = data.readUInt16LE(0); const handleAttribute = handle.attribute; if (handle.values) { handle.values.set(connection, data); } else { handle.value = data; } if (value & 0x0003) { const updateValueCallback = (function (valueHandle, attribute, connection) { return function (data) { const { mtu } = this._connections.get(connection); const dataLength = Math.min(data.length, mtu - 3); const useNotify = attribute.properties.indexOf('notify') !== -1; const useIndicate = attribute.properties.indexOf('indicate') !== -1; if (useNotify) { const notifyMessage = Buffer.alloc(3 + dataLength); notifyMessage.writeUInt8(ATT_OP_HANDLE_NOTIFY, 0); notifyMessage.writeUInt16LE(valueHandle, 1); for (let i = 0; i < dataLength; i++) { notifyMessage[3 + i] = data[i]; } debug('notify message: ' + notifyMessage.toString('hex')); this.send(notifyMessage, connection); attribute.emit('notify', connection); } else if (useIndicate) { const indicateMessage = Buffer.alloc(3 + dataLength); indicateMessage.writeUInt8(ATT_OP_HANDLE_IND, 0); indicateMessage.writeUInt16LE(valueHandle, 1); for (let i = 0; i < dataLength; i++) { indicateMessage[3 + i] = data[i]; } this._lastIndicatedAttributes.set(connection, attribute); debug('indicate message: ' + indicateMessage.toString('hex')); this.send(indicateMessage, connection); } }.bind(this); }.bind(this))(valueHandle - 1, handleAttribute, connection); if (handleAttribute.emit) { const { mtu } = this._connections.get(connection); handleAttribute.emit('subscribe', connection, mtu - 3, updateValueCallback); } } else { if (handleAttribute.emit) { handleAttribute.emit('unsubscribe', connection); } } result = ATT_ECODE_SUCCESS; } callback(result); } else { handle.attribute.emit('writeRequest', connection, 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; } handlePrepareWriteRequest (request, connection) { let response; const requestType = request[0]; const valueHandle = request.readUInt16LE(1); const offset = request.readUInt16LE(3); const data = request.slice(5); let handle = this._handles[valueHandle]; if (handle) { if (handle.type === 'characteristicValue') { handle = this._handles[valueHandle - 1]; const handleProperties = handle.properties; const handleSecure = handle.secure; if (handleProperties && (handleProperties & 0x08)) { if ((handleSecure & 0x08) && !this._connections.get(connection).aclStream.encrypted) { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_AUTHENTICATION); } else if (this._preparedWriteRequests.has(connection)) { const preparedWriteRequest = this._preparedWriteRequests.get(connection); if (preparedWriteRequest.handle !== handle) { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_UNLIKELY); } else if (offset === (preparedWriteRequest.offset + preparedWriteRequest.data.length)) { preparedWriteRequest.data = Buffer.concat([ preparedWriteRequest.data, data ]); response = Buffer.alloc(request.length); request.copy(response); response[0] = ATT_OP_PREP_WRITE_RESP; } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_INVALID_OFFSET); } } else { this._preparedWriteRequests.set(connection, { handle, valueHandle, offset, data }); response = Buffer.alloc(request.length); request.copy(response); response[0] = ATT_OP_PREP_WRITE_RESP; } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_WRITE_NOT_PERM); } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_ATTR_NOT_LONG); } } else { response = this.errorResponse(requestType, valueHandle, ATT_ECODE_INVALID_HANDLE); } return response; } handleExecuteWriteRequest (request, connection) { let response = null; const requestType = request[0]; const flag = request[1]; if (this._preparedWriteRequests.has(connection)) { const preparedWriteRequest = this._preparedWriteRequests.get(connection); const valueHandle = preparedWriteRequest.valueHandle; if (flag === 0x00) { response = Buffer.from([ATT_OP_EXEC_WRITE_RESP]); } else if (flag === 0x01) { const callback = (function (requestType, valueHandle, connection) { return function (result) { let callbackResponse; if (ATT_ECODE_SUCCESS === result) { callbackResponse = Buffer.from([ATT_OP_EXEC_WRITE_RESP]); } else { callbackResponse = this.errorResponse(requestType, valueHandle, result); } debug('execute write response: ' + callbackResponse.toString('hex')); this.send(callbackResponse, connection); }.bind(this); }.bind(this))(requestType, valueHandle, connection); preparedWriteRequest.handle.attribute.emit('writeRequest', connection, preparedWriteRequest.data, preparedWriteRequest.offset, false, callback); } else { response = this.errorResponse(requestType, 0x0000, ATT_ECODE_UNLIKELY); } this._preparedWriteRequests.delete(connection); } else { response = this.errorResponse(requestType, 0x0000, ATT_ECODE_UNLIKELY); } return response; } handleConfirmation (request, connection) { if (this._lastIndicatedAttributes.has(connection) === false) { return; } const lastIndicatedAttribute = this._lastIndicatedAttributes.get(connection); if (lastIndicatedAttribute.emit) { lastIndicatedAttribute.emit('indicate', connection); } this._lastIndicatedAttributes.delete(connection); this._lastIndicatedAttributes.delete(connection); } } module.exports = Gatt;