

A Node.js BLE (Bluetooth Low Energy) central module.
Want to implement a peripheral? Check out @stoprocent/bleno.
Note: Currently, running both noble (central) and bleno (peripheral) together only works with macOS bindings or when using separate HCI/UART dongles. Support for running both on a single HCI adapter (e.g., on Linux systems) will be added in future releases.
About This Fork
This fork of noble was created to introduce several key improvements and new features:
-
Flexible Bluetooth Driver Selection:
- This library enables flexible selection of Bluetooth drivers through the new
withBindings() API. Use native platform bindings (Mac, Windows) or HCI bindings with UART/serial support for hardware dongles, allowing Bluetooth connectivity across various platforms and hardware setups.
-
Native Bindings Improvements:
- Fixed and optimized native bindings for macOS, ensuring better compatibility and performance on Apple devices
- Overhauled Windows native bindings with support for
Service Data from advertisements
- Aligned behavior across different bindings (macOS, Windows, HCI) for consistent behavior
-
Modern JavaScript Support:
- Added full Promise-based API with async/await support throughout the library
- Implemented async iterators for device discovery with
for await...of syntax
- Refactored codebase to use modern JavaScript patterns and best practices
-
Enhanced Testing and Reliability:
- Migrated tests to Jest for improved coverage and reliability
- Added comprehensive TypeScript type definitions
- Fixed numerous edge cases and stability issues
-
New Features:
- A
setAddress(...) function to set the MAC address of the central device
- Direct device connection with
connect(...)/connectAsync(...) without requiring a prior scan
waitForPoweredOnAsync(...) function to simplify async workflows
- Support for multiple adapter configurations through the new
withBindings() API
- Extended debugging capabilities and error handling
- Additionally, I plan to add raw L2CAP channel support, enhancing low-level Bluetooth communication capabilities
If you appreciate these enhancements and the continued development of this project, please consider supporting my work.

