3

I have created BLE sender class for the sending large ByteArray via Bluetooth LE The logic of the send process following:

  1. Write descriptor to enable notification on characteristics that sends data
  2. Notify peripheral about data sending process via writing to corresponding characteristics (Size of data: chunk size: number of chunks)
  3. Wait for peripheral to notify for chunk 0 to send on data sending characteristics
  4. On notification received start sending the first chunk 1000 byte by blocks of 20 bytes (BLE restriction) where each block contains block number and 18 bytes of data, after 1000 bytes sent, send block of checksum for the data sent
  5. Peripheral verify the data by the checksum and notify descriptor for the next chunk

My Question is: is there any better approach? I have found that writing characteristics multiple times requires some delay of at least 20 milliseconds. Is there any way to avoid this?

Changed the implementation instead of 20 millis, I'm waiting for a callback onCharacteristicWrite as Emil advised. and Also changed the prepare method to decrease calculation time between 18bytes blocks sends:

class BluetoothLEDataSender(
            val characteristicForSending: BluetoothGattCharacteristic,
            val characteristicForNotifyDataSend: BluetoothGattCharacteristic,
            private val config: BluetoothLESenderConfiguration = BluetoothLESenderConfiguration(),
            val  bluetoothLeService: WeakReference<BluetoothLeService>) : HandlerThread("BluetoothLEDataSender") {

    data class BluetoothLESenderConfiguration(val sendingIntervalMillis: Long = 20L, val chunkSize: Int = 1000, val retryForFailureInSeconds: Long = 3)
       private val  toaster by lazy { Toast.makeText(bluetoothLeService.get()!!,"",Toast.LENGTH_SHORT) }

    companion object {

        val ACTION_DATA_SEND_FINISHED = "somatix.com.bleplays.ACTION_DATA_SEND_FINISHED"
        val ACTION_DATA_SEND_FAILED = "somatix.com.bleplays.ACTION_DATA_SEND_FAILED"
    }

    lateinit var  dataToSend: List<BlocksQueue>
    val messageHandler by lazy { SenderHandler()}

    var currentIndex = 0

    public fun notifyDataState(receivedChecksum: String) {
        val msg = Message()
        msg.arg1 = receivedChecksum.toInt()
        messageHandler.sendMessage(msg)
    }
    inner class BlocksQueue(val initialCapacity:Int):ArrayBlockingQueue<ByteArray>(initialCapacity)
   inner class  BlockSendingTask:Runnable{
      override fun run() {
        executeOnUiThread({ toaster.setText("Executing block: $currentIndex")
        toaster.show()})
        sendNext()
      }
   }

        public fun sendMessage(messageByteArray: ByteArray) {
            start()
             dataToSend = prepareSending(messageByteArray)
            bluetoothLeService.get()?.setEnableNotification(characteristicForSending,true)
            val descriptor = characteristicForSending.getDescriptor(DESCRIPTOR_CONFIG_UUID)
            descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
            bluetoothLeService.get()?.writeDescriptor(descriptor)
            characteristicForNotifyDataSend.value = "${messageByteArray.size}:${config.chunkSize}:${dataToSend.size}".toByteArray()
            toaster.setText(String(characteristicForNotifyDataSend.value))
            toaster.show()
            messageHandler.postDelayed({bluetoothLeService.get()?.writeCharacteristic(characteristicForNotifyDataSend)}, config.sendingIntervalMillis)

        }


     private fun prepareSending(messageByteArray: ByteArray): ArrayList<BlocksQueue> {
       with(config)
        {
            var chunksNumber = messageByteArray.size / config.chunkSize
            chunksNumber = if (messageByteArray.size == chunksNumber * config.chunkSize) chunksNumber else chunksNumber + 1
            val chunksArray = ArrayList<BlocksQueue>()


           (0 until chunksNumber).mapTo(chunksArray) {
              val start = it * chunkSize
              val end = if ((start + chunkSize) > messageByteArray.size) messageByteArray.size else start + chunkSize
              val sliceArray = messageByteArray.sliceArray(start until end)
              listOfCheckSums.add(sliceArray.checkSum())
              var capacity = sliceArray.size / 18
              capacity = if(sliceArray.size - capacity*18 == 0) capacity else capacity + 1
              //Add place for checksum
              val queue = BlocksQueue(capacity+1)
              for(i in 0 until  capacity){
                val  start1 = i *18
                val end1 = if((start1 + 18)<sliceArray.size) start1 +18 else sliceArray.size
                queue.add(sliceArray.sliceArray(start1 until end1))
            }
            queue.add(sliceArray.checkSum().toByteArray())
            queue
         }
        return chunksArray
    }
}


    fun  sendNext(){
        val currentChunk = dataToSend.get(currentIndex)
        val peek = currentChunk.poll()
        if(peek != null)
        {
            if(currentChunk.initialCapacity > currentBlock+1)
            {
                val indexByteArray = if(currentBlock>9) "$currentBlock".toByteArray() else "0${currentBlock}".toByteArray()
               characteristicForSending.value = indexByteArray + peek
            }
            else{
               characteristicForSending.value = peek
            }

   bluetoothLeService.get()?.writeCharacteristic(characteristicForSending)
              currentBlock++
            }
            else
            {
                Log.i(TAG, "Finished chunk $currentIndex")
                currentBlock = 0
             }

         }


        private val TAG= "BluetoothLeService"

        @SuppressLint("HandlerLeak")
        inner class SenderHandler:Handler(looper){
            private var failureCheck:FailureCheck? = null
            override fun handleMessage(msg: Message) {
                super.handleMessage(msg)
                    currentIndex = msg.arg1
                    if(currentIndex < dataToSend.size)
                    {
                        if (currentIndex!= 0 &&  failureCheck != null)
                        {
                            removeCallbacks(failureCheck)
                        }
                        failureCheck = FailureCheck(currentIndex)
                        post(BlockSendingTask())
                        postDelayed(failureCheck,TimeUnit.MILLISECONDS.convert(config.retryForFailureInSeconds,TimeUnit.SECONDS))


                     }
                    else {
                        if (currentIndex!= 0 &&  failureCheck != null)
                        {
                            removeCallbacks(failureCheck)
                        }
                        val intent= Intent(ACTION_DATA_SEND_FINISHED)
                        bluetoothLeService.get()?.sendBroadcast(intent)
                    }

            }
            private inner class FailureCheck(val index:Int):Runnable{
                override fun run() {
                    if (index==currentIndex){
                        val intent= Intent(ACTION_DATA_SEND_FAILED)
                        bluetoothLeService.get()?.sendBroadcast(intent)
                    }
                }

            }
        }


    }
