安卓:通过BLE发送大于20字节的数据

62

我能通过连接外部BLE设备发送最多20字节的数据。如果我要发送超过20字节的数据,该怎么办?我读到了我们必须要么分段数据,要么将特征值拆分为所需的部分。如果我假设我的数据是32字节,你能告诉我需要对代码进行哪些更改才能使其工作吗?以下是我代码中的必要片段:

public boolean send(byte[] data) {
    if (mBluetoothGatt == null || mBluetoothGattService == null) {
        Log.w(TAG, "BluetoothGatt not initialized");
        return false;
    }

    BluetoothGattCharacteristic characteristic =
            mBluetoothGattService.getCharacteristic(UUID_SEND);

    if (characteristic == null) {
        Log.w(TAG, "Send characteristic not found");
        return false;
    }

    characteristic.setValue(data);
    characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);
    return mBluetoothGatt.writeCharacteristic(characteristic);
}

这是我用来发送数据的代码。在下面的onclick事件中使用了“send”函数。

sendValueButton = (Button) findViewById(R.id.sendValue);
    sendValueButton.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            String text = dataEdit.getText().toString();                           
            yableeService.send(text.getBytes());
        }
    });
当字符串text大于20字节时,只有前20字节被接收。如何纠正?
为了测试发送多个特征,我尝试了这个:
sendValueButton = (Button) findViewById(R.id.sendValue);
sendValueButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        String text = "Test1";                           
        yableeService.send(text.getBytes());

        text = "Test2";                           
        yableeService.send(text.getBytes());

        text = "Test3";                           
        yableeService.send(text.getBytes());
    }
});

但我只收到了“Test3”,也就是最后一个特征,我犯了什么错误?我是BLE的新手,请忽略任何愚蠢的问题。

编辑:

在接受答案之后,对于以后查看此内容的任何人。

两种方法可以完成这个过程。 1. 将您的数据拆分并像所选答案一样使用循环写入。 2. 拆分您的数据并使用回调来写入,即onCharacterisitcWrite()。如果在写入过程中出现任何错误,这将使您免受错误的影响。

但是,在写入之间使用Thread.sleep(200)非常重要,如果您只是写入而不等待来自固件的响应。这将确保您的所有数据都到达。如果没有sleep,我总是得到最后一个数据包。如果您注意到已接受的答案,他也在写入之间使用了sleep


你尝试过将“data”分成20字节的块,并在循环中为每个块创建和发送多个“BluetoothGattCharacteristic”吗? - Smutje
我之前尝试的是,在onclick事件中将文本更改了三次(使用一些预定义的常量),然后分别使用yableeService.send(text.getBytes());三次。但是只有第一个特征被发送出去了,之后的内容都没有发送成功。如果我做错了,请告诉我应该怎么做? - Ankit Aggarwal
@Smutje:请查看我的编辑答案。还有什么我可以帮忙的吗? - Ankit Aggarwal
请问您能否分享完整的代码? - User6006
您可以请求MTU更新,然后一次性发送整个数据包。我相信目前最多可以发送512字节(在一个数据包中)。我相信下面有2个或更多的答案提到了这一点。 - user5803705
10个回答

31

更新:请查看更多答案以了解其他实现期望的方法。

在2014年,BLE允许您传输最大20个字节。

如果您想发送超过20个字节,您应该定义一个包含您想要的数据包数量的byte[]数组。

以下示例适用于发送少于160个字符(160个字节)的情况。

p/s:请根据需要编辑以下内容。不要完全按照我的方式进行操作。

实际上,在使用BLE时,移动端和固件端需要设置密钥(例如0x03...)来定义两侧之间的连接门。

思路是:

  • 当我们继续传输数据包时,不是最后一个。门是byte[1] = 0x01

  • 如果我们发送最后一个,则门为byte[1] = 0x00

数据结构(20个字节):

