profile

suspend fun profile(serviceUuid: Uuid, required: Boolean = true, name: String? = null, block: suspend CoroutineScope.(RemoteService) -> Unit)

Registers a profile implementation that runs when the specified GATT service is discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each service, i.e. Battery Profile and Heart Rate Profile.

This method suspends only to get the current coroutine scope using currentCoroutineContext. The block is called in a child coroutine.

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The block will be called every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the job in which this method is called, or use the profile method with custom scope.

If multiple services share the same serviceUuid, only the first one is passed to block.

Validation

If the profile was marked as required and the service is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Block completion

The device will NOT be disconnected when the block ends, unless the situation described in the Validation section.

Example

override suspend fun connect(
block: suspend CoroutineScope.(HeartRateProfile.State) -> Unit,
): Unit = withContext(Dispatchers.IO) {
// First, register profile.
peripheral.profile(
serviceUuid = HeartRateProfile.heartRateServiceUuid,
required = true,
name = "Heart Rate Profile",
) { remoteService ->
val state = HeartRateServiceImpl(remoteService, this)

// Call the block with the Heart Rare service state, separating Bluetooth LE from the logic.
block(state)
}
// Connect.
centralManager.connect(peripheral)

// Await disconnection.
peripheral.awaitDisconnection()
}

See profile for more information.

Parameters

serviceUuid

The UUID of the profile service.

required

Whether the service is required. In example, a Heart Rate app may require a Heart Rate Service, but also support an optional Battery Service to indicate the battery level.

name

An optional name of the profile, used only in log messages. This is useful when an app registers multiple profiles, to easily distinguish them in logs.

block

The profile implementation.


fun profile(scope: CoroutineScope, serviceUuid: Uuid, required: Boolean = true, name: String? = null, block: suspend CoroutineScope.(RemoteService) -> Unit)

Registers a profile implementation that runs when the specified GATT service is discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each service, i.e. Battery Profile and Heart Rate Profile.

The provided block is launched on a child coroutine in the given scope when a matching RemoteService is emitted by the services flow. The launched job is canceled when the peripheral disconnects (the cancellation cause is PeripheralNotConnectedException) or when the job completes. When the block finishes (normally or exceptionally) the peripheral will be disconnected (with the reason Success).

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The block will be called every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the scope.

If multiple services share the same serviceUuid, only the first one is passed to block.

Validation

If the profile was marked as required and the service is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Block completion

The device will NOT be disconnected when the block ends, unless the situation described in the Validation section.

Example

In this example, the app is connecting to a Heart Rate device with an optional Sensor Location and HR Control Point characteristics. It updates the UI using locationFlow and receives Reset button events using resetButtonEvents.

// Helper methods.
val RemoteService.heartRateMeasurement: RemoteCharacteristic? = characteristics
.firstOrNull { it.uuid = HeartRateProfile.heartRateMeasurementUuid }
val RemoteService.heartRateControlPoint: RemoteCharacteristic? = characteristics
.firstOrNull { it.uuid = HeartRateProfile.heartRateControlPointUuid }
val RemoteService.bodySensorLocation: RemoteCharacteristic? = characteristics
.firstOrNull { it.uuid = HeartRateProfile.bodySensorLocationUuid }

// LBS profile implementation.
peripheral.profile(
scope = scope,
serviceUuid = HeartRateProfile.heartRateServiceUuid,
required = true,
name = "Heart Rate Profile",
) { hrmService ->
// 1. Validate the service.

// HRM characteristic is required.
val hrMeasurement = requireNotNull(hrmService.heartRateMeasurement) {
"HRM characteristic not found"
}
require(hrMeasurement.isSubscribable()) {
"HRM characteristic must have the NOTIFY property"
}
// Other characteristics are optional.
val hrControlPoint = hrmService.heartRateControlPoint
val bodySensorLocation = hrmService.bodySensorLocation

// 2. Initialize the profile.

// Read the sensor location characteristic.
val location = bodySensorLocation?.read()
.map { it.toBodySensorLocation() }
?: BodySensorLocation.NOT_SUPPORTED
locationFlow.update { location }

// Subscribe to the Heart Rate Measurement characteristic.
hrMeasurement
.subscribe {
// Set up the (optional) Control Point when HRM subscription is complete:
hrControlPoint?.let { cp ->
resetButtonEvents
.onEach {
cp.write(HeartRateControlPoint.RESET)
}
// Note, that the collection is launched in the profile scope,
// not the outer scope.
.launchIn(this)
}
}
.onEach {
// Update UI or something.
}
.launchIn(this)
}