libinm
  • 71
  • 1
  • 9
  • Can you change the MTU? You can increase it up to 512 bytes, if the peripheral supports it? – Christopher Jan 16 '18 at 10:39
  • Another option might be using a BLE long write: https://devzone.nordicsemi.com/question/34572/ble-long-write/ – Christopher Jan 16 '18 at 10:40
  • I have tried to change MTU to the maximum as you advised and it looks that this approach makes the process less stable, and probably I will not be able to do so in future of my project. Another thing I have noticed that although I have changed MTU to 512 and received 512 in onMTUChanged callback the actual size that I could transfer was 509 bytes So I had to use additional characteristic with notify descriptor to discover the real MTU – libinm Jan 17 '18 at 12:30
  • Yes, I encountered the same stability issues, when setting the MTU to 512. But I was not sure if my implementation was buggy. :) The max. size of 509 is due to additional 3 header bytes. – Christopher Jan 17 '18 at 13:25
  • But you do not have this header on 20 bytes messages – libinm Jan 17 '18 at 14:23
  • You have it, because the default MTU is 23 bytes. https://punchthrough.com/blog/posts/maximizing-ble-throughput-part-2-use-larger-att-mtu – Christopher Jan 17 '18 at 14:55

2 Answers2

3

What's this thing about waiting 20 ms? The preferred way to pump data using characteristic writes is to first use "Write Without Response" (https://developer.android.com/reference/android/bluetooth/BluetoothGattCharacteristic.html#WRITE_TYPE_NO_RESPONSE), then perform a Write, then wait for the onCharacteristicWrite callback and then immediately perform the next Write. You need to wait for the onCharacteristicWrite callback since the API doesn't allow you to have multiple pending commands/requests at a time.

Emil
  • 16,784
  • 2
  • 41
  • 52
  • I'm using the WRITE_TYPE_NO_RESPONCE type of characteristics, you are right I'm have missed the onCharacterristicWrite callback, however, looks, I have tried this approach and it looks very not stable files larger than 2K~4k are stack and transfer failed. While when I just using this delay (which I also not happy about) I'm sending files 300K without any issue. – libinm Jan 15 '18 at 09:16
  • What Andorid device are you using? And does the device disconnect or stay connected? Is your observation that the onCharacteristicWrite just sometimes never gets called? – Emil Jan 15 '18 at 13:51
  • I'm using Nexus 5 as central and Xiaomi A1 as a peripheral, and devices were just one next to each other, anyway I have changed an implementation in order to make as fewer calculations as possible between blocks in chunk, and increased chunk timeout window from 3 seconds to 6 and it looks like it started to work now, Thanks – libinm Jan 16 '18 at 07:47
1

I work with @libinm (OP) on the same project and would like to refer to @Emil comment above -

  • @Emil mentioned that "API doesn't allow you to have multiple pending commands/requests at a time", I'm wondering if there is any kind of message buffering that enables to increase throughput (by sending multiple messages on a single BLE connection event). I know that much lighter (embedded) BLE stacks enable buffering of 4/6 messages (TI/Nordic stacks respectively) per connection event.
  • How will the Android BLE central respond to multiple message notifications per single connection event (sent by peripheral)? Are there any limitations?
eyalasko
  • 75
  • 6
  • 1
    If you want to refer to comment - leave a comment. If you want to ask question - add a new question. Have a nice day ! :) – Jakub Licznerski Jan 16 '18 at 11:21
  • For 1 - https://stackoverflow.com/questions/43741849/oncharacteristicwrite-and-onnotificationsent-are-being-called-too-fast-how-to/43744888#43744888. When you use Write Without Response, you will pump data as fast as the controller accepts. For 2 - no there are no limitations except memory (you should process the notifications at least as fast as they are received). – Emil Jan 17 '18 at 01:24