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

356 lines (264 loc) 10.6 kB
var debug = require('debug')('gap'); var events = require('events'); var util = require('util'); var Gap = function(hci) { this._hci = hci; this._advertiseState = null; this._hci.on('error', this.onHciError.bind(this)); this._scanState = null; this._discoveries = {}; this._hci.on('error', this.onHciError.bind(this)); this._hci.on('leScanParametersSet', this.onHciLeScanParametersSet.bind(this)); this._hci.on('leScanEnableSet', this.onHciLeScanEnableSet.bind(this)); this._hci.on('leAdvertisingReport', this.onHciLeAdvertisingReport.bind(this)); //Bleno this._hci.on('leAdvertisingParametersSet', this.onHciLeAdvertisingParametersSet.bind(this)); this._hci.on('leAdvertisingDataSet', this.onHciLeAdvertisingDataSet.bind(this)); this._hci.on('leScanResponseDataSet', this.onHciLeScanResponseDataSet.bind(this)); this._hci.on('leAdvertiseEnableSet', this.onHciLeAdvertiseEnableSet.bind(this)); }; util.inherits(Gap, events.EventEmitter); Gap.prototype.startScanning = function(allowDuplicates) { this._scanState = 'starting'; this._hci.setScanEnabled(true, !allowDuplicates); }; Gap.prototype.stopScanning = function() { this._scanState = 'stopping'; this._hci.setScanEnabled(false, true); }; Gap.prototype.onHciError = function(error) { }; Gap.prototype.onHciLeScanParametersSet = function() { }; Gap.prototype.onHciLeScanEnableSet = function() { if (this._scanState === 'starting') { this._scanState = 'stared'; this.emit('scanStart'); } else if (this._scanState === 'stopping') { this._scanState = 'stopped'; this.emit('scanStop'); } }; Gap.prototype.onHciLeAdvertisingReport = function(status, type, address, addressType, eir, rssi) { var previouslyDiscovered = !!this._discoveries[address]; var advertisement = previouslyDiscovered ? this._discoveries[address].advertisement : { localName: undefined, txPowerLevel: undefined, manufacturerData: undefined, serviceData: [], serviceUuids: [] }; var discoveryCount = previouslyDiscovered ? this._discoveries[address].count : 0; var hasScanResponse = previouslyDiscovered ? this._discoveries[address].hasScanResponse : false; if (type === 0x04) { hasScanResponse = true; } else { // reset service data every non-scan response event advertisement.serviceData = []; advertisement.serviceUuids = []; } discoveryCount++; var i = 0; var j = 0; var serviceUuid = null; while ((i + 1) < eir.length) { var length = eir.readUInt8(i); if (length < 1) { debug('invalid EIR data, length = ' + length); break; } var eirType = eir.readUInt8(i + 1); // https://www.bluetooth.org/en-us/specification/assigned-numbers/generic-access-profile if ((i + length + 1) > eir.length) { debug('invalid EIR data, out of range of buffer length'); break; } var bytes = eir.slice(i + 2).slice(0, length - 1); switch(eirType) { case 0x02: // Incomplete List of 16-bit Service Class UUID case 0x03: // Complete List of 16-bit Service Class UUIDs for (j = 0; j < bytes.length; j += 2) { serviceUuid = bytes.readUInt16LE(j).toString(16); if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { advertisement.serviceUuids.push(serviceUuid); } } break; case 0x06: // Incomplete List of 128-bit Service Class UUIDs case 0x07: // Complete List of 128-bit Service Class UUIDs for (j = 0; j < bytes.length; j += 16) { serviceUuid = bytes.slice(j, j + 16).toString('hex').match(/.{1,2}/g).reverse().join(''); if (advertisement.serviceUuids.indexOf(serviceUuid) === -1) { advertisement.serviceUuids.push(serviceUuid); } } break; case 0x08: // Shortened Local Name case 0x09: // Complete Local Name» advertisement.localName = bytes.toString('utf8'); break; case 0x0a: // Tx Power Level advertisement.txPowerLevel = bytes.readInt8(0); break; case 0x16: // Service Data, there can be multiple occurences var serviceDataUuid = bytes.slice(0, 2).toString('hex').match(/.{1,2}/g).reverse().join(''); var serviceData = bytes.slice(2, bytes.length); advertisement.serviceData.push({ uuid: serviceDataUuid, data: serviceData }); break; case 0xff: // Manufacturer Specific Data advertisement.manufacturerData = bytes; break; } i += (length + 1); } debug('advertisement = ' + JSON.stringify(advertisement, null, 0)); var connectable = (type === 0x04) ? this._discoveries[address].connectable : (type !== 0x03); this._discoveries[address] = { address: address, addressType: addressType, connectable: connectable, advertisement: advertisement, rssi: rssi, count: discoveryCount, hasScanResponse: hasScanResponse }; // only report after a scan response event or more than one discovery without a scan response, so more data can be collected if (type === 0x04 || (discoveryCount > 1 && !hasScanResponse) || process.env.NOBLE_REPORT_ALL_HCI_EVENTS) { this.emit('discover', status, address, addressType, connectable, advertisement, rssi); } }; /* BLENO Start*/ Gap.prototype.startAdvertising = function(name, serviceUuids) { debug('startAdvertising: name = ' + name + ', serviceUuids = ' + JSON.stringify(serviceUuids, null, 2)); var advertisementDataLength = 3; var scanDataLength = 0; var serviceUuids16bit = []; var serviceUuids128bit = []; var i = 0; if (name && name.length) { scanDataLength += 2 + name.length; } if (serviceUuids && serviceUuids.length) { for (i = 0; i < serviceUuids.length; i++) { var serviceUuid = new Buffer(serviceUuids[i].match(/.{1,2}/g).reverse().join(''), 'hex'); if (serviceUuid.length === 2) { serviceUuids16bit.push(serviceUuid); } else if (serviceUuid.length === 16) { serviceUuids128bit.push(serviceUuid); } } } if (serviceUuids16bit.length) { advertisementDataLength += 2 + 2 * serviceUuids16bit.length; } if (serviceUuids128bit.length) { advertisementDataLength += 2 + 16 * serviceUuids128bit.length; } var advertisementData = new Buffer(advertisementDataLength); var scanData = new Buffer(scanDataLength); // flags advertisementData.writeUInt8(2, 0); advertisementData.writeUInt8(0x01, 1); advertisementData.writeUInt8(0x06, 2); var advertisementDataOffset = 3; if (serviceUuids16bit.length) { advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset); advertisementDataOffset++; advertisementData.writeUInt8(0x03, advertisementDataOffset); advertisementDataOffset++; for (i = 0; i < serviceUuids16bit.length; i++) { serviceUuids16bit[i].copy(advertisementData, advertisementDataOffset); advertisementDataOffset += serviceUuids16bit[i].length; } } if (serviceUuids128bit.length) { advertisementData.writeUInt8(1 + 16 * serviceUuids128bit.length, advertisementDataOffset); advertisementDataOffset++; advertisementData.writeUInt8(0x06, advertisementDataOffset); advertisementDataOffset++; for (i = 0; i < serviceUuids128bit.length; i++) { serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset); advertisementDataOffset += serviceUuids128bit[i].length; } } // name if (name && name.length) { var nameBuffer = new Buffer(name); scanData.writeUInt8(1 + nameBuffer.length, 0); scanData.writeUInt8(0x08, 1); nameBuffer.copy(scanData, 2); } this.startAdvertisingWithEIRData(advertisementData, scanData); }; Gap.prototype.startAdvertisingIBeacon = function(data) { debug('startAdvertisingIBeacon: data = ' + data.toString('hex')); var dataLength = data.length; var manufacturerDataLength = 4 + dataLength; var advertisementDataLength = 5 + manufacturerDataLength; var scanDataLength = 0; var advertisementData = new Buffer(advertisementDataLength); var scanData = new Buffer(0); // flags advertisementData.writeUInt8(2, 0); advertisementData.writeUInt8(0x01, 1); advertisementData.writeUInt8(0x06, 2); advertisementData.writeUInt8(manufacturerDataLength + 1, 3); advertisementData.writeUInt8(0xff, 4); advertisementData.writeUInt16LE(0x004c, 5); // Apple Company Identifier LE (16 bit) advertisementData.writeUInt8(0x02, 7); // type, 2 => iBeacon advertisementData.writeUInt8(dataLength, 8); data.copy(advertisementData, 9); this.startAdvertisingWithEIRData(advertisementData, scanData); }; Gap.prototype.startAdvertisingWithEIRData = function(advertisementData, scanData) { advertisementData = advertisementData || new Buffer(0); scanData = scanData || new Buffer(0); debug('startAdvertisingWithEIRData: advertisement data = ' + advertisementData.toString('hex') + ', scan data = ' + scanData.toString('hex')); var error = null; if (advertisementData.length > 31) { error = new Error('Advertisement data is over maximum limit of 31 bytes'); } else if (scanData.length > 31) { error = new Error('Scan data is over maximum limit of 31 bytes'); } if (error) { this.emit('advertisingStart', error); } else { this._advertiseState = 'starting'; this._hci.setScanResponseData(scanData); this._hci.setAdvertisingData(advertisementData); this._hci.setAdvertiseEnable(true); //this._hci.setScanResponseData(scanData); //this._hci.setAdvertisingData(advertisementData); } }; Gap.prototype.restartAdvertising = function() { this._advertiseState = 'restarting'; this._hci.setAdvertiseEnable(true); }; Gap.prototype.stopAdvertising = function() { this._advertiseState = 'stopping'; this._hci.setAdvertiseEnable(false); }; Gap.prototype.onHciLeAdvertisingParametersSet = function(status) { }; Gap.prototype.onHciLeAdvertisingDataSet = function(status) { }; Gap.prototype.onHciLeScanResponseDataSet = function(status) { }; Gap.prototype.onHciLeAdvertiseEnableSet = function(status) { if (this._advertiseState === 'starting') { this._advertiseState = 'started'; var error = null; if (status) { error = new Error( 'Unknown (' + status + ')'); } this.emit('advertisingStart', error); } else if (this._advertiseState === 'stopping') { this._advertiseState = 'stopped'; this.emit('advertisingStop'); } }; /* BLENO Stop */ module.exports = Gap;