Search code examples
androidimagebluetooth-lowenergytransfer

Transfer Image throug BLE in android


I'm transfering an image of 1 mb using the following code. The image gets transferred successfully if a thread delay is implemented between each packets. If the thread delay is not set all the packets are sent from BluetoothGattServer but the BluetoothGattCallback does not receive all the packets.

Can anyone guide in sending the packets without the thread delay

Implement thread between each packets

private void sendingContinuePacket(BluetoothGattCharacteristic characteristic,
                                   byte[] CHARACTERS) {
    boolean isComplete = false;
    runOnUiThread(() -> {
        tv_status.setText("Sending Data...!!");
        startTime = SystemClock.uptimeMillis();
        customHandler.postDelayed(updateTimerThread, 0);
    });

    // Check the data length is large how many times with Default Data (BLE)

    int times = CHARACTERS.length / DEFAULT_BYTES_IN_CONTINUE_PACKET;
    totalPackets = times;
    Log.i("", "CHARACTERS.length() " + CHARACTERS.length);

    byte[] packetNoByte;
    byte[] sending_continue_hex = new byte[DEFAULT_BYTES_IN_CONTINUE_PACKET];
    for (int time = 0; time <= times; time++) {

        final int remainingTime = time;
        if (!hasDisconnected) {
            this.runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    mRelativeLayout.setVisibility(View.VISIBLE);
                    if (totalPackets != 0) {
                        showProgress(totalPackets, remainingTime);
                    }
                }
            });
        } else {
            runOnUiThread(() -> {
                mProgressBar.setProgress(0);
                tv_progress.setText(0 + "%");
                tv_timer.setText("00:00:00");
                tv_imageSize.setText("");
                tv_status.setText("");
                Toast.makeText(PeripheralRoleActivity.this, "Something went wrong, Please Try again", Toast.LENGTH_SHORT).show();
                customHandler.removeCallbacks(updateTimerThread);
            });
            return;
        }
        int a;
        int b;

        /**
         * @param THREAD_SLEEP_TIME_FOR_NOTIFICATION
         * this delay is placed to give a small pause while sending the data packe
         * */
        try {
            Thread.sleep(Constants.THREAD_SLEEP_TIME_FOR_NOTIFICATION);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        sentPacket = sentPacket + 1;
        byte[] packetArray = Utils.getUtilsClass().toByteArray(sentPacket);
        packetNoByte = Arrays.copyOf(packetArray, packetArray.length);

        if (time == times) {
            Log.i("", "LAST PACKET ");
            int character_length = CHARACTERS.length
                    - DEFAULT_BYTES_IN_CONTINUE_PACKET * times;
            byte[] sending_last_hex = new byte[character_length];
            a = (sending_continue_hex.length) * time;
            b = a + character_length;
            if(b-a ==0){
                return;
            }
            sending_last_hex = Arrays.copyOfRange(CHARACTERS, a, b);

            byte[] last_packet =
                    new byte[packetNoByte.length + character_length];
            System.arraycopy(packetNoByte, 0, last_packet,
                    0, packetNoByte.length);
            System.arraycopy(sending_last_hex, 0, last_packet,
                    packetNoByte.length, sending_last_hex.length);


            Log.d("Sending packets", Arrays.toString(last_packet));
            // Set value for characteristic
            characteristic.setValue(last_packet);
            notifyCharacteristicChanged();
            isComplete = true;
            customHandler.removeCallbacks(updateTimerThread);
            currentDateTimeString = DateFormat.getDateTimeInstance().format(new Date());
            Log.d("Collection", "End Time: " + currentDateTimeString);
            Utils.getUtilsClass().sendNotification(getApplicationContext(), "Data Transfer", "Transfer Complete");


        } else {

            Log.i("", "CONTINUE PACKET ");

            a = ((sending_continue_hex.length) * time);
            b = a + DEFAULT_BYTES_IN_CONTINUE_PACKET;

            sending_continue_hex = Arrays.copyOfRange(CHARACTERS, a, b);


            byte[] sending_continue_packet =
                    new byte[packetNoByte.length + sending_continue_hex.length];
            System.arraycopy(packetNoByte, 0, sending_continue_packet,
                    0, packetNoByte.length);
            System.arraycopy(sending_continue_hex, 0, sending_continue_packet,
                    packetNoByte.length, sending_continue_hex.length);


            Log.d("data transfer a", String.valueOf(a));
            Log.d("data transfer b", String.valueOf(b));
            Log.d("data trans bytes", String.valueOf(sending_continue_hex.length));
            if(output == null){
                output = new ByteArrayOutputStream();
            }
            try {
                if {
                    characteristic.setValue(sending_continue_packet);
                    Log.d("Sending packets", Arrays.toString(sending_continue_packet));
                    notifyCharacteristicChanged();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        Log.d("Data byte", "times " + time);

        if (isComplete) {
            characteristic.setValue("Completed");
            notifyCharacteristicChanged();
        }
        runOnUiThread(() -> tv_status.setText("Data sent!!"));
    }
}

Updated Code

//the following function is used break the image byte [] into packets and store it in an arraylist

private void breakPackets(byte[] CHARACTERS) {
        // Check the data length is large how many times with Default Data (BLE)

        int times = CHARACTERS.length / DEFAULT_BYTES_IN_CONTINUE_PACKET;
        totalPackets = times;
        packetList = new ArrayList<>();
        sendingPacket = 0;
        Log.i("", "CHARACTERS.length() " + CHARACTERS.length);


        byte[] sending_continue_hex = new byte[DEFAULT_BYTES_IN_CONTINUE_PACKET];
        for (int time = 0; time <= times; time++) {


            int a;
            int b;

            if (time == times) {
                Log.i("", "LAST PACKET ");
                int character_length = CHARACTERS.length
                        - DEFAULT_BYTES_IN_CONTINUE_PACKET * times;
                byte[] sending_last_hex = new byte[character_length];
                a = (sending_continue_hex.length) * time;
                b = a + character_length;

                sending_last_hex = Arrays.copyOfRange(CHARACTERS, a, b);
                //packetList is an ArrayList<byte[]>
                packetList.add(sending_last_hex);
                startSendingPackets(sendingPacket);
            } else {
                a = (sending_continue_hex.length) * time;
                b = a + DEFAULT_BYTES_IN_CONTINUE_PACKET;
                sending_continue_hex = Arrays.copyOfRange(CHARACTERS, a, b);

                packetList.add(sending_continue_hex);
            }
            Log.d("Data byte", "times " + time);

        }
    }

    //the following function is used to set the byte[] from the arraylist to the characteristics and then notify the characteristics
     private void startSendingPackets(int packet) {
        isCommand = false;
        mSampleCharacteristic.setValue(packetList.get(packet));
        notifyCharacteristicChanged();
        Log.i("packeting", "Sending  ------------> " + packet);
    }


    /*************************************************/



     @Override
        public void onNotificationSent(BluetoothDevice device, int status) {
            super.onNotificationSent(device, status);
            //check if status is success
            if (status == BluetoothGatt.GATT_SUCCESS) {
            //if status is not successful isExecutable is false and the else loop is executed to resend the same packet that has failed
                if (isExecutable) {
//                    Log.i("packeting", "Sent  ------------> " + sendingPacket);
                    sendingPacket = sendingPacket + 1;
                    int size = packetList.size();
                    if (sendingPacket <= size-1) {

                        startSendingPackets(sendingPacket);

                        Log.d(MainActivity.TAG, "Notification sent. Status: " + status + " sending packet no --" + sendingPacket);
                    } else {
                        sendCommand("Completed");
                    }
                } else {
                    startSendingPackets(sendingPacket);
                    isExecutable = true;
                    Log.d(MainActivity.TAG, "Notification sent. Status: " + status + " sending packet no --" + sendingPacket);
                }
            }else{
            //if status is not successful
                isExecutable = false;

                Log.d(MainActivity.TAG, "Notification sent. fail Status: " + status );
            }


        }

Solution

  • As can be read in the documentation at https://developer.android.com/reference/android/bluetooth/BluetoothGattServerCallback.html#onNotificationSent(android.bluetooth.BluetoothDevice,%20int):

    When multiple notifications are to be sent, an application must wait for this callback to be received before sending additional notifications.

    This means after you have called notifyCharacteristicChanged, you cannot call notifyCharacteristicChanged again until the callback onNotificationSent has been received. So you need to remove your for-loop and refactor your code to follow the API rules.

    The reason for this is to get flow control. If you just push new packets faster than the BLE link's throughput, the internal buffers get full and packet loss will occur. That's why a delay might seem to work, but it's not a robust solution so that's why you should wait for the onNotificationSent callback since that means the BLE stack is ready to accept new packets.