1 - 字节1 - 定义门ID:例如消息门ID byte[0] = 0x03

2 - 字节2 - 定义识别:是最后一个数据包0x00还是继续发送数据包0x01

3 - 字节3(在消除字节1字节2后应为18个字节)- 在此处附加消息内容。

请在阅读下面的代码之前理解我的逻辑。

以下是发送多个数据包的消息示例,每个数据包都是大小为20字节的数组。

private void sendMessage(BluetoothGattCharacteristic characteristic, String CHARACTERS){
        byte[] initial_packet = new byte[3];
        /**
         * Indicate byte
         */
        initial_packet[0] = BLE.INITIAL_MESSAGE_PACKET;
        if (Long.valueOf(
                String.valueOf(CHARACTERS.length() + initial_packet.length))
                > BLE.DEFAULT_BYTES_VIA_BLE) {
            sendingContinuePacket(characteristic, initial_packet, CHARACTERS);
        } else {
            sendingLastPacket(characteristic, initial_packet, CHARACTERS);
        }
    }

private void sendingContinuePacket(BluetoothGattCharacteristic characteristic,
            byte[] initial_packet, String CHARACTERS){
        /**
         * TODO If data length > Default data can sent via BLE : 20 bytes
         */
        // Check the data length is large how many times with Default Data (BLE)
        int times = Byte.valueOf(String.valueOf(
                CHARACTERS.length() / BLE.DEFAULT_BYTES_IN_CONTINUE_PACKET));

        Log.i(TAG, "CHARACTERS.length() " + CHARACTERS.length());
        Log.i(TAG, "times " + times);
        
        // TODO
        // 100 : Success
        // 101 : Error
        byte[] sending_continue_hex = new byte[BLE.DEFAULT_BYTES_IN_CONTINUE_PACKET];
        for (int time = 0; time <= times; time++) {
            /**
             * Wait second before sending continue packet 
             */
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (time == times) {
                Log.i(TAG, "LAST PACKET ");
                
                /**
                 * If you do not have enough characters to send continue packet,
                 * This is the last packet that will be sent to the band
                 */

                /**
                 * Packet length byte :
                 */
                /**
                 * Length of last packet
                 */
                int character_length = CHARACTERS.length()
                        - BLE.DEFAULT_BYTES_IN_CONTINUE_PACKET*times;
                
                initial_packet[1] = Byte.valueOf(String.valueOf(character_length
                        + BLE.INITIAL_MESSAGE_PACKET_LENGTH));
                initial_packet[2] = BLE.SENDING_LAST_PACKET;
                
                Log.i(TAG, "character_length " + character_length);

                /**
                 * Message
                 */
                // Hex file
                byte[] sending_last_hex = new byte[character_length];
                
                // Hex file : Get next bytes
                for (int i = 0; i < sending_last_hex.length; i++) {
                    sending_last_hex[i] = 
                            CHARACTERS.getBytes()[sending_continue_hex.length*time + i];
                }

                // Merge byte[]
                byte[] last_packet = 
                        new byte[character_length + BLE.INITIAL_MESSAGE_PACKET_LENGTH];
                System.arraycopy(initial_packet, 0, last_packet,
                        0, initial_packet.length);
                System.arraycopy(sending_last_hex, 0, last_packet, 
                        initial_packet.length, sending_last_hex.length);

                // Set value for characteristic
                characteristic.setValue(last_packet);
            } else {
                Log.i(TAG, "CONTINUE PACKET ");
                /**
                 * If you have enough characters to send continue packet,
                 * This is the continue packet that will be sent to the band
                 */
                /**
                 * Packet length byte
                 */
                int character_length = sending_continue_hex.length;
                
                /**
                 * TODO Default Length : 20 Bytes
                 */
                initial_packet[1] = Byte.valueOf(String.valueOf(
                        character_length + BLE.INITIAL_MESSAGE_PACKET_LENGTH));
                
                /**
                 * If sent data length > 20 bytes (Default : BLE allow send 20 bytes one time)
                 * -> set 01 : continue sending next packet
                 * else or if after sent until data length < 20 bytes
                 * -> set 00 : last packet
                 */
                initial_packet[2] = BLE.SENDING_CONTINUE_PACKET;
                /**
                 * Message
                 */
                // Hex file : Get first 17 bytes
                for (int i = 0; i < sending_continue_hex.length; i++) {
                    Log.i(TAG, "Send stt : " 
                            + (sending_continue_hex.length*time + i));
                    
                    // Get next bytes
                    sending_continue_hex[i] = 
                            CHARACTERS.getBytes()[sending_continue_hex.length*time + i];
                }

                // Merge byte[]
                byte[] sending_continue_packet = 
                        new byte[character_length + BLE.INITIAL_MESSAGE_PACKET_LENGTH];
                System.arraycopy(initial_packet, 0, sending_continue_packet, 
                        0, initial_packet.length);
                System.arraycopy(sending_continue_hex, 0, sending_continue_packet, 
                        initial_packet.length, sending_continue_hex.length);

                // Set value for characteristic
                characteristic.setValue(sending_continue_packet);
            }
            
            // Write characteristic via BLE
            mBluetoothGatt.writeCharacteristic(characteristic);
        }
    }

