Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

write suspends indefinitely on subsequent calls when WithoutResponse is used #157

Closed
renatosmf opened this issue Sep 1, 2021 · 38 comments · Fixed by #312
Closed

write suspends indefinitely on subsequent calls when WithoutResponse is used #157

renatosmf opened this issue Sep 1, 2021 · 38 comments · Fixed by #312
Labels
apple bug Something isn't working

Comments

@renatosmf
Copy link

I’m attempting to send a write command sequentially. This approach works over android Bluetooth API, but in iOS I’m receiving the notification that the characteristics change only for the first command.

For the iOS write command, I'm using the write without response. In another way, If I set to write with the response, I'm receiving the error for the write permission.

@twyatt
Copy link
Member

twyatt commented Sep 1, 2021

Thanks for the report!

Can you provide logs using the Kable logging mechanism? With log level either Data or Events, please.

@renatosmf
Copy link
Author

renatosmf commented Sep 1, 2021

Thanks for the report!

Can you provide logs using the Kable logging mechanism? With log level either Data or Events, please.

@twyatt Do you want Scanner log level or Peripheral?

@renatosmf
Copy link
Author

Thanks for the report!

Can you provide logs using the Kable logging mechanism? With log level either Data or Events, please.

@twyatt I got the Datalog over Peripheral, but I needed to censure some information.

FYI: I have retry treatment for reconnection. So when I get a response with disconnected status I try to reconnect again and the library executes 2 times the last write event that had no response.

kable-data.log

@twyatt
Copy link
Member

twyatt commented Sep 1, 2021

I’m attempting to send a write command sequentially

If I understand correctly, you have a characteristic you're writing to and responses come back on a dedicated characteristic (via characteristic change notifications), right?

Looking at a snippet of your logs, is this where it is problematic?

2021-09-01 18:23:44.530598-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:44.531612-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:44.857803-0300 iosApp[23165:8564787] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 54 3A 4F 4B 3A 56 34 2E [CENSURED]
2021-09-01 18:23:44.887689-0300 iosApp[23165:8564785] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 43 4B 45 44 3B 30 32 30 [CENSURED]
2021-09-01 18:23:44.888221-0300 iosApp[23165:8564785] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 24

Whereas you wrote to 0000ffe0-0000-1000-8000-[CENSURED] twice and expected the response to come back twice on the corresponding peripheral.observe(<service: FFE0, characteristic: FFE1>)?

According to the log snippet, the OS wrote your two requests and also provided the two characteristic changes but Kable didn't have the 2 change events come through the Peripheral.observe flow? Is that the issue at hand?


If possible, it would be helpful if you could provide a simple code snippet of how you're doing the "write for response" operation.

@renatosmf
Copy link
Author

renatosmf commented Sep 3, 2021

@twyatt

If I understand correctly, you have a characteristic you're writing to and responses come back on a dedicated characteristic (via characteristic change notifications), right?

Yes.

Looking at a snippet of your logs, is this where it is problematic?

So... I've 2 problems.

1 - I'm not able to write on characteristic more than one time per connection. So, If I need to write on characteristic for the second time, I need to disconnect and connect again to achieve it.

2 - When I attempt to write on the characteristic for a second time and after waiting some time for a characteristic value change notification, I receive the ConnectionLostException, in which I have a re-connect implementation to handle with this event calling the connect( ) function of peripheral. Which results in twice write event as we can see in the log.

Whereas you wrote to 0000ffe0-0000-1000-8000-[CENSURED] twice and expected the response to come back twice on the corresponding peripheral.observe(<service: FFE0, characteristic: FFE1>)?

Yes

According to the log snippet, the OS wrote your two requests and also provided the two characteristic changes but Kable didn't have the 2 change events come through the Peripheral.observe flow? Is that the issue at hand?

No.. When debugging my peripheral, the second writing is not performed. I can only write once after connecting to the peripheral.

