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:
bare-bluetooth-appleon iOS and macOS.bare-bluetooth-androidon Android.
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: itstest()function and itst.ok()andt.is()assertions. - Both snippets use the
isCIflag from the package'stest/helpers.js. - The Apple snippet also uses two helpers from
test/helpers.js:waitForPoweredOn(emitter), andwaitForEvent(emitter, name, timeout), which resolves with the event's arguments or withnullon 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
PeripheralManageron Apple andServeron Android.bare-bluetoothcalls itServereverywhere. - States differ.
bare-bluetooth-androidreports'on'and'off'wherebare-bluetoothreports'poweredOn'and'poweredOff'. Apple's states are the same as the states ofbare-bluetooth, listed underBluetoothState. - Objects don't mix. A
Peripheralfrombare-bluetoothcan't be passed to a platform package'sCentral, 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:
- Store the
id. - Next time, pass it to
central.retrievePeripherals([id]), which turns it back into aRetrievedPeripheral. - Call
central.connect()with it.
The test below scans once to learn an id, then retrieves and connects:
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, anameand a connectionstate, but norssiorserviceData. It comes from the system's records, not from an advertisement. retrievePeripherals()throws unless Bluetooth is powered on, and it throws for anidthat isn't a UUID. An unknownidreturns 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 callscentral.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:
- List the bonded devices with
central.getBondedDevices(). - Make one dialable with
new Peripheral({ device }).
The test below dials the first bonded Device that supports Bluetooth Low Energy:
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 theBLUETOOTH_CONNECTpermission, 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
Devicehas noscanResult, so itsrssiisnull. - 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:
- Call
peripheral.requestMtu(517). - Wait for the
mtuChangedevent, 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:
const isAndroid = Bare.platform === 'android'
const scanOptions = isAndroid ? { scanMode: Central.SCAN_MODE_LOW_LATENCY } : undefinedIt passes the option along with the service filter when it starts scanning:
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:
- Call
server.open()oncestateis'on', beforeaddService(). - 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
bare-bluetooth-applereference andbare-bluetooth-androidreference: everything that each platform package exposes, including Android'sScanResultandScanRecordfor raw advertisement data.- Bluetooth in Bare: how the cross-platform package maps onto the platform ones.
- Set up Bluetooth in a mobile app: the permissions these calls need.
Last updated on