public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic,
            String data) {
        if (mBluetoothAdapter == null || mBluetoothGatt == null) {
            Log.w(TAG, "BluetoothAdapter not initialized");
            return false;
        }

        if (ActivityBLEController.IS_FIRST_TIME) {
            /**
             * In the first time, 
             * should send the Title
             */
            byte[] merge_title = sendTitle(data);

            // Set value for characteristic
            characteristic.setValue(merge_title);

            // Write characteristic via BLE
            mBluetoothGatt.writeCharacteristic(characteristic);

            // Reset
            ActivityBLEController.IS_FIRST_TIME = false;

            return true;
        } else {
            /**
             * In the second time, 
             * should send the Message
             */
            if (data.length() <= BLE.LIMIT_CHARACTERS) {
                sendMessage(characteristic, data);

                // Reset
                ActivityBLEController.IS_FIRST_TIME = true; 

                return true;
            } else {
                // Typed character
                typed_character = data.length();

                return false;
            }
        }
    }

如果我尝试发送像deepakkumargar这样的数据,它只会发送deepakkum。 - Deepak
为什么这个问题会找上我? - Deepak
5
我认为针对这种情况,你应该通过 {@code}onCharacteristicWrite(){@code} 得到一个回复,而不是让线程随机睡眠一段时间。这样可以避免两种问题:(a) 睡眠的时间不够,以及 (b) 睡眠的时间过长,从而使你的代码效率取决于你发送的字节数,非常低效。 - AllDayAmazing
2
有没有人可以用其他的话和一个小例子来解释一下这个问题?BLE的包是什么?如果有人已经实现了它,请发送代码链接。 - zed
2
发送最后一个数据包的函数在哪里?设置标题的方法是什么? - D.J
显示剩余11条评论

23

在 Lollipop 版本上,您可以发送高达 512 字节的数据。您需要使用 BluetoothGatt.requestMtu() 方法,并将参数值设置为 512。另外,正如 @Devunwired 所提到的,您需要等待任何先前的操作完成后,再调用此方法。


2
只有在服务器端的设备支持更改其MTU时,才是正确的。 - Mohammad Haseeb
@MohammadHaseeb如果不行会发生什么?还有进行MTU协商的回复吗? - André Fratelli
2
@AndréFratelli 不,你没有收到响应是因为服务器不承认或支持更改 MTU。 - Mohammad Haseeb
@MohammadHaseeb是正确的,没有商量的余地。在实际设备上进行测试是必要的。此外,我认为更大的MTU值是不稳定的。 - ThomasW

20

这里有很多误导。

