@abandonware/bleno
Version:
A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals
212 lines (156 loc) • 6.37 kB
JavaScript
const debug = require('debug')('gap');
const { EventEmitter } = require('events');
const os = require('os');
const Hci = require('./hci');
const isLinux = (os.platform() === 'linux');
const isIntelEdison = isLinux && (os.release().includes('edison'));
const isYocto = isLinux && (os.release().includes('yocto'));
class Gap extends EventEmitter {
constructor(hci) {
super();
this._hci = hci;
this._advertiseState = null;
this._hci.on('error', this.onHciError.bind(this));
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));
}
startAdvertising(name, serviceUuids) {
debug('startAdvertising: name = ' + name + ', serviceUuids = ' + JSON.stringify(serviceUuids, null, 2));
let advertisementDataLength = 3;
let scanDataLength = 0;
const serviceUuids16bit = [];
const serviceUuids128bit = [];
if (name && name.length) {
scanDataLength += 2 + name.length;
}
if (serviceUuids && serviceUuids.length) {
for (let i = 0; i < serviceUuids.length; i++) {
const serviceUuid = Buffer.from(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;
}
const advertisementData = Buffer.alloc(advertisementDataLength);
const scanData = Buffer.alloc(scanDataLength);
// flags
advertisementData.writeUInt8(2, 0);
advertisementData.writeUInt8(0x01, 1);
advertisementData.writeUInt8(0x06, 2);
let advertisementDataOffset = 3;
if (serviceUuids16bit.length) {
advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset);
advertisementDataOffset++;
advertisementData.writeUInt8(0x03, advertisementDataOffset);
advertisementDataOffset++;
for (let 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 (let i = 0; i < serviceUuids128bit.length; i++) {
serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset);
advertisementDataOffset += serviceUuids128bit[i].length;
}
}
// name
if (name && name.length) {
const nameBuffer = Buffer.from(name);
scanData.writeUInt8(1 + nameBuffer.length, 0);
scanData.writeUInt8(0x08, 1);
nameBuffer.copy(scanData, 2);
}
this.startAdvertisingWithEIRData(advertisementData, scanData);
}
startAdvertisingIBeacon(data) {
debug('startAdvertisingIBeacon: data = ' + data.toString('hex'));
const dataLength = data.length;
const manufacturerDataLength = 4 + dataLength;
const advertisementDataLength = 5 + manufacturerDataLength;
const scanDataLength = 0;
const advertisementData = Buffer.alloc(advertisementDataLength);
const scanData = Buffer.alloc(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);
}
startAdvertisingWithEIRData(advertisementData, scanData) {
advertisementData = advertisementData || Buffer.alloc(0);
scanData = scanData || Buffer.alloc(0);
debug('startAdvertisingWithEIRData: advertisement data = ' + advertisementData.toString('hex') + ', scan data = ' + scanData.toString('hex'));
let 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';
if (isIntelEdison || isYocto) {
// work around for Intel Edison
debug('skipping first set of scan response and advertisement data');
} else {
this._hci.setScanResponseData(scanData);
this._hci.setAdvertisingData(advertisementData);
}
this._hci.setAdvertiseEnable(true);
this._hci.setScanResponseData(scanData);
this._hci.setAdvertisingData(advertisementData);
}
}
restartAdvertising() {
this._advertiseState = 'restarting';
this._hci.setAdvertiseEnable(true);
}
stopAdvertising() {
this._advertiseState = 'stopping';
this._hci.setAdvertiseEnable(false);
}
onHciError(error) {
}
onHciLeAdvertisingParametersSet(status) {
}
onHciLeAdvertisingDataSet(status) {
}
onHciLeScanResponseDataSet(status) {
}
onHciLeAdvertiseEnableSet(status) {
if (this._advertiseState === 'starting') {
this._advertiseState = 'started';
let error = null;
if (status) {
error = new Error(Hci.STATUS_MAPPER[status] || ('Unknown (' + status + ')'));
}
this.emit('advertisingStart', error);
} else if (this._advertiseState === 'stopping') {
this._advertiseState = 'stopped';
this.emit('advertisingStop');
}
}
}
module.exports = Gap;