@stoprocent/noble
Version:
A Node.js BLE (Bluetooth Low Energy) central library.
801 lines (651 loc) • 26.6 kB
JavaScript
const debug = require('debug')('noble');
const NobleEventEmitter = require('./noble-event-emitter');
const Peripheral = require('./peripheral');
const Service = require('./service');
const Characteristic = require('./characteristic');
const Descriptor = require('./descriptor');
class Noble extends NobleEventEmitter {
constructor (bindings) {
super();
this._address = 'unknown';
this._state = 'unknown';
this._initialized = false;
this._bindings = bindings;
this._discoveredPeripherals = new Set();
this._peripherals = new Map();
this._services = {};
this._characteristics = {};
this._descriptors = {};
this._cleanupPeriperals();
this.on('warning', message => console.warn(`noble: ${message}`));
this.on('newListener', event => {
if (event === 'stateChange' && this._initialized === false) {
this._initialized = true;
process.nextTick(this._initializeBindings.bind(this));
}
});
}
get state () {
if (this._initialized === false) {
this._initializeBindings();
}
return this._state;
}
get address () {
return this._address;
}
_initializeBindings () {
this._initialized = true;
this._registerListeners();
this._bindings.start();
}
_registerListeners () {
this._bindings.on('stateChange', this._onStateChange.bind(this));
this._bindings.on('addressChange', this._onAddressChange.bind(this));
this._bindings.on('scanParametersSet', this._onScanParametersSet.bind(this));
this._bindings.on('scanStart', this._onScanStart.bind(this));
this._bindings.on('scanStop', this._onScanStop.bind(this));
this._bindings.on('discover', this._onDiscover.bind(this));
this._bindings.on('connect', this._onConnect.bind(this));
this._bindings.on('disconnect', this._onDisconnect.bind(this));
this._bindings.on('rssiUpdate', this._onRssiUpdate.bind(this));
this._bindings.on('servicesDiscover', this._onServicesDiscover.bind(this));
this._bindings.on('servicesDiscovered', this._onServicesDiscovered.bind(this));
this._bindings.on('includedServicesDiscover', this._onIncludedServicesDiscover.bind(this));
this._bindings.on('characteristicsDiscover', this._onCharacteristicsDiscover.bind(this));
this._bindings.on('characteristicsDiscovered', this._onCharacteristicsDiscovered.bind(this));
this._bindings.on('read', this._onRead.bind(this));
this._bindings.on('write', this._onWrite.bind(this));
this._bindings.on('broadcast', this._onBroadcast.bind(this));
this._bindings.on('notify', this._onNotify.bind(this));
this._bindings.on('descriptorsDiscover', this._onDescriptorsDiscover.bind(this));
this._bindings.on('valueRead', this._onValueRead.bind(this));
this._bindings.on('valueWrite', this._onValueWrite.bind(this));
this._bindings.on('handleRead', this._onHandleRead.bind(this));
this._bindings.on('handleWrite', this._onHandleWrite.bind(this));
this._bindings.on('handleNotify', this._onHandleNotify.bind(this));
this._bindings.on('onMtu', this._onMtu.bind(this));
}
_createPeripheral (uuid, address, addressType, connectable, advertisement, rssi, scannable) {
const peripheral = new Peripheral(this, uuid, address, addressType, connectable, advertisement, rssi, scannable);
this._peripherals.set(uuid, peripheral);
this._services[uuid] = {};
this._characteristics[uuid] = {};
this._descriptors[uuid] = {};
return peripheral;
}
_cleanupPeriperals (uuid = null) {
const terminateConnection = (peripheral) => {
if (peripheral.state === 'connecting') {
// To unblock the async connect call
this._onConnect(peripheral.id, new Error('cleanup'));
}
else if (peripheral.state !== 'disconnected') {
this._onDisconnect(peripheral.id, 'cleanup');
}
};
if (uuid) {
const peripheral = this._peripherals.get(uuid);
if (peripheral) {
terminateConnection(peripheral);
}
this._peripherals.delete(uuid);
this._discoveredPeripherals.delete(uuid);
delete this._services[uuid];
delete this._characteristics[uuid];
delete this._descriptors[uuid];
} else {
this._peripherals.forEach(peripheral => terminateConnection(peripheral));
this._peripherals.clear();
this._discoveredPeripherals.clear();
this._services = {};
this._characteristics = {};
this._descriptors = {};
}
}
_onStateChange (state) {
debug(`stateChange ${state}`);
// If the state is poweredOff and the previous state was poweredOn, clean up the peripherals
if (state === 'poweredOff' && this._state === 'poweredOn') {
this._cleanupPeriperals();
}
this._state = state;
this.emit('stateChange', state);
}
_onAddressChange (address) {
debug(`addressChange ${address}`);
this._address = address;
this.emit('addressChange', address);
}
setScanParameters (interval, window, callback) {
if (callback) {
this.onceExclusive('scanParametersSet', callback);
}
this._bindings.setScanParameters(interval, window);
}
_onScanParametersSet () {
debug('scanParametersSet');
this.emit('scanParametersSet');
}
setAddress (address) {
if (this._bindings.setAddress) {
this._bindings.setAddress(address);
} else {
this.emit('warning', 'current binding does not implement setAddress method.');
}
}
async waitForPoweredOnAsync (timeout = 10000) {
return new Promise((resolve, reject) => {
if (this.state === 'poweredOn') {
resolve();
return;
}
const timeoutId = setTimeout(() => {
this.removeListener('stateChange', listener);
reject(new Error('Timeout waiting for Noble to be powered on'));
}, timeout);
const listener = (state) => {
if (state === 'poweredOn') {
clearTimeout(timeoutId);
resolve();
} else {
this.once('stateChange', listener);
}
};
this.once('stateChange', listener);
});
}
startScanning (serviceUuids, allowDuplicates, callback) {
const self = this;
const scan = (state) => {
if (state !== 'poweredOn') {
const boundScan = scan.bind(self);
self.once('stateChange', boundScan);
const error = new Error(`Could not start scanning, state is ${state} (not poweredOn)`);
if (typeof callback === 'function') {
self.removeListener('stateChange', boundScan);
callback(error);
} else {
throw error;
}
} else {
if (callback) {
this.onceExclusive('scanStart', filterDuplicates => callback(null, filterDuplicates));
}
this._discoveredPeripherals.clear();
this._allowDuplicates = allowDuplicates;
this._bindings.startScanning(serviceUuids, allowDuplicates);
}
};
// if bindings still not init, do it now
if (this._initialized === false) {
this.once('stateChange', scan.bind(this));
this._initializeBindings();
} else {
scan.call(this, this._state);
}
}
async startScanningAsync (serviceUUIDs, allowDuplicates) {
return new Promise((resolve, reject) => {
this.startScanning(serviceUUIDs, allowDuplicates, error => error ? reject(error) : resolve());
});
}
_onScanStart (filterDuplicates) {
debug('scanStart');
this.emit('scanStart', filterDuplicates);
}
stopScanning (callback) {
if (this._initialized === false || this._bindings === null) {
callback(new Error('Bindings are not initialized'));
return;
}
if (callback) {
this.onceExclusive('scanStop', callback);
}
this._bindings.stopScanning();
}
async stopScanningAsync () {
return new Promise((resolve, reject) => {
this.stopScanning(error => error ? reject(error) : resolve());
});
}
async *discoverAsync () {
const deviceQueue = [];
let scanning = true;
// Main discover listener to add devices to the queue
const discoverListener = peripheral => deviceQueue.push(peripheral);
// State change listener
const scanStopListener = () => scanning = false;
// Set up listeners
this.on('discover', discoverListener);
this.once('scanStop', scanStopListener);
try {
// Start the scanning process
await this.startScanningAsync();
// Process discovered devices
while (scanning || deviceQueue.length > 0) {
if (deviceQueue.length > 0) {
// If we have devices in the queue, yield them
yield deviceQueue.shift();
} else if (scanning) {
// Wait for either a new device or scan stop
await new Promise(resolve => {
let resolved = false;
let timeoutId = null;
const cleanup = () => {
if (resolved) return;
resolved = true;
this.removeListener('discover', tempDiscoverListener);
this.removeListener('scanStop', tempScanStopListener);
if (timeoutId) clearTimeout(timeoutId);
resolve();
};
const tempDiscoverListener = () => cleanup();
const tempScanStopListener = () => cleanup();
this.once('discover', tempDiscoverListener);
this.once('scanStop', tempScanStopListener);
// Handle race condition where a device might arrive during promise setup
if (deviceQueue.length > 0) {
cleanup();
return;
}
// Add a maximum wait time with proper cleanup
if (scanning) {
timeoutId = setTimeout(() => cleanup(), 1000);
}
});
}
}
} finally {
// Clean up listeners
this.removeListener('discover', discoverListener);
this.removeListener('scanStop', scanStopListener);
// Ensure scanning is stopped
if (scanning) {
await this.stopScanningAsync();
}
}
}
_onScanStop () {
debug('scanStop');
this.emit('scanStop');
}
reset () {
if (typeof this._bindings.reset !== 'function') { return; }
this._bindings.reset();
}
stop () {
if (typeof this._bindings.stop !== 'function') { return; }
this._bindings.stop();
}
_onDiscover (uuid, address, addressType, connectable, advertisement, rssi, scannable) {
let peripheral = this._peripherals.get(uuid);
if (!peripheral) {
peripheral = this._createPeripheral(uuid, address, addressType, connectable, advertisement, rssi, scannable);
} else {
// "or" the advertisment data with existing
for (const i in advertisement) {
if (advertisement[i] !== undefined) {
peripheral.advertisement[i] = advertisement[i];
}
}
peripheral.connectable = connectable;
peripheral.scannable = scannable;
peripheral.rssi = rssi;
}
const previouslyDiscoverd = this._discoveredPeripherals.has(uuid);
if (!previouslyDiscoverd) {
this._discoveredPeripherals.add(uuid);
}
if (this._allowDuplicates || !previouslyDiscoverd || (!scannable && !connectable)) {
this.emit('discover', peripheral);
}
}
_getPeripheralId (idOrAddress) {
let identifier;
// Convert the peripheralId to an identifier
if (/^[0-9A-Fa-f]+$/.test(idOrAddress) === false) {
identifier = this._bindings.addressToId(idOrAddress);
if (identifier === null) {
throw new Error(`Invalid peripheral ID or Address ${idOrAddress}`);
}
} else {
identifier = idOrAddress;
}
return identifier;
}
connect (idOrAddress, parameters, callback) {
// Get the identifier for the peripheral
const identifier = this._getPeripheralId(idOrAddress);
// Check if callback is a function
if (typeof callback === 'function') {
// Add a one-time listener for this specific event
this.onceExclusive(`connect:${identifier}`, error => callback(error, this._peripherals.get(identifier)));
}
// Proceed to initiate the connection
this._bindings.connect(identifier, parameters);
}
async connectAsync (idOrAddress, parameters) {
return new Promise((resolve, reject) => {
this.connect(idOrAddress, parameters, (error, peripheral) => error ? reject(error) : resolve(peripheral));
});
}
_onConnect (peripheralId, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
// Emit a unique connect event for the specific peripheral
this.emit(`connect:${peripheralId}`, error);
peripheral.state = error ? 'error' : 'connected';
// Also emit the general 'connect' event for a peripheral
peripheral.emit('connect', error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} connected!`);
}
}
cancelConnect (idOrAddress, parameters) {
// Get the identifier for the peripheral
const identifier = this._getPeripheralId(idOrAddress);
// Check if the peripheral is connecting
const peripheral = this._peripherals.get(identifier);
if (peripheral && peripheral.state === 'connecting') {
peripheral.state = 'disconnected';
}
// Emit a unique connect event for the specific peripheral
this.emit(`connect:${identifier}`, new Error('connection canceled!'));
// Cancel the connection
this._bindings.cancelConnect(identifier, parameters);
}
disconnect (peripheralId) {
// Disconnect the peripheral
this._bindings.disconnect(peripheralId);
}
_onDisconnect (peripheralId, reason = 'unknown') {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
peripheral.state = 'disconnected';
peripheral.emit('disconnect', reason);
this.emit(`disconnect:${peripheralId}`, reason);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} disconnected!`);
}
}
updateRssi (peripheralId) {
this._bindings.updateRssi(peripheralId);
}
_onRssiUpdate (peripheralId, rssi, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
peripheral.rssi = rssi;
peripheral.emit('rssiUpdate', rssi, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} RSSI update!`);
}
}
/// add an array of service objects (as retrieved via the servicesDiscovered event)
addServices (peripheralId, services) {
const servObjs = [];
for (let i = 0; i < services.length; i++) {
const o = this.addService(peripheralId, services[i]);
servObjs.push(o);
}
return servObjs;
}
/// service is a ServiceObject { uuid, startHandle, endHandle,..}
addService (peripheralId, service) {
const peripheral = this._peripherals.get(peripheralId);
// pass on to lower layers (gatt)
if (this._bindings.addService) {
this._bindings.addService(peripheralId, service);
}
if (!peripheral.services) {
peripheral.services = [];
}
// allocate internal service object and return
const serv = new Service(this, peripheralId, service.uuid);
this._services[peripheralId][service.uuid] = serv;
this._characteristics[peripheralId][service.uuid] = {};
this._descriptors[peripheralId][service.uuid] = {};
peripheral.services.push(serv);
return serv;
}
/// callback receiving a list of service objects from the gatt layer
_onServicesDiscovered (peripheralId, services) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) { peripheral.emit('servicesDiscovered', peripheral, services); } // pass on to higher layers
}
discoverServices (peripheralId, uuids) {
this._bindings.discoverServices(peripheralId, uuids);
}
_onServicesDiscover (peripheralId, serviceUuids, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
const services = [];
for (let i = 0; i < serviceUuids.length; i++) {
const serviceUuid = serviceUuids[i];
const service = new Service(this, peripheralId, serviceUuid);
this._services[peripheralId][serviceUuid] = service;
this._characteristics[peripheralId][serviceUuid] = {};
this._descriptors[peripheralId][serviceUuid] = {};
services.push(service);
}
peripheral.services = services;
peripheral.emit('servicesDiscover', services, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} services discover!`);
}
}
discoverIncludedServices (peripheralId, serviceUuid, serviceUuids) {
this._bindings.discoverIncludedServices(peripheralId, serviceUuid, serviceUuids);
}
_onIncludedServicesDiscover (peripheralId, serviceUuid, includedServiceUuids, error) {
const service = this._services[peripheralId][serviceUuid];
if (service) {
service.includedServiceUuids = includedServiceUuids;
service.emit('includedServicesDiscover', includedServiceUuids, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid} included services discover!`);
}
}
/// add characteristics to the peripheral; returns an array of initialized Characteristics objects
addCharacteristics (peripheralId, serviceUuid, characteristics) {
// first, initialize gatt layer:
if (this._bindings.addCharacteristics) {
this._bindings.addCharacteristics(peripheralId, serviceUuid, characteristics);
}
const service = this._services[peripheralId][serviceUuid];
if (!service) {
this.emit('warning', `unknown service ${peripheralId}, ${serviceUuid} characteristics discover!`);
return;
}
const characteristics_ = [];
for (let i = 0; i < characteristics.length; i++) {
const characteristicUuid = characteristics[i].uuid;
const characteristic = new Characteristic(
this,
peripheralId,
serviceUuid,
characteristicUuid,
characteristics[i].properties
);
this._characteristics[peripheralId][serviceUuid][characteristicUuid] = characteristic;
this._descriptors[peripheralId][serviceUuid][characteristicUuid] = {};
characteristics_.push(characteristic);
}
service.characteristics = characteristics_;
return characteristics_;
}
_onCharacteristicsDiscovered (peripheralId, serviceUuid, characteristics) {
const service = this._services[peripheralId][serviceUuid];
service.emit('characteristicsDiscovered', characteristics);
}
discoverCharacteristics (peripheralId, serviceUuid, characteristicUuids) {
this._bindings.discoverCharacteristics(peripheralId, serviceUuid, characteristicUuids);
}
_onCharacteristicsDiscover (peripheralId, serviceUuid, characteristics, error) {
const service = this._services[peripheralId][serviceUuid];
if (service) {
const characteristics_ = [];
for (let i = 0; i < characteristics.length; i++) {
const characteristicUuid = characteristics[i].uuid;
const characteristic = new Characteristic(
this,
peripheralId,
serviceUuid,
characteristicUuid,
characteristics[i].properties
);
this._characteristics[peripheralId][serviceUuid][characteristicUuid] = characteristic;
this._descriptors[peripheralId][serviceUuid][characteristicUuid] = {};
characteristics_.push(characteristic);
}
service.characteristics = characteristics_;
service.emit('characteristicsDiscover', characteristics_, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid} characteristics discover!`);
}
}
read (peripheralId, serviceUuid, characteristicUuid) {
this._bindings.read(peripheralId, serviceUuid, characteristicUuid);
}
_onRead (peripheralId, serviceUuid, characteristicUuid, data, isNotification, error) {
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
if (characteristic) {
characteristic.emit('data', data, isNotification, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} read!`);
}
}
write (peripheralId, serviceUuid, characteristicUuid, data, withoutResponse) {
this._bindings.write(peripheralId, serviceUuid, characteristicUuid, data, withoutResponse);
}
_onWrite (peripheralId, serviceUuid, characteristicUuid, error) {
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
if (characteristic) {
characteristic.emit('write', error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} write!`);
}
}
broadcast (peripheralId, serviceUuid, characteristicUuid, broadcast) {
this._bindings.broadcast(peripheralId, serviceUuid, characteristicUuid, broadcast);
}
_onBroadcast (peripheralId, serviceUuid, characteristicUuid, state) {
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
if (characteristic) {
characteristic.emit('broadcast', state);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} broadcast!`);
}
}
notify (peripheralId, serviceUuid, characteristicUuid, notify) {
this._bindings.notify(peripheralId, serviceUuid, characteristicUuid, notify);
}
_onNotify (peripheralId, serviceUuid, characteristicUuid, state, error) {
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
if (characteristic) {
characteristic.emit('notify', state, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} notify!`);
}
}
discoverDescriptors (peripheralId, serviceUuid, characteristicUuid) {
this._bindings.discoverDescriptors(peripheralId, serviceUuid, characteristicUuid);
}
_onDescriptorsDiscover (peripheralId, serviceUuid, characteristicUuid, descriptors, error) {
const characteristic = this._characteristics[peripheralId][serviceUuid][characteristicUuid];
if (characteristic) {
const descriptors_ = [];
for (let i = 0; i < descriptors.length; i++) {
const descriptorUuid = descriptors[i];
const descriptor = new Descriptor(
this,
peripheralId,
serviceUuid,
characteristicUuid,
descriptorUuid
);
this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid] = descriptor;
descriptors_.push(descriptor);
}
characteristic.descriptors = descriptors_;
characteristic.emit('descriptorsDiscover', descriptors_, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid} descriptors discover!`);
}
}
readValue (peripheralId, serviceUuid, characteristicUuid, descriptorUuid) {
this._bindings.readValue(peripheralId, serviceUuid, characteristicUuid, descriptorUuid);
}
_onValueRead (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data, error) {
const descriptor = this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid];
if (descriptor) {
descriptor.emit('valueRead', data, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid}, ${descriptorUuid} value read!`);
}
}
writeValue (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data) {
this._bindings.writeValue(peripheralId, serviceUuid, characteristicUuid, descriptorUuid, data);
}
_onValueWrite (peripheralId, serviceUuid, characteristicUuid, descriptorUuid, error) {
const descriptor = this._descriptors[peripheralId][serviceUuid][characteristicUuid][descriptorUuid];
if (descriptor) {
descriptor.emit('valueWrite', error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId}, ${serviceUuid}, ${characteristicUuid}, ${descriptorUuid} value write!`);
}
}
readHandle (peripheralId, handle) {
this._bindings.readHandle(peripheralId, handle);
}
_onHandleRead (peripheralId, handle, data, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
peripheral.emit(`handleRead${handle}`, data, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} handle read!`);
}
}
writeHandle (peripheralId, handle, data, withoutResponse) {
this._bindings.writeHandle(peripheralId, handle, data, withoutResponse);
}
_onHandleWrite (peripheralId, handle, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
peripheral.emit(`handleWrite${handle}`, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} handle write!`);
}
}
_onHandleNotify (peripheralId, handle, data, error) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral) {
peripheral.emit('handleNotify', handle, data, error);
} else {
this.emit('warning', `unknown peripheral ${peripheralId} handle notify!`);
}
}
_onMtu (peripheralId, mtu) {
const peripheral = this._peripherals.get(peripheralId);
if (peripheral && mtu) {
peripheral.mtu = mtu;
peripheral.emit('mtu', mtu);
}
}
async _withDisconnectHandler (peripheralId, operation) {
return new Promise((resolve, reject) => {
const disconnectListener = reason => reject(new Error(`Disconnected ${reason}`));
this.once(`disconnect:${peripheralId}`, disconnectListener);
Promise.resolve(operation())
.then(result => {
this.removeListener(`disconnect:${peripheralId}`, disconnectListener);
resolve(result);
})
.catch(error => {
this.removeListener(`disconnect:${peripheralId}`, disconnectListener);
reject(error);
});
});
}
}
module.exports = Noble;