Bare Docs

Use platform-specific Bluetooth features

Use bare-bluetooth-apple and bare-bluetooth-android for what bare-bluetooth leaves out: reconnect without scanning, dial bonded devices, tune MTU and scan mode.

bare-bluetooth gives you one API on every platform, and leaves out what only one platform can do. When you need one of those features, write that part of your code against the platform package that bare-bluetooth would load anyway: bare-bluetooth-apple on iOS and macOS, bare-bluetooth-android on Android.

Every example below comes from the platform package's own test suite or from an app that ships, so expect test assertions (t.ok(), t.is()) in some of them. The test snippets also lean on the suite's setup: brittle's test(), the isCI flag, and two helpers from the package's test/helpers.js, waitForPoweredOn(emitter) and waitForEvent(emitter, name, timeout), which resolves with the event's arguments or with null on timeout. They import from ../lib/...; in your own code, import from the package (require('bare-bluetooth-apple') or require('bare-bluetooth-android')) and use plain event listeners instead.

Before you begin

The platform packages are not drop-in replacements for bare-bluetooth:

  • Names differ. The peripheral role is PeripheralManager on Apple and Server on Android. bare-bluetooth calls it Server everywhere.
  • States differ. bare-bluetooth-android reports 'on' and 'off' where bare-bluetooth reports 'poweredOn' and 'poweredOff'. Apple's states are the same as bare-bluetooth's.
  • Objects don't mix. A Peripheral from bare-bluetooth can't be passed to a platform package's Central, or the other way round. Use one package per Bluetooth manager.

Some Android-only options need no platform package at all: bare-bluetooth passes them through and ignores them elsewhere. Those are marked below.

Apple: reconnect without scanning

On Apple, a peripheral's id is a UUID that the system assigns and keeps on this host, rather than the device's address. Store it, and next time central.retrievePeripherals([id]) turns it back into something you can connect() to, without waiting for a scan to hear it. The test below scans once to learn an id, then retrieves and connects:

test/central.js (bare-bluetooth-apple)
test('connect and disconnect a retrieved peripheral', { skip: isCI, timeout: 30000 }, async (t) => {
  using central = new Central()
  await waitForPoweredOn(central)

  central.on('error', () => {})

  let retrieved = null
  let connected = null

  central.startScan()

  while (connected === null) {
    const discovered = await new Promise((resolve) => {
      central.once('discover', resolve)
    })

    central.stopScan()

    retrieved = central.retrievePeripherals([discovered.id])[0]

    if (retrieved) {
      central.connect(retrieved)

      const event = await waitForEvent(central, 'connect', 5000)

      // A connect attempt never times out on its own, so a pending one has to be
      // cancelled or it outlives this loop and fires later.
      if (event) connected = event[0]
      else central.disconnect(retrieved)
    }

    if (connected === null) central.startScan()
  }

  t.is(connected.id, retrieved.id)
  t.is(central.retrievePeripherals([retrieved.id])[0].state, 'connected')

  central.disconnect(retrieved)

  t.ok(await waitForEvent(central, 'disconnect', 5000))
  t.is(central.retrievePeripherals([retrieved.id])[0].state, 'disconnected')
})
  • A retrieved peripheral has an id, a name and a connection state, but no rssi or serviceData: it comes from the system's records, not from an advertisement.
  • retrievePeripherals() throws unless Bluetooth is powered on, and throws for an id that isn't a UUID. An unknown id returns an empty array.
  • Like any connection attempt, connect() on a retrieved peripheral never times out by itself. The test gives up after 5 seconds and calls disconnect() to cancel the attempt; your app needs its own timeout too.

central.retrieveConnectedPeripherals(serviceUUIDs) works the same way for peripherals that are already connected to the system and offer one of the given services, for example through another app. Its state reports that system connection. You still call connect() before this central can use them.

Apple: let iOS prompt to turn Bluetooth on

