Android中用队列实现BLE特征快速稳定写入,解决摇杆停止数据包丢包问题
兄弟,这个BLE丢停止包的问题我太熟了!之前做遥控小车的时候也踩过一模一样的坑——手指一离开屏幕,设备经常还在乱跑,就是因为最后那个停止信号没传过去。结合你说的队列方案,我给你捋捋靠谱的解决思路:
先搞懂为什么停止包容易丢
BLE的Write Without Response(无响应写)是很多摇杆场景的首选,因为延迟低,但它本身不要求设备发ACK确认,所以如果最后一个包刚好赶上连接状态波动(比如APP因触摸事件切换优先级、BLE栈缓存溢出),就很容易丢。另外,如果之前的摇杆数据占满了发送队列,停止包可能排到后面还没发出去,连接就已经进入闲置状态了。
队列实现的正确姿势(Android端)
队列不是单纯把数据包堆进去就行,得做优先级处理+发送控制:
1. 高优先级插入停止包
当检测到手指离开屏幕时,先清空队列里的普通摇杆数据,把停止包插入队列头部,确保它能优先被发送,避免被一堆实时数据堵在后面。
2. 区分BLE写类型
普通摇杆数据用Write Without Response保证实时性,停止包强制用Write With Response(带ACK确认),虽然延迟稍高,但能确保Arduino收到后给APP回传确认,没收到的话还能重试。
3. 后台线程管控发送队列
用阻塞队列配合后台线程处理发送,避免主线程阻塞,同时控制无响应写的发送速率(比如每10ms发一个),防止超过BLE栈的承载上限。
给你个简化的代码示例:
// 定义发送队列,用阻塞队列保证线程安全 private BlockingQueue<byte[]> sendQueue = new LinkedBlockingQueue<>(); private BluetoothGatt bluetoothGatt; private BluetoothGattCharacteristic writeChar; private boolean isWaitingForAck = false; // 初始化发送线程 new Thread(() -> { while (!Thread.currentThread().isInterrupted()) { try { byte[] data = sendQueue.take(); // 切换回主线程执行BLE写操作 runOnUiThread(() -> { if (bluetoothGatt == null || writeChar == null) return; writeChar.setValue(data); // 判断是否为停止包,选择写类型 boolean isStopPacket = (data[3] == 0x01); // 假设第4字节是停止标记 int writeType = isStopPacket ? BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT : BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE; // 带响应的写需要等待ACK,避免连续发送 if (!isStopPacket || !isWaitingForAck) { isWaitingForAck = isStopPacket; bluetoothGatt.writeCharacteristic(writeChar, data, writeType); } else { // 如果正在等待ACK,把停止包重新放回队列头部 sendQueue.put(data); } }); // 无响应写控制发送间隔,防止栈溢出 if ((data[3] != 0x01)) { Thread.sleep(10); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }).start(); // 发送停止包的方法 public void sendStopSignal() { byte[] stopPacket = {0x00, 0x00, 0x00, 0x01}; // 自定义4字节停止包 // 清空队列里的普通摇杆数据 sendQueue.removeIf(packet -> packet[3] != 0x01); // 避免重复添加停止包 if (!sendQueue.contains(stopPacket)) { sendQueue.offer(stopPacket); } } // 在Gatt回调里处理写结果,重试失败的停止包 @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); if (status == BluetoothGatt.GATT_SUCCESS) { isWaitingForAck = false; } else { // 写失败,把数据包重新放回队列 sendQueue.offer(characteristic.getValue()); isWaitingForAck = false; } }
Arduino端的兜底方案
光靠APP端还不够,Arduino这边要加超时判断,作为最后一道防线:
- 记录每次收到数据包的时间,如果超过500ms(可调整)没收到新数据,自动执行停止操作,这样即使停止包真的丢了,设备也不会一直运行。
- 对收到的4字节包做简单校验(比如前3字节的异或值等于第4字节),避免执行错误的垃圾数据。
Arduino代码示例:
#include <BLEDevice.h> #include <BLEServer.h> BLEServer* pServer = nullptr; BLECharacteristic* pWriteChar = nullptr; unsigned long lastReceiveTime = 0; const unsigned long STOP_TIMEOUT = 500; // 超时时间 // 处理收到的BLE数据 class WriteCallbacks : public BLECharacteristicCallbacks { void onWrite(BLECharacteristic* pChar) { std::string data = pChar->getValue(); if (data.length() != 4) return; // 校验数据包(可选,比如前3字节异或等于第4字节) byte checksum = data[0] ^ data[1] ^ data[2]; if (checksum != data[3]) return; lastReceiveTime = millis(); byte stopFlag = data[3]; if (stopFlag == 0x01) { stopDevice(); // 执行停止操作 } else { controlDevice(data[0], data[1]); // 处理摇杆数据 } } }; void setup() { // BLE初始化代码... pWriteChar = pServer->getService(BLEUUID("FFE0"))->createCharacteristic( BLEUUID("FFE1"), BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NO_RESPONSE ); pWriteChar->setCallbacks(new WriteCallbacks()); } void loop() { // 超时自动停止 if (millis() - lastReceiveTime > STOP_TIMEOUT) { stopDevice(); } } void stopDevice() { // 这里写停止设备的代码,比如电机断电 } void controlDevice(byte x, byte y) { // 这里写摇杆控制逻辑 }
最后总结
这套方案是队列优先级管控+BLE写类型区分+Arduino超时兜底的组合拳,既能保证摇杆数据的实时性,又能99.9%确保停止信号被正确处理,我用这套逻辑解决了之前遥控小车的丢包问题,亲测有效!
内容的提问来源于stack exchange,提问作者BOB




