JuulLabs / kable

Kotlin Asynchronous Bluetooth Low-Energy
https://juullabs.github.io/kable
Apache License 2.0
788 stars 76 forks source link

Multiple ProfileServiceNotBound errors when trying to write to a Peripheral #714

Open PavlosTze opened 1 month ago

PavlosTze commented 1 month ago

Hello,

I have the following code:

    private suspend fun enableDescriptorNotification(descriptor: Descriptor) {
        try {
            peripheral.write(descriptor, ENABLE_NOTIFICATION_VALUE)
        } catch (e: GattWriteException) {
            Timber.w(e, "[enable descriptor notification]: GattWriteException - Result: ${e.result}")
        }
    }

And for some weeks already this code is producing a lot of ProfileServiceNotBound types of exception, the log is the following:

Non-fatal Exception: f7.N: Write failed: ProfileServiceNotBound at com.juul.kable.BluetoothGattKt.writeCharacteristicOrThrow(BluetoothGatt.kt:58) at com.juul.kable.BluetoothDeviceAndroidPeripheral$write$$inlined$execute$1.invokeSuspend(Connection.kt:126) at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:104) at android.os.Handler.handleCallback(Handler.java:959) at android.os.Handler.dispatchMessage(Handler.java:100) at android.os.Looper.loopOnce(Looper.java:232) at android.os.Looper.loop(Looper.java:317) at android.os.HandlerThread.run(HandlerThread.java:85)

As you can see from these 3 screenshots issues started in early June: εικόνα εικόνα εικόνα

In case that helps, our app producing those is Open Source.

Based on what I read in your writeResultFrom and in the Android-related code:

    /**
     * Error code indicating that the profile service is not bound. You can bind a profile service
     * by calling {@link BluetoothAdapter#getProfileProxy}.
     */
    public static final int ERROR_PROFILE_SERVICE_NOT_BOUND = 9;

Any chance you can catch this error without letting it fail? What is this related to? As there aren't any logs producing it in the past and we've been using Kable for around 2 years already.

twyatt commented 1 month ago

Can you provide a breakdown of the failure rate per Android version? I'm wondering if this is the result of behavior changes in newer versions of Android?

PavlosTze commented 1 month ago

Can you provide a breakdown of the failure rate per Android version? I'm wondering if this is the result of behavior changes in newer versions of Android?

Sure, here it is: εικόνα

twyatt commented 1 month ago

Thanks for the info. We're seeing this in our apps as well. I'll try to allocate some time soon to investigate.

twyatt commented 1 month ago

@PavlosTze I'm currently investigating this issue. Do you happen to know what version of Kable you first saw this issue in?

PavlosTze commented 1 month ago

@twyatt It started with 0.31.1.

twyatt commented 1 month ago

Thanks, I believe I've tracked down the cause:

I believe it was introduced in #648, when a bug was fixed (whereas Kable would silently ignore failures to write to a characteristic). In #648, that behavior was fixed (to now throw an exception if the write failed).

Unfortunately, some might be relying on the bug (that was fixed) where it didn't propagate a failure when the write fails.

I'm trying to determine what the best API/behavior might be for this situation. I'll follow up soon.

PavlosTze commented 1 month ago

Sure, I just want to mention that this is a non-crash failure. Which means that if it failed before in the same way, the only thing happening now is that it's been showing up in the logs, not that big of a deal.

I'm wondering if we can solve it in the background based on what it says here?

    /**
     * Error code indicating that the profile service is not bound. You can bind a profile service
     * by calling {@link BluetoothAdapter#getProfileProxy}.
     */
    public static final int ERROR_PROFILE_SERVICE_NOT_BOUND = 9;

Is it possible to bind a profile service if we get across this failure?

twyatt commented 1 month ago

I suspect that the code comment you referenced is misleading in this case.

I believe the failure happens when a write occurs after a connection is lost / disposed; because the connection was disposed, the underlying Android GATT object no longer has a "profile service" (essentially a reference to the underlying Android BLE system) and so propagates that failure. I believe it is unrelated to getProfileProxy; perhaps Android just re-used this failure case for multiple failure conditions. 🤷

I think in most cases it is safe to ignore the failure (in the cases where it happens because the connection was recently disposed). The case where it might be problematic is where BluetoothAdapter#getProfileProxy is being utilized (I suspect this isn't common though), in which case the failure could indicate other issues (aside from just a connection being lost).

The reason it is important for Kable to propagate the failure though, is the consumer should be aware that the requested write operation did not complete as requested.