Local GATT server

AbleLib allows you to set up and run a local GATT server, allowing you device to behave as if it was a BLE peripheral. What's even better, you can set your services and characteristics declaratively, making them easy to write and maintan. We'll call the block of code that contains the server specification server builder, and you'll add more features to it as the guide progresses.

The server described here is the same one as in our demo apps (Android and iOS). It's meant to manage an L2CAP channel for a server socket.

Opening a server and using server builder

Use AbleManager's startGattServer to obtain a new AbleGattServer instance and start it up. As its parameter, pass the server builder block that contains:

  1. Services to add to the server.
  2. Connectivity callback.
  3. Specific action callbacks.
// Store the UUIDs for services, characteristics and descriptors for later use
val serviceId = AbleUUID("12E61727-B41A-436F-B64D-4777B35F2294")
val charId = AbleUUID("ABDD3056-28FA-441D-A470-55A75A52553A")
val descId = AbleUUID("00002902-0000-1000-8000-00805f9b34fb")
var char: AbleCharacteristic
var myPsm: AblePSM

// If you plan on opening the server multiple times, it's best to store the builder
// in a local variable.
val serverBuilder: AbleGattServerBuilder = {

    // define a service with its UUID, type and builder block
    service(serviceId, AbleService.Type.PRIMARY) {

        // define and store characteristic with its UUID, properties, 
        // permissions and builder block
        char = characteristic(charId,
            setOf(AbleCharacteristic.Property.READ, AbleCharacteristic.Property.INDICATE),
            setOf(AbleCharacteristic.Permission.READ)) {

            // define descriptor with its UUID and permissions
            descriptor(descId, setOf(AbleCharacteristic.Permission.READ, AbleCharacteristic.Permission.WRITE))
        }
    }

    // the server will manage connected devices automatically
    autoManageDevices()

    // define what happens when characteristic with 'charId' is read
    onReadCharacteristic(charId) { request ->
        myPsm?.let { psm ->
            val data = psm.getData()
            request.characteristic.value = data
            respondTo(request.withValue(data), AbleGattServer.Status.SUCCESS)
        }
    }

    // track connection changes and notify connected peripherals
    onConnectionStateChanged { device, state, i ->
        if (state == AbleGattConnectionState.CONNECTED) {
            char?.let { char ->
                myPsm?.let { psm ->
                    notifyCharacteristicChanged(
                        device,
                        char,
                        psm.getData(),
                        false
                    )
                }
            }
        }
    }

    // define what happens when descriptor with 'descId' is written to
    onWriteDescriptor(descId) { request, _, _ ->
        respondTo(request, AbleGattServer.Status.SUCCESS)
    }
}
val server = AbleManager.shared.startGattServer(serverBuilder)

Available server builder components

Here's a table of all the components you can put in the server builder block:

Android server builder is a block with the server instance set as its receiver, so you're effectively just invoking methods on the server instance.

Component Description
service Add a service (and, through it, characteristics and descriptors).
autoManageDevices Should the server manage connected devices automatically.
onConnectionStateChanged Callback for when a peripheral connects or disconnects.
onWriteCharacteristic Callback for when the listed characteristics are written to. You can have as many of these for any number of characteristics.
onWriteDescriptor Callback for when the listed descriptors are written to. You can have as many of these for any number of descriptors.
onReadCharacteristic Callback for when the listed characteristics are read. You can have as many of these for any number of characteristics.
onReadDescriptor Callback for when the listed descriptors are read. You can have as many of these for any number of descriptors.
onWriteAnyCharacteristic Callback for when a characteristic not covered by an onWriteCharacteristic is written to.
onReadAnyCharacteristic Callback for when a characteristic not covered by an onReadCharacteristic is read.
onWriteAnyDescriptor Callback for when a descriptor not covered by an onWriteDescriptor is written to.
onReadAnyDescriptor Callback for when a descriptor not covered by an onReadDescriptor is read.

Advertisting

Once your server is open, make sure to turn on BLE advertising to let other devices detect and connect to it.

CoroutineScope(Dispatchers.IO).launch {
    AbleManager.shared.startAdvertising(
        AdvertiseSettings.Builder()
            .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_BALANCED)
            .setConnectable(true)
            .setTimeout(0)
            .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_MEDIUM)
            .build(),
        AdvertiseData.Builder()
            .setIncludeDeviceName(false)
            .setIncludeTxPowerLevel(false)
            .addServiceUuid(ParcelUuid(serviceId.toUUID))
            .build())
}

Of course, you can stop advertising at any time:

AbleManager.shared.stopAdvertising()

Communicating with peripherals

There are two ways for a server to communicate with its connected peripherals - responding to requests and notifying characteristic changes.

  • Respond to a request in any read or write callbacks by calling respondTo/respond(to:status:) method and providing the request and its status:
onReadCharacteristic(charId) { request ->
    val data = psm.getData()
    request.characteristic.value = data // update the requests's value
    respondTo(request.withValue(data), AbleGattServer.Status.SUCCESS)
}
  • Notify peripherals that a characteristic changed at any time via a reference to updated AbleCharacteristic.

You need to provide the connected AbleDevice to respond to, the characteristic, data and if the response is a notification or an indication:

notifyCharacteristicChanged(device, char, psm.getData(), false)

Closing the server

After you're done using your server, be sure to close it:

server.close()