@stoprocent/noble
Version:
A Node.js BLE (Bluetooth Low Energy) central library.
633 lines (454 loc) • 20.5 kB
Markdown
[](https://www.npmjs.com/package/@stoprocent/noble)
[](https://www.npmjs.com/package/@stoprocent/noble)
A Node.js BLE (Bluetooth Low Energy) central module.
Want to implement a peripheral? Check out [@stoprocent/bleno](https://github.com/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:
1. **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.
2. **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
3. **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
4. **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
5. **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.
[](https://www.buymeacoffee.com/stoprocent)
## Install
```sh
npm install @stoprocent/noble
```
## Usage
### TypeScript (Recommended)
```typescript
// Auto-select based on platform
import noble from '@stoprocent/noble';
// or
import { withBindings } from '@stoprocent/noble';
// Auto-select based on platform
const noble = withBindings('default'); // 'hci', 'win', 'mac'
```
For more detailed examples and API documentation, see [Binding Types](
```javascript
const noble = require('@stoprocent/noble');
// or
const { withBindings } = require('@stoprocent/noble');
const noble = withBindings('default'); // 'hci', 'win', 'mac'
```
```typescript
import noble from '@stoprocent/noble';
// Discover peripherals as an async generator
try {
// Wait for Adapter poweredOn state
await noble.waitForPoweredOnAsync();
// Start scanning first
await noble.startScanningAsync();
// Use the async generator with proper boundaries
for await (const peripheral of noble.discoverAsync()) {
console.log(`Found device: ${peripheral.advertisement.localName || 'Unknown'}`);
// Process the peripheral as needed
// Optional: stop scanning when a specific device is found
if (peripheral.advertisement.localName === 'MyDevice') {
break;
}
}
// Clean up after discovery
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](examples/peripheral-explorer.ts)
Alternatively, you can still use the legacy event-based API:
``` javascript
const noble = require('@stoprocent/noble');
// State change event is emitted when adapter state changes
noble.on('stateChange', function (state) {
if (state === 'poweredOn') {
// Start scanning when adapter is ready
noble.startScanning();
} else {
// Stop scanning if adapter becomes unavailable
noble.stopScanning();
}
});
// Discover event is emitted when a peripheral is found
noble.on('discover', peripheral => {
console.log(peripheral);
// From here you can work with the peripheral:
// - Connect to it: peripheral.connect()
// - Check advertisement data: peripheral.advertisement
// - See signal strength: peripheral.rssi
});
```
``` typescript
// Stop scan
await noble.stopScanningAsync();
// Connect
await peripheral.connectAsync();
// Discover
const { services, characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
```
```typescript
async function exploreServices(peripheral) {
// Discover all services and characteristics at once
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
};
// Read the characteristic if it's readable
if (characteristic.properties.includes('read')) {
characteristicInfo.value = await characteristic.readAsync();
}
serviceInfo.characteristics.push(characteristicInfo);
}
results.push(serviceInfo);
}
return results;
}
```
```typescript
async function readBatteryLevel(peripheral) {
// Get battery service (0x180F is the standard UUID for Battery Service)
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
['180f'], // Battery Service
['2a19'] // Battery Level Characteristic
);
if (characteristics.length > 0) {
const data = await characteristics[0].readAsync();
return data[0]; // Battery percentage
}
return null;
}
async function writeCharacteristic(peripheral, serviceUuid, characteristicUuid, data) {
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
[],
[]
);
if (characteristics.length > 0) {
// false = with response, true = without response
const requiresResponse = !characteristics[0].properties.includes('writeWithoutResponse');
await characteristics[0].writeAsync(data, !requiresResponse);
return true;
}
return false;
}
```
```javascript
const { withBindings } = require('@stoprocent/noble');
// Read the battery level of the first found peripheral exposing the Battery Level characteristic
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();
```
Noble provides both callback-based and Promise-based (Async) APIs:
```typescript
// Default binding (automatically selects based on platform)
import noble from '@stoprocent/noble';
// or
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('default');
// Specific bindings
const nobleHci = withBindings('hci'); // HCI socket binding
const nobleMac = withBindings('mac'); // macOS binding
const nobleWin = withBindings('win'); // Windows binding
// Custom options for HCI binding (Using UART HCI Dongle)
const nobleCustom = withBindings('hci', {
hciDriver: 'uart',
bindParams: {
uart: {
port: '/dev/ttyUSB0',
baudRate: 1000000
}
}
});
// Custom options for HCI binding (Native)
const nobleCustom = withBindings('hci', {
hciDriver: 'native',
deviceId: 0 // This could be also set by env.NOBLE_HCI_DEVICE_ID=0
});
```
```typescript
// Wait for adapter to be powered on
await noble.waitForPoweredOnAsync(timeout?: number);
// Start scanning
await noble.startScanningAsync(serviceUUIDs?: string[], allowDuplicates?: boolean);
// Stop scanning
await noble.stopScanningAsync();
// Discover peripherals as an async generator
for await (const peripheral of noble.discoverAsync()) {
// handle each discovered peripheral
}
// Connect directly to a peripheral by ID or address
const peripheral = await noble.connectAsync(idOrAddress, options?);
// Set adapter address (HCI only on supported devices)
noble.setAddress('00:11:22:33:44:55');
// Reset adapter
noble.reset();
// Stop noble
noble.stop();
```
```typescript
// Connect to peripheral
await peripheral.connectAsync();
// Disconnect from peripheral
await peripheral.disconnectAsync();
// Update RSSI
const rssi = await peripheral.updateRssiAsync();
// Discover services
const services = await peripheral.discoverServicesAsync(['180f']); // Optional service UUIDs
// Discover all services and characteristics
const { services, characteristics } = await peripheral.discoverAllServicesAndCharacteristicsAsync();
// Discover specific services and characteristics
const { services, characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
['180f'], ['2a19']
);
// Read and write handles
const data = await peripheral.readHandleAsync(handle);
await peripheral.writeHandleAsync(handle, data, withoutResponse);
```
```typescript
// Discover included services
const includedServiceUuids = await service.discoverIncludedServicesAsync([serviceUUIDs]);
// Discover characteristics
const characteristics = await service.discoverCharacteristicsAsync([characteristicUUIDs]);
```
> **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.
```typescript
// Read characteristic value
const data = await characteristic.readAsync();
// Write characteristic value
await characteristic.writeAsync(data, withoutResponse);
// Subscribe to notifications
await characteristic.subscribeAsync();
// Unsubscribe from notifications
await characteristic.unsubscribeAsync();
// Receive notifications using async iterator
for await (const data of characteristic.notificationsAsync()) {
console.log(`Received notification: ${data}`);
}
// Discover descriptors
const descriptors = await characteristic.discoverDescriptorsAsync();
```
```typescript
// Receive data (both read responses and notifications)
characteristic.on('data', (data: Buffer, isNotification: boolean) => {
console.log(`Received ${isNotification ? 'notification' : 'read response'}: ${data}`);
});
// Write completion
characteristic.on('write', (error: Error | undefined) => {
console.log('Write completed');
});
// Descriptor discovery
characteristic.on('descriptorsDiscover', (descriptors: Descriptor[]) => {
console.log('Descriptors discovered');
});
```
```typescript
// Read descriptor value
const value = await descriptor.readValueAsync();
// Write descriptor value
await descriptor.writeValueAsync(data);
```
* [Prerequisites](
* [UART](
* [OS X](
* [Linux](
* [Ubuntu, Debian, Raspbian](
* [Fedora and other RPM-based distributions](
* [Intel Edison](
* [FreeBSD](
* [Windows](
* [Docker](
* [Installing and using the package](
Please refer to [https://github.com/stoprocent/node-bluetooth-hci-socket#uartserial-any-os](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:
```typescript
import { withBindings } from '@stoprocent/noble';
const noble = withBindings('hci', {
hciDriver: 'uart',
bindParams: {
uart: {
port: '/dev/ttyUSB0',
baudRate: 1000000
}
}
});
```
```bash
$ 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.
```typescript
import noble from '@stoprocent/noble';
```
* Install [Xcode](https://itunes.apple.com/ca/app/xcode/id497799835?mt=12)
* On newer versions of OSX, allow bluetooth access on the terminal app: "System Preferences" —> "Security & Privacy" —> "Bluetooth" -> Add terminal app (see [Sandboxed terminal](#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](#running-without-rootsudo-linux-specific). 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](#linux) first.
```sh
sudo apt-get install bluetooth bluez libbluetooth-dev libudev-dev
```
Make sure `node` is on your `PATH`. If it's not, some options:
* Symlink `nodejs` to `node`: `sudo ln -s /usr/bin/nodejs /usr/bin/node`
* [Install Node.js using the NodeSource package](https://nodejs.org/en/download/package-manager/#debian-and-ubuntu-based-linux-distributions)
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
See the [generic Linux notes above](
```sh
sudo yum install bluez bluez-libs bluez-libs-devel
```
See the [generic Linux notes above](
See [Configure Intel Edison for Bluetooth LE (Smart) Development](http://rexstjohn.com/configure-intel-edison-for-bluetooth-le-smart-development/).
#### FreeBSD
Make sure you have GNU Make:
```sh
sudo pkg install gmake
```
Disable automatic loading of the default Bluetooth stack by putting [no-ubt.conf](https://gist.github.com/myfreeweb/44f4f3e791a057bc4f3619a166a03b87) into `/usr/local/etc/devd/no-ubt.conf` and restarting devd (`sudo service devd restart`).
Unload `ng_ubt` kernel module if already loaded:
```sh
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](https://github.com/TooTallNate/node-gyp#installation)
Install the required tools and configurations using Microsoft's [windows-build-tools](https://github.com/felixrieseberg/windows-build-tools) from an elevated PowerShell or cmd.exe (run as Administrator).
```cmd
npm install --global --production windows-build-tools
```
[node-bluetooth-hci-socket prerequisites](#windows)
* 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](https://msdn.microsoft.com/en-ca/library/windows/hardware/ff540196(v=vs.85).aspx) driver setup for Bluetooth 4.0 USB adapter, using [Zadig tool](http://zadig.akeo.ie/)
See [@don](https://github.com/don)'s setup guide on [Bluetooth LE with Node.js and Noble on Windows](https://www.youtube.com/watch?v=mL9B8wuEdms&feature=youtu.be&t=1m46s)
#### 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:
```sh
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)
```typescript
import { withBindings } from '@stoprocent/noble';
// Specify HCI adapter in code
const noble = withBindings('hci', {
hciDriver: 'native',
deviceId: 1 // Using hci1
});
```
#### 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`:
```sh
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:
``` typescript
import { withBindings } from '@stoprocent/noble';
// Create two noble instances with different HCI adapters
const nobleAdapter0 = withBindings('hci', {
hciDriver: 'native',
deviceId: 0 // Using hci0
});
const nobleAdapter1 = withBindings('hci', {
hciDriver: 'native',
deviceId: 1 // Using hci1
});
```
### 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.
```sh
sudo NOBLE_REPORT_ALL_HCI_EVENTS=1 node <your file>.js
```
## Environment Variables
The following environment variables can configure noble's behavior:
| Variable | Purpose | Default | Example |
|----------|---------|---------|---------|
| 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.