Parameters

scope

The coroutine scope to launch the user block in.

serviceUuid

The UUID of the profile service.

required

Whether the service is required by the app. In example, a Heart Rate app may require a Heart Rate Service, but also support an optional Battery Service to indicate the battery level.

name

An optional name of the profile, used only in log messages. This is useful when an app registers multiple profiles, to easily distinguish them in logs.

block

The profile implementation.


suspend fun profile(requiredServiceUuids: List<Uuid>, optionalServiceUuids: List<Uuid> = emptyList(), required: Boolean = true, name: String? = null, block: suspend CoroutineScope.(List<RemoteService>) -> Unit)

Registers a profile implementation that runs when the specified GATT services are discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each service, i.e. Battery Profile and Heart Rate Profile.

Note, that this overload of the profile method returns all RemoteServices matching any of the requiredServiceUuids or optionalServiceUuids, even if multiple instances of the same service were discovered.

This method suspends only to get the current coroutine scope using currentCoroutineContext. The block is called in a child coroutine.

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The block will be called every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the job in this method is called, or use profile method with custom scope.

Validation

If the profile was marked as required and at least one of the required services is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Block completion

The device will NOT be disconnected when the block ends, unless the situation described in the Validation section.

Example

override suspend fun connect(
block: suspend CoroutineScope.(Proximity.State) -> Unit,
): Unit = withContext(Dispatchers.IO) {
// First, register profile. Do this only once for a peripheral.
// The profile block will get called each time the peripheral is connected.
peripheral.profile(
requiredServiceUuids = listOf(
Proximity.linkLossServiceUuid
),
optionalServiceUuids = listOf(
Proximity.immediateAlertServiceUuid,
Proximity.txPowerServiceUuid,
),
required = true,
name = "Proximity",
) { remoteServices ->
val state = ProximityImpl(remoteServices, this)

// Call the block with the Proximity profile state, separating Bluetooth LE from the logic.
block(state)
}
// Connect.
centralManager.connect(peripheral)

// Await disconnection.
try {
peripheral.awaitDisconnection()
} catch (e: CancellationException) {
// The scope may get canceled when user leaves the screen.
// In that case, make sure to disconnect.
// Don't disconnect when services were invalidated, as the profile will be re-launched.
if (e.cause !is InvalidAttributeException) {
peripheral.disconnect()
}
// Rethrow.
throw e
}
}

See profile for more information.

Parameters

requiredServiceUuids

The list of UUIDs of the GATT services required by the profile.

optionalServiceUuids

The list of UUIDs of the optional GATT services.

required

Whether support for this profile is required by the app. In example, a Heart Rate app may require a Heart Rate Profile, but also support an optional Battery Profile to indicate the battery level. If true (default), and at least one of the required services is not found on the peripheral, the connection will be terminated with reason RequiredServiceNotFound. If false, the block won't be called, but the connection won't be terminated.

name

An optional name of the profile, used only in log messages. This is useful when an app registers multiple profiles, to easily distinguish them in logs.

block

The profile implementation.


fun profile(scope: CoroutineScope, requiredServiceUuids: List<Uuid>, optionalServiceUuids: List<Uuid> = emptyList(), required: Boolean = true, name: String? = null, block: suspend CoroutineScope.(List<RemoteService>) -> Unit)

Registers a profile implementation that runs when the specified GATT services are discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each group of services, i.e. Battery Profile and Heart Rate Profile.

Note, that this overload of the profile method returns all RemoteServices matching any of the requiredServiceUuids or optionalServiceUuids, even if multiple instances of the same service were discovered.

The provided block is launched on a child coroutine in the given scope when all matching RemoteServices are emitted by the services flow. The coroutine is canceled when the peripheral disconnects (the cancellation cause is PeripheralNotConnectedException) or service are invalidated (cause is InvalidAttributeException).

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The block will be called every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the scope.

Validation

If the profile was marked as required and at least one of the required services is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Block completion

The device will NOT be disconnected when the block ends, unless the situation described in the Validation section.

Example

In this example, the app is connecting to a Heart Rate device with an optional Sensor Location and HR Control Point characteristics. It updates the UI using locationFlow and receives Reset button events using resetButtonEvents.

// Helper methods.
val List<RemoteService>.linkLossService: RemoteService? = services
.firstOrNull { it.uuid = ProximityProfile.linkLossServiceUuid }
val List<RemoteService>.immediateAlertService: RemoteService? = services
.firstOrNull { it.uuid = ProximityProfile.immediateAlertServiceUuid }
val List<RemoteService>.txPowerService: RemoteService? = services
.firstOrNull { it.uuid = ProximityProfile.txPowerServiceUuid }

