Bare Docs

Bluetooth in Bare

How Bare apps talk over Bluetooth Low Energy: the bare-bluetooth module family, the central and peripheral roles, GATT versus L2CAP, and where encryption and peer identity fit.

Most peer-to-peer connections in the Pear stack start on a network: Hyperswarm finds peers through the DHT and holepunches a path between them. Bluetooth Low Energy (BLE) needs no network at all. Two devices within radio range can find each other and exchange data with no Wi-Fi, no internet and no server, which makes it the transport for places with no connectivity, such as a plane or a crowded venue, and a useful second path when the network is there.

This page explains how Bluetooth is put together in Bare. For working code, see the Connect over Bluetooth guides.

One API over three platform bindings

Every operating system exposes Bluetooth differently: CoreBluetooth on Apple platforms, the Android Bluetooth API on Android, the BlueZ daemon on Linux. Bare has one native binding per platform, and one module that puts a common API over them:

darwin, ios android linux Your code bare-bluetooth bare-bluetooth-apple(CoreBluetooth) bare-bluetooth-android(Android Bluetooth API) bare-bluetooth-linux(BlueZ over D-Bus) ble-swarm
  • bare-bluetooth picks the binding through an import map keyed on the platform, so require('bare-bluetooth') loads only the one for the platform it runs on. It unifies class names, state strings and constants, so consumer code needs no platform conditionals for the common cases. There is no entry for Windows, where the require() fails.
  • bare-bluetooth-apple wraps CoreBluetooth on macOS and iOS. Its peripheral class is CoreBluetooth's PeripheralManager.
  • bare-bluetooth-android calls the Android Bluetooth API through JNI, plus Java helper classes it ships alongside the addon. It needs the app's Java VM, so it only runs inside an app that hosts Bare, such as a Bare Kit worklet.
  • bare-bluetooth-linux talks to bluetoothd over D-Bus, and handles L2CAP sockets itself. bare-bluetooth wraps it from 0.6.3-0.

The platform bindings stay available underneath. Their APIs follow each platform's own shape, and expose things bare-bluetooth leaves out; see Use platform-specific Bluetooth features.

Higher up, ble-swarm builds a Hyperswarm-shaped transport on bare-bluetooth; see Security below.

Two roles: central and server

Bluetooth Low Energy is asymmetric. One side, the peripheral, advertises and waits; the other, the central, scans for advertisements and connects. bare-bluetooth names them after what your code does with them:

Rolebare-bluetooth classDoes
PeripheralServerAdvertises, holds GATT services, answers reads and writes, publishes L2CAP channels
CentralCentralScans, connects, and talks to the connected device through a Peripheral

A device can play both roles at once, and for peer-to-peer apps it usually does: each phone advertises so others can find it, and scans so it can find others. Whoever connects first becomes the central for that connection. bare-mobile-doctor's chat runs a Central and a Server side by side on every phone for this reason.

Two ways to move data: GATT and L2CAP

Once connected, there are two ways to exchange data, and they suit different jobs:

GATTL2CAP channel
ShapeNamed values: services holding characteristics, each read, written or subscribed toA byte stream, a Duplex you read and write
Size per operationAt most the ATT MTU minus 3 bytes: 20 before the MTU is negotiated up, a few hundred afterLimited only by the stream (and mtu per write on Linux)
How the other side finds itBy service and characteristic UUIDBy a PSM number the operating system assigns
Talks toAny BLE device, including standard ones like heart-rate monitorsYour own app on the other end

GATT is the lingua franca of BLE: every device speaks it, and standard services have well-known UUIDs. But every value is small, and every operation is a round trip. L2CAP connection-oriented channels give you a real stream with flow control, which is what you want for messages, files or replication.

The catch with L2CAP is the PSM. Neither CoreBluetooth nor BlueZ lets an app choose the number its channel listens on; the operating system assigns one when you call publishChannel(). So the other side can't know it in advance. The usual answer combines both mechanisms: advertise a service UUID so the central can find you, put the PSM in a readable GATT characteristic, and open the channel once the central has read it. Both bare-mobile-doctor and ble-swarm work this way; Hand the PSM over automatically shows the code.

Everything is an event

bare-bluetooth has no promises. Each step, from the radio turning on to a value arriving, is an event on one of its objects, and each call starts an operation whose result comes back as an event:

stateChange: poweredOn startScan() discover connect() connect discoverServices() servicesDiscover

Failures follow the same rule. Apart from startScan(), which throws when Bluetooth isn't powered on, a failed operation arrives as an error event with a code, not as an exception. That includes a failed connection attempt and, on Apple and Android, a disconnect caused by an error, which never shows up as disconnect. So the shape of a Bluetooth module in Bare is a small state machine driven by events, as in bare-mobile-doctor's BLECentral and BLEServer classes. Bluetooth best practices collects the rules that keep that state machine honest.

Who is on the other end

A device's id means something different on each platform:

  • On Apple, it's a UUID the system assigns per host. CoreBluetooth never reveals the device's Bluetooth address, so the same phone can show up under different ids on different hosts.
  • On Android and Linux, it's the Bluetooth address, such as AA:BB:CC:DD:EE:FF.

Names aren't much better: on Android, startAdvertising({ name }) can only switch the phone's own system Bluetooth name on or off, and on every platform a name may arrive late or not at all. So apps identify what a device is by an advertised service UUID, and who it is by something exchanged after connecting: a name in the first message, as in bare-mobile-doctor's invite, or a public key, as in ble-swarm.

Security

Bluetooth offers link-level encryption, but it comes with pairing. publishChannel({ encrypted: true }) asks for an encrypted L2CAP channel, which requires the two devices to be paired first. On Apple and Android the operating system runs the pairing and may prompt the user; on Linux there is no system prompt, so bare-bluetooth emits pairRequest for your app to accept or reject, and only supports "just works" pairing. Pairing also authenticates devices, not people or apps: it says nothing about which key the app on the other end holds.

The alternative is to leave the link unencrypted and encrypt above it, the way the rest of the Pear stack does. ble-swarm publishes plain channels and runs a Noise handshake over each one, keyed by the same kind of key pair Hyperswarm uses, so each connection comes out as an encrypted, authenticated stream to a known public key, with no pairing prompt. Its connections are the same kind Hyperswarm emits, so they plug straight into corestore.replicate(). ble-swarm runs on macOS 13 and later, iOS and Android; it doesn't yet load the Linux binding.

Common questions

See also

Last updated on

Was this helpful?

On this page