Look below for my implementation

    private val peripheralFlow: Flow<Peripheral> by lazy {
        newPeripheralFlow()
    }
    private val scanFlow: Flow<Advertisement> by lazy {
        newScanFlow()
    }
    private val characteristic: Characteristic = characteristicOf(bleConfig.serviceID, bleConfig.characteristicID)
    private val peripheral = AtomicReference<Peripheral?>(null)
    private val advertisement = AtomicReference<Advertisement?>(null)
    private val jobs = ArrayList<Job>()
    private var userAskedDisconnection: Boolean = false

   fun scanDevices() {
        scanFlow.onEach(carInterface::didDiscover).launchIn(ioScope).addJobToList()
    }

   fun connectTo(device: Advertisement) {
        advertisement.set(device)
        peripheral.set(getPeripheralFrom(device))
        connectDevice()
    }

    private fun getPeripheralFrom(advertisement: Advertisement) = ioScope.peripheral(advertisement) {
        logging {
            level = Logging.Level.Data // or Data
        }
    }

    private fun connectDevice() {
        interface.onStateChanged(CONNECTING)
        flow {
            emit(
                peripheral.get()?.apply {
                    stateObserver()
                    connect()
                }
            )
        }.retryWhen { cause, attempt ->
            Napier.w(
                "Attemping to connect with: ${getId()} Attemp: $attempt of $TRY_AGAIN",
                throwable = cause
            )
            delay(TRY_AGAIN_DELAY)
            attempt < TRY_AGAIN
        }.catch { error ->
            Napier.e("Failure to attemp to connect with ${getId()}", error)
        }.launchIn(ioScope).addJobToList()
    }

    private fun disconnectDevice() {
        flow {
            emit(peripheral.get()?.disconnect())
        }.catch { error ->
            Napier.e("Failure to attemp to disconnect from ${getId()}", error)
        }.launchIn(ioScope)
    }

   fun stopService() {
        Napier.d("Stoping service")
        userAskedDisconnection = true
        jobs.forEach {
            it.cancel()
        }
        disconnectDevice()
    }

    private fun Job.addJobToList(): Job {
        jobs.add(this)
        return this
    }

    private fun Peripheral.stateObserver() {
        characteristicObserver()

        state.onEach { state ->
            when (state) {
                is State.Connecting -> {
                    Napier.i("Connecting with ${getId()}")
                    interface.onStateChanged(CONNECTING)
                }
                is State.Connected -> {
                    Napier.i("Connected with ${getId()}")
                    interface.onStateChanged(CONNECTED)
                }
                is State.Disconnecting -> {
                    Napier.i("Disconnecting from ${getId()}")
                    interface.onStateChanged(DISCONNECTING)
                }
                is State.Disconnected -> {
                    Napier.i("Device disconnected ${getId()}")
                    interface.onStateChanged(DISCONNECTED)
                    if (!userAskedDisconnection) {
                        userAskedDisconnection = false
                        interface.onStateChanged(CONNECTING)
                        connect()
                    }
                }
            }
        }.retryWhen { cause, attempt ->
            Napier.w(
                "Retry to connect with: ${getId()} attemp: $attempt of $TRY_AGAIN",
                throwable = cause
            )
            delay(TRY_AGAIN_DELAY)
            attempt < TRY_AGAIN
        }.catch { error ->
            Napier.e("Connection device failure: ", error)
        }.launchIn(ioScope).addJobToList()
    }

    private fun Peripheral.characteristicObserver() {
        observe(characteristic).onEach {
            characteristicObserver.emit(it)
        }.catch { error ->
            Napier.e("Characteristic observer failure: ", error)
            interface.onStateChanged(DEVICE_UNKNOWN)
            disconnectDevice()
        }.launchIn(ioScope).addJobToList()
    }

    fun getStatus() {
        peripheral.get()?.sendCommand(bleCarCommunicator.getCommandStatus(), characteristic)
            ?.launchIn(ioScope)?.addJobToList()
    }

    fun unlockDevice() {
        peripheral.get()?.sendCommand(bleCarCommunicator.getCommandUnlock(), characteristic)
            ?.launchIn(ioScope)?.addJobToList()
    }

    fun lockDevice() {
        peripheral.get()?.sendCommand(bleCommunicator.getCommandLock(), characteristic)
            ?.launchIn(ioScope)?.addJobToList()
    }

    private fun Peripheral.sendCommand(
        command: ByteArray,
        characteristic: Characteristic
    ): Flow<Unit?> {
        return flow {
            emit(
                write(
                    characteristic,
                    command,
                    WriteType.IOT
                )
            )
        }.onStart {
            Napier.i("Write command sent...")
            interface.onStateChanged(SENDING_DATA)
        }.retryWhen { cause, attempt ->
            Napier.e(cause.toString())
            when (cause) {
                is ConnectionLostException -> {
                    Napier.w("Connection Exception: ", cause)
                    connect()
                }
                else -> {
                    Napier.e("Write command failure: ", cause)
                }
            }
            delay(TRY_AGAIN_DELAY)
            attempt <= TRY_AGAIN
        }.catch { cause ->
            Napier.e(cause.toString())
        }
    }

    private fun newPeripheralFlow() = scanFlow.filter {
        Napier.i("Finding...")
        bleConfig.deviceUuid?.let { uuid ->
            it.uuids.contains(uuidFrom(uuid))
        } ?: bleConfig.deviceName?.let { name ->
            it.name == name
        } ?: throw Exception("Uuid not defined")
    }.take(1).map {
        advertisement.set(it)
        getPeripheralFrom(it)
    }.onEach {
        Napier.i("Device found!: $it")
        interface.onStateChanged(DEVICE_FOUND)
    }

    private fun newScanFlow() = Scanner().advertisements.onStart {
        interface.onStateChanged(DISCOVERING_DEVICES)
    }.filter {
        !it.name.isNullOrEmpty()
    }.retryWhen { cause, attempt ->
        Napier.w(
            "Retry.. $attempt de $TRY_AGAIN",
            cause
        )
        delay(TRY_AGAIN_DELAY)
        attempt <= TRY_AGAIN
    }.catch { error ->
        Napier.e("Exception", error)
    }