BLE能够发送的数据不仅限于20字节,而且在Android上很容易实现。

您需要更改的是链接MTU,默认设置为23(只能使用其中的20个来设置值)。 如果要发送的数据包大于当前的链接MTU,则Android提供了分段机制(这是onCharacteristicRead(...) API中offset参数的目的)。

因此,您可以通过请求中央使用requestMtu(...) API来增加MTU。后者将导致在周边设备上调用回调onMtuChanged,它将通知其新的MTU。 完成此操作后,您可以发送更大的数据包,而无需发出Android分段机制。

其他方案是构建自己的分段机制,不发送大于MTU的数据包。 或依赖于Android机制并使用'offset'参数进行操作。


1
这仅在棒棒糖中得到支持,并且仅适用于支持更改MTU大小的设备(目前不是很常见)。此外,使用偏移参数与发送更大的块无关,它只是覆盖该偏移处读取的特征。 - zed
你肯定是指 BluetoothGattServerCallback.onCharacteristicReadRequest(),而不是 BluetoothGattCallback.onCharacteristicRead() - Albus Dumbledore

13

您需要请求MTU更新。这是最大传输单元。目前,BLE在单个数据包中最多接受512字节。但是,如果不请求此MTU更新,则您的设备将无法发送超过23字节的数据包(当前情况下)。


使用您的 BluetoothGatt 对象调用 requestMtu()

这里是开发者页面的链接

enter image description here


BluetoothGattCallback 将会接收到如下所示的 onMtuChanged() 事件。在成功更新 MTU 后,您可以将数据作为一个数据包发送。这里有一个 link 指向该开发者页面。

enter image description here


通常在连接到要写入的特征之后,我会调用requestMtu()。祝好运。


1
FYI,它不仅会改变写入操作的最大长度,还会改变读取和通知的最大长度。 非常感谢您的帮助。 - save_jeff

10

您是正确的,BLE规范不允许写操作超过20个字节。如果您无法将有效负载分成多个特征(这在逻辑上更容易维护),那么分块机制就是另一种方法。

但是,请注意,BLE堆栈不喜欢尝试排队多个操作。每个读/写都是异步的,结果通过BluetoothGattCallback实例上的onCharacteristicRead()onCharacteristicWrite()回调返回。您编写的代码尝试在彼此之上发送三个特征写操作,而不等待之间的回调。您的代码需要遵循以下路径:

send(Test1)
  -> Wait for onCharacteristicWrite()
  -> send(Test2)
    -> Wait for onCharacteristicWrite()
    -> send(Test3)
      -> Wait for onCharacteristicWrite()
Done!