By default, neither Central nor PeripheralManager asks the system to show its "turn on Bluetooth" alert when Bluetooth is off. Opt in when you create the manager. The package's tests pass this option but show no usage, so this short snippet is written for this page from its type declarations:

const { Central } = require('bare-bluetooth-apple')

const central = new Central({ showPowerAlert: true })

Android: dial a bonded device

Devices the user has paired with in system settings are bonded. central.getBondedDevices() lists them, and new Peripheral({ device }) makes one dialable without a scan. The test below dials the first bonded device that supports Bluetooth Low Energy:

test/peripheral.js (bare-bluetooth-android)
test('dial a bonded device', { skip: isCI, timeout: 30000 }, async (t) => {
  const central = new Central()
  t.teardown(() => central.destroy())

  const state = await new Promise((resolve) => {
    central.on('stateChange', resolve)
  })

  if (state !== 'on') {
    t.comment('bluetooth not on: ' + state + ', skipping')
    return
  }

  const [device] = central.getBondedDevices().filter((d) => d.type !== Device.DEVICE_TYPE_CLASSIC)

  if (!device) {
    t.comment('no bonded LE device, skipping')
    return
  }

  const peripheral = new Peripheral({ device })

  central.on('error', () => {})
  central.connect(peripheral)

  t.ok(peripheral.connecting, 'dial is in flight')

  const connected = await new Promise((resolve) => {
    central.once('connect', (p) => resolve(p === peripheral))

    setTimeout(() => resolve(false), 15000)
  })

  if (!connected) {
    t.comment('bonded device not reachable, skipping')
    central.disconnect(peripheral)
    return
  }

  t.ok(peripheral.connected, 'peripheral is connected')
  t.is(peripheral.id, device.address)

  central.disconnect(peripheral)
})
  • getBondedDevices() throws without the BLUETOOTH_CONNECT permission, and returns an empty array unless the adapter is on.
  • Filter out Device.DEVICE_TYPE_CLASSIC: a classic-only device can't take a Bluetooth Low Energy connection.
  • A peripheral built from a Device has no scanResult, so its rssi is null.
  • The test gives up after 15 seconds and calls disconnect() to cancel the attempt if the device isn't reachable.

Android: ask for a larger MTU

Each GATT write or notification carries at most the connection's MTU minus 3 bytes. Apple and Linux negotiate a larger MTU themselves; on Android, ask for one after connecting with peripheral.requestMtu(517), and wait for mtuChanged with the size the two sides agreed on. A failure arrives as error with code MTU_CHANGE_FAILED.

requestMtu() is also on bare-bluetooth's Peripheral, where it does nothing on Apple and Linux. ble-swarm calls it unconditionally after every connection for that reason.

Android: choose a scan mode

On Android, a scan runs in low-latency mode unless you say otherwise: bare-bluetooth-android defaults scanMode to SCAN_MODE_LOW_LATENCY, not to Android's own power-saving default. That suits a device picker the user is waiting on. For a long scan in the background, pass SCAN_MODE_BALANCED or SCAN_MODE_LOW_POWER to trade discovery speed for battery.

scanMode is one of bare-bluetooth's own scan options, passed to Android and ignored elsewhere, so it needs no platform package. bare-mobile-doctor sets it only on Android, spelling out the default:

bare/bluetooth.js
const isAndroid = Bare.platform === 'android'
const scanOptions = isAndroid ? { scanMode: Central.SCAN_MODE_LOW_LATENCY } : undefined

and passes it along with the service filter when it starts scanning:

bare/bluetooth.js
    this._central.startScan([this.serviceUUID], scanOptions)

Whatever the mode, stop the scan as soon as you've found what you need.

Android: open the GATT server yourself

bare-bluetooth opens Android's GATT server for you whenever the adapter turns on. With bare-bluetooth-android directly, that's your job: call server.open() once state is 'on', before addService(). The server doesn't survive the adapter turning off, and its services don't survive a reopen, so open it and add your services again on every transition back to 'on'. server.close() closes it and drops its services.

Where to go next

Last updated on

Was this helpful?

On this page