juullabs / kable Goto Github PK
View Code? Open in Web Editor NEWKotlin Asynchronous Bluetooth Low-Energy
Home Page: https://juullabs.github.io/kable
License: Apache License 2.0
Kotlin Asynchronous Bluetooth Low-Energy
Home Page: https://juullabs.github.io/kable
License: Apache License 2.0
Hello there :)
Currently I am still using able, or better: I am using a slightly customised version of it where I set the transport flag of the connectGatt
method to BluetoothDevice.TRANSPORT_LE
.
I was wondering what the reasoning behind this decision is. Using BluetoothDevice.TRANSPORT_LE
might reduce the appearances of the infamous 133
error. Other then anecdotal once I sadly have no proof to back up this claim.
I saw that you want to make this a customisable parameter, still I am wondering why in Able you didn't use the flag and here you use TRANSPORT_AUTO
.
Also: Great library, really looking forward to incorporating Kable at one point :)
I think that I came across a small flaw in the Advertisement
cleanup here. When bluetooth is turned on, everything works fine. But in the case that I open the screen, turn off bluetooth and leave it, I get the following warning:
[CoreBluetooth] API MISUSE: <CBCentralManager: 0x2831be780> can only accept this command while in the powered on state
I know that this case is not very likely to happen, but for the sake of correctness, central.stopScan()
should only be called if the state is .poweredOn
.
Currently, when attempting to write/read a characteristic that was not discovered then it will throw a NoSuchElementException
(as thrown by first
):
kable/core/src/jsMain/kotlin/Peripheral.kt
Lines 207 to 212 in c750de5
Instead, we should throw an exception that clearly indicates what service or characteristic wasn't found.
The Scanner
API presently exposes a flow of unfiltered peripherals which can of course be filtered after the fact. Internally the scan mechanisms typically provide for filtering during the scan process (part of the BLE spec) which is intended to be more efficient/save battery etc.
When instantiating the scanner, the API should allow injection of said filters to be fed through to the underlying systems.
A characteristic of my device is Indicate.
This differs from Notify in such a way that an update from characteristic requires an acknoladgement.
Seems Kable does not send such acknowladgement.
This is a fragment from log
2021-04-16 22:54:06.721 6155-6155/com.pulsariodev D/BluetoothGatt: setCharacteristicNotification() - uuid: 00008a82-0000-1000-8000-00805f9b34fb enable: true
2021-04-16 22:54:06.862 6155-6155/com.pulsariodev V/ZewaDevice: Received data A0C22656D4000000000000000000000000000000
2021-04-16 22:54:06.952 6155-6155/com.pulsariodev D/BluetoothGatt: setCharacteristicNotification() - uuid: 00008a82-0000-1000-8000-00805f9b34fb enable: false
2021-04-16 22:54:06.964 6155-6155/com.pulsariodev V/ZewaDevice: Writing int data 215BD16264
2021-04-16 22:54:07.039 6155-6155/com.pulsariodev D/ZewaDevice: Pairing, password -1037674796, Account 1540448868
2021-04-16 22:54:07.040 6155-6155/com.pulsariodev D/ZewaDevice: Verification. Reading random number
2021-04-16 22:54:07.066 6155-6155/com.pulsariodev D/BluetoothGatt: setCharacteristicNotification() - uuid: 00008a82-0000-1000-8000-00805f9b34fb enable: true
2021-04-16 22:54:07.148 6155-6173/com.pulsariodev D/BluetoothGatt: onClientConnectionState() - status=19 clientIf=13 device=CA:41:D4:56:26:C2
2021-04-16 22:54:07.150 6155-6350/com.pulsariodev D/BluetoothGatt: close()
2021-04-16 22:54:07.150 6155-6350/com.pulsariodev D/BluetoothGatt: unregisterApp() - mClientIf=13
status 19 means GATT_RSP_EXEC_WRITE
that my device closed the connection since it does not receive an acknowledgment for an indicate.
For every I/O operation, the internal platformServices
and services
properties are used to retrieve the native bluetooth characteristic object. These properties will map the native services to Kable types on every read.
internal val platformServices: List<PlatformService>?
get() = cbPeripheral.services?.map { service ->
service as CBService
service.toPlatformService()
}
public override val services: List<DiscoveredService>?
get() = platformServices?.map { it.toDiscoveredService() }
These mappings should be cached on service discovery (discoverServices
), and cache should be cleared on disconnect (as is done on the Android platform).
Hello guys.
Currently the peripheral is retrieved only with advertisement data. I think interesting to be possible to retrieve the peripheral with its UUID identifier even knowing it is not 100% relied upon for peripheral re-identification.
Example:
Peripheral.kt
public fun CoroutineScope.peripheral(
uuid: Uuid
): Peripheral {
val peripheral = CentralManager.Default.retrievePeripheral(uuid)
return ApplePeripheral(coroutineContext, peripheral)
}
CentralManager.kt
internal fun retrievePeripheral(withIdentifiers: Uuid) : CBPeripheral {
val peripheral = cbCentralManager.retrievePeripheralsWithIdentifiers(listOf(withIdentifiers))
return peripheral.firstOrNull() as CBPeripheral
}
Thank you
When requesting MTU change, it internally succeeded but throws an exception. After that call, I am able to send for example 170B (20B before the call). You can see in logcat that it has status 0, which is GATT_SUCCESS in that callback.
Library version: 0.3.0
Phone: Pixel 5, Android 11
Code:
try {
Timber.d("Requesting MTU: $REQUESTED_MTU")
peripheral.requestMtu(REQUESTED_MTU)
} catch (e: Exception) {
e.printStackTrace()
}
Stack trace:
Requesting MTU: 512
configureMTU() - device: 7D:CC:76:A0:A5:A0 mtu: 512
onConfigureMTU() - Device=7D:CC:76:A0:A5:A0 mtu=512 status=0
com.juul.kable.OutOfOrderGattCallbackException: Unexpected response type OnMtuChanged received
at com.juul.kable.AndroidPeripheral.requestMtu(Peripheral.kt:374)
at com.juul.kable.AndroidPeripheral$requestMtu$1.invokeSuspend(Unknown Source:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
Is there something like a tutorial how to move from Able to Kable?
Hello,
I am trying to test the kable library in a KMM project. For that I have created a new KMM project with the KMM plugin in Android Studio. So I add the kable library in commonMain shared module with:
val commonMain by getting {
dependencies {
implementation("com.juul.kable:core:0.2.0")
}
}
Also I commented the iOS target because kable is not support it yet. The gradle sync successfully, but I do not get to import the kable library to commonMain shared module or whatever.
Do anyone help me?
Thanks
Android BLE API supports requesting various connection priorities. Kable should expose this functionality for platforms that support it.
Implemented in Able via JuulLabs/able#95.
getting No matching variant of com.juul.kable:core-android:0.3.0 was found. The consumer was configured to find a runtime of a component, as well as attribute when updated my project gradle to 7.0.0 for using Jetpack Compose
First of all: awesome library, it works perfect on iOS and Android!
For my project, I need want to show the available devices. For that, I want get all advertisements in common code, filter them and publish the result as list. It would be really useful to have an identifier in this case to distinguish the devices. I can see from the source code that on iOS the UUID is used while on Android the address is returned. I know that on iOS you cannot get the physical device address. But what about a platform dependent id for the common code? Currently my simple approach looks like this:
// common
expect fun getIDForBluetoothDevice(advertisement: Advertisement): String
// Android
actual fun getIDForBluetoothDevice(advertisement: Advertisement): String = advertisement.address
// iOS
actual fun getIDForBluetoothDevice(advertisement: Advertisement): String = advertisement.identifier.toString()
I think it might be a good idea to add something like this as it seems to me that it's a common use case to distinguish devices by unique ids.
It has been pointed out that Apple platform has some awkward behavior where CBPeripheral may be invalid upon disconnecting (e.g. when it has gone out of range).
So I started trying out the library following the readme and noticed that after collecting the advertisements flow, a scanner was registered, as per the log: BluetoothLeScanner: onScannerRegistered() - status=0 scannerId=9 mScannerId=0
.
However I wasn't getting any scan record, or advertisement as the library calls it, and wondered whether I was making some mistake.
Turns out I hadn't allowed location permission, but at the same time I wondered whether the library should check for it instead of silently doing nothing. Maybe throw an error if location permission is missing? As it stands, if bluetooth is disabled when collecting the advertisements flow an error is thrown, I'd expect to do the same if location permissions are missing.
What are your thoughts?
Request to have access to the BLE name
of the peripheral, whether that is surfaced from the advertisement packet, from the system specific device (e.g. Web BlueoothDevice.name
), or from the BLE name characteristic.
It is expected this would be a nullable string on the Peripheral
interface.
Consider supporting Node.js.
As pointed out in #24 (comment), the current implementation likely does not work outside of the browser.
We are trying to make a proof of concept of our app using Kable library (0.5.0) for connecting with the devices. Right now, we have devices with different bluetooth modules and of course, different services/characteristics/descriptors.
In our devices with a characteristic which contains a descriptor, if the device notify a response, we are able to receive the response in the "peripheral.observe" method.
On other hand, we have some old devices which not have a descriptor for each characteristic and we are not able to receive the response in the "peripheral.observe" method. I was taking a look to the issues and I found an issue from December that says that you implemented a piece of code to automatically detect if the characteristic contains a descriptor or not and act in consequence.
We were wondering if we need to have something in consideration to solve this or if exist any impediment.
Thanks a lot for you work. The library is awesome.
I have BLE device with this strange firmware. There are two services with the same UUIDs and with identical sets of characteristics. The only difference is in one of descriptors. So, there are two services S1 and S2, they both have characteristic C, and value of a descriptor D differs. All uuids are the same.
S1.uuid == S2.uuid
S1.C.uuid == S2.C.uuid
S1.C.D.uuid == S2.C.D.uuid
S1.C.D.value != S1.C.D.value.
This way I should distinct those 2 services.
So after services are discovered I do a loop over them and for every service S1 and S2 I read that descriptor.
Unfortunately I always get the same value, as in both cases I read only descriptor of the service S1.
readDescriptor
function searches for descriptor through all services and always return the first one found.
Is there a way to read identical descriptors (and characteristics) of two different services?
For convenience sake, it would be nice if the BLE Options
interface automagically to-lowered any provided UUIDs, as Web BLE will throw an exception if any uppercase UUID strings are passed into the Web BLE API.
Hello guys,
Is it possible to expose the device's bond state on Advertisement class? For example:
public actual class Advertisement(
private val scanResult: ScanResult,
) {
internal val bluetoothDevice: BluetoothDevice
get() = scanResult.device
public actual val name: String?
get() = bluetoothDevice.name
public actual val bondState: String?
get() = bluetoothDevice.bondState
.......
}
Internally our team as begun to see DOMException: GATT operation already in progress.
errors.
I believe for the current JavaScript implementation I made the incorrect assumption that Web Bluetooth would handle the "queueing" of multiple I/O operations. This appears to be the wrong assumption and similar to Android and Apple platforms, we need to put all I/O operations within a lock (to prevent concurrent I/O requests).
Current locking implementations in Android and Apple that prevent concurrent I/O operations:
Hello,
Peripheral.observe function throws NotReadyException on iOS when the peripheral is shutdown or the application disconnect from it. That behaviour do not happen on Android.
I did not investigate if that is correct, when I have some time I can investigate it.
Issue opened for tracking and discussion.
I am getting the exception below when trying to read data from charactertistic
java.lang.NullPointerException: value must not be null
at com.juul.kable.gatt.Callback.onCharacteristicRead(Callback.kt:110)
at android.bluetooth.BluetoothGatt$1$6.run(BluetoothGatt.java:394)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.os.HandlerThread.run(HandlerThread.java:67)
This is my code from ModelView
init {
val TAG = "HomeViewModel"
val _deviceId = "A8:7E:EA:71:89:54" // Emulator
// val _deviceId = "B4:52:A9:04:E2:88" // Oximeter
viewModelScope.launch {
val adapter = BluetoothAdapter.getDefaultAdapter()
val btd = adapter.getRemoteDevice(_deviceId)
val peripheral = peripheral(btd)
Timber.tag(TAG).d("Connecting to device")
peripheral.connect()
Timber.tag(TAG).d("Connected to device")
delay(2000)
val ch = characteristicOf("0000180A-0000-1000-8000-00805F9B34FB", "00002a29-0000-1000-8000-00805f9b34fb")
val data = peripheral.read(ch)
Timber.tag(TAG).d("Manufacturer data $data")
}
}
There are devices which requests enabling indication (BluetoothGattDescriptor.ENABLE_INDICATION_VALUE) instead of notification (BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) in order to receive (observe in kable) values from them.
Maybe kable-api should allow such thing, as I do not see any kind of workaround at the moment.
This could be checked against characteristic property like so:
characteristic.getProperties() and PROPERTY_NOTIFY / PROPERTY_INDICATION > 0
Hi, I am trying to build an Android Gradle module selecting the "staging" build type and I am getting the following error
Could not determine the dependencies of task ':app:lintVitalStaging'.
> Could not resolve all artifacts for configuration ':app:stagingCompileClasspath'.
> Could not resolve com.juul.kable:core:0.4.1.
Required by:
project :app
> No matching variant of com.juul.kable:core:0.4.1 was found. The consumer was configured to find an API of a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'staging', attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm' but:
- Variant 'commonMainMetadataElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'common' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'debugApiElements-published' capability com.juul.kable:core:0.4.1 declares an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm':
- Incompatible because this component declares a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'debug' and the consumer needed a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'staging'
- Variant 'debugRuntimeElements-published' capability com.juul.kable:core:0.4.1 declares a runtime of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm':
- Incompatible because this component declares a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'debug' and the consumer needed a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'staging'
- Variant 'iosArm64ApiElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'iosArm64MetadataElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-metadata' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'iosX64ApiElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'iosX64MetadataElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-metadata' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'jsApiElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'js' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'jsRuntimeElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-runtime' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'js' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'macosX64ApiElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-api' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'macosX64MetadataElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-metadata' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'native' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'metadataApiElements-published' capability com.juul.kable:core:0.4.1:
- Incompatible because this component declares a usage of 'kotlin-metadata' of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'common' and the consumer needed an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm'
- Other compatible attribute:
- Doesn't say anything about com.android.build.api.attributes.BuildTypeAttr (required 'staging')
- Variant 'releaseApiElements-published' capability com.juul.kable:core:0.4.1 declares an API of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm':
- Incompatible because this component declares a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'release' and the consumer needed a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'staging'
- Variant 'releaseRuntimeElements-published' capability com.juul.kable:core:0.4.1 declares a runtime of a component, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'androidJvm':
- Incompatible because this component declares a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'release' and the consumer needed a component, as well as attribute 'com.android.build.api.attributes.BuildTypeAttr' with value 'staging'
The problem seems related to #70. How can I fix the problem?
Javascript has a requestDevice
function which takes parameters to filter which peripheral is of interest.
Generally, a developer is seeking out a single peripheral; as such, it would be nice to provide the Javascript requestDevice
-like API for all platforms (i.e. suspend
function that returns a single Advertisement
).
This may be a bit challenging being that on Javascript, the function causes the Chrome browser to provide a list of possible matches to the user. So the user is actually narrowing down discovered peripherals down to a single peripheral. It would be difficult to match this logic on other platforms.
This idea was originally raised in #1 (comment).
NOTE: Public Repos are disabled for this organization! Repository was automatically converted to a Private Repo. Please contact an admin to override.
/cc @twyatt
/cc @perl2ruby @jessecollier @zafarabbas @IamMrCupp @carterandrew
I have an android gradle module and I am following the platform specific instructions in the readme that say to add:
dependencies {
implementation("com.juul.kable:core-android:0.2.0")
}
Gradle successfully syncs, but I don't see the library show up in the project window under external libraries and I cannot seem to use any kable classes in my project.
I'm not sure whats missing, I have never used a multiplatform library from an android project.
If I set the version to 0.1.0, android studio will recommend the next available highest version. (0.2.0 in my case although I see 0.3.0 was published yesterday). Because of this, I don't think its gradle failing to sync silently. not sure where to look next.
Issue/question raised on StackOverflow.
I believe the issue is with Kable's internals not properly handling the premature closure in the callbackFlow
.
As per the callbackFlow
KDoc, we should be catching exceptions when trying to emit within the callbackFlow
(emphasis shown in green):
fun flowFrom(api: CallbackBasedApi): Flow<T> = callbackFlow {
val callback = object : Callback { // Implementation of some callback interface
override fun onNextValue(value: T) {
// To avoid blocking you can configure channel capacity using
// either buffer(Channel.CONFLATED) or buffer(Channel.UNLIMITED) to avoid overfill
+ try {
sendBlocking(value)
+ } catch (e: Exception) {
+ // Handle exception from the channel: failure in flow or premature closing
+ }
}
override fun onApiError(cause: Throwable) {
cancel(CancellationException("API Error", cause))
}
override fun onCompleted() = channel.close()
}
api.register(callback)
/*
* Suspends until either 'onCompleted'/'onApiError' from the callback is invoked
* or flow collector is cancelled (e.g. by 'take(1)' or because a collector's coroutine was cancelled).
* In both cases, callback will be properly unregistered.
*/
awaitClose { api.unregister(callback) }
}
There are some BLE devices which does not have client characteristic config descriptor. It's a violation of BLE specification and the bug of the firmware of peripheral, but there is no possibility to change it for mobile developers. For a workaround, it's good to have the possibility to subscribe on notification by only setCharacteristicNotification(characteristic, boolean) call without writing to the descriptor.
Library RXAndroidBle has this possibility (NotificationSetupMode.Compact) and earlier I have used it but unfortunately, it's not friendly for kotlin implementation.
We're frequently polling peripheral.read()
on a background coroutine and noticed that manually disabling bluetooth will not always throw an exception
An exception will be thrown if the read occurs after the OS disconnects from the device:
2021-05-18 11:39:35.032 14297-15286/com.v I/FTRepository: start read
2021-05-18 11:39:35.471 14297-15286/com.v I/FTRepository: finish read
2021-05-18 11:39:35.481 14297-15286/com.v I/FTRepository: start read
2021-05-18 11:39:35.650 14297-15286/com.v I/FTRepository: finish read
2021-05-18 11:39:35.654 14297-16277/com.v D/BluetoothGatt: onBluetoothStateChange: up=false
2021-05-18 11:39:35.654 14297-16277/com.v D/BluetoothGatt: Bluetooth is turned off, disconnect all client connections
2021-05-18 11:39:35.654 14297-16277/com.v D/BluetoothGatt: close()
2021-05-18 11:39:35.658 14297-16277/com.v D/BluetoothGatt: unregisterApp() - mClientIf=15
2021-05-18 11:39:35.661 14297-15286/com.v I/FTRepository: start read
2021-05-18 11:39:35.662 14297-15286/com.v E/FTRepository: com.juul.kable.GattRequestRejectedException
And exception will not be thrown if the read occurs before the OS disconnects:
2021-05-18 11:11:36.786 11390-12135/com.v I/FTRepository: start read
2021-05-18 11:11:36.956 11390-12135/com.v I/FTRepository: finish read
2021-05-18 11:11:36.966 11390-12135/com.v I/FTRepository: start read
2021-05-18 11:11:37.135 11390-12135/com.v I/FTRepository: finish read
2021-05-18 11:11:37.146 11390-12135/com.v I/FTRepository: start read <-- hangs forever
2021-05-18 11:11:37.251 11390-12401/com.v D/BluetoothGatt: onBluetoothStateChange: up=false
2021-05-18 11:11:37.251 11390-12401/com.v D/BluetoothGatt: Bluetooth is turned off, disconnect all client connections
2021-05-18 11:11:37.251 11390-12401/com.v D/BluetoothGatt: close()
2021-05-18 11:11:37.254 11390-12401/com.v D/BluetoothGatt: unregisterApp() - mClientIf=15
2021-05-18 11:11:37.255 11390-12200/com.v I/BluetoothAdapter: onBluetoothStateChange: up=false
As was seen in #99 (comment), an unprovoked (prior to writing to the config descriptor to enable Notify/Indicate) characteristic may trigger a "characteristic change" event.
Code used in test branch that should be brought in to main code:
Observers.kt
fun acquire(characteristic: Characteristic) = flow {
try {
characteristicChanges
.onSubscription {
peripheral.suspendUntilReady()
if (observers.incrementAndGet(characteristic) == 1) {
peripheral.startObservation(characteristic)
}
}
.collect {
if (it.characteristic.characteristicUuid == characteristic.characteristicUuid &&
it.characteristic.serviceUuid == characteristic.serviceUuid
) emit(it.data)
}
} finally {
if (observers.decrementAndGet(characteristic) < 1) {
try {
peripheral.stopObservation(characteristic)
} catch (e: NotReadyException) {
// Silently ignore as it is assumed that failure is due to connection drop, in which case Android
// will clear the notifications.
Log.d(TAG, "Stop notification failure ignored.")
}
}
}
}
Are there any milestone to support iOS target?
Hello guys.
I get the IllegalStateException when trying read a characteristic inside peripheral state collect.
peripheral.state.collect { state ->
if (state == State.Connected) {
// delay(200); // this line fix the exception
peripheral.read(characteristic)
}
}
This behavioural is normal because the services are discovered after peripheral connection is established. The commented line fixes the exception because the delay permits the peripheral to discover the services before the reading is performed.
Maybe we can change this behaviour. My suggestion is: update the peripheral state to CONNECTED after the services discovery is performed or creating a new peripheral state SERVICES_DISCOVERED.
What do you think?
Is it or will it be possible in the future to use Kabel to adertise the device as the peripheral? e.g. a mobile app that can communicate with other instances of itself on multiple phones.
I guess this is because these functions throw IOException so AS thinks these methods are blocking.
What is the preffered way to fix this? Should I suppress this warning?
@Suppress("BlockingMethodInNonBlockingContext") // This is how I can suppress
private suspend fun readData() {
Timber.tag(TAG).i("Reading data from the device")
val time = device.read(CHARACTERISTIC_TIME) // Here is the warning
Timber.tag(TAG).i("Time from device ${time.toHex()}")
val capture = device.read(CHARACTERISTIC_CAPTURE)
Timber.tag(TAG).i("Capturing state ${capture.toHex()}")
val config = device.read(CHARACTERISTIC_OXIMETER_CONFIG)
Timber.tag(TAG).i("Oximeter config ${config.toHex()}")
}
WithResponse
WithoutResponse
Currently stubbed:
kable/core/src/jsMain/kotlin/Peripheral.kt
Lines 57 to 59 in 09619e5
I am getting the following exception when trying to change the MTU
com.juul.kable.GattStatusException: OnMtuChanged(mtu=23, status=GATT_INVALID_PDU(4))
at com.juul.kable.AndroidPeripheral.requestMtu(Peripheral.kt:403)
at com.juul.kable.AndroidPeripheral$requestMtu$1.invokeSuspend(Unknown Source:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
This is how I am trying to do that
override suspend fun connect() {
peripheral.connect()
try {
(peripheral as AndroidPeripheral).requestMtu(23)
} catch (e: GattStatusException) {
Log.d(TAG, e.stackTraceToString())
}
isReady.emit(State.Connected)
}
Am I doing somthing wrong?
Dokka HTML documentation can currently be generated locally via ./gradlew dokkaHtml
(outputs to core/build/dokka/html/core/index.html
) but we should have this available online.
Should be configured to publish automatically via CI.
The BluetoothGattCallback() method for onMtuChanged needs to be serialized with the execute method when the libraries consumer calls readMtu(), but unlike other onBlankEvent() methods, this one can also be called in response to a peripheral device requesting an MTU change.
Note the UPPERCASED part of the documentation:
/**
* Callback indicating the MTU for a given device connection has changed.
*
* This callback is triggered in response to the requestMtu function,
* OR IN RESPONSE TO A CONNECTION EVENT.
*
* @param gatt GATT client invoked {@link BluetoothGatt#requestMtu}
* @param mtu The new MTU size
* @param status {@link BluetoothGatt#GATT_SUCCESS} if the MTU has been changed successfully
*/
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
}
I forked the old able library and fixed it there by exposing MTU Updates through a separate stateflow that is exposed publicly and is used for retrieving a serialized response to readMtu() with the execute method.
The Scanner
and Advertisement
for JS target are currently stubbed, but should be implemented before release.
kable/core/src/jsMain/kotlin/Scanner.kt
Lines 7 to 16 in 09619e5
kable/core/src/jsMain/kotlin/Advertisement.kt
Lines 3 to 10 in 09619e5
@twyatt yes I could not reproduce this crash but I have found another crash when does connect/disconnect a lot of times in raw.
kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed
Unfortunately that there are not other lines of the stack trace.
It happens just after call method connect.
kotlinx.coroutines.flow.internal.AbortFlowException: Flow was aborted, no more elements needed 2020-12-29 12:39:45.027 6461-6511/com.xx D/BluetoothGatt: connect() - device: 43:95:9A:31:C7:6C, auto: false 2020-12-29 12:39:45.028 6461-6511/com.xx D/BluetoothAdapter: isSecureModeEnabled 2020-12-29 12:39:45.029 6461-6511/com.xx D/BluetoothGatt: registerApp() 2020-12-29 12:39:45.030 6461-6511/com.xx D/BluetoothGatt: registerApp() - UUID=1228b816-0f28-48f8-a49d-ef4c752b9027 2020-12-29 12:39:45.034 6461-6476/com.xx D/BluetoothGatt: onClientRegistered() - status=0 clientIf=10
Originally posted by @kostya29-strikersoft in #44 (comment)
In order to build Apple-based targets (and publish) we'll need to utilize a macOS GitHub hosted runner.
I'm trying to read some data of a UART Service that provides a "write without response" and a "notify" characteristic. I can write to it fine using cable but Kable but get the following error when using observer.collect { ... }
to subscribe to notifications.
java.util.NoSuchElementException: Collection contains no element matching the predicate.
The characteristic works fine to read using other apps and sends out data about once a second. As probably will be apparent from my sample code below I'm fairly new to Kotlin but as far as I understand after reading up and digging through the source code I can't see why this error is produced.
I looked at issue 38 but since I go through the steps to get the characteristic I get this error even if it is discovered. Or did i misunderstand the cause of that issue?
The characteristic and observer objects looks fine to me, what I can notice is that the
Sample code
bluetoothScope.launch {
val peripheral: Peripheral? = getPeripheral() // Get Peripheral using Scanner()
if (peripheral != null) {
peripheral.connect()
val characteristic = peripheral.services
?.first { service -> service.serviceUuid == UUID.fromString(UART_SERVICE)}
?.characteristics?.first { characteristic ->
characteristic.characteristicUuid == UUID.fromString(RX_CHARACTERISTIC_UUID)
}
if (characteristic != null ) {
val observer = peripheral.observe(characteristic)
try {
observer.collect { data ->
Log.d(TAG, data)
}
} catch (e: NoSuchElementException) {
Log.d(TAG, "Error: $e")
}
}
peripheral.state?.collect { state ->
when (state) {
State.Connected -> {
Log.d(TAG, "Connected")
}
else -> Log.d(TAG, "Not Connected")
}
}
}
}
}
Scan result
DiscoveredService(serviceUuid=6e400001-b5a3-f393-e0a9-e50e24dcca9e,
characteristics=[
DiscoveredCharacteristic(
serviceUuid=6e400001-b5a3-f393-e0a9-e50e24dcca9e,
characteristicUuid=6e400003-b5a3-f393-e0a9-e50e24dcca9e,
descriptors=[]),
DiscoveredCharacteristic(
serviceUuid=6e400001-b5a3-f393-e0a9-e50e24dcca9e,
characteristicUuid=6e400002-b5a3-f393-e0a9-e50e24dcca9e,
descriptors=[])]
)]
Steps to reproduce:
Start connecting to peripheral by method peripheral.connect and quickly stop Bluetooth server on the peripheral side.
Stack trace:
com.juul.kable.ConnectionLostException at com.juul.kable.PeripheralKt$suspendUntilConnected$2.invokeSuspend(Peripheral.kt:220) at com.juul.kable.PeripheralKt$suspendUntilConnected$2.invoke(Unknown Source:10) at kotlinx.coroutines.flow.FlowKt__TransformKt$onEach$$inlined$unsafeTransform$1$2.emit(Collect.kt:134) at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:348) at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:12) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106) at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678) at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
In the early development stages logging is essential for debugging, but prior to a major release it should be removed (or substantially reduced).
Perhaps an opt-in debug-mode option to enable logging?
println
statementsI have a device to which I cannot connect using Kable however BLE Scanner connects fine to it.
It crashes on connect
call function with an exception.
With some other device my code works fine.
Also as a side note the stack trace does not show the place where the exception raisen until I added try/catch. Is there a good solution for this?
2021-03-30 13:13:26.364 29915-29915/com.myprojectdev I/HomeViewModel: Connecting to device 00:02:72:C8:73:3E, attempt 1
2021-03-30 13:13:26.385 29915-29915/com.myprojectdev D/BluetoothGatt: connect() - device: 00:02:72:C8:73:3E, auto: false
2021-03-30 13:13:26.385 29915-29915/com.myprojectdev D/BluetoothGatt: registerApp()
2021-03-30 13:13:26.385 29915-29915/com.myprojectdev D/BluetoothGatt: registerApp() - UUID=97455b6f-bfc2-412d-8926-d3899622a2bd
2021-03-30 13:13:26.391 29915-29940/com.myprojectdev D/BluetoothGatt: onClientRegistered() - status=0 clientIf=8
2021-03-30 13:13:26.408 29915-29915/com.myprojectdev I/HomeViewModel: Changed connect state to com.juul.kable.State$Connecting@5ab9aab
2021-03-30 13:13:26.408 29915-29915/com.myprojectdev D/HomeViewModel: Unhandled state com.juul.kable.State$Connecting@5ab9aab
2021-03-30 13:13:26.519 29915-29915/com.myprojectdev I/Choreographer: Skipped 33 frames! The application may be doing too much work on its main thread.
2021-03-30 13:13:26.681 29915-30063/com.myprojectdev I/OpenGLRenderer: Davey! duration=725ms; Flags=1, IntendedVsync=22043658834306, Vsync=22044208834317, OldestInputEvent=9223372036854775807, NewestInputEvent=0, HandleInputStart=22044223732544, AnimationStart=22044223836236, PerformTraversalsStart=22044224763467, DrawStart=22044330493852, SyncQueued=22044341897390, SyncStart=22044342854775, IssueDrawCommandsStart=22044343481467, SwapBuffers=22044383623775, FrameCompleted=22044385422467, DequeueBufferDuration=640000, QueueBufferDuration=670000,
2021-03-30 13:13:34.093 29915-29940/com.myprojectdev D/BluetoothGatt: onClientConnectionState() - status=133 clientIf=8 device=00:02:72:C8:73:3E
2021-03-30 13:13:34.094 29915-30113/com.myprojectdev D/BluetoothGatt: close()
2021-03-30 13:13:34.094 29915-30113/com.myprojectdev D/BluetoothGatt: unregisterApp() - mClientIf=8
2021-03-30 13:13:34.097 29915-30113/com.myprojectdev D/BluetoothGatt: close()
2021-03-30 13:13:34.097 29915-30113/com.myprojectdev D/BluetoothGatt: unregisterApp() - mClientIf=0
2021-03-30 13:13:34.105 29915-29915/com.myprojectdev D/BluetoothGatt: close()
2021-03-30 13:13:34.105 29915-29915/com.myprojectdev D/BluetoothGatt: unregisterApp() - mClientIf=0
2021-03-30 13:13:34.111 29915-29915/com.myprojectdev E/HomeViewModel: Failed to connect device
com.juul.kable.ConnectionLostException
at com.juul.kable.PeripheralKt$suspendUntilConnected$2.invokeSuspend(Peripheral.kt:281)
at com.juul.kable.PeripheralKt$suspendUntilConnected$2.invoke(Unknown Source:10)
at kotlinx.coroutines.flow.FlowKt__TransformKt$onEach$$inlined$unsafeTransform$1$2.emit(Collect.kt:134)
at kotlinx.coroutines.flow.StateFlowImpl.collect(StateFlow.kt:348)
at kotlinx.coroutines.flow.StateFlowImpl$collect$1.invokeSuspend(Unknown Source:12)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7386)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:980)
2021-03-30 13:13:34.112 29915-29915/com.myprojectdev I/HomeViewModel: Changed connect state to Disconnected(status=Unknown(status=133))
2021-03-30 13:13:34.113 29915-29915/com.myprojectdev D/HomeViewModel: Waiting 500 ms to reconnect...
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.