Skip to content

Commit 8b5378c

Browse files
authored
Merge pull request #297 from nordicsemi/improvement/profiles
[Improvement] Profiles logging
2 parents cf8322c + b1706ed commit 8b5378c

1 file changed

Lines changed: 103 additions & 21 deletions

File tree

  • client-core/src/main/java/no/nordicsemi/kotlin/ble/client

client-core/src/main/java/no/nordicsemi/kotlin/ble/client/Peripheral.kt

Lines changed: 103 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -492,7 +492,18 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
492492
/**
493493
* Returns a flow emitting [RemoteServices] events.
494494
*
495-
* ### State machine
495+
* ## Overview
496+
*
497+
* This method is the main entry point to discover GATT services on the peripheral and observe
498+
* changes in the services. It returns a flow that emits the current state of service discovery
499+
* process, which can be observed to get the list of services when they are discovered, or to
500+
* handle errors when service discovery fails.
501+
*
502+
* Note, that the flow may emit [RemoteServices.Discovered] state multiple times, for example
503+
* when the peripheral reconnects or when the services get invalidated and rediscovered.
504+
* This is not a one-time operation, but a continuous observation of the services on the peripheral.
505+
*
506+
* ## State machine
496507
*
497508
* Initially, the state is [Unknown][RemoteServices.Unknown]. Shortly after calling [services]
498509
* the flow will emit [Discovering][RemoteServices.Discovering] state, followed by
@@ -564,7 +575,23 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
564575
* This method suspends only to get the current coroutine scope using [currentCoroutineContext].
565576
* The [block] is called in a child coroutine.
566577
*
567-
* It is safe and recommended to call this method before connecting the peripheral.
578+
* ## Services
579+
*
580+
* As this is using [services] under the hood, it is safe and recommended to call this method
581+
* before connecting the peripheral.
582+
*
583+
* The [block] will be called every time the service is discovered,
584+
* which may happen multiple times (e.g. when the peripheral reconnects, or when the service
585+
* gets invalidated and rediscovered). To stop, cancel the scope in which the [block] is
586+
* running, or use [profile] method with a custom scope.
587+
*
588+
* ## Validation
589+
*
590+
* If `block` throws [IllegalArgumentException] during service validation, and the profile
591+
* was marked as [required], the connection will be terminated with reason
592+
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
593+
*
594+
* If multiple services share the same [serviceUuid], only the first one is passed to `block`.
568595
*
569596
* ## Example
570597
*
@@ -576,6 +603,7 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
576603
* peripheral.profile(
577604
* serviceUuid = HeartRateProfile.heartRateServiceUuid,
578605
* required = true,
606+
* name = "Heart Rate Profile",
579607
* ) { remoteService ->
580608
* val state = HeartRateServiceImpl(remoteService, this)
581609
*
@@ -596,20 +624,23 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
596624
* @param required Whether the service is required. In example, a Heart Rate app may require
597625
* a Heart Rate Service, but also support an optional Battery Service to indicate the battery
598626
* level.
627+
* @param name An optional name of the profile, used only in log messages. This is useful when
628+
* an app registers multiple profiles, to easily distinguish them in logs.
599629
* @param block The profile implementation.
600630
*/
601631
@OptIn(ExperimentalUuidApi::class)
602632
suspend fun profile(
603633
serviceUuid: Uuid,
604634
required: Boolean = true,
635+
name: String? = null,
605636
block: suspend CoroutineScope.(RemoteService) -> Unit,
606637
) {
607638
// Get the current context. This will allow creating a scope, that will get closed
608639
// together with the outer scope.
609640
val context = currentCoroutineContext()
610641
val userScope = CoroutineScope(context)
611642

612-
profile(serviceUuid, required, userScope, block)
643+
profile(userScope, serviceUuid, required, name, block)
613644
}
614645

