#本人是初次接觸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() {@Overridepublic void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {}@Overridepublic 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
@Overridepublic 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<>();//作為數據緩存存儲@Overridepublic 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.將字節先轉換為字符串格式后再整合為字節完整數據
以我的案例為例子來進行講解。
@Overridepublic 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()函數
@Overridepublic 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接收數據的感悟,希望對你有所幫助。