#本人是初次接触Android蓝牙开发,若有不对地方,欢迎指出。
#由于是讲接收数据策略(其中还包含数据发送的部分策略),因此其他问题部分不会讲述,只描述数据接收。
简介(对于客户端---手机端)
博主在处理数据接收的时候,发现BLE的数据接收有很多不一样的地方,就是BLE的数据接收是采用它自己的MTU格式,目前一次BLE数据接收只能接收20字节数据,那么我们一旦涉及到超出20字节的数据,那么BLE的接收数据是按照20字节来划分 --- 我们想要发送的数据,那么这个完整的数据被分成多个包的数据包。我们就是想处理完整的数据,而不是处理某一个单一的数据包,所以我们遇到的难点就是——怎么将接收到分开的数据包,整理成完整的数据来进行处理。
对于MTU的解释
从字面上来说,MTU 是英文 Maximum Transmission Unit 的缩写,即最大传输单元,它的单位是字节,指的是数据链路层的最大payload,由硬件网卡设置MTU,是一个硬性限制。
1.数据接收部分
引用别的博主的对于BLE的解释——这里我就简要介绍一下。
对于BLE最重要的部分就是 GATT回调(处理连接和服务发现,以及数据接收以及发送)。所以我们目前要用到 BluetoothGattCallback 以及 它里面的回调函数。
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
}
};
对于gattCallback 它的两个回调函数有---
@Override
onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {}
第一个是用于数据接收,也就是我们这个blog的重点部分,它也是一次只能接收一个20字节的数据分包。
第二个是用于处理数据是否写出,当你的数据被发送出去的时候会进行回调。(注意了,它也是遵循MTU的,它也是一次性只能发送20个字节。)
2.对于onCharacteristicChanged的处理
首先可以看看我的写的案例。
这个函数仍然是在
private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
//我们的 onCharacteristicChanged()在这里面进行重写。
};
onCharacteristicChanged
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// value为设备发送的数据,根据数据协议进行解析
byte[] value = characteristic.getValue();
String received = new String(value);
processReceivedData(value);
Log.i(TAG,"接收到数据: " + received);
// Log.d(TAG, "收到分段数据: " + new String(value, StandardCharsets.UTF_8));
// runOnUiThread(() -> processReceivedData(value));
}
注释的部分不用管,这个是我进行测试时候用的。
首先,我们可以看到value是为对应的BLE设备发送过来的数据,我们使用characteristic.getValue()接收,你可以看到这个函数里的变量characteristic 这个不用管,你直接使用这个函数就可以了。
然后,我们接收到的数据,还是byte,就是字节格式的数据包。
现在有两种方式来进行数据包整合:
- 字节直接整合为完整数据。
- 将字节先转换为字符串格式后再整合为字节完整数据。
根据自己的需求来进行选择,byte格式是可以转换为字节格式的。
1.对于字节直接整合为完整数据
// 首先在gattCallback这个变量外面 先申明一个全局变量
private List<byte[]> packets = new ArrayList<>();
//作为数据缓存存储
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] value = characteristic.getValue();
packets.add(value);
// 这里加入你自己的数据末尾判断
if(End)
{
byte[] Alldata = GetWholeData(packets); //组成完整的数据
/*下面同时可以加入你想在数据组合完整时,调用数据处理函数
(可选部分)
*/
}
}
对于数据整合函数
// 组装数据
private byte[] GetWholeData(List<byte[]> packets) {
ByteArrayOutputStream DataStream = new ByteArrayOutputStream();
for (byte[] packet : packets) {
DataStream.write(packet);
}
return DataStream.toByteArray();
}
2.将字节先转换为字符串格式后再整合为字节完整数据
以我的案例为例子来进行讲解。
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// value为设备发送的数据,根据数据协议进行解析
byte[] value = characteristic.getValue();
// String received = new String(value);
// 可加可不加,我用来调试的
/* 主要核心函数 这个兼具数据处理*/
processReceivedData(value);
// Log.i(TAG,"接收到数据: " + received);
}
这个函数其实和第一个差不多,主要是数据处理部分,不相同。
对于ProcessReceivedData函数
private final StringBuilder dataBuffer = new StringBuilder();
// 对于dataBuffer是用来进行完整数据拼包的,全局变量
private void processReceivedData(byte[] data) {
String packet = new String(data, StandardCharsets.UTF_8);
dataBuffer.append(packet);
// 前两个部分,就是用来进行接收的数据包合包操作。
/* 后面是作为数据处理部分,你可以理解为前面的操作是合包
只有真正完成数据包整合后,才会真正进行数据处理部分。
下面的代码作为案例,你可以自己改写。
*/
while (true) {
int startIndex = dataBuffer.indexOf("#");
if (startIndex == -1) {
dataBuffer.setLength(0);
break;
}
int endIndex = dataBuffer.indexOf("\n", startIndex);
if (endIndex == -1) {
String remaining = dataBuffer.substring(startIndex);
dataBuffer.setLength(0);
dataBuffer.append(remaining);
break;
}
String rawData = dataBuffer.substring(startIndex, endIndex + 1);
dataBuffer.delete(0, endIndex + 1);
if (!rawData.startsWith("#") || !rawData.endsWith("\n")) {
Log.e(TAG, "协议格式错误: " + rawData);
continue;
}
try {
String jsonStr = rawData.substring(1, rawData.length() - 1);
JSONObject json = new JSONObject(jsonStr);
double lat = json.getDouble("lat");
double lon = json.getDouble("lon");
float angle = (float) json.getDouble("angle");
if (lastLat != null && lastLon != null) {
CaneVectorControl.ControlCommand command = CaneVectorControl.computeControl(
lastLat, lastLon, endLat, endLon, lat, lon
);
String jsonCommand = "#" + command.toJson() + "\n";
sendCommand(jsonCommand);
Log.i(TAG, "控制指令已发送: " + jsonCommand);
} else {
Log.e(TAG, "用户位置未更新,无法计算指令");
}
} catch (JSONException e) {
Log.e(TAG, "JSON解析失败: " + e.getMessage() + "\n原始数据: " + rawData);
}
}
}
if (!rawData.startsWith("#") || !rawData.endsWith("\n")) { Log.e(TAG, "协议格式错误: " + rawData); continue; }
这一部分是用于检测发送的数据格式是否正确。上面的代码,可以按照注释理解,不符合条件就结束循环,提前进行下一次的数据包处理。
try { String jsonStr = rawData.substring(1, rawData.length() - 1); JSONObject json = new JSONObject(jsonStr); double lat = json.getDouble("lat"); double lon = json.getDouble("lon"); float angle = (float) json.getDouble("angle"); if (lastLat != null && lastLon != null) { CaneVectorControl.ControlCommand command = CaneVectorControl.computeControl( lastLat, lastLon, endLat, endLon, lat, lon ); String jsonCommand = "#" + command.toJson() + "\n"; sendCommand(jsonCommand); Log.i(TAG, "控制指令已发送: " + jsonCommand); } else { Log.e(TAG, "用户位置未更新,无法计算指令"); } } catch (JSONException e) { Log.e(TAG, "JSON解析失败: " + e.getMessage() + "\n原始数据: " + rawData); }
对于 try{}catch{}部分就是我所提及的数据处理部分。 你可以自己更改。
对于里面的一个 sendCommand(jsonCommand) 是处理完数据后发送相应的数据。
这里我再加上我们一般都会处理完数据后发送想要的相应部分。
3.对于数据接收处理后,数据发送部分
可以看到之前博主提及过的
@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, "写入成功:" + new String(characteristic.getValue(), StandardCharsets.UTF_8)); sendNextPacket(); } else { Log.e(TAG, "写入失败,状态码: " + status); } }
这个就是我们接下来要使用的数据发送回调。
先看我写的完整例子。
SendCommand函数
private Queue<byte[]> writeQueue = new LinkedList<>();
@SuppressLint("MissingPermission")
public void sendCommand(String command) {
if (bluetoothGatt == null || writeCharateristic == null) {
Log.e(TAG, "GATT未连接或写特征不可用");
return;
}
byte[] data = command.getBytes(StandardCharsets.UTF_8);
int maxLength = 20; // 或 MTU - 3
// 博主使用的是HC-08 BLE 它的MTU大小为20字节
// 清空队列并分包
writeQueue.clear();
for (int i = 0; i < data.length; i += maxLength) {
int end = Math.min(data.length, i + maxLength);
byte[] chunk = Arrays.copyOfRange(data, i, end);
writeQueue.offer(chunk);
}
// 发送第一个包
sendNextPacket();
}
sendNextPacket()函数
@SuppressLint("MissingPermission")
private void sendNextPacket() {
if (writeQueue.isEmpty()) return;
byte[] chunk = writeQueue.poll();
writeCharateristic.setValue(chunk);
boolean success = bluetoothGatt.writeCharacteristic(writeCharateristic);
Log.i(TAG, "发送分包: " + new String(chunk, StandardCharsets.UTF_8) + ",成功: " + success);
// 如果写失败,考虑重试机制
}
onCharacteristicWrite()函数
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.i(TAG, "写入成功:" + new String(characteristic.getValue(), StandardCharsets.UTF_8));
sendNextPacket();
// 如果是还没有发送完数据,那么就继续发送它的下一个数据包
} else {
Log.e(TAG, "写入失败,状态码: " + status);
}
}
问题的本质就是:
BLE 发送数据包时,每包最大只能发 20 字节(传统 BLE 协议限制),你的完整字符串超过这个长度就会被自动分包。
解决方案:和接收端一样,用缓存缓冲区来拼接这些碎片
我们在 发送端什么都不用改(只要发送时不要太快),只需要在 接收端缓冲所有片段,直到遇到 \n
表示完整数据包结尾。(这里可以根据你自己定义的协议更改结尾)。
以上就是我作为小白开发BLE接收数据的感悟,希望对你有所帮助。