615646
/**
@@ -629,10 +660,20 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
629660
* or when the job completes. When the `block` finishes (normally or exceptionally) the
630661
* peripheral will be disconnected (with the reason [Success][ConnectionState.Disconnected.Reason.Success]).
631662
*
663+
* ## Services
664+
*
665+
* As this is using [services] under the hood, it is safe and recommended to call this method
666+
* before connecting the peripheral.
667+
*
668+
* The [block] will be called every time the service is discovered,
669+
* which may happen multiple times (e.g. when the peripheral reconnects, or when the service
670+
* gets invalidated and rediscovered). To stop, cancel the scope in which the [block] is
671+
* running, or use [profile] method with a custom scope.
672+
*
632673
* ## Validation
633674
*
634-
* If `block` throws [IllegalArgumentException] during service validation, the connection will
635-
* be terminated with reason
675+
* If `block` throws [IllegalArgumentException] during service validation, and the profile
676+
* was marked as [required], the connection will be terminated with reason
636677
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
637678
*
638679
* If multiple services share the same [serviceUuid], only the first one is passed to `block`.
@@ -654,9 +695,10 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
654695
*
655696
* // LBS profile implementation.
656697
* peripheral.profile(
698+
* scope = scope,
657699
* serviceUuid = HeartRateProfile.heartRateServiceUuid,
658700
* required = true,
659-
* scope = scope,
701+
* name = "Heart Rate Profile",
660702
* ) { hrmService ->
661703
* // 1. Validate the service.
662704
*
@@ -702,23 +744,27 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
702744
* }
703745
* ```
704746
*
747+
* @param scope The coroutine scope to launch the user block in.
705748
* @param serviceUuid The UUID of the profile service.
706749
* @param required Whether the service is required by the app. In example, a Heart Rate app
707750
* may require a Heart Rate Service, but also support an optional Battery Service to indicate
708751
* the battery level.
709-
* @param scope The coroutine scope to launch the user block in.
752+
* @param name An optional name of the profile, used only in log messages. This is useful when
753+
* an app registers multiple profiles, to easily distinguish them in logs.
710754
* @param block The profile implementation.
711755
*/
712756
@OptIn(ExperimentalUuidApi::class)
713757
fun profile(
758+
scope: CoroutineScope,
714759
serviceUuid: Uuid,
715760
required: Boolean = true,
716-
scope: CoroutineScope,
761+
name: String? = null,
717762
block: suspend CoroutineScope.(RemoteService) -> Unit,
718763
) = profile(
764+
scope = scope,
719765
requiredServiceUuids = listOf(serviceUuid),
720766
required = required,
721-
scope = scope
767+
name = name,
722768
) { services ->
723769
block(services.first())
724770
}
@@ -741,7 +787,21 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
741787
* This method suspends only to get the current coroutine scope using [currentCoroutineContext].
742788
* The [block] is called in a child coroutine.
743789
*
744-
* It is safe and recommended to call this method before connecting the peripheral.
790+
* ## Services
791+
*
792+
* As this is using [services] under the hood, it is safe and recommended to call this method
793+
* before connecting the peripheral.
794+
*
795+
* The [block] will be called every time the service is discovered,
796+
* which may happen multiple times (e.g. when the peripheral reconnects, or when the service
797+
* gets invalidated and rediscovered). To stop, cancel the scope in which the [block] is
798+
* running, or use [profile] method with a custom scope.
799+
*
800+
* ## Validation
801+
*
802+
* If `block` throws [IllegalArgumentException] during service validation, and the profile
803+
* was marked as [required], the connection will be terminated with reason
804+
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
745805
*
746806
* ## Example
747807
*
@@ -759,6 +819,7 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
759819
* ProximityProfile.txPowerServiceUuid,
760820
* ),
761821
* required = true,
822+
* name = "Proximity",
762823
* ) { remoteServices ->
763824
* val state = ProximityProfile(remoteServices, this)
764825
*
@@ -783,21 +844,24 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
783844
* required services is not found on the peripheral, the connection will be terminated with reason
784845
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
785846
* If `false`, the [block] won't be called, but the connection won't be terminated.
847+
* @param name An optional name of the profile, used only in log messages. This is useful when
848+
* an app registers multiple profiles, to easily distinguish them in logs.
786849
* @param block The profile implementation.
787850
*/
788851
@OptIn(ExperimentalUuidApi::class)
789852
suspend fun profile(
790853
requiredServiceUuids: List<Uuid>,
791854
optionalServiceUuids: List<Uuid> = emptyList(),
792855
required: Boolean = true,
856+
name: String? = null,
793857
block: suspend CoroutineScope.(List<RemoteService>) -> Unit,
794858
) {
795859
// Get the current context. This will allow creating a scope, that will get closed
796860
// together with the outer scope.
797861
val context = currentCoroutineContext()
798862
val userScope = CoroutineScope(context)
799863

800-
profile(requiredServiceUuids, optionalServiceUuids, required, userScope, block)
864+
profile(userScope, requiredServiceUuids, optionalServiceUuids, required, name, block)
801865
}
802866

803867
/**
@@ -821,10 +885,20 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
821885
* or when the job completes. When the `block` finishes (normally or exceptionally) the
822886
* peripheral will be disconnected (with the reason [Success][ConnectionState.Disconnected.Reason.Success]).
823887
*
888+
* ## Services
889+
*
890+
* As this is using [services] under the hood, it is safe and recommended to call this method
891+
* before connecting the peripheral.
892+
*
893+
* The [block] will be called every time the service is discovered,
894+
* which may happen multiple times (e.g. when the peripheral reconnects, or when the service
895+
* gets invalidated and rediscovered). To stop, cancel the scope in which the [block] is
896+
* running, or use [profile] method with a custom scope.
897+
*
824898
* ## Validation
825899
*
826-
* If `block` throws [IllegalArgumentException] during service validation, the connection will
827-
* be terminated with reason
900+
* If `block` throws [IllegalArgumentException] during service validation, and the profile
901+
* was marked as [required], the connection will be terminated with reason
828902
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
829903
*
830904
* ## Example
@@ -849,6 +923,7 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
849923
*
850924
* // Proximity profile implementation.
851925
* peripheral.profile(
926+
* scope = scope,
852927
* requiredServiceUuids = listOf(
853928
* ProximityProfile.linkLossServiceUuid
854929
* ),
@@ -857,7 +932,7 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
857932
* ProximityProfile.txPowerServiceUuid,
858933
* ),
859934
* required = true,
860-
* scope = scope,
935+
* name = "Proximity",
861936
* ) { services ->
862937
* // 1. Validate the services.
863938
*
@@ -898,6 +973,7 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
898973
* }
899974
* ```
900975
*
976+
* @param scope The coroutine scope to launch the user block in.
901977
* @param requiredServiceUuids The list of UUIDs of the GATT services required by the profile.
902978
* @param optionalServiceUuids The list of UUIDs of the optional GATT services.
903979
* @param required Whether support for this profile is required by the app. In example,
@@ -906,18 +982,21 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
906982
* required services is not found on the peripheral, the connection will be terminated with reason
907983
* [RequiredServiceNotFound][ConnectionState.Disconnected.Reason.RequiredServiceNotFound].
908984
* If `false`, the [block] won't be called, but the connection won't be terminated.
909-
* @param scope The coroutine scope to launch the user block in.
985+
* @param name An optional name of the profile, used only in log messages. This is useful when
986+
* an app registers multiple profiles, to easily distinguish them in logs.
910987
* @param block The profile implementation.
911988
*/
912989
@OptIn(ExperimentalUuidApi::class)
913990
fun profile(
991+
scope: CoroutineScope,
914992
requiredServiceUuids: List<Uuid>,
915993
optionalServiceUuids: List<Uuid> = emptyList(),
916994
required: Boolean = true,
917-
scope: CoroutineScope,
995+
name: String? = null,
918996
block: suspend CoroutineScope.(List<RemoteService>) -> Unit,
919997
) {
920-
require(requiredServiceUuids.isNotEmpty()) { "Service UUIDs list cannot be empty" }
998+
val name = name ?: "Profile"
999+
require(requiredServiceUuids.isNotEmpty()) { "$name: Service UUIDs list cannot be empty" }
9211000

9221001
/**
9231002
* The user job will be started to execute the user block.
@@ -961,17 +1040,19 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
9611040
disconnect()
9621041
} catch (e: Exception) {
9631042
when (e) {
1043+
// Rethrow cancellation exceptions, without any action.
1044+
is CancellationException -> throw e
9641045
// The implementation may use require(...) methods
9651046
// to verify the service.
9661047
// Catch them and report as if the service was not found.
9671048
is IllegalArgumentException -> {
968-
logger.warn("Profile validation failed", e)
1049+
logger.warn("$name: Validation failed", e)
9691050
if (required) {
9701051
disconnect(ConnectionState.Disconnected.Reason.RequiredServiceNotFound)
9711052
}
9721053
}
9731054
else -> {
974-
logger.error("Profile block failed", e)
1055+
logger.error("$name: Block failed with exception", e)
9751056
throw e
9761057
}
9771058
}
@@ -982,17 +1063,18 @@ abstract class Peripheral<ID: Any, EX: Peripheral.Executor<ID>>(
9821063
} else {
9831064
// If any required service was not found disconnect, disconnect.
9841065
if (required) {
985-
logger.warn("Required services not supported, missing: $missingServices")
1066+
logger.warn("$name: Required services not supported (missing: $missingServices)")
9861067
disconnect(ConnectionState.Disconnected.Reason.RequiredServiceNotFound)
9871068
} else {
988-
logger.warn("Optional services not supported, missing: $missingServices")
1069+
logger.warn("$name: Optional services not supported (missing: $missingServices)")
9891070
// Do not disconnect or cancel the user scope.
9901071
// The device may change its services and the Discovered state
9911072
// may be emitted again.
9921073
}
9931074
}
9941075
}
9951076
is RemoteServices.Failed -> {
1077+
logger.error("$name: Service discovery failed (reason: ${state.reason})")
9961078
// In case of a service discovery failure, act as if the service was not found.
9971079
// TODO Is this expected behavior?
9981080
disconnect(ConnectionState.Disconnected.Reason.RequiredServiceNotFound)

0 commit comments

Comments
 (0)