Install
npm install @stoprocent/noble
Usage
TypeScript (Recommended)
import noble from '@stoprocent/noble';
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('default');
For more detailed examples and API documentation, see Binding Types below.
JavaScript
const noble = require('@stoprocent/noble');
const { withBindings } = require('@stoprocent/noble');
const noble = withBindings('default');
Quick Start Example
TypeScript Example (Modern Async/Await)
Basic Scan
import noble from '@stoprocent/noble';
try {
await noble.waitForPoweredOnAsync();
await noble.startScanningAsync();
for await (const peripheral of noble.discoverAsync()) {
console.log(`Found device: ${peripheral.advertisement.localName || 'Unknown'}`);
if (peripheral.advertisement.localName === 'MyDevice') {
break;
}
}
await noble.stopScanningAsync();
} catch (error) {
console.error('Discovery error:', error);
await noble.stopScanningAsync();
}
For a more detailed example, please check out examples/peripheral-explorer.ts
Alternatively, you can still use the legacy event-based API:
const noble = require('@stoprocent/noble');
noble.on('stateChange', function (state) {
if (state === 'poweredOn') {
noble.startScanning();
} else {
noble.stopScanning();
}
});
noble.on('discover', peripheral => {
console.log(peripheral);
});
Connecting to the device
await noble.stopScanningAsync();
await peripheral.connectAsync();
const { services, characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
Working with Services and Characteristics
async function exploreServices(peripheral) {
const { services } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
const results = [];
for (const service of services) {
const serviceInfo = {
uuid: service.uuid,
characteristics: []
};
for (const characteristic of service.characteristics) {
const characteristicInfo = {
uuid: characteristic.uuid,
properties: characteristic.properties
};
if (characteristic.properties.includes('read')) {
characteristicInfo.value = await characteristic.readAsync();
}
serviceInfo.characteristics.push(characteristicInfo);
}
results.push(serviceInfo);
}
return results;
}
Reading and Writing Data
async function readBatteryLevel(peripheral) {
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
['180f'],
['2a19']
);
if (characteristics.length > 0) {
const data = await characteristics[0].readAsync();
return data[0];
}
return null;
}
async function writeCharacteristic(peripheral, serviceUuid, characteristicUuid, data) {
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
[serviceUuid],
[characteristicUuid]
);
if (characteristics.length > 0) {
const requiresResponse = !characteristics[0].properties.includes('writeWithoutResponse');
await characteristics[0].writeAsync(data, !requiresResponse);
return true;
}
return false;
}
JavaScript Example (Battery Level)
const { withBindings } = require('@stoprocent/noble');
async function readBatteryLevel() {
const noble = withBindings('default');
try {
await noble.waitForPoweredOnAsync();
await noble.startScanningAsync(['180f'], false);
noble.on('discover', async (peripheral) => {
await noble.stopScanningAsync();
await peripheral.connectAsync();
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(['180f'], ['2a19']);
const batteryLevel = (await characteristics[0].readAsync())[0];
console.log(`${peripheral.address} (${peripheral.advertisement.localName}): ${batteryLevel}%`);
await peripheral.disconnectAsync();
process.exit(0);
});
} catch (error) {
console.error(error);
}
}
readBatteryLevel();
API Overview
Noble provides both callback-based and Promise-based (Async) APIs:
Binding Types
import noble from '@stoprocent/noble';
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('default');
const nobleHci = withBindings('hci');
const nobleDbus = withBindings('dbus');
const nobleMac = withBindings('mac');
const nobleWin = withBindings('win');
const nobleCustom = withBindings('hci', {
hciDriver: 'uart',
bindParams: {
uart: {
port: '/dev/ttyUSB0',
baudRate: 1000000
}
}
});
const nobleCustom = withBindings('hci', {
hciDriver: 'native',
deviceId: 0
});
const nobleDbus = withBindings('dbus', {
adapterId: 'hci0'
});
Core Methods
await noble.waitForPoweredOnAsync(timeout?: number);
await noble.startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean);
await noble.stopScanningAsync();
for await (const peripheral of noble.discoverAsync()) {
}
const peripheral = await noble.connectAsync(idOrAddress, options?);
noble.setAddress('00:11:22:33:44:55');
noble.reset();
noble.stop();
Peripheral Methods
await peripheral.connectAsync();
await peripheral.disconnectAsync();
const rssi = await peripheral.updateRssiAsync();
const services = await peripheral.discoverServicesAsync(['180f']);
const { services, characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
const { services, characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
['180f'], ['2a19']
);
const data = await peripheral.readHandleAsync(handle);
await peripheral.writeHandleAsync(handle, data, withoutResponse);
Service Methods
const includedServiceUuids = await service.discoverIncludedServicesAsync([serviceUUIDs]);
const characteristics = await service.discoverCharacteristicsAsync([characteristicUUIDs]);
Characteristic Methods
Note: The data event is the primary event for handling both read responses and notifications. When using the event-based approach, you can differentiate between read responses and notifications using the isNotification parameter. The previously used read event has been deprecated and removed. Instead, use the data event with isNotification=false to identify read responses.
const data = await characteristic.readAsync();
await characteristic.writeAsync(data, withoutResponse);
await characteristic.subscribeAsync();
await characteristic.unsubscribeAsync();
for await (const data of characteristic.notificationsAsync()) {
console.log(`Received notification: ${data}`);
}
const descriptors = await characteristic.discoverDescriptorsAsync();
Characteristic Events
characteristic.on('data', (data: Buffer, isNotification: boolean) => {
console.log(`Received ${isNotification ? 'notification' : 'read response'}: ${data}`);
});
characteristic.on('write', (error: Error | undefined) => {
console.log('Write completed');
});
characteristic.on('descriptorsDiscover', (descriptors: Descriptor[]) => {
console.log('Descriptors discovered');
});
Descriptor Methods
const value = await descriptor.readValueAsync();
await descriptor.writeValueAsync(data);
Installation
Prerequisites
UART (Any OS)
Please refer to https://github.com/stoprocent/node-bluetooth-hci-socket#uartserial-any-os
NOTE: While environmental variables are still supported for backward compatibility, the recommended approach is to specify driver options directly in the withBindings() call as shown below:
Recommended Approach (UART port specified in bindParams)
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('hci', {
hciDriver: 'uart',
bindParams: {
uart: {
port: '/dev/ttyUSB0',
baudRate: 1000000
}
}
});
Legacy Approach (Using environmental variables - not recommended for new implementations)
$ export BLUETOOTH_HCI_SOCKET_UART_PORT=/dev/tty...
$ export BLUETOOTH_HCI_SOCKET_UART_BAUDRATE=1000000
NOTE: BLUETOOTH_HCI_SOCKET_UART_BAUDRATE defaults to 1000000 so only needed if different.
import noble from '@stoprocent/noble';
OS X
- Install Xcode
- On newer versions of OSX, allow bluetooth access on the terminal app: "System Preferences" —> "Security & Privacy" —> "Bluetooth" -> Add terminal app (see Sandboxed terminal)
Linux
- Kernel version 3.6 or above
libbluetooth-dev needs to be installed. For instructions for specific distributions, see below.
- To set the necessary privileges to run without sudo, see this section. This is required for all distributions (Raspbian, Ubuntu, Fedora, etc). You will not get any errors if running without sudo, but nothing will happen.
Ubuntu, Debian, Raspbian
See the generic Linux notes above first.
sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
Make sure node is on your PATH. If it's not, some options:
If you are having trouble connecting to BLE devices on a Raspberry Pi, you should disable the pnat plugin. Add the following line at the bottom of /etc/bluetooth/main.conf:
DisablePlugins=pnat
Then restart the system.
See Issue #425 · OpenWonderLabs/homebridge-switchbot.
Fedora and other RPM-based distributions
See the generic Linux notes above first.
sudo yum install bluez bluez-libs bluez-libs-devel
Intel Edison
See the generic Linux notes above first.
See Configure Intel Edison for Bluetooth LE (Smart) Development.
FreeBSD
Make sure you have GNU Make:
sudo pkg install gmake
Disable automatic loading of the default Bluetooth stack by putting no-ubt.conf into /usr/local/etc/devd/no-ubt.conf and restarting devd (sudo service devd restart).
Unload ng_ubt kernel module if already loaded:
sudo kldunload ng_ubt
Make sure you have read and write permissions on the /dev/usb/* device that corresponds to your Bluetooth adapter.
Windows
node-gyp requirements for Windows
Install the required tools and configurations using Microsoft's windows-build-tools from an elevated PowerShell or cmd.exe (run as Administrator).
npm install --global --production windows-build-tools
node-bluetooth-hci-socket prerequisites
- Compatible Bluetooth 5.0 Zephyr HCI-USB adapter (you need to add BLUETOOTH_HCI_SOCKET_USB_VID and BLUETOOTH_HCI_SOCKET_USB_PID to the process env)
- Compatible Bluetooth 4.0 USB adapter
- WinUSB driver setup for Bluetooth 4.0 USB adapter, using Zadig tool
See @don's setup guide on Bluetooth LE with Node.js and Noble on Windows
Docker
Make sure your container runs with --network=host options and all specific environment prerequisites are verified.
Running without root/sudo (Linux-specific)
Run the following command:
sudo setcap cap_net_raw+eip $(eval readlink -f `which node`)
This grants the node binary cap_net_raw privileges, so it can start/stop BLE advertising.
Note: The above command requires setcap to be installed.
It can be installed the following way:
- apt:
sudo apt-get install libcap2-bin
- yum:
su -c \'yum install libcap2-bin\'
Multiple Adapters (Linux-specific)
hci0 is used by default.
You can specify which HCI adapter to use in two ways:
1. Using withBindings (Recommended)
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('hci', {
hciDriver: 'native',
deviceId: 1
});
2. Using environment variable
To override using environment variables, set the NOBLE_HCI_DEVICE_ID environment variable to the interface number.
For example, to specify hci1:
sudo NOBLE_HCI_DEVICE_ID=1 node <your file>.js
If you are using multiple HCI devices in one setup you can run two instances of noble with different binding configurations by initializing them seperatly in code:
import { withBindings } from '@stoprocent/noble';
const nobleAdapter0 = withBindings('hci', {
hciDriver: 'native',
deviceId: 0
});
const nobleAdapter1 = withBindings('hci', {
hciDriver: 'native',
deviceId: 1
});
Reporting all HCI events (Linux-specific)
By default, noble waits for both the advertisement data and scan response data for each Bluetooth address. If your device does not use scan response, the NOBLE_REPORT_ALL_HCI_EVENTS environment variable can be used to bypass it.
sudo NOBLE_REPORT_ALL_HCI_EVENTS=1 node <your file>.js
Environment Variables
The following environment variables can configure noble's behavior:
| NOBLE_HCI_DEVICE_ID | Specify which HCI adapter to use | 0 | export NOBLE_HCI_DEVICE_ID=1 |
| NOBLE_REPORT_ALL_HCI_EVENTS | Report HCI events without waiting for scan response | false | export NOBLE_REPORT_ALL_HCI_EVENTS=1 |
| BLUETOOTH_HCI_SOCKET_UART_PORT | UART port for HCI communication | none | export BLUETOOTH_HCI_SOCKET_UART_PORT=/dev/ttyUSB0 |
| BLUETOOTH_HCI_SOCKET_UART_BAUDRATE | UART baudrate | 1000000 | export BLUETOOTH_HCI_SOCKET_UART_BAUDRATE=1000000 |
Note: The preferred method for configuration is now using the withBindings() API rather than environment variables.