@@ -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