那么onCharacteristicWrite()是如何工作的呢?您能否添加一个实现onCharacteristicWrite()的代码片段呢?并且,您能否给出一个多个特征值的示例(它们将在send方法中添加对吧?)。我想测试一下速度,看看哪个适合我的工作。谢谢! - Ankit Aggarwal
这是一个示例应用程序,可以很好地演示在调用下一个函数之前等待回调的方法:https://github.com/devunwired/accessory-samples/blob/master/BluetoothGatt/src/com/example/bluetoothgatt/MainActivity.java - devunwired
我还没有实现onCharacteristicWrite()。我看了你的代码,发现你在onCharacteristicWrite()之后进行读取。我不想这样做,所以我能否只使用以下代码?@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { //写入启用标志后,接下来我们读取初始值 return; } - Ankit Aggarwal
1
你可以在回调函数中做任何你想做的事情。在你的情况下,那就是发送下一个写入操作。 - devunwired
1
@Devunwired,您的回答中的方法是否比在单个特征上每次发送20字节的方法更快,还是它们最终会具有相同的吞吐量? - Sean
显示剩余5条评论

8

如果另一端设备支持,您实际上可以触发BLE长写操作。

您可以通过将写类型设置为BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT来完成此操作。

在这种情况下,您可以发送超过20个字节。


这个功能有官方文档吗?长写仅支持ATT写请求(来自对等设备的确认)吗? - metch
1
虽然没有官方文档,但是如果你查看AOSP项目中的设备驱动程序代码,你会发现该功能得到了支持。 - Zac Siegel
2
我在哪里可以找到这段代码?你能在回答中添加一个链接吗?谢谢! - Christopher

4
如果你想通过BLE发送大量的数据,则最好使用两个特征值,一个用来发送大部分数据,另一个用来发送最后一段数据。这样,您就不需要将响应设置为WRITE_NO_RESPONSE,并使用回调函数一直发送下一段数据,直到您到达最后一段数据,在这一点上,您将把它写到第二个特征值中,这将让设备知道您已经完成了写入数据,并且可以将所有数据连接在一起形成一个大数据包。

1
抱歉,我不熟悉BLE,能否用一段代码片段解释你的想法,引用我已经编写的代码会更加清晰。 - Ankit Aggarwal
你不应该这样做。这会使你的读写速度变得非常慢,并且完全没有反馈,无法确定写入是否成功以及出现了哪些错误。如果没有回调系统,你也将无法订阅通知。 - Zomb
1
@zed 不确定我是否理解你的问题。远程设备正在监听这两个特征。如果它看到第一个特征上的写入,它将始终假定至少还有一个要进行的写入。只要您继续向第一个特征写入,它就会等待。当您拥有最后一个数据包时,您将把它写入第二个特征。 - Zomb
@Zomb 如果我想写一个32字节长的特征(例如“123456789-123456789-123456789-12”),我会这样做:characteristic.setValue("123456789-123455678"); bluetoothGatt.writeCharacteristic(characteristic);,那么我该如何通知它我要发送更多数据呢?如果在onCharacteristicWrite()中再次写入,只有最后一组字节将被保存为“9-123456789-12”。 - zed
1
你需要两个特征,所以你需要执行 char1.setValue("123456789-123455678") 和 btGatt.writeChar(char1)。此时,你的设备会缓存 char1 的值。然后,你需要执行 char2.setValue(9-123456789-12") 和 btGatt.writeChar(char2)。现在,你的设备知道所有数据传输已经完成,并将 char1 和 char2 进行连接。 - Zomb
显示剩余7条评论

4
使用队列的另一种解决方案,可以处理任何大小的无限消息(通过自己管理协议来放置消息分隔符)。没有睡眠,没有额外的延迟。
private volatile boolean isWriting; 
private Queue<String> sendQueue; //To be inited with sendQueue = new ConcurrentLinkedQueue<String>();

public int send(String data) {
    while (data.length()>20) {
        sendQueue.add(data.substring(0,20));
        data=data.substring(20);
    }
    sendQueue.add(data);
    if (!isWriting) _send();
    return ST_OK; //0
}

private boolean _send() {
    if (sendQueue.isEmpty()) {
        Log.d("TAG", "_send(): EMPTY QUEUE");
        return false;
    }
    Log.d(TAG, "_send(): Sending: "+sendQueue.peek());
    tx.setValue(sendQueue.poll().getBytes(Charset.forName("UTF-8")));
    isWriting = true; // Set the write in progress flag
    mGatt.writeCharacteristic(tx);
    return true;
}

@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    super.onCharacteristicWrite(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d("TAG","onCharacteristicWrite(): Successful");
    }
    isWriting = false;
    _send();
}

2

我有一个非常类似于Huy Tower的答案,但我花了时间编写一些更好的代码来完成它。这不使用任意延迟,而是使用onCharacteristicWrite回调函数。我会参考他的解释来理解它的工作原理。

首先,声明了两个类范围变量:

private byte[][] byteMessageToWrite;
private int currentMessageProgress = 0;

然后声明了以下三个函数:

// Send a message
    public void sendMessage(String strMessage) {
        if (characteristic == null || strMessage == null || strMessage.isEmpty()) {
            // Do nothing if there is no device or message to send.
            return;
        }



        //Note that byte arrays after the first are written in the onCharacteristicWrite Callback

        byteMessageToWrite = getMessageByteArray(strMessage);
        writeBytes(byteMessageToWrite[0]);
        currentMessageProgress = 1;
    }

    //writes an array of bytes
    //note the max BLE message limit
    private void writeBytes(byte[] bytesToWrite) {
        characteristic.setValue(bytesToWrite);
    }

    // Note: BLE Only allows 20 bytes to be written to a characteristic at once. Have to write
    // multiple times if sending larger data. This function breaks a string up to do that.
    // The first byte is reserved as a key
    // Note, the second byte in every 20byte section is either a 1 or a 2. A 2 indicates that it is
    // The last message in the set
    private byte[][] getMessageByteArray(String message) {
        byte[] initBytes = message.getBytes(Charset.forName("UTF-8"));
        int currentIndex = 0;
        int msgLength = initBytes.length;

        int numMessages = (int) (Math.ceil((double) (Math.ceil((double) msgLength) / (double) (BLE_BYTE_LIMIT-2))));

        byte[][] retMessage = new byte[numMessages][20];

        for (int i = 0; i < numMessages; i++) {
            //set key
            retMessage[i][0] = 0x03;
            //set second byte (indicator of termination)
            if (i == numMessages - 1) {//final message, set byte 1 to 2
                retMessage[i][1] = 0x02;
            } else {//not final message
                retMessage[i][1] = 0x01;
            }
            //set other bytes
            for (int ii = 2; ii < BLE_BYTE_LIMIT; ii++) {// fill in the data
                int index = (i * (BLE_BYTE_LIMIT - 2)) + ii - 2;
                if(index>=msgLength){
                    // Have reached the end of the message, don't fill any other bytes
                    return retMessage;
                }
                retMessage[i][ii] = initBytes[index];
            }
        }
        return retMessage;
    }

最后,在OnCharacteristicWrite函数中,我有以下内容:
      @Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        super.onCharacteristicWrite(gatt, characteristic, status);
        if (currentMessageProgress < byteMessageToWrite.length) {
            //there are more bytes to write

            writeBytes(byteMessageToWrite[currentMessageProgress]);
            currentMessageProgress++;
        }
    }

