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 it 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:

For how the cross-platform package maps onto the platform ones, read Bluetooth in Bare first.

Every example below comes from the platform package's own test suite or from an app that ships, so some of them contain test code:

  • The tests use brittle: its test() function and its t.ok() and t.is() assertions.
  • Both snippets use the isCI flag from the package's test/helpers.js.
  • The Apple snippet also uses two helpers from test/helpers.js: waitForPoweredOn(emitter), and waitForEvent(emitter, name, timeout), which resolves with the event's arguments or with null on timeout.

The tests 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 the states of bare-bluetooth, listed under BluetoothState.
  • 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. The sections below mark those options.

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. To reconnect without waiting for a scan to hear the device:

  1. Store the id.
  2. Next time, pass it to central.retrievePeripherals([id]), which turns it back into a RetrievedPeripheral.
  3. Call central.connect() with 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')
})

Keep these points in mind:

  • 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 it 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 central.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. To opt in, set showPowerAlert in the CentralOptions (or PeripheralManagerOptions) 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 the type declarations:

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

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

Android: dial a bonded device

Devices that the user has paired with in system settings are bonded. To dial one without a scan:

  1. List the bonded devices with central.getBondedDevices().
  2. Make one dialable with new Peripheral({ device }).

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)
})

Keep these points in mind:

  • getBondedDevices() throws without the BLUETOOTH_CONNECT permission, and it 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:

  1. Call peripheral.requestMtu(517).
  2. Wait for the mtuChanged event, which carries the size that 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. For that reason, ble-swarm calls it unconditionally after every connection.

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 default suits a device picker that the user is waiting on. For a long scan in the background, trade discovery speed for battery by passing SCAN_MODE_BALANCED or SCAN_MODE_LOW_POWER.

scanMode is one of the scan options of central.startScan(). bare-bluetooth passes it to Android and ignores it 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

It passes the option along with the service filter when it starts scanning:

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

Whatever the mode, stop the scan with central.stopScan() 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:

  1. Call server.open() once state is 'on', before addService().
  2. Open the server and add your services again on every transition back to 'on'. The server doesn't survive the adapter turning off, and its services don't survive a reopen.

server.close() closes the server and drops its services.

Where to go next

Last updated on

Was this helpful?

On this page