@renatosmf
Copy link
Author

@twyatt did you find the problem?

@twyatt
Copy link
Member

twyatt commented Sep 7, 2021

@twyatt did you find the problem?

Sorry, I have not had time to look at this yet.
Really appreciate providing the reproducing code.
I will try to find some time soon to look it over.

@twyatt
Copy link
Member

twyatt commented Sep 8, 2021

@renatosmf until I have time to look over your code sample, have you explored the sample app?

It may provide some helpful patterns for peripheral I/O. I do notice you're leaning a lot on flows, which are great, but having too many of the launches can make it hard to manage the sequential execution of various operations.

@renatosmf
Copy link
Author

It may provide some helpful patterns for peripheral I/O. I do notice you're leaning a lot on flows, which are great, but having too many of the launches can make it hard to manage the sequential execution of various operations.

Hi @twyatt ,

I already looked at the sample app and your example is considering only Mac OS hardware and running IO events over the main thread, which is different for the iOS context. I can not execute the write event over the main thread because freezes the UI thread of the App. In my context, I run each command only after the end message received over the characteristic notification, therefore I do not execute simultaneous write events in parallel.

@twyatt
Copy link
Member

twyatt commented Sep 9, 2021

According to the log snippet, the OS wrote your two requests and also provided the two characteristic changes but Kable didn't have the 2 change events come through the Peripheral.observe flow? Is that the issue at hand?

No.. When debugging my peripheral, the second writing is not performed. I can only write once after connecting to the peripheral.

Looking over your logs, it does appear that it wrote twice? Can you clarify what in the logs is indicating otherwise?

2021-09-01 18:23:34.441472-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:34.443711-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:34.777389-0300 iosApp[23165:8564787] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 54 3A 4F 4B 3A 56 34 2E [CENSURED]
2021-09-01 18:23:34.808114-0300 iosApp[23165:8564785] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 43 4B 45 44 3B 30 32 30 [CENSURED]

In my context, I run each command only after the end message received over the characteristic notification, therefore I do not execute simultaneous write events in parallel.

Looks like you've implemented a form of queueing? This isn't entirely necessary as Kable won't execute write commands in parallel even if write is called multiple times concurrently. It is backed by a Mutex which is fair (executes only 1 request at a time, in the order they are received).


If you were to implement code similar to:

private val inbound = peripheral.observe(...).shareIn(scope, started = Eagerly)
private val outbound = characteristicOf(...)

