Giter Club home page Giter Club logo

kable's People

Contributors

azlekov avatar burnhamd avatar cedrickcooke avatar davidtaylor-juul avatar degill avatar djweber avatar ebabel avatar fabiocornelli avatar francismariano avatar jahor avatar joharei avatar juliusmh avatar juul-mobile-bot avatar kemalfaust avatar khebrati avatar konpach avatar kvelasco avatar lammertw avatar mmaleiter avatar mmeisel avatar mtrewartha avatar pavlostze avatar phoenix7351 avatar quantumrand avatar robvs avatar rocketraman avatar sdonn3 avatar twyatt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kable's Issues

Set transport flag to BluetoothDevice.TRANSPORT_LE by default

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 :)

iOS API misuse warning

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 .

Throw appropriate exception when performing I/O on unknown characteristic

Currently, when attempting to write/read a characteristic that was not discovered then it will throw a NoSuchElementException (as thrown by first):

val characteristics = services
.first { it.serviceUuid == characteristic.serviceUuid }
.characteristics
return characteristics
.first { it.characteristicUuid == characteristic.characteristicUuid }
.bluetoothRemoteGATTCharacteristic

Instead, we should throw an exception that clearly indicates what service or characteristic wasn't found.

Ability to provide filters to the scanner

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.

Indicate does not work

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.

Cache internal mapped services on MacOS target

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).

Retrieve peripheral on iOS/macOS from previous identifier peripheral

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

AndroidPeripheral.requestMtu() throws OutOfOrderGattCallbackException

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)

Kable library is not imported to KMM project

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

com.android.tools.build:gradle:7.0.0-alpha09 problem

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

Identifier in common code

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.

Advertisement flow should throw an error if location permission is missing

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?

Feature Request: Expose peripheral name

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.

Support Node.js?

Consider supporting Node.js.

As pointed out in #24 (comment), the current implementation likely does not work outside of the browser.

Peripheral.observe has not been notified with a characteristic without descriptor

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.

Services with the same UUIDs

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?

Web BLE requires lowercase UUIDs

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.

To expose device's bond state

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
        
    .......
}

Prevent concurrent I/O operations on JavaScript

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:

Android

): T = lock.withLock {

Apple

): T = semaphore.withPermit {

Peripheral.observe function throws NotReadyException on iOS

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.

NullPointerException when trying to read data

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")
        }
    }

Add support for enable indication

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

Add support for different build types

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?

Provide requestPeripheral function for all platforms?

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).

Using Kable in an Android Project

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.

Scanner throws exception from advertisements Flow on premature closure

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) }
      }

Add notification setup mode for devices that do not contain client characteristic config descriptor

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.

[Android] peripheral.read() hangs after manually disabling bluetooth

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

Early characteristic changes can be missed

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.")
                }
            }
        }
    }

IllegalStateException: Services have not been discovered for Peripheral

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?

Kable to support advertising as a peripheral

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.

Reads and writes give me "inappropriate blocking method call" lint warning in Android Studio

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()}")
    }

MTU change issue (GATT_INVALID_PDU)

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?

Publish Dokka KDoc HTML to GitHub Pages

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.

Peripheral initiated MTU Changes can cause OutOfOrderGattCallbackException

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.

Scanner support for JS target

The Scanner and Advertisement for JS target are currently stubbed, but should be implemented before release.

/**
* Only available on Chrome 79+ with "Experimental Web Platform features" enabled via:
* chrome://flags/#enable-experimental-web-platform-features
*
* See also: [Chrome Platform Status: Web Bluetooth Scanning](https://www.chromestatus.com/feature/5346724402954240)
*/
public class JsScanner : Scanner {
public override val peripherals: Flow<Advertisement> = TODO()
}

public actual class Advertisement {
public actual val name: String?
get() = TODO("Not yet implemented")
public actual val rssi: Int
get() = TODO("Not yet implemented")
}

AbortFlowException crash on repeated connect/disconnect calls

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

Android - observer.collect throws NoSuchElementException

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=[])]
)]

Force disconnecting peripheral causes crash by ConnectionLostException

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)

Remove and/or reduce logging

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?

  • Remove all println statements

Cannot connect to a device (status code 133)

I 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...

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.