SimpleBLE

Migrating from noble (Node.js)

A side-by-side mapping between noble's event-driven Node.js API and SimpleJSBLE.

noble was the de-facto BLE central library for Node.js for a decade. Its maintained fork was archived in July 2025. SimpleJSBLE is SimpleBLE's Node.js and Electron binding: promises, EventEmitter events, and TypeScript types over the same core as every other SimpleBLE binding. Migrating is mostly a mechanical translation that removes code.

What changes

  • No raw HCI access. SimpleJSBLE uses each operating system's own Bluetooth stack: WinRT on Windows, CoreBluetooth on macOS, and BlueZ over D-Bus on Linux. There is no setcap on the Node.js binary, no root, and no replacement USB driver.
  • No build step. npm install simplejsble brings prebuilt addons for Windows, macOS, and Linux on x64 and arm64. Nothing compiles on install, and Electron uses the same package.
  • No poweredOn dance. Adapters are ready when Adapter.getAdapters() resolves; there is no state machine to wait for.
  • Service discovery is part of connect(). When the promise resolves, peripheral.services() lists every service, characteristic, and descriptor.
  • Characteristics are addressed by UUID pair, not by objects held from a discovery callback: peripheral.read(serviceUuid, characteristicUuid). UUIDs are full 128-bit strings, so noble's '180d' is '0000180d-0000-1000-8000-00805f9b34fb'.
  • One object per device. The Peripheral from scanFound is the same object as the one in scanGetResults(), so === works and state can live on it.

Concept mapping

nobleSimpleJSBLE
noble.on('stateChange', ...), wait for poweredOnawait Adapter.bluetoothEnabled()
noble.startScanningAsync(serviceUuids)adapter.scanStart(), or adapter.scanFor(ms) for a bounded scan; filter on peripheral.services()
noble.on('discover', peripheral => ...)adapter.on('scanFound', peripheral => ...), and scanUpdated for later advertisements
noble.stopScanningAsync()adapter.scanStop()
peripheral.connectAsync()peripheral.connect()
peripheral.discoverAllServicesAndCharacteristicsAsync()peripheral.services() after connect()
characteristic.readAsync()peripheral.read(service, characteristic)
characteristic.writeAsync(data, false)peripheral.writeRequest(service, characteristic, data)
characteristic.writeAsync(data, true)peripheral.writeCommand(service, characteristic, data)
characteristic.subscribeAsync() and characteristic.on('data', ...)peripheral.notify(service, characteristic, data => ...), or indicate
characteristic.unsubscribeAsync()peripheral.unsubscribe(service, characteristic)
descriptor.readValueAsync() / writeValueAsync(data)peripheral.readDescriptor(...) / writeDescriptor(...)
peripheral.once('disconnect', ...)peripheral.on('disconnected', ...)
peripheral.disconnectAsync()peripheral.disconnect()
peripheral.address, advertisement.localNameperipheral.address(), peripheral.identifier()
peripheral.rssi, advertisement.txPowerLevelperipheral.rssi(), peripheral.txPower()
advertisement.manufacturerDataperipheral.manufacturerData(), a Map by company identifier
advertisement.serviceUuids, serviceDataperipheral.services() before connecting, with each service's data
bleno, for the peripheral roleadapter.createLocalPeripheral(), see Advertise

Values are Buffers in both libraries, and failures reject with a SimpleBLEError whose code names the cause.

noble's heart-rate example, translated

noble's README example scans for the heart rate service, connects, and subscribes to measurements. With SimpleJSBLE:

import { Adapter } from 'simplejsble';

const HEART_RATE = '0000180d-0000-1000-8000-00805f9b34fb';
const MEASUREMENT = '00002a37-0000-1000-8000-00805f9b34fb';

const [adapter] = await Adapter.getAdapters();

const found = new Promise(resolve => {
  adapter.on('scanFound', peripheral => {
    if (peripheral.services().some(service => service.uuid === HEART_RATE)) resolve(peripheral);
  });
});
await adapter.scanStart();
const peripheral = await found;
await adapter.scanStop();

peripheral.on('disconnected', () => console.log('Disconnected'));
await peripheral.connect();
await peripheral.notify(HEART_RATE, MEASUREMENT, data => {
  console.log(`Heart rate payload: ${data.toString('hex')}`);
});

The heart rate monitor recipe covers parsing the measurement payload. For events, lifetime, Electron packaging, and Dongl pairing, see the SimpleJSBLE usage guide.

Licensing

noble is MIT-licensed; SimpleBLE is BUSL-1.1, free for non-commercial use, with commercial licenses described in Licensing.

On this page