@stoprocent/bleno
Version:
A Node.js module for implementing BLE (Bluetooth Low Energy) peripherals
236 lines (178 loc) • 6.24 kB
JavaScript
const debug = require('debug')('bindings');
const BlenoEventEmitter = require('../bleno-event-emitter');
const os = require('os');
const AclStream = require('./acl-stream');
const Hci = require('./hci');
const Gap = require('./gap');
const Gatt = require('./gatt');
class BlenoBindings extends BlenoEventEmitter {
constructor (options) {
super();
this._state = null;
this._advertising = false;
this._hci = new Hci(options);
this._gap = new Gap(this._hci);
this._gatt = new Gatt(this._hci);
this._connections = new Map();
}
setAddress (address) {
this._hci.setAddress(address);
}
startAdvertising (name, serviceUuids) {
this._advertising = true;
this._gatt.updateName(name);
this._gap.startAdvertising(name, serviceUuids);
}
startAdvertisingIBeacon (data) {
this._advertising = true;
this._gap.startAdvertisingIBeacon(data);
}
startAdvertisingWithEIRData (advertisementData, scanData) {
this._advertising = true;
this._gap.startAdvertisingWithEIRData(advertisementData, scanData);
}
stopAdvertising () {
this._advertising = false;
this._gap.stopAdvertising();
}
setServices (services) {
this._gatt.setServices(services);
this.emit('servicesSet');
}
disconnect (handle = null) {
if (handle) {
debug('disconnect by server for handle', handle);
this._hci.disconnect(handle);
} else {
for (const [handle, connection] of this._connections) {
debug('disconnect by server for handle', handle, connection.address);
this._hci.disconnect(handle);
}
}
}
updateRssi () {
if (this._handle) {
this._hci.readRssi(this._handle);
}
}
init () {
this._sigIntHandler = this.onSigInt.bind(this);
this._exitHandler = this.onExit.bind(this);
process.on('SIGINT', this._sigIntHandler);
process.on('exit', this._exitHandler);
this._gap.on('advertisingStart', this.onAdvertisingStart.bind(this));
this._gap.on('advertisingStop', this.onAdvertisingStop.bind(this));
this._gatt.on('mtuChange', this.onMtuChange.bind(this));
this._hci.on('stateChange', this.onStateChange.bind(this));
this._hci.on('addressChange', this.onAddressChange.bind(this));
this._hci.on('readLocalVersion', this.onReadLocalVersion.bind(this));
this._hci.on('leConnComplete', this.onLeConnComplete.bind(this));
this._hci.on('leConnUpdateComplete', this.onLeConnUpdateComplete.bind(this));
this._hci.on('rssiRead', this.onRssiRead.bind(this));
this._hci.on('disconnComplete', this.onDisconnComplete.bind(this));
this._hci.on('encryptChange', this.onEncryptChange.bind(this));
this._hci.on('leLtkNegReply', this.onLeLtkNegReply.bind(this));
this._hci.on('aclDataPkt', this.onAclDataPkt.bind(this));
this.emit('platform', os.platform());
this._hci.init();
}
stop () {
process.removeListener('SIGINT', this._sigIntHandler);
process.removeListener('exit', this._exitHandler);
this.stopAdvertising();
this.disconnect();
this._hci.stop();
}
onStateChange (state) {
if (this._state === state) {
return;
}
this._state = state;
if (state === 'unauthorized') {
console.log('Bleno warning: adapter state unauthorized, please run as root or with sudo');
console.log(' or see README for information on running without root/sudo:');
console.log(' https://github.com/abandonware/bleno#running-on-linux');
} else if (state === 'unsupported') {
console.log('Bleno warning: adapter does not support Bluetooth Low Energy (BLE, Bluetooth Smart).');
console.log(' Try to run with environment variable:');
console.log(' [sudo] BLENO_HCI_DEVICE_ID=x node ...');
}
this.emit('stateChange', state);
}
onAddressChange (address) {
this.emit('addressChange', address);
}
onReadLocalVersion (hciVer, hciRev, lmpVer, manufacturer, lmpSubVer) {
}
onAdvertisingStart (error) {
this.emit('advertisingStart', error);
}
onAdvertisingStop () {
this.emit('advertisingStop');
}
onLeConnComplete (status, handle, role, addressType, address, interval, latency, supervisionTimeout, masterClockAccuracy) {
if (role !== 1) {
return;
}
const aclStream = new AclStream(this._hci, handle, this._hci.addressType, this._hci.address, addressType, address);
this._connections.set(handle, { address, aclStream });
this._gatt.registerAclStream(aclStream, handle);
this.emit('accept', address, handle);
if (this._advertising) {
this._gap.restartAdvertising();
}
}
onLeConnUpdateComplete (handle, interval, latency, supervisionTimeout) {
// no-op
}
onDisconnComplete (handle, reason) {
if (this._connections.has(handle) === false) {
return;
}
const { address, aclStream } = this._connections.get(handle);
if (aclStream) {
aclStream.close();
}
this._connections.delete(handle);
this.emit('disconnect', address, handle);
if (this._advertising) {
this._gap.restartAdvertising();
}
}
onEncryptChange (handle, encrypt) {
const connection = this._connections.get(handle);
if (connection && connection.aclStream) {
connection.aclStream.pushEncrypt(encrypt);
}
}
onLeLtkNegReply (handle) {
const connection = this._connections.get(handle);
if (connection && connection.aclStream) {
connection.aclStream.pushLtkNegReply();
}
}
onMtuChange (mtu) {
this.emit('mtuChange', mtu);
}
onRssiRead (handle, rssi) {
this.emit('rssiUpdate', rssi);
}
onAclDataPkt (handle, cid, data) {
const connection = this._connections.get(handle);
if (connection && connection.aclStream) {
connection.aclStream.push(cid, data);
}
}
onSigInt () {
const sigIntListeners = process.listeners('SIGINT');
if (sigIntListeners[sigIntListeners.length - 1] === this._sigIntHandler) {
// we are the last listener, so exit
// this will trigger onExit, and clean up
process.exit(1);
}
}
onExit () {
this.stop();
}
}
module.exports = BlenoBindings;