我还要指出,与Huy建议使用0x00和0x01不同,我选择使用0x01和0x02。原因是我发现我的蓝牙设备无法成功读取任何带有0x00的数据包。我无法解释这个问题。


1
这是使用分块方法实现的示例,但不使用 Thread.sleep 。我发现对于我的应用程序发送超过20位数据更好和更有效。
数据包将在 onCharacteristicWrite()触发后发送。我刚刚发现,此方法将在外围设备(BluetoothGattServer)发送 sendResponse()方法后自动触发。
首先,我们必须使用此函数将数据包转换为块:
public void sendData(byte [] data){
    int chunksize = 20; //20 byte chunk
    packetSize = (int) Math.ceil( data.length / (double)chunksize); //make this variable public so we can access it on the other function

    //this is use as header, so peripheral device know ho much packet will be received.
    characteristicData.setValue(packetSize.toString().getBytes());
    mGatt.writeCharacteristic(characteristicData);
    mGatt.executeReliableWrite();

    packets = new byte[packetSize][chunksize];
    packetInteration =0;
    Integer start = 0;
    for(int i = 0; i < packets.length; i++) {
        int end = start+chunksize;
        if(end>data.length){end = data.length;}
        packets[i] = Arrays.copyOfRange(data,start, end);
        start += chunksize;
    }

在我们的数据准备好后,我将我的迭代放在这个函数上:

@Override
    public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
        if(packetInteration<packetSize){
        characteristicData.setValue(packets[packetInteration]);
        mGatt.writeCharacteristic(characteristicData);
            packetInteration++;
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接