noble
Version:
A Node.js BLE (Bluetooth Low Energy) central library.
684 lines (519 loc) • 19.9 kB
JavaScript
var debug = require('debug')('hci');
var events = require('events');
var util = require('util');
var BluetoothHciSocket = require('bluetooth-hci-socket');
var HCI_COMMAND_PKT = 0x01;
var HCI_ACLDATA_PKT = 0x02;
var HCI_EVENT_PKT = 0x04;
var ACL_START_NO_FLUSH = 0x00;
var ACL_CONT = 0x01;
var ACL_START = 0x02;
var EVT_DISCONN_COMPLETE = 0x05;
var EVT_ENCRYPT_CHANGE = 0x08;
var EVT_CMD_COMPLETE = 0x0e;
var EVT_CMD_STATUS = 0x0f;
var EVT_LE_META_EVENT = 0x3e;
var EVT_LE_CONN_COMPLETE = 0x01;
var EVT_LE_ADVERTISING_REPORT = 0x02;
var EVT_LE_CONN_UPDATE_COMPLETE = 0x03;
var OGF_LINK_CTL = 0x01;
var OCF_DISCONNECT = 0x0006;
var OGF_HOST_CTL = 0x03;
var OCF_SET_EVENT_MASK = 0x0001;
var OCF_RESET = 0x0003;
var OCF_READ_LE_HOST_SUPPORTED = 0x006C;
var OCF_WRITE_LE_HOST_SUPPORTED = 0x006D;
var OGF_INFO_PARAM = 0x04;
var OCF_READ_LOCAL_VERSION = 0x0001;
var OCF_READ_BD_ADDR = 0x0009;
var OGF_STATUS_PARAM = 0x05;
var OCF_READ_RSSI = 0x0005;
var OGF_LE_CTL = 0x08;
var OCF_LE_SET_EVENT_MASK = 0x0001;
var OCF_LE_SET_SCAN_PARAMETERS = 0x000b;
var OCF_LE_SET_SCAN_ENABLE = 0x000c;
var OCF_LE_CREATE_CONN = 0x000d;
var OCF_LE_CONN_UPDATE = 0x0013;
var OCF_LE_START_ENCRYPTION = 0x0019;
var DISCONNECT_CMD = OCF_DISCONNECT | OGF_LINK_CTL << 10;
var SET_EVENT_MASK_CMD = OCF_SET_EVENT_MASK | OGF_HOST_CTL << 10;
var RESET_CMD = OCF_RESET | OGF_HOST_CTL << 10;
var READ_LE_HOST_SUPPORTED_CMD = OCF_READ_LE_HOST_SUPPORTED | OGF_HOST_CTL << 10;
var WRITE_LE_HOST_SUPPORTED_CMD = OCF_WRITE_LE_HOST_SUPPORTED | OGF_HOST_CTL << 10;
var READ_LOCAL_VERSION_CMD = OCF_READ_LOCAL_VERSION | (OGF_INFO_PARAM << 10);
var READ_BD_ADDR_CMD = OCF_READ_BD_ADDR | (OGF_INFO_PARAM << 10);
var READ_RSSI_CMD = OCF_READ_RSSI | OGF_STATUS_PARAM << 10;
var LE_SET_EVENT_MASK_CMD = OCF_LE_SET_EVENT_MASK | OGF_LE_CTL << 10;
var LE_SET_SCAN_PARAMETERS_CMD = OCF_LE_SET_SCAN_PARAMETERS | OGF_LE_CTL << 10;
var LE_SET_SCAN_ENABLE_CMD = OCF_LE_SET_SCAN_ENABLE | OGF_LE_CTL << 10;
var LE_CREATE_CONN_CMD = OCF_LE_CREATE_CONN | OGF_LE_CTL << 10;
var LE_CONN_UPDATE_CMD = OCF_LE_CONN_UPDATE | OGF_LE_CTL << 10;
var LE_START_ENCRYPTION_CMD = OCF_LE_START_ENCRYPTION | OGF_LE_CTL << 10;
var HCI_OE_USER_ENDED_CONNECTION = 0x13;
var STATUS_MAPPER = require('./hci-status');
var Hci = function() {
this._socket = new BluetoothHciSocket();
this._isDevUp = null;
this._state = null;
this._deviceId = null;
this._handleBuffers = {};
this.on('stateChange', this.onStateChange.bind(this));
};
util.inherits(Hci, events.EventEmitter);
Hci.STATUS_MAPPER = STATUS_MAPPER;
Hci.prototype.init = function() {
this._socket.on('data', this.onSocketData.bind(this));
this._socket.on('error', this.onSocketError.bind(this));
var deviceId = process.env.NOBLE_HCI_DEVICE_ID ? parseInt(process.env.NOBLE_HCI_DEVICE_ID, 10) : undefined;
if (process.env.HCI_CHANNEL_USER) {
this._deviceId = this._socket.bindUser(deviceId);
this._socket.start();
this.reset();
} else {
this._deviceId = this._socket.bindRaw(deviceId);
this._socket.start();
this.pollIsDevUp();
}
};
Hci.prototype.pollIsDevUp = function() {
var isDevUp = this._socket.isDevUp();
if (this._isDevUp !== isDevUp) {
if (isDevUp) {
this.setSocketFilter();
this.setEventMask();
this.setLeEventMask();
this.readLocalVersion();
this.writeLeHostSupported();
this.readLeHostSupported();
this.readBdAddr();
} else {
this.emit('stateChange', 'poweredOff');
}
this._isDevUp = isDevUp;
}
setTimeout(this.pollIsDevUp.bind(this), 1000);
};
Hci.prototype.setSocketFilter = function() {
var filter = new Buffer(14);
var typeMask = (1 << HCI_COMMAND_PKT) | (1 << HCI_EVENT_PKT) | (1 << HCI_ACLDATA_PKT);
var eventMask1 = (1 << EVT_DISCONN_COMPLETE) | (1 << EVT_ENCRYPT_CHANGE) | (1 << EVT_CMD_COMPLETE) | (1 << EVT_CMD_STATUS);
var eventMask2 = (1 << (EVT_LE_META_EVENT - 32));
var opcode = 0;
filter.writeUInt32LE(typeMask, 0);
filter.writeUInt32LE(eventMask1, 4);
filter.writeUInt32LE(eventMask2, 8);
filter.writeUInt16LE(opcode, 12);
debug('setting filter to: ' + filter.toString('hex'));
this._socket.setFilter(filter);
};
Hci.prototype.setEventMask = function() {
var cmd = new Buffer(12);
var eventMask = new Buffer('fffffbff07f8bf3d', 'hex');
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(SET_EVENT_MASK_CMD, 1);
// length
cmd.writeUInt8(eventMask.length, 3);
eventMask.copy(cmd, 4);
debug('set event mask - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.reset = function() {
var cmd = new Buffer(4);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(OCF_RESET | OGF_HOST_CTL << 10, 1);
// length
cmd.writeUInt8(0x00, 3);
debug('reset - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.readLocalVersion = function() {
var cmd = new Buffer(4);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(READ_LOCAL_VERSION_CMD, 1);
// length
cmd.writeUInt8(0x0, 3);
debug('read local version - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.readBdAddr = function() {
var cmd = new Buffer(4);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(READ_BD_ADDR_CMD, 1);
// length
cmd.writeUInt8(0x0, 3);
debug('read bd addr - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.setLeEventMask = function() {
var cmd = new Buffer(12);
var leEventMask = new Buffer('1f00000000000000', 'hex');
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_SET_EVENT_MASK_CMD, 1);
// length
cmd.writeUInt8(leEventMask.length, 3);
leEventMask.copy(cmd, 4);
debug('set le event mask - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.readLeHostSupported = function() {
var cmd = new Buffer(4);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(READ_LE_HOST_SUPPORTED_CMD, 1);
// length
cmd.writeUInt8(0x00, 3);
debug('read LE host supported - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.writeLeHostSupported = function() {
var cmd = new Buffer(6);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(WRITE_LE_HOST_SUPPORTED_CMD, 1);
// length
cmd.writeUInt8(0x02, 3);
// data
cmd.writeUInt8(0x01, 4); // le
cmd.writeUInt8(0x00, 5); // simul
debug('write LE host supported - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.setScanParameters = function() {
var cmd = new Buffer(11);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_SET_SCAN_PARAMETERS_CMD, 1);
// length
cmd.writeUInt8(0x07, 3);
// data
cmd.writeUInt8(0x01, 4); // type: 0 -> passive, 1 -> active
cmd.writeUInt16LE(0x0010, 5); // internal, ms * 1.6
cmd.writeUInt16LE(0x0010, 7); // window, ms * 1.6
cmd.writeUInt8(0x00, 9); // own address type: 0 -> public, 1 -> random
cmd.writeUInt8(0x00, 10); // filter: 0 -> all event types
debug('set scan parameters - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.setScanEnabled = function(enabled, filterDuplicates) {
var cmd = new Buffer(6);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_SET_SCAN_ENABLE_CMD, 1);
// length
cmd.writeUInt8(0x02, 3);
// data
cmd.writeUInt8(enabled ? 0x01 : 0x00, 4); // enable: 0 -> disabled, 1 -> enabled
cmd.writeUInt8(filterDuplicates ? 0x01 : 0x00, 5); // duplicates: 0 -> duplicates, 0 -> duplicates
debug('set scan enabled - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.createLeConn = function(address, addressType) {
var cmd = new Buffer(29);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_CREATE_CONN_CMD, 1);
// length
cmd.writeUInt8(0x19, 3);
// data
cmd.writeUInt16LE(0x0060, 4); // interval
cmd.writeUInt16LE(0x0030, 6); // window
cmd.writeUInt8(0x00, 8); // initiator filter
cmd.writeUInt8(addressType === 'random' ? 0x01 : 0x00, 9); // peer address type
(new Buffer(address.split(':').reverse().join(''), 'hex')).copy(cmd, 10); // peer address
cmd.writeUInt8(0x00, 16); // own address type
cmd.writeUInt16LE(0x0006, 17); // min interval
cmd.writeUInt16LE(0x000c, 19); // max interval
cmd.writeUInt16LE(0x0000, 21); // latency
cmd.writeUInt16LE(0x00c8, 23); // supervision timeout
cmd.writeUInt16LE(0x0004, 25); // min ce length
cmd.writeUInt16LE(0x0006, 27); // max ce length
debug('create le conn - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.connUpdateLe = function(handle, minInterval, maxInterval, latency, supervisionTimeout) {
var cmd = new Buffer(18);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_CONN_UPDATE_CMD, 1);
// length
cmd.writeUInt8(0x0e, 3);
// data
cmd.writeUInt16LE(handle, 4);
cmd.writeUInt16LE(Math.floor(minInterval / 1.25), 6); // min interval
cmd.writeUInt16LE(Math.floor(maxInterval / 1.25), 8); // max interval
cmd.writeUInt16LE(latency, 10); // latency
cmd.writeUInt16LE(Math.floor(supervisionTimeout / 10), 12); // supervision timeout
cmd.writeUInt16LE(0x0000, 14); // min ce length
cmd.writeUInt16LE(0x0000, 16); // max ce length
debug('conn update le - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.startLeEncryption = function(handle, random, diversifier, key) {
var cmd = new Buffer(32);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(LE_START_ENCRYPTION_CMD, 1);
// length
cmd.writeUInt8(0x1c, 3);
// data
cmd.writeUInt16LE(handle, 4); // handle
random.copy(cmd, 6);
diversifier.copy(cmd, 14);
key.copy(cmd, 16);
debug('start le encryption - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.disconnect = function(handle, reason) {
var cmd = new Buffer(7);
reason = reason || HCI_OE_USER_ENDED_CONNECTION;
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(DISCONNECT_CMD, 1);
// length
cmd.writeUInt8(0x03, 3);
// data
cmd.writeUInt16LE(handle, 4); // handle
cmd.writeUInt8(reason, 6); // reason
debug('disconnect - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.readRssi = function(handle) {
var cmd = new Buffer(6);
// header
cmd.writeUInt8(HCI_COMMAND_PKT, 0);
cmd.writeUInt16LE(READ_RSSI_CMD, 1);
// length
cmd.writeUInt8(0x02, 3);
// data
cmd.writeUInt16LE(handle, 4); // handle
debug('read rssi - writing: ' + cmd.toString('hex'));
this._socket.write(cmd);
};
Hci.prototype.writeAclDataPkt = function(handle, cid, data) {
var pkt = new Buffer(9 + data.length);
// header
pkt.writeUInt8(HCI_ACLDATA_PKT, 0);
pkt.writeUInt16LE(handle | ACL_START_NO_FLUSH << 12, 1);
pkt.writeUInt16LE(data.length + 4, 3); // data length 1
pkt.writeUInt16LE(data.length, 5); // data length 2
pkt.writeUInt16LE(cid, 7);
data.copy(pkt, 9);
debug('write acl data pkt - writing: ' + pkt.toString('hex'));
this._socket.write(pkt);
};
Hci.prototype.onSocketData = function(data) {
debug('onSocketData: ' + data.toString('hex'));
var eventType = data.readUInt8(0);
var handle;
var cmd;
var status;
debug('\tevent type = ' + eventType);
if (HCI_EVENT_PKT === eventType) {
var subEventType = data.readUInt8(1);
debug('\tsub event type = ' + subEventType);
if (subEventType === EVT_DISCONN_COMPLETE) {
handle = data.readUInt16LE(4);
var reason = data.readUInt8(6);
debug('\t\thandle = ' + handle);
debug('\t\treason = ' + reason);
this.emit('disconnComplete', handle, reason);
} else if (subEventType === EVT_ENCRYPT_CHANGE) {
handle = data.readUInt16LE(4);
var encrypt = data.readUInt8(6);
debug('\t\thandle = ' + handle);
debug('\t\tencrypt = ' + encrypt);
this.emit('encryptChange', handle, encrypt);
} else if (subEventType === EVT_CMD_COMPLETE) {
cmd = data.readUInt16LE(4);
status = data.readUInt8(6);
var result = data.slice(7);
debug('\t\tcmd = ' + cmd);
debug('\t\tstatus = ' + status);
debug('\t\tresult = ' + result.toString('hex'));
this.processCmdCompleteEvent(cmd, status, result);
} else if (subEventType === EVT_CMD_STATUS) {
status = data.readUInt8(3);
cmd = data.readUInt16LE(5);
debug('\t\tstatus = ' + status);
debug('\t\tcmd = ' + cmd);
this.processCmdStatusEvent(cmd, status);
} else if (subEventType === EVT_LE_META_EVENT) {
var leMetaEventType = data.readUInt8(3);
var leMetaEventStatus = data.readUInt8(4);
var leMetaEventData = data.slice(5);
debug('\t\tLE meta event type = ' + leMetaEventType);
debug('\t\tLE meta event status = ' + leMetaEventStatus);
debug('\t\tLE meta event data = ' + leMetaEventData.toString('hex'));
this.processLeMetaEvent(leMetaEventType, leMetaEventStatus, leMetaEventData);
}
} else if (HCI_ACLDATA_PKT === eventType) {
var flags = data.readUInt16LE(1) >> 12;
handle = data.readUInt16LE(1) & 0x0fff;
if (ACL_START === flags) {
var cid = data.readUInt16LE(7);
var length = data.readUInt16LE(5);
var pktData = data.slice(9);
debug('\t\tcid = ' + cid);
if (length === pktData.length) {
debug('\t\thandle = ' + handle);
debug('\t\tdata = ' + pktData.toString('hex'));
this.emit('aclDataPkt', handle, cid, pktData);
} else {
this._handleBuffers[handle] = {
length: length,
cid: cid,
data: pktData
};
}
} else if (ACL_CONT === flags) {
if (!this._handleBuffers[handle] || !this._handleBuffers[handle].data) {
return;
}
this._handleBuffers[handle].data = Buffer.concat([
this._handleBuffers[handle].data,
data.slice(5)
]);
if (this._handleBuffers[handle].data.length === this._handleBuffers[handle].length) {
this.emit('aclDataPkt', handle, this._handleBuffers[handle].cid, this._handleBuffers[handle].data);
delete this._handleBuffers[handle];
}
}
} else if (HCI_COMMAND_PKT === eventType) {
cmd = data.readUInt16LE(1);
var len = data.readUInt8(3);
debug('\t\tcmd = ' + cmd);
debug('\t\tdata len = ' + len);
if (cmd === LE_SET_SCAN_ENABLE_CMD) {
var enable = (data.readUInt8(4) === 0x1);
var filterDuplicates = (data.readUInt8(5) === 0x1);
debug('\t\t\tLE enable scan command');
debug('\t\t\tenable scanning = ' + enable);
debug('\t\t\tfilter duplicates = ' + filterDuplicates);
this.emit('leScanEnableSetCmd', enable, filterDuplicates);
}
}
};
Hci.prototype.onSocketError = function(error) {
debug('onSocketError: ' + error.message);
if (error.message === 'Operation not permitted') {
this.emit('stateChange', 'unauthorized');
} else if (error.message === 'Network is down') {
// no-op
}
};
Hci.prototype.processCmdCompleteEvent = function(cmd, status, result) {
if (cmd === RESET_CMD) {
this.setEventMask();
this.setLeEventMask();
this.readLocalVersion();
this.readBdAddr();
} else if (cmd === READ_LE_HOST_SUPPORTED_CMD) {
if (status === 0) {
var le = result.readUInt8(0);
var simul = result.readUInt8(1);
debug('\t\t\tle = ' + le);
debug('\t\t\tsimul = ' + simul);
}
} else if (cmd === READ_LOCAL_VERSION_CMD) {
var hciVer = result.readUInt8(0);
var hciRev = result.readUInt16LE(1);
var lmpVer = result.readInt8(3);
var manufacturer = result.readUInt16LE(4);
var lmpSubVer = result.readUInt16LE(6);
if (hciVer < 0x06) {
this.emit('stateChange', 'unsupported');
} else if (this._state !== 'poweredOn') {
this.setScanEnabled(false, true);
this.setScanParameters();
}
this.emit('readLocalVersion', hciVer, hciRev, lmpVer, manufacturer, lmpSubVer);
} else if (cmd === READ_BD_ADDR_CMD) {
this.addressType = 'public';
this.address = result.toString('hex').match(/.{1,2}/g).reverse().join(':');
debug('address = ' + this.address);
this.emit('addressChange', this.address);
} else if (cmd === LE_SET_SCAN_PARAMETERS_CMD) {
this.emit('stateChange', 'poweredOn');
this.emit('leScanParametersSet');
} else if (cmd === LE_SET_SCAN_ENABLE_CMD) {
this.emit('leScanEnableSet', status);
} else if (cmd === READ_RSSI_CMD) {
var handle = result.readUInt16LE(0);
var rssi = result.readInt8(2);
debug('\t\t\thandle = ' + handle);
debug('\t\t\trssi = ' + rssi);
this.emit('rssiRead', handle, rssi);
}
};
Hci.prototype.processLeMetaEvent = function(eventType, status, data) {
if (eventType === EVT_LE_CONN_COMPLETE) {
this.processLeConnComplete(status, data);
} else if (eventType === EVT_LE_ADVERTISING_REPORT) {
this.processLeAdvertisingReport(status, data);
} else if (eventType === EVT_LE_CONN_UPDATE_COMPLETE) {
this.processLeConnUpdateComplete(status, data);
}
};
Hci.prototype.processLeConnComplete = function(status, data) {
var handle = data.readUInt16LE(0);
var role = data.readUInt8(2);
var addressType = data.readUInt8(3) === 0x01 ? 'random': 'public';
var address = data.slice(4, 10).toString('hex').match(/.{1,2}/g).reverse().join(':');
var interval = data.readUInt16LE(10) * 1.25;
var latency = data.readUInt16LE(12); // TODO: multiplier?
var supervisionTimeout = data.readUInt16LE(14) * 10;
var masterClockAccuracy = data.readUInt8(16); // TODO: multiplier?
debug('\t\t\thandle = ' + handle);
debug('\t\t\trole = ' + role);
debug('\t\t\taddress type = ' + addressType);
debug('\t\t\taddress = ' + address);
debug('\t\t\tinterval = ' + interval);
debug('\t\t\tlatency = ' + latency);
debug('\t\t\tsupervision timeout = ' + supervisionTimeout);
debug('\t\t\tmaster clock accuracy = ' + masterClockAccuracy);
this.emit('leConnComplete', status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy);
};
Hci.prototype.processLeAdvertisingReport = function(count, data) {
for (var i = 0; i < count; i++) {
var type = data.readUInt8(0);
var addressType = data.readUInt8(1) === 0x01 ? 'random' : 'public';
var address = data.slice(2, 8).toString('hex').match(/.{1,2}/g).reverse().join(':');
var eirLength = data.readUInt8(8);
var eir = data.slice(9, eirLength + 9);
var rssi = data.readInt8(eirLength + 9);
debug('\t\t\ttype = ' + type);
debug('\t\t\taddress = ' + address);
debug('\t\t\taddress type = ' + addressType);
debug('\t\t\teir = ' + eir.toString('hex'));
debug('\t\t\trssi = ' + rssi);
this.emit('leAdvertisingReport', 0, type, address, addressType, eir, rssi);
data = data.slice(eirLength + 10);
}
};
Hci.prototype.processLeConnUpdateComplete = function(status, data) {
var handle = data.readUInt16LE(0);
var interval = data.readUInt16LE(2) * 1.25;
var latency = data.readUInt16LE(4); // TODO: multiplier?
var supervisionTimeout = data.readUInt16LE(6) * 10;
debug('\t\t\thandle = ' + handle);
debug('\t\t\tinterval = ' + interval);
debug('\t\t\tlatency = ' + latency);
debug('\t\t\tsupervision timeout = ' + supervisionTimeout);
this.emit('leConnUpdateComplete', status, handle, interval, latency, supervisionTimeout);
};
Hci.prototype.processCmdStatusEvent = function(cmd, status) {
if (cmd === LE_CREATE_CONN_CMD) {
if (status !== 0) {
this.emit('leConnComplete', status);
}
}
};
Hci.prototype.onStateChange = function(state) {
this._state = state;
};
module.exports = Hci;