suspend fun Peripheral.writeForResponse(data: ByteArray): ByteArray =
    inbound.onSubscription { write(outbound, data) }.first()

suspend fun Peripheral.connectAndWriteTwice() {
    connect()
    val response1 = writeForResponse(request1)
    val response2 = writeForResponse(request2)
}

...does it perform the expected writes and see the responses?

@renatosmf
Copy link
Author

renatosmf commented Sep 17, 2021

According to the log snippet, the OS wrote your two requests and also provided the two characteristic changes but Kable didn't have the 2 change events come through the Peripheral.observe flow? Is that the issue at hand?

No.. When debugging my peripheral, the second writing is not performed. I can only write once after connecting to the peripheral.

Looking over your logs, it does appear that it wrote twice? Can you clarify what in the logs is indicating otherwise?

2021-09-01 18:23:34.441472-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:34.443711-0300 iosApp[23165:8562722] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF write
  service: 0000ffe0-0000-1000-8000-[CENSURED]
  characteristic: 0000ffe1-0000-1000-8000-[CENSURED]
  writeType: WithoutResponse
  data: 54 53 3A 39 39 31 38 39 39 32 30 2D 33 39 30 [CENSURED]
2021-09-01 18:23:34.777389-0300 iosApp[23165:8564787] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 54 3A 4F 4B 3A 56 34 2E [CENSURED]
2021-09-01 18:23:34.808114-0300 iosApp[23165:8564785] D/Kable: 7BCADA26-F146-A594-B32A-1DE58DBCE7AF didUpdateValueForCharacteristic
  service: FFE0
  characteristic: FFE1
  data: 43 4B 45 44 3B 30 32 30 [CENSURED]

In my context, I run each command only after the end message received over the characteristic notification, therefore I do not execute simultaneous write events in parallel.

Looks like you've implemented a form of queueing? This isn't entirely necessary as Kable won't execute write commands in parallel even if write is called multiple times concurrently. It is backed by a Mutex which is fair (executes only 1 request at a time, in the order they are received).

If you were to implement code similar to:

private val inbound = peripheral.observe(...).shareIn(scope, started = Eagerly)
private val outbound = characteristicOf(...)

suspend fun Peripheral.writeForResponse(data: ByteArray): ByteArray =
    inbound.onSubscription { write(outbound, data) }.first()

suspend fun Peripheral.connectAndWriteTwice() {
    connect()
    val response1 = writeForResponse(request1)
    val response2 = writeForResponse(request2)
}

...does it perform the expected writes and see the responses?

Hi @twyatt,

I tried your suggestion but it didn't work.
A particular behavior of my peripheral is that after establishing the connection and hearing the characteristic value changes, the peripheral always sends an "AT" message to show that it is On.
What happens is that when I write to the feature for the first time, I am notified that the value of the feature has changed, and following the end of the received message it sends the message "AT" again as a signal that it is ready to receive a new message. But when I try to write in the characteristic for the second time, it looks like it doesn't run and I don't get any change to the characteristic value other than the "AT" message.

Ps.: This behavior is only happening in the iOS environment, on Android, it works fine.

@renatosmf
Copy link
Author

@twyatt
I was debugging my peripheral and looking into the communication log and I saw that the second write event was not sent to my peripheral

@renatosmf
Copy link
Author

Hi @twyatt,

We have found the fix for this issue. Please check out the Pull Request opened here

@cedrickcooke cedrickcooke linked a pull request Sep 22, 2021 that will close this issue
@renatosmf
Copy link
Author

@twyatt
I rushed into the possible solution to the problem. You were right about changing the value of the semaphore, it would simply be going to work with multithreading.
What I identified when performing more in-depth tests is that the semaphore is not being released for the next task in the queue. I can't say why it doesn't return to the available state and running next to the queue, but the fact that the problem is running at this location in the context of iOS

@renatosmf
Copy link
Author

@twyatt

Debugging the library, I found a different behavior for Connection.execute method.
I noticed that when connection.execute <DidWriteValueForCharacteristic> is called by the Peripheral.write, the execution stops at this line val response = delegate.response.receive() and never returns to continue the process. So the next execution attempt of connection.execute <DidWriteValueForCharacteristic> is never executed.
Could you check this out?

