# Read, write and subscribe with GATT



GATT is how Bluetooth Low Energy devices expose small values. It has two roles:

* A **server** publishes services, each holding characteristics.
* A **central** connects to the server, then reads and writes the characteristics, or subscribes to be notified when they change.

Use GATT for small values and for talking to existing devices, such as a heart-rate monitor. For a stream of data between two of your own apps, an [L2CAP channel](/bare/how-to/connect-over-bluetooth/exchange-data-over-l2cap) is usually the better fit.

This guide follows two scripts from the [`bare-bluetooth`](https://github.com/holepunchto/bare-bluetooth/tree/d1ef34487f57cc183b5d7a0d6c588d95447eb420/example) repository:

* `scan.js` lists what's nearby.
* `gattpong.js` has a central write `ping` to a characteristic, and the server answers with a `pong` notification.

<Callout type="tip">
  For how the pieces fit together, read [Bluetooth in Bare](/bare/explanation/bluetooth) first. For the rules that keep GATT code reliable across platforms, read [Bluetooth best practices](/bare/explanation/bluetooth/best-practices).
</Callout>

## Before you begin [#before-you-begin]

* For `scan.js`, one machine with Bluetooth. For `gattpong.js`, two. macOS works. Linux works with `bare-bluetooth` 0.6.3 or later, with `bluetoothd` (the [BlueZ](https://github.com/bluez/bluez) daemon) running and the adapter powered on (`bluetoothctl power on`).
* [Bare](https://github.com/holepunchto/bare) on each machine: `npm i -g bare`. On macOS, the terminal app that you run it from needs Bluetooth access under System Settings, Privacy & Security.
* The example scripts, which live in the repository rather than the npm package. Clone the `v0.6.3` tag, which is the version that this guide's code comes from:

```bash skip="install-instruction"
git clone --branch v0.6.3 https://github.com/holepunchto/bare-bluetooth
cd bare-bluetooth
npm install
```

## Scan for nearby devices [#scan-for-nearby-devices]

`scan.js` scans for ten seconds and prints every advertisement it hears. It's the quickest way to check that Bluetooth works on a machine at all:

```bash skip="needs-bluetooth-radio"
bare example/scan.js
```

```js file=<rootDir>/examples/how-to/connect-over-bluetooth/bare-bluetooth/example/scan.js#L1-L19 title="example/scan.js" skip="example-import"
const { Central } = require('..')

const central = new Central()

central.on('stateChange', (state) => {
  console.log('state:', state)
  if (state === 'poweredOn') central.startScan()
})

central.on('discover', (peripheral) => {
  console.log('found:', peripheral.id, peripheral.name, peripheral.rssi)
})

central.on('error', (err) => console.error('error:', err.message))

setTimeout(() => {
  central.stopScan()
  central.destroy()
}, 10000)
```

[`central.startScan()`](/bare/reference/bare/modules/bare-bluetooth#centralstartscanserviceuuids-options) takes an optional array of service UUIDs:

* With no arguments, it reports every device in range.
* With an array of service UUIDs, it reports only devices that advertise one of them.

Keep these points in mind:

* The [`discover`](/bare/reference/bare/modules/bare-bluetooth#central) event fires for advertisements rather than devices, so the same device turns up many times. Key what you find by the [`peripheral.id`](/bare/reference/bare/modules/bare-bluetooth#discoveredperipheral) property, and keep the latest.
* `startScan()` throws unless the state is `'poweredOn'`. That is why the call sits in the [`stateChange`](/bare/reference/bare/modules/bare-bluetooth#central) listener.

## Run the example [#run-the-example]

On one machine, start the server:

```bash skip="needs-bluetooth-radio"
bare example/gattpong.js listen
```

On the other, start the central:

```bash skip="needs-bluetooth-radio"
bare example/gattpong.js connect
```

Unlike the L2CAP example, there's nothing to copy between the two machines. The central finds the server by its advertised service UUID.

## Expose a characteristic from the server [#expose-a-characteristic-from-the-server]

The server defines one [`Characteristic`](/bare/reference/bare/modules/bare-bluetooth#const-characteristic--new-characteristicuuid-options) that centrals can write to and subscribe to. Once the radio is on, it adds the characteristic in a [`Service`](/bare/reference/bare/modules/bare-bluetooth#const-service--new-serviceuuid-characteristics-options) with [`server.addService()`](/bare/reference/bare/modules/bare-bluetooth#serveraddserviceservice). Once the service has been added, it starts advertising with [`server.startAdvertising()`](/bare/reference/bare/modules/bare-bluetooth#serverstartadvertisingoptions):

```js file=<rootDir>/examples/how-to/connect-over-bluetooth/bare-bluetooth/example/gattpong.js#L33-L65 title="example/gattpong.js" skip="example-import"
function listen() {
  const server = new Server()
  const characteristic = new Characteristic(CHARACTERISTIC_UUID, { write: true, notify: true })

  server.on('stateChange', (state) => {
    console.log('state:', state)
    if (state !== 'poweredOn') return

    server.addService(new Service(SERVICE_UUID, [characteristic]))
  })

  server.on('serviceAdd', () => {
    server.startAdvertising({ name: NAME, serviceUUIDs: [SERVICE_UUID] })
    console.log(`advertising as "${NAME}"`)
    console.log('run: bare example/gattpong.js connect')
  })

  server.on('subscribe', (peer, uuid) => {
    if (same(uuid, CHARACTERISTIC_UUID)) console.log('central subscribed')
  })

  // The central writes, the server answers with a notification.
  const reply = rally((message) => server.updateValue(characteristic, Buffer.from(message)))

  server.on('writeRequest', (requests) => {
    for (const request of requests) {
      if (request.responseNeeded) server.respondToRequest(request, Server.ATT_SUCCESS)
      reply(Buffer.from(request.data).toString())
    }
  })

  server.on('error', (err) => console.error('error:', err.message))
}
```

The helper that answers each message, defined later in `gattpong.js`, waits a second and replies with the other word. The server handles three kinds of traffic:

* **Writes** arrive as a [`writeRequest`](/bare/reference/bare/modules/bare-bluetooth#server) event with an array of [`WriteRequest`](/bare/reference/bare/modules/bare-bluetooth#writerequest) objects. If a request's [`responseNeeded`](/bare/reference/bare/modules/bare-bluetooth#writerequest) is set, answer it with [`server.respondToRequest()`](/bare/reference/bare/modules/bare-bluetooth#serverrespondtorequestrequest-result-data) and an ATT result code such as [`Server.ATT_SUCCESS`](/bare/reference/bare/modules/bare-bluetooth#server). The central's write doesn't complete until you do. Answer before you do any slow work with the data.
* **Notifications** go out with [`server.updateValue(characteristic, data)`](/bare/reference/bare/modules/bare-bluetooth#serverupdatevaluecharacteristic-data), which reaches every central that is subscribed to that characteristic. The [`subscribe`](/bare/reference/bare/modules/bare-bluetooth#server) and [`unsubscribe`](/bare/reference/bare/modules/bare-bluetooth#server) events tell you when a central starts or stops listening.
* **Reads** arrive as a [`readRequest`](/bare/reference/bare/modules/bare-bluetooth#server) event with a [`ReadRequest`](/bare/reference/bare/modules/bare-bluetooth#readrequest). This example doesn't use them. Answer a read with `respondToRequest(request, Server.ATT_SUCCESS, data)`, and use [`request.offset`](/bare/reference/bare/modules/bare-bluetooth#readrequest) if a central reads a long value in parts. On Android, every read reaches your listener.

For a value that never changes, you can create the characteristic with a [`value`](/bare/reference/bare/modules/bare-bluetooth#const-characteristic--new-characteristicuuid-options) instead of answering reads yourself. Apple then serves the value itself, and so does Linux as long as nothing listens for `readRequest`. Android doesn't: every read still reaches your `readRequest` listener, even for a characteristic with a `value`, so keep answering reads there.

<Callout type="warn">
  `value` belongs to characteristics that you create. From `bare-bluetooth` 0.6.3, assigning `value` on a characteristic that the central discovered throws on every platform. To change a remote value, use [`peripheral.write()`](/bare/reference/bare/modules/bare-bluetooth#peripheralwritecharacteristic-data-withresponse).
</Callout>

`bare-mobile-doctor` answers reads to hand out an L2CAP channel's PSM. See [Hand the PSM over automatically](/bare/how-to/connect-over-bluetooth/exchange-data-over-l2cap#hand-the-psm-over-automatically).

## Connect and subscribe from the central [#connect-and-subscribe-from-the-central]

The central scans for the service, stops scanning with [`central.stopScan()`](/bare/reference/bare/modules/bare-bluetooth#centralstopscan), and connects to the first device it hears with [`central.connect()`](/bare/reference/bare/modules/bare-bluetooth#centralconnectperipheral):

```js file=<rootDir>/examples/how-to/connect-over-bluetooth/bare-bluetooth/example/gattpong.js#L75-L98 title="example/gattpong.js" skip="example-import"
  central.on('stateChange', (state) => {
    console.log('state:', state)
    if (state !== 'poweredOn') return

    console.log('scanning for', SERVICE_UUID)
    central.startScan([SERVICE_UUID])

    searching = setTimeout(() => {
      console.error('nothing advertising that service after 20s')
      console.error('is "bare example/gattpong.js listen" still running on the other machine?')
    }, 20000)
  })

  // BlueZ replays devices cached from earlier scans too; no RSSI means this
  // scan has not actually heard it.
  central.on('discover', (discovered) => {
    if (connecting || discovered.rssi === null) return

    connecting = true
    clearTimeout(searching)
    console.log('found:', discovered.id, discovered.rssi)
    central.stopScan()
    central.connect(discovered)
  })
```

Once connected, everything happens through events on the [`Peripheral`](/bare/reference/bare/modules/bare-bluetooth#peripheral), one step at a time:

1. Discover the service with [`peripheral.discoverServices()`](/bare/reference/bare/modules/bare-bluetooth#peripheraldiscoverservicesserviceuuids).
2. Discover its characteristics with [`peripheral.discoverCharacteristics()`](/bare/reference/bare/modules/bare-bluetooth#peripheraldiscovercharacteristicsservice-characteristicuuids).
3. Subscribe with [`peripheral.subscribe()`](/bare/reference/bare/modules/bare-bluetooth#peripheralsubscribecharacteristic).
4. Write with [`peripheral.write()`](/bare/reference/bare/modules/bare-bluetooth#peripheralwritecharacteristic-data-withresponse) once the [`notifyState`](/bare/reference/bare/modules/bare-bluetooth#peripheral) event confirms the subscription.

```js file=<rootDir>/examples/how-to/connect-over-bluetooth/bare-bluetooth/example/gattpong.js#L100-L135 title="example/gattpong.js" skip="example-import"
  central.on('connect', (peripheral) => {
    console.log('connected:', peripheral.id)

    let characteristic = null

    // The server notifies, the central answers with a write.
    const reply = rally((message) => peripheral.write(characteristic, Buffer.from(message)))

    peripheral.on('servicesDiscover', (services) => {
      for (const service of services) {
        if (same(service.uuid, SERVICE_UUID)) peripheral.discoverCharacteristics(service)
      }
    })

    peripheral.on('characteristicsDiscover', (service, characteristics) => {
      for (const found of characteristics) {
        if (!same(found.uuid, CHARACTERISTIC_UUID)) continue
        characteristic = found
        peripheral.subscribe(characteristic)
      }
    })

    peripheral.on('notifyState', (subscribed, isNotifying) => {
      if (!isNotifying) return
      console.log('subscribed')
      console.log('>', 'ping')
      peripheral.write(characteristic, Buffer.from('ping'))
    })

    peripheral.on('notify', (notified, data) => reply(Buffer.from(data).toString()))

    peripheral.on('disconnect', () => console.log('disconnected'))
    peripheral.on('error', (err) => console.error('peripheral error:', err.message))

    peripheral.discoverServices([SERVICE_UUID])
  })
```

Two details about writing and reading:

* `peripheral.write()` waits for the server to confirm by default. To write without a response, pass `false` as the third argument, on a characteristic created with [`writeWithoutResponse: true`](/bare/reference/bare/modules/bare-bluetooth#const-characteristic--new-characteristicuuid-options).
* To read a value once instead of subscribing, call [`peripheral.read(characteristic)`](/bare/reference/bare/modules/bare-bluetooth#peripheralreadcharacteristic) and listen for the [`read`](/bare/reference/bare/modules/bare-bluetooth#peripheral) event.

## Compare UUIDs the way the example does [#compare-uuids-the-way-the-example-does]

Every UUID comparison in `gattpong.js` goes through `same()`, never `===`:

```js file=<rootDir>/examples/how-to/connect-over-bluetooth/bare-bluetooth/example/gattpong.js#L10-L21 title="example/gattpong.js" skip="example-import"
// CoreBluetooth reports UUIDs uppercase, and Bluetooth-base ones in their
// 16-bit short form; BlueZ and Android report them long and lowercase.
function same(uuid, other) {
  return expand(uuid) === expand(other)
}

function expand(uuid) {
  uuid = uuid.toLowerCase()
  if (uuid.length === 4) return `0000${uuid}-0000-1000-8000-00805f9b34fb`
  if (uuid.length === 8) return `${uuid}-0000-1000-8000-00805f9b34fb`
  return uuid
}
```

The platforms report UUIDs differently:

* CoreBluetooth reports UUIDs uppercase. It reports those in the Bluetooth base range in their short 16-bit form.
* Android and BlueZ report UUIDs long and lowercase.

Whichever form you write your UUIDs in, a plain `===` fails on at least one platform. Copy this helper into any code that matches services or characteristics.

## Keep values small [#keep-values-small]

A single write or notification carries at most the connection's ATT MTU minus 3 bytes.

* Apple and Linux negotiate the MTU themselves.
* On Android, call [`peripheral.requestMtu()`](/bare/reference/bare/modules/bare-bluetooth#peripheralrequestmtumtu) after connecting, and wait for the [`mtuChanged`](/bare/reference/bare/modules/bare-bluetooth#peripheral) event. See [Use platform-specific Bluetooth features](/bare/how-to/connect-over-bluetooth/use-platform-specific-bluetooth-features#android-ask-for-a-larger-mtu).

Move anything bigger than a few hundred bytes to an [L2CAP channel](/bare/how-to/connect-over-bluetooth/exchange-data-over-l2cap).

## Where to go next [#where-to-go-next]

* [Exchange data over L2CAP](/bare/how-to/connect-over-bluetooth/exchange-data-over-l2cap): streams instead of single values.
* [Bluetooth best practices](/bare/explanation/bluetooth/best-practices): flow control for notifications, caching, and other details of running GATT across platforms.
* [`bare-bluetooth` reference](/bare/reference/bare/modules/bare-bluetooth): [`Server`](/bare/reference/bare/modules/bare-bluetooth#server), [`Central`](/bare/reference/bare/modules/bare-bluetooth#central), [`Peripheral`](/bare/reference/bare/modules/bare-bluetooth#peripheral), [`Service`](/bare/reference/bare/modules/bare-bluetooth#service), and [`Characteristic`](/bare/reference/bare/modules/bare-bluetooth#characteristic) in full.