val RemoteService.alertLevel: RemoteCharacteristic? = characteristics
.firstOrNull { it.uuid = ProximityProfile.alertLevelUuid }
val RemoteService.txPowerLevel: RemoteCharacteristic? = characteristics
.firstOrNull { it.uuid = ProximityProfile.txPowerLevelUuid }

// Proximity profile implementation.
peripheral.profile(
scope = scope,
requiredServiceUuids = listOf(
ProximityProfile.linkLossServiceUuid
),
optionalServiceUuids = listOf(
ProximityProfile.immediateAlertServiceUuid,
ProximityProfile.txPowerServiceUuid,
),
required = true,
name = "Proximity",
) { services ->
// 1. Validate the services.

// Link Loss Service is required.
val linkLossAlertLevel = requireNotNull(services.linkLossService?.alertLevel) {
"Link Loss Alert Level characteristic not found"
}
require(linkLossAlertLevel.isWritable()) {
"Link Loss Alert Level characteristic must have the WRITE property"
}

// Other services are optional, but can only be used when both are found.
val immediateAlertService = services.immediateAlertService
val txPowerService = services.txPowerService
val optionalServicesSupported = immediateAlertService != null && txPowerService != null

// [...]

// 2. Initialize the profile.

// Write Link Loss Alert Level.
linkLossAlertLevel?.write(ProximityProfile.ALERT_HIGH)

// Set up immediate alert.
// Note, that the collection is launched in the profile scope, not the outer scope.
if (optionalServicesSupported) {
buttonState
.onEach {
immediateAlertService?.alertLevel?.let { level ->
level.write(ProximityProfile.ALERT_HIGH)
}
}
.launchIn(this)
}
}

Parameters

scope

The coroutine scope to launch the user block in.

requiredServiceUuids

The list of UUIDs of the GATT services required by the profile.

optionalServiceUuids

The list of UUIDs of the optional GATT services.

required

Whether support for this profile is required by the app. In example, a Heart Rate app may require a Heart Rate Profile, but also support an optional Battery Profile to indicate the battery level. If true (default), and at least one of the required services is not found on the peripheral, the connection will be terminated with reason RequiredServiceNotFound. If false, the block won't be called, but the connection won't be terminated.

name

An optional name of the profile, used only in log messages. This is useful when an app registers multiple profiles, to easily distinguish them in logs.

block

The profile implementation.


suspend fun profile(profile: Profile, required: Boolean = true)

Registers a profile implementation that runs when the specified GATT services are discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each service, i.e. Battery Profile and Heart Rate Profile.

This method suspends only to get the current coroutine scope using currentCoroutineContext. The profile is executed in a child coroutine.

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The profile will be executed every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the scope.

Validation

If the profile was marked as required and at least one of the required services is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Example

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.
private val _buttonState = MutableStateFlow(false)
override val buttonState: StateFlow<Boolean> = _buttonState.asStateFlow()
override val ledState: MutableStateFlow<Boolean> = MutableStateFlow(false)

// Implementation.
override fun prepare(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() {
// 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) }

Parameters

profile

The profile implementation.

required

Whether support for this profile is required by the app.


fun profile(scope: CoroutineScope, profile: Profile, required: Boolean = true)

Registers a profile implementation that runs when the specified GATT services are discovered.

Overview

Decouples the Bluetooth LE profile interface from application logic, allowing each device feature (profile) to run in its own coroutine.

Multiple profiles can be added by calling this method once for each service, i.e. Battery Profile and Heart Rate Profile.

This method suspends only to get the current coroutine scope using currentCoroutineContext. The profile is executed in a child coroutine.

Services

As profile is using services under the hood, it is safe and recommended to call this method before connecting the peripheral.

The profile will be executed every time the services are discovered, which may happen multiple times (e.g. when the peripheral reconnects, or when the service gets invalidated and rediscovered). To stop observing services cancel the scope.

Validation

If the profile was marked as required and at least one of the required services is not found, or the block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.

Example

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.
private val _buttonState = MutableStateFlow(false)
override val buttonState: StateFlow<Boolean> = _buttonState.asStateFlow()
override val ledState: MutableStateFlow<Boolean> = MutableStateFlow(false)

// Implementation.
override fun prepare(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() {
// 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) }

Parameters

scope

The coroutine scope to launch the user block in.

profile

The profile implementation.

required

Whether support for this profile is required by the app.