profile
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.
It is safe and recommended to call this method before connecting the peripheral.
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,
) { 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
The UUID of the profile service.
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.
The profile implementation.
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).
Validation
If block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.
If multiple services share the same serviceUuid, only the first one is passed to block.
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(
serviceUuid = HeartRateProfile.heartRateServiceUuid,
required = true,
scope = scope,
) { 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)
}
.launchIn(this)
}
}
.onEach {
// Update UI or something.
}
.launchIn(this)
// 3. Await the scope cancellation. The scope will be cancelled when the device disconnects,
// or the scope in which this method is called is canceled.
awaitCancellation()
}Parameters
The UUID of the profile service.
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.
The coroutine scope to launch the user block in.
The profile implementation.
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.
It is safe and recommended to call this method before connecting the peripheral.
Example
override suspend fun connect(
block: suspend CoroutineScope.(HeartRateProfile.State) -> Unit,
): Unit = withContext(Dispatchers.IO) {
// First, register profile.
peripheral.profile(
requiredServiceUuids = listOf(
ProximityProfile.linkLossServiceUuid
),
optionalServiceUuids = listOf(
ProximityProfile.immediateAlertServiceUuid,
ProximityProfile.txPowerServiceUuid,
),
required = true,
) { remoteServices ->
val state = ProximityProfile(remoteServices, this)
// Call the block with the Proximity profile state, separating Bluetooth LE from the logic.
block(state)
}
// Connect.
centralManager.connect(peripheral)
// Await disconnection.
peripheral.awaitDisconnection()
}See profile for more information.
Parameters
The list of UUIDs of the GATT services required by the profile.
The list of UUIDs of the optional GATT services.
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.
The profile implementation.
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 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).
Validation
If block throws IllegalArgumentException during service validation, the connection will be terminated with reason RequiredServiceNotFound.
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(
requiredServiceUuids = listOf(
ProximityProfile.linkLossServiceUuid
),
optionalServiceUuids = listOf(
ProximityProfile.immediateAlertServiceUuid,
ProximityProfile.txPowerServiceUuid,
),
required = true,
scope = scope,
) { 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.
if (optionalServicesSupported) {
buttonState
.onEach {
immediateAlertService?.alertLevel?.let { level ->
level.write(ProximityProfile.ALERT_HIGH)
}
}
.launchIn(this)
}
// 3. Await the scope cancellation. The scope will be cancelled when the device disconnects,
// or the scope in which this method is called is canceled.
awaitCancellation()
}Parameters
The list of UUIDs of the GATT services required by the profile.
The list of UUIDs of the optional GATT services.
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.
The coroutine scope to launch the user block in.
The profile implementation.