suspend inline fun <T> execute(
        action: () -> Unit,
    ): T = semaphore.withPermit {
        action.invoke()
        val response = delegate.response.receive()
        val error = response.error
        if (error != null) throw IOException(error.description, cause = null)
        response as T
    }

@twyatt
Copy link
Member

twyatt commented Sep 25, 2021

I rushed into the possible solution to the problem.

No worries at all. I really appreciate you taking the time to try and debug/solve the issue.

Could you check this out?

What you are describing can happen if Core Bluetooth doesn't call one of the PeripheralDelegate calls when excepted, specifically the following:

override fun peripheral(
peripheral: CBPeripheral,
didWriteValueForCharacteristic: CBCharacteristic,
error: NSError?
) {
logger.debug(error) {
message = "${peripheral.identifier} didWriteValueForCharacteristic"
detail(didWriteValueForCharacteristic)
}
_response.sendBlocking(
DidWriteValueForCharacteristic(
peripheral.identifier,
didWriteValueForCharacteristic,
error
)
)
}

It would be helpful if you provided the Kable logs that lead up to the stalling of the Peripheral.write call.
For debugging this particular issue, using logLevel of Events should be sufficient (so that you don't have to go through the trouble of censoring your data — since it will be absent when using Events log level).

@renatosmf
Copy link
Author

I rushed into the possible solution to the problem.

No worries at all. I really appreciate you taking the time to try and debug/solve the issue.

Could you check this out?

What you are describing can happen if Core Bluetooth doesn't call one of the PeripheralDelegate calls when excepted, specifically the following:

override fun peripheral(
peripheral: CBPeripheral,
didWriteValueForCharacteristic: CBCharacteristic,
error: NSError?
) {
logger.debug(error) {
message = "${peripheral.identifier} didWriteValueForCharacteristic"
detail(didWriteValueForCharacteristic)
}
_response.sendBlocking(
DidWriteValueForCharacteristic(
peripheral.identifier,
didWriteValueForCharacteristic,
error
)
)
}

It would be helpful if you provided the Kable logs that lead up to the stalling of the Peripheral.write call.
For debugging this particular issue, using logLevel of Events should be sufficient (so that you don't have to go through the trouble of censoring your data — since it will be absent when using Events log level).

@twyatt The problem isn't in PerfipheralDelegate and yes in the Peripheral class.

@Throws(CancellationException::class, IOException::class, NotReadyException::class)
public suspend fun write(
characteristic: Characteristic,
data: NSData,
writeType: WriteType,
) {
logger.debug {
message = "write"
detail(characteristic)
detail(writeType)
detail(data)
}
val cbCharacteristic = cbCharacteristicFrom(characteristic)
connection.execute<DidWriteValueForCharacteristic> {
centralManager.write(cbPeripheral, data, cbCharacteristic, writeType.cbWriteType)
}
}

Line 264 is never executed after the first time. I believe that the connection.execute continues to suspend and never releases after the first execution.

I had put an extra log with the prefix IOS - out and IOS - inner to easily see the problem.

@Throws(CancellationException::class, IOException::class, NotReadyException::class)
    public suspend fun write(
        characteristic: Characteristic,
        data: NSData,
        writeType: WriteType,
    ) {
        logger.debug {
            message = "write"
            detail(characteristic)
            detail(writeType)
            detail(data)
        }

        val cbCharacteristic = cbCharacteristicFrom(characteristic)
        logger.debug {
            message = "IOS - out connection.execute"
        }

        connection.execute<DidWriteValueForCharacteristic> {
            logger.debug {
                message = "IOS - inner connection.execute"
            }
            centralManager.write(cbPeripheral, data, cbCharacteristic, writeType.cbWriteType)
        }
    }

Look at the output log with the logs.
kable-data2.log

@twyatt
Copy link
Member

twyatt commented Sep 26, 2021

Wow, great debugging.

Looked into the expected behavior of Core Bluetooth and my assumptions that it behaves similar to Android's APIs was completely wrong.

In other words, Kable needs to treat WithoutResponse differently than WithResponse (which it currently does not).

According to Core Bluetooth documentation (emphasis mine):

When you call this method to write the value of a characteristic, the peripheral calls the peripheral(_:didWriteValueFor:error:) method of its delegate object only if you specified the write type as CBCharacteristicWriteType.withResponse. The response you receive through the peripheral(_:didWriteValueFor:error:) delegate method indicates whether the write was successful; if the write failed, it details the cause of the failure in an error.

On the other hand, if you specify the write type as CBCharacteristicWriteType.withoutResponse, Core Bluetooth attempts to write the value but doesn’t guarantee success. If the write doesn’t succeed in this case, you aren’t notified and you don’t receive an error indicating the cause of the failure.

Currently, Kable is expecting (waiting for) a response on the didWriteValueFor callback, but as documented, this will never happen for WithoutResponse.

According to PunchThrough's The Ultimate Guide to Apple’s Core Bluetooth, Kable needs to use canSendWriteWithoutResponse and peripheralIsReady methods when using WithoutResponse.

I'll try to find some time soon to make this change.

Thanks again for the investigation and tracking down the issue!

@twyatt twyatt added the bug Something isn't working label Sep 26, 2021
@twyatt twyatt changed the title Sequential write characteristic command not work on iOS write suspends indefinitely on subsequent calls when WithoutResponse is used Sep 26, 2021
@twyatt
Copy link
Member

twyatt commented Sep 27, 2021

@renatosmf what iOS version are you seeing the issue on?

According to The Ultimate Guide to Apple’s Core Bluetooth, the callback Kable needs to listen for (canSendWriteWithoutResponse) was added in iOS 11.

If you're testing pre-iOS 11 then it would make sense the behavior you are seeing where it stalls on subsequent writes. Although if you're testing on iOS 11 or newer, then I would've thought that Kable would (incorrectly) try to cast to DidWriteValueForCharacteristic:

connection.execute<DidWriteValueForCharacteristic> {
centralManager.write(cbPeripheral, data, cbCharacteristic, writeType.cbWriteType)
}

Knowing what iOS version you're testing on will help me decide the best approach to fix the issue. Thanks!

@renatosmf
Copy link
Author

@renatosmf what iOS version are you seeing the issue on?

According to The Ultimate Guide to Apple’s Core Bluetooth, the callback Kable needs to listen for (canSendWriteWithoutResponse) was added in iOS 11.

If you're testing pre-iOS 11 then it would make sense the behavior you are seeing where it stalls on subsequent writes. Although if you're testing on iOS 11 or newer, then I would've thought that Kable would (incorrectly) try to cast to DidWriteValueForCharacteristic:

connection.execute<DidWriteValueForCharacteristic> {
centralManager.write(cbPeripheral, data, cbCharacteristic, writeType.cbWriteType)
}

Knowing what iOS version you're testing on will help me decide the best approach to fix the issue. Thanks!

@twyatt I'm in iOS 13 or newer

@twyatt
Copy link
Member

twyatt commented Sep 27, 2021

@twyatt I'm in iOS 13 or newer

Thanks, though that means that the canSendWriteWithoutResponse should've been called but it was not (as was suggested by The Ultimate Guide to Apple’s Core Bluetooth).

Going to have to do some more research of what the correct way to approach this is. Without any callback (from iOS) or other means of knowing when iOS is ready to process more messages makes this complete guesswork as to when the next write operation should be allowed.

There must be a standard way to do this, otherwise it would be very error-prone to perform writes without response on iOS.

@renatosmf
Copy link
Author

renatosmf commented Sep 28, 2021

@twyatt I believe the kable library should act as a broadcast, simply passing the data to the Bluetooth Core. As the Core Bluetooth itself doesn't guarantee message delivery and doesn't have a delivery success fallback, I don't see much that can be done in this case. You should probably assume that every message you send should release the channel for new messages

@twyatt
Copy link
Member

twyatt commented Sep 28, 2021

@twyatt I believe the kable library should act as a broadcast, simply passing the data to the Bluetooth Core. As the Core Bluetooth itself doesn't guarantee message delivery and doesn't have a delivery success fallback, I don't see much that can be done in this case. You should probably assume that every message you send should release the channel for new messages

It may come to that, but it would totally surprise me that Apple didn't provide a way to not overwhelm their BLE stack. I know there are only so many messages that can be queued, it would only make sense for Apple to provide an API to know when that queue is full and you should stop flooding it. Otherwise I imagine it must either drop requests (or crash).

To your point, yes, Kable could simply allow writes to go through without any checks, I just worry library consumers will use Kable with the expectation that that function won't allow the system to be overwhelmed, since that guarantee can be provided for every other supported OS/operation.

I'll do a bit more research before resorting to lifting the semaphore for iOS write operations (without response).

@renatosmf
Copy link
Author

renatosmf commented Oct 4, 2021

Hi @twyatt, did you found some solution to handle with write events withoutResponse for iOS 11 or earlier?

@twyatt
Copy link
Member

twyatt commented Oct 4, 2021

Sorry, been swamped with some other internal work projects. I hope to get some time allocated for Kable updates soon (hopefully in the coming week or two).

If you have the opportunity to try and find how "without response" is usually implemented on iOS, it'd be great to see how others solve this problem.

@renatosmf
Copy link
Author

hi @twyatt,

Do you have some news about this fix?

@twyatt
Copy link
Member

twyatt commented Oct 20, 2021

Another project I'm working on should be winding down next week and give me more time to focus on Kable.

At that time I'll start researching how other libraries / iOS implementations handle writing without response.

If you happen to find any examples of code that does consecutive writes without response, that would be incredibly helpful.

@renatosmf
Copy link
Author

Another project I'm working on should be winding down next week and give me more time to focus on Kable.

At that time I'll start researching how other libraries / iOS implementations handle writing without response.

If you happen to find any examples of code that does consecutive writes without response, that would be incredibly helpful.

Hi @twyatt , Unfortunately I don't found any solution to cover this scenario for iOS 11 and earlier.

@renatosmf
Copy link
Author

renatosmf commented Nov 26, 2021

Hi @twyatt,
Don't you think it would be good to handle the scenario for versions after iOS 11 and in the future try to apply an improvement that solves this situation for version 11 and earlier?

@twyatt
Copy link
Member

twyatt commented Dec 4, 2021

Hi @twyatt, Don't you think it would be good to handle the scenario for versions after iOS 11 and in the future try to apply an improvement that solves this situation for version 11 and earlier?

Unfortunately, the API that was supposed to exist to allow proper usage in iOS >11 didn't work; at least not in the logs you provided. Although according to https://stackoverflow.com/a/52527853 it should exist/work. I'll try to find some time to test out of API mentioned in the Stackoverflow answer.

@renatosmf
Copy link
Author

Hi @twyatt, Don't you think it would be good to handle the scenario for versions after iOS 11 and in the future try to apply an improvement that solves this situation for version 11 and earlier?

Unfortunately, the API that was supposed to exist to allow proper usage in iOS >11 didn't work; at least not in the logs you provided. Although according to https://stackoverflow.com/a/52527853 it should exist/work. I'll try to find some time to test out of API mentioned in the Stackoverflow answer.

@twyatt,
Do you have some news about this fix?

@twyatt
Copy link
Member

twyatt commented Jan 7, 2022

I should be able to look into this some more after #193 and #238 are merged.

@renatosmf
Copy link
Author

Nice... Do you have an estimated time for that?

I should be able to look into this some more after #193 and #238 are merged.

@twyatt
Copy link
Member

twyatt commented Feb 4, 2022

Other work has wrapped up, so I'll hopefully be able to find some time to look into this again soon. Although I can't really offer an estimate as to when this might be fixed.

@twyatt twyatt added the apple label Feb 4, 2022
@renatosmf
Copy link
Author

Hi @twyatt, have you some news?

@twyatt

This comment was marked as outdated.

@twyatt
Copy link
Member

twyatt commented Apr 10, 2022

Apologies it took so long to dedicate enough time to this issue; but I think I have a fix for this (#312).

@twyatt
Copy link
Member

twyatt commented Apr 13, 2022

You should be able to perform writes "without response" (should be fixed) in 0.16.1.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
apple bug Something isn't working
Projects
None yet
2 participants