[Feature] Scanner: easy way to detect when device is no longer available
The Scanner should provide a way to determine when a device is no longer available, because it has been turned off or is no longer in range.
As I understand it, at least on Android, the right way to do this is to scan again, and determine from the new scan if any devices are no longer present relative to the previous scan.
See https://stackoverflow.com/questions/33033906/how-to-detect-when-a-ble-device-is-not-in-range-anymore.
I'm reluctant to implement such a feature in Kable since I believe what should be considered "no longer available" comes down a lot to the needs of the app that is using Kable.
Additionally, I think implementing such logic app-side should be fairly straight forward?
Something to the effect of (pseudo code):
// Fake advertisements identified as a single character.
val advertisements = MutableSharedFlow<Char>()
launch {
listOf(
'a', 'b', 'e', 'f', 'a', 'c', 'd', 'a', 'a', 'a', 'b', 'd', 'e', 'a', 'b', 'b', 'a',
'a', 'b', 'b', 'a', 'b', 'a', 'b', 'b', 'a', 'b', 'a', 'b', 'b', 'a', 'b', 'a', 'b',
).forEach {
advertisements.emit(it)
delay(1.seconds)
}
}
val mark1 = Clock.System.asTimeSource().markNow() + 10.24.seconds
val pass1 = advertisements.takeWhile { mark1.hasNotPassedNow() }.toSet()
val mark2 = Clock.System.asTimeSource().markNow() + 10.24.seconds
val pass2 = advertisements.takeWhile { mark2.hasNotPassedNow() }.toSet()
val noLongerAvailable = pass1 - pass2
println(noLongerAvailable)
Thanks I'll experiment with this. If it does work, then what do you think about a utility method on advertisements like scanOneInterval()? That would abstract away the low-level detail of the 10.24 seconds that covers one advertising interval.
I'm reluctant to add such a utility method only because (as far as I understand) advertisement interval is customizable (on the peripheral side), so some firmware might use a more frequent interval while others use less, and 10.24 is just the maximum — so the naming scanOneInterval may not be accurate for all peripherals.
The timeout could be configurable (and default to 10.24), but that becomes a lot of code in Kable for something that is fairly trivial to implement on the consumer side?
Ultimately, I appreciate the suggestion and always open to further discussion (I'm just cautious about growing the API unnecessarily).
This approach works but there are a few complexities to keep in mind.
-
You can't use
toSetdirectly, as (at least for my device) eachAndroidAdvertisementis not equal, even though its the same address. Mapping to theaddressfield of the advertisement is needed for the set comparison. -
Obvious in hindsight but caused some problems initially: BLE devices stop advertising when they are connected. Therefore, we can't assume an already connected device is "gone" relative to the current device when it is no longer present in the advertisements.
I went with an approach that looks something like this. My peripheral advertises every 250 ms so a 1 second chunk time was sufficient, and this approach does not require multiple separate scans:
var visibleAddresses = emptySet<String>()
// any peripherals already connected from other app state
var connectedAddresses = setOf(...)
scanner.advertisements
// custom implementation
// see https://github.com/Kotlin/kotlinx.coroutines/issues/1302
.chunked(100, 1.seconds)
.map { it.associateBy { it.address } }
.distinctUntilChanged()
.collect { ads ->
val removed = lastAddresses - connectedAddresses - ads.keys
val added = ads.keys - lastAddresses
if (added.isNotEmpty() || removed.isNotEmpty()) {
lastAddresses = ads.keys
}
}
If you are curious about my implementation of chunked I can share it.
I don't feel this is trivial to do for an application consumer, so to me it makes sense to have some of this logic in Kable. However, I understand not wanting to expand the API surface. Feel free to close this issue.
I see. You've definitely exposed more complexity than I had originally anticipated.
I'll keep the issue open, as it allows others to discover the feature request and 👍 if they too would find it useful. That would at least gauge interest.
~~I'm still on the fence as to if it should be added to Kable, especially with needing a custom (chunked) operator, makes for a potentially larger maintenance burden. Lots of tradeoffs to consider.~~ Regardless, I would appreciate you sharing your chunked implementation, it would prove useful if later we do decide to add this feature.
EDIT: I'd be happy to have extra features like this live in a separate module/artifact.
@rocketraman if you're up for creating a PR, I'd be happy to have the feature live in a separate module/artifact that consumers can optionally pull into their projects.