profile

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

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

A convenience overload of profile that suspends only to obtain the current coroutine scope, so block is canceled together with the calling coroutine instead of a scope managed by the caller. See the profile overload taking an explicit scope parameter for the full documentation - behavior, validation rules and an example.

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.(ProfileServices<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

The block is called with a ProfileServices describing the outcome of service discovery: Found when the service was found, or Unsupported/Failed otherwise. If the profile was marked as required and the service is not found, or discovery fails, the connection will be terminated (right after block returns) with reason RequiredServiceNotFound.

As there is no separate validation step for this overload, block is also responsible for rejecting a service found to be structurally invalid: throwing IllegalArgumentException or NoSuchElementException while handling Found is treated exactly like the service not being found in the first place. Any other exception - or either of these two, thrown while handling Unsupported/Failed - is logged and does not affect the connection; it is not rethrown, so it cannot cancel scope or any other profile registered on it.

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",
) { result ->
// Handle the case where the service is not (yet, or ever) available.
val hrmService = when (result) {
is ProfileServices.Found -> result.services
is ProfileServices.Unsupported,
is ProfileServices.Failed -> return@profile
}

// 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.(ProfileServices<List<RemoteService>>) -> Unit)

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

A convenience overload of profile that suspends only to obtain the current coroutine scope, so block is canceled together with the calling coroutine instead of a scope managed by the caller. See the profile overload taking an explicit scope parameter for the full documentation - behavior, validation rules and an example.

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, block is still called (with Unsupported or Failed), 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.(ProfileServices<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 every time the services flow resolves the profile's services, with the outcome as its parameter: Found, Unsupported or Failed. The coroutine lives as long as that outcome holds: it is canceled when the peripheral disconnects (the cancellation cause is PeripheralNotConnectedException), when the services are invalidated (cause is InvalidAttributeException), and before the block is called again for a new outcome. A finally block is therefore the place to undo whatever was set up for the previous one.

Note, that the block keeps running after it returns, until it is canceled. The coroutine is started undispatched, so everything the block does before its first suspension point happens before the services flow moves on.

Invocations never overlap: the next outcome is reported only after the previous invocation has completed, including its finally blocks. Cleanup must therefore not wait for the services to be resolved again, as that would be waiting for itself.

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

The block is called with a ProfileServices describing the outcome of service discovery: Found when all required services were found, or Unsupported/Failed otherwise. If the profile was marked as required and at least one required service is not found, or discovery fails, the connection will be terminated (right after block returns) with reason RequiredServiceNotFound.

As there is no separate validation step for this overload, block throwing IllegalArgumentException or NoSuchElementException while handling Found is the only way to reject services found to be structurally invalid, and is treated exactly like the services not being found in the first place - it is the only code running for Found. Profile gets a safer variant of this, since it can run its synchronous Profile.prepare check before committing to Found at all, downgrading a rejection to Unsupported instead - see Profile.unsupported. Any other exception - or either of these two, thrown while handling Unsupported/Failed - is logged and does not affect the connection; it is not rethrown, so it cannot cancel scope or any other profile registered on it.

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",
) { result ->
// Handle the case where the required service is not (yet, or ever) available.
val services = when (result) {
is ProfileServices.Found -> result.services
is ProfileServices.Unsupported,
is ProfileServices.Failed -> return@profile
}

// 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, block is still called (with Unsupported or Failed), 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.

A convenience overload of profile that suspends only to obtain the current coroutine scope, so the profile is executed in a coroutine canceled together with the calling coroutine instead of a scope managed by the caller. See the profile overload taking an explicit scope parameter for the full documentation - behavior, validation rules and an example.

Parameters

profile

The profile implementation. Its prepare and initialize methods are called when the services are found; otherwise its unsupported or failed method is called instead (see ProfileServices).

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

The profile's prepare and initialize methods are called when all required services were found and accepted by prepare; otherwise its unsupported/failed method is called instead (see ProfileServices) - this includes the case where prepare itself rejects the services, by throwing IllegalArgumentException or NoSuchElementException, which is treated exactly like the services not being found. If the profile was marked as required and at least one required service is not found or accepted, or discovery fails, the connection will be terminated (right after unsupported/failed returns) with reason RequiredServiceNotFound. Unlike prepare, initialize, unsupported and failed have no "throw this to reject" contract - if any of them throws, it is only logged and does not affect the connection; it is not rethrown, so it cannot cancel scope or any other profile registered on it.

See Profile for how to implement one, with examples for both single- and multi-service profiles.

Parameters

scope

The coroutine scope to launch the user block in.

profile

The profile implementation. Its prepare and initialize methods are called when the services are found; otherwise its unsupported or failed method is called instead (see ProfileServices).

required

Whether support for this profile is required by the app.