Profile

sealed class Profile(requiredServiceUuids: List<Uuid>, optionalServiceUuids: List<Uuid> = emptyList(), val name: String? = null)

A class representing a Bluetooth LE profile.

The profiles are intended to separate the underlying GATT services and characteristics and the application logic and device API. For example, the profile can expose methods to turn a light ON or OFF, while internally it would use Bluetooth LE connection.

Use Peripheral.profile to register a profile on a peripheral.

Example

Simple profile

A profile based on a single GATT service extends Simple.

Profile definition

/**
* API of the profile.
*/
interface LedButton {
/** The current button state on the DK. */
val buttonState: StateFlow<Boolean>
/** The LED state. */
val ledState: MutableStateFlow<Boolean>
}

class LedButtonProfile: Profile.Simple(
serviceUuid = SERVICE_UUID,
name = "LBS",
), LedButton {
companion object {
val SERVICE_UUID = Uuid.parse("00001523-1212-efde-1523-785feabcd123")
val BUTTON_CHARACTERISTIC_UUID = Uuid.parse("00001524-1212-efde-1523-785feabcd123")
val LED_CHARACTERISTIC_UUID = Uuid.parse("00001525-1212-efde-1523-785feabcd123")
}

// GATT characteristics.
private lateinit var buttonCharacteristic: RemoteCharacteristic
private lateinit var ledCharacteristic: RemoteCharacteristic

// Public API.
override val buttonState: StateFlow<Boolean>
field = MutableStateFlow(false)
override val ledState: MutableStateFlow<Boolean> = MutableStateFlow(false)

// Implementation.
override fun prepare(peripheral: Peripheral<*, *>, service: RemoteService) {
// This should always pass.
require(service.uuid == SERVICE_UUID)

// Obtain characteristics from the service.
buttonCharacteristic = service.characteristics.first { it.uuid == BUTTON_CHARACTERISTIC_UUID }
ledCharacteristic = service.characteristics.first { it.uuid == LED_CHARACTERISTIC_UUID }

// Validate properties.
require(buttonCharacteristic.isSubscribable()) { "Button characteristic must be subscribable." }
require(ledCharacteristic.isWritable()) { "LED characteristic must be writable." }
}

override suspend fun CoroutineScope.initialize(peripheral: Peripheral<*, *>) {
// Subscribe to button characteristic.
buttonCharacteristic
.subscribe()
.map { value -> value.singleOrNull() == 1.toByte() }
.onEach { isPressed -> buttonState.update { isPressed } }
.launchIn(this)

// Read current Button state.
try {
val currentState = buttonCharacteristic.read()
buttonState.update { currentState.singleOrNull() == 1.toByte() }
} catch (e: OperationFailedException) {
println("Reading button characteristic failed: ${e.message}")
}

// Handle LED state updates.
ledState
.map { isOn -> byteArrayOf(if (isOn) 1 else 0) }
.onEach { value ->
try {
ledCharacteristic.write(value)
} catch (e: OperationFailedException) {
println("Writing LED characteristic failed: ${e.message}")
}
}
.launchIn(this)
}
}

Usage

val api: LedButton = LedButtonProfile()
.also { peripheral.profile(it) }

Multiservice profile

A profile based on multiple GATT services, some of which may be optional, extends MultiService.

Profile definition

/**
* API of the profile.
*/
interface Proximity {
/** Whether the peripheral also exposes the optional Immediate Alert Service. */
val isImmediateAlertSupported: Boolean
/** Writes the given alert level to the Link Loss Service. */
suspend fun setLinkLossAlertLevel(level: Int)
}

class ProximityProfile : Profile.MultiService(
requiredServiceUuids = listOf(LINK_LOSS_SERVICE_UUID),
optionalServiceUuids = listOf(IMMEDIATE_ALERT_SERVICE_UUID),
name = "Proximity",
), Proximity {
companion object {
val LINK_LOSS_SERVICE_UUID = Uuid.parse("00001803-0000-1000-8000-00805f9b34fb")
val IMMEDIATE_ALERT_SERVICE_UUID = Uuid.parse("00001802-0000-1000-8000-00805f9b34fb")
val ALERT_LEVEL_UUID = Uuid.parse("00002a06-0000-1000-8000-00805f9b34fb")
const val HIGH_ALERT = 2
}

// GATT characteristics.
private lateinit var linkLossAlertLevel: RemoteCharacteristic
private var immediateAlertLevel: RemoteCharacteristic? = null

// Public API.
override val isImmediateAlertSupported: Boolean
get() = immediateAlertLevel != null

// Implementation.
override fun prepare(peripheral: Peripheral<*, *>, services: List<RemoteService>) {
// Link Loss Service is required.
val linkLossService = services.first { it.uuid == LINK_LOSS_SERVICE_UUID }
linkLossAlertLevel = linkLossService.characteristics.first { it.uuid == ALERT_LEVEL_UUID }
require(linkLossAlertLevel.isWritable()) { "Alert Level characteristic must be writable." }

// Immediate Alert Service is optional - only present if the peripheral supports it.
immediateAlertLevel = services.firstOrNull { it.uuid == IMMEDIATE_ALERT_SERVICE_UUID }
?.characteristics?.first { it.uuid == ALERT_LEVEL_UUID }
}

override suspend fun CoroutineScope.initialize(peripheral: Peripheral<*, *>) {
// Set the Link Loss Alert Level to High as soon as the profile is initialized.
setLinkLossAlertLevel(HIGH_ALERT)
}

override suspend fun setLinkLossAlertLevel(level: Int) {
linkLossAlertLevel.write(byteArrayOf(level.toByte()))
}
}

Usage

val api: Proximity = ProximityProfile()
.also { peripheral.profile(it) }

See also

Inheritors

Constructors

Link copied to clipboard
protected constructor(requiredServiceUuids: List<Uuid>, optionalServiceUuids: List<Uuid> = emptyList(), name: String? = null)

Types

Link copied to clipboard
abstract class MultiService(requiredServiceUuids: List<Uuid>, optionalServiceUuids: List<Uuid> = emptyList(), name: String? = null) : Profile

A class representing a multiservice GATT profile.

Link copied to clipboard
abstract class Simple(serviceUuid: Uuid, name: String? = null) : Profile

A class representing a simple GATT profile, based on a single GATT service.

Properties

Link copied to clipboard
val name: String?

The name of the profile. This is for convenience, used only in logging.

Functions

Link copied to clipboard
protected open suspend fun failed(peripheral: Peripheral<*, *>, reason: RemoteServices.Failed.Reason)

Notifies the profile implementation that service discovery has failed.

Link copied to clipboard
protected abstract suspend fun CoroutineScope.initialize(peripheral: Peripheral<*, *>)

This method should initialize the profile.

Link copied to clipboard
protected abstract fun prepare(peripheral: Peripheral<*, *>, services: List<RemoteService>)

Validates and prepares the services to be used in initialize.

Link copied to clipboard
protected open suspend fun unsupported(peripheral: Peripheral<*, *>)

Notifies the profile implementation, that the peripheral does not support the required profile services.