События Android BLE отсутствуют в журнале отслеживания HCI
Я работаю над приложением, в котором мы используем BLE для отправки и получения данных. Телефон центральный и разговаривает с периферией. Мы используем уведомления для получения данных и записи без ответа для отправки данных.
Однако мы замечаем, что иногда пакеты не принимаются (могут происходить как на центральной, так и на периферийной стороне). Даже если gatt.writeCharacteristics возвращает успех, и вызывается соответствующий onCharacteristicsWrite, пакет никогда не принимается на периферийной стороне. Чтобы исследовать это дальше, я вытащил журнал отслеживания HCI, и вот мои выводы:
- Для пакетов, которые получены на периферийной стороне, я могу видеть соответствующие события HCI в журнале отслеживания HCI.
- Для пакетов, которые НЕ получены, соответствующие события HCI отсутствуют в журнале отслеживания HCI.
На данный момент у меня нет хороших инструментов, чтобы понюхать радио. Однако, поскольку эти события отсутствуют в журнале отслеживания HCI, я считаю, что эти команды неправильно отправляются в стек Bluetooth на устройстве Android.
Интересно, что может вызвать эту проблему, и если есть какие-либо известные проблемы с этим или как я могу решить эту проблему. Я приложу исходный код для коммуникационной части приложения. Возможно, есть какие-то тривиальные ошибки в API.
Я также заметил, что если отключить сканирование BLE во время сопряжения с периферийным устройством, я достигну большей стабильности, но все еще недостаточно стабильно.
package test.androidlib;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothGattService;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.atomic.AtomicBoolean;
import test.common.ToWaitFor;
import test.common.cut.Hex;
import saltchannel.ByteChannel;
import saltchannel.ComException;
import saltchannel.StreamChannel;
/**
* The client-side of a BLE channel session.
*
*/
public class BleClientChannel implements ByteChannel, Handler.Callback {
private final String TAG = "BleClientChannel";
private static final int MAX_NUM_CONNECTION_RETRIES = 0;
private static final int MAX_MTU_SIZE = 247;
private Context context;
private LogOutput logOutput;
private Listener listener;
private long timeout = 6000;
private final Object stateLock = new Object();
private final AtomicBoolean connectCalled = new AtomicBoolean(false);
private final AtomicBoolean isClosed = new AtomicBoolean(false);
private HandlerThread handlerThread;
private Handler bleHandler;
private BluetoothDevice mDevice;
private BluetoothGatt mGatt;
private BluetoothGattCharacteristic inCha;
private BluetoothGattCharacteristic outCha;
private int effectiveMtu = 20;
private int connectionRetries = 0;
private GattEvent connectedEvent;
private GattEvent writeEvent;
private GattEvent disconnectedEvent;
private byte[] nextPacketToSend;
private CircularByteBuffer circularByteBuffer;
private ByteChannel byteChannel;
private OutputStream bleOutputStream;
private BluetoothManager manager;
public BleClientChannel(Context context) {
this.context = context;
this.logOutput = new LogOutput() {
@Override
public void print(String s) {
Log.i(TAG, s);
}
};
this.listener = new Listener() {
@Override
public void disconnected() {
log("Disconnected");
}
};
this.handlerThread = new HandlerThread("BLE-Worker");
manager = (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE);
}
public void initLog(LogOutput logOutput) {
this.logOutput = logOutput;
}
public void initListener(BleClientChannel.Listener listener) {
this.listener = listener;
}
public void initTimeout(int millis) {
this.timeout = millis;
}
/**
* Connects to the device.
*
* @param device The BLE device to connect to.
* @throws IllegalStateException if a connect attempt has been done with this object already.
* @throws IOException if the connection could not be established.
*
*/
public void connect(BluetoothDevice device) throws IOException {
if (connectCalled.get()) {
connectCalled.set(true);
throw new IllegalStateException("connect() have already been called");
}
Log.i("BleClientChannel", "Connected devices");
for (BluetoothDevice tmpDevice : manager.getConnectedDevices(BluetoothProfile.GATT)) {
// TODO: Verify not connected already
Log.i(TAG, tmpDevice.getAddress());
}
this.mDevice = device;
connectedEvent = new GattEvent();
// Start BLE Worker thread
handlerThread.start();
bleHandler = new Handler(handlerThread.getLooper(), this);
addTask(MyGattTask.GattTask.GATT_TASK_CONNECT);
connectedEvent.waitForIt(timeout);
if (!connectedEvent.isOk) {
closeHard(true);
throw new IOException(connectedEvent.message);
}
connectedEvent = null;
this.circularByteBuffer = new CircularByteBuffer(100*1000, false);
createBleStreamChannel();
}
public void close() {
log("close enter");
if (isClosed.get()) {
isClosed.set(true);
return;
}
log("Close handeled");
if (connectedEvent != null) {
connectedEvent.isOk = false;
connectedEvent.message = "Canceled by user";
connectedEvent.reportHappened();
}
synchronized (stateLock) {
if (bleHandler != null) {
disconnectedEvent = new GattEvent();
bleHandler.removeMessages(0);
addTask(MyGattTask.GattTask.GATT_TASK_DISCONNECT);
disconnectedEvent.waitForIt(200);
}
}
closeHard(false);
log("close done");
}
private void closeHard(boolean reallyHard) {
log("Closing hard, reallyHard: " + reallyHard);
if (reallyHard) {
if (isClosed.get()) {
isClosed.set(true);
return;
}
}
synchronized (stateLock) {
if (bleHandler != null) {
bleHandler.removeCallbacksAndMessages(null);
bleHandler.getLooper().quit();
bleHandler = null;
}
if (this.mGatt != null) {
this.mGatt.disconnect();
this.mGatt.close();
this.mGatt = null;
new Thread(new Runnable() {
@Override
public void run() {
listener.disconnected();
}
}).start();
}
if (byteChannel != null) {
try {
circularByteBuffer.getInputStream().close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
private void addTask(MyGattTask.GattTask task) {
if (bleHandler != null) {
if (bleHandler.getLooper().getThread().isAlive()) {
Message msg = bleHandler.obtainMessage();
msg.what = 0;
msg.obj = new MyGattTask(task);
msg.sendToTarget();
}
}
}
private void handleInternalError(final String error) {
log("Internal error: " + error);
/*
* This function is called from the bluetoothGattCallback, due to the syncronized block
* this method might block for a while. One should never block bluetoothGattCallback.
* Therefore, a new thread/runnable is started.
*/
new Thread(new Runnable() {
@Override
public void run() {
synchronized (stateLock) {
if (writeEvent != null) {
writeEvent.isOk = false;
writeEvent.message = error;
writeEvent.reportHappened();
}
if (connectedEvent != null) {
connectedEvent.message = error;
connectedEvent.isOk = false;
connectedEvent.reportHappened();
}
closeHard(true);
}
}
}).start();
}
@Override
public byte[] read() throws ComException {
log("read enter");
if (isClosed.get()) {
throw new ComException("Channel closed");
}
if (this.byteChannel != null) {
byte[] tmp = this.byteChannel.read();
log("Read done");
return tmp;
} else {
throw new ComException("No byte channel available.");
}
}
@Override
public void write(byte[]... bytes) throws ComException {
if (this.byteChannel != null) {
this.byteChannel.write(bytes);
} else {
throw new ComException("No byte channel available.");
}
}
@Override
public boolean handleMessage(Message msg) {
boolean ok = true;
String errorMsg = "";
MyGattTask.GattTask task = ((MyGattTask) msg.obj).task;
log("task to handle: " + task);
switch (task) {
case GATT_TASK_CONNECT:
this.mGatt = this.mDevice.connectGatt(this.context, false, mGattCallback);
break;
case GATT_TASK_RETRY_CONNECT:
this.mGatt.close();
this.mGatt = this.mDevice.connectGatt(this.context, false, mGattCallback);
break;
case GATT_TASK_NEGOTIATE_MTU:
ok = this.mGatt.requestMtu(MAX_MTU_SIZE);
errorMsg = "requestMtu() unexpectedly returned false";
break;
case GATT_TASK_DISCOVER_SERVICES:
ok = this.mGatt.discoverServices();
errorMsg = "discoverServices() unexpectedly returned false";
break;
case GATT_TASK_VERIFY_SERVICES:
try {
setupTestService();
addTask(MyGattTask.GattTask.GATT_TASK_ENABLE_NOTIFICATIONS);
} catch (IllegalStateException e) {
ok = false;
errorMsg = e.toString();
}
break;
case GATT_TASK_ENABLE_NOTIFICATIONS:
try {
enableNotifications();
} catch (IllegalStateException e) {
errorMsg = e.toString();
ok = false;
}
break;
case GATT_TASK_NOTIFICATIONS_ENABLED:
if (connectedEvent != null) {
connectedEvent.isOk = true;
connectedEvent.reportHappened();
}
break;
case GATT_TASK_SEND_DATA:
if (nextPacketToSend != null) {
inCha.setValue(nextPacketToSend);
ok = this.mGatt.writeCharacteristic(inCha);
errorMsg = "writeCharacteristic() unexpectedly returned false";
}
break;
case GATT_TASK_DATA_SENT:
if (writeEvent != null) {
writeEvent.isOk = true;
writeEvent.reportHappened();
}
break;
case GATT_TASK_DISCONNECT:
this.mGatt.disconnect();
break;
case GATT_TASK_DISCONNECTED:
break;
default:
log("Unhandled event");
break;
}
if (!ok) {
handleInternalError(errorMsg);
}
return ok;
}
private BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
log(String.format("onConnectionStateChange, status %d, newState %d", status, newState));
if (status != BluetoothGatt.GATT_SUCCESS) {
if (BleClientChannel.this.connectionRetries < MAX_NUM_CONNECTION_RETRIES && status == 133) {
// Status 133 seems to be recoverable, we try to reconnect here
BleClientChannel.this.connectionRetries++;
addTask(MyGattTask.GattTask.GATT_TASK_RETRY_CONNECT);
} else {
handleInternalError("status != GATT_SUCCESS in onConnectionStateChange");
}
handleInternalError("status != GATT_SUCCESS in onConnectionStateChange");
return;
}
if (newState == BluetoothProfile.STATE_CONNECTED) {
addTask(MyGattTask.GattTask.GATT_TASK_NEGOTIATE_MTU);
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
if (disconnectedEvent != null) {
disconnectedEvent.isOk = true;
disconnectedEvent.reportHappened();
}
handleInternalError("Disconnected by peer");
}
}
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
log(String.format("onMtuChanged, status: %d, mtu: %d", status, mtu));
if (status != BluetoothGatt.GATT_SUCCESS) {
handleInternalError("status != GATT_SUCCESS in onMtuChanged");
return;
}
if (mtu <= MAX_MTU_SIZE) {
effectiveMtu = mtu - 3;
} else {
effectiveMtu = 23;
}
addTask(MyGattTask.GattTask.GATT_TASK_DISCOVER_SERVICES);
}
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
log(String.format("onServicesDiscovered, status: %d", status));
if (status != BluetoothGatt.GATT_SUCCESS) {
handleInternalError("status != GATT_SUCCESS in onServicesDiscovered");
return;
}
addTask(MyGattTask.GattTask.GATT_TASK_VERIFY_SERVICES);
}
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
log(String.format("onDescriptorWrite, status: %d", status));
if (status != BluetoothGatt.GATT_SUCCESS) {
handleInternalError("status != GATT_SUCCESS in onDescriptorWrite");
return;
}
addTask(MyGattTask.GattTask.GATT_TASK_NOTIFICATIONS_ENABLED);
}
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
// A message (chunk) was sent to the BLE device, now we can proceed sending next
log(String.format("onCharacteristicWrite, status: %d", status));
log("cha.getValue(): " + Hex.create(characteristic.getValue()));
if (status != BluetoothGatt.GATT_SUCCESS) {
handleInternalError("status != GATT_SUCCESS in onCharacteristicWrite");
return;
}
addTask(MyGattTask.GattTask.GATT_TASK_DATA_SENT);
}
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic cha) {
// Data was sent from BLE device
log("onCharacteristicChanged");
if (BleTestServer.OUT_UUID.equals(cha.getUuid())) {
byte[] bytes = cha.getValue();
log("cha.getValue(): " + Hex.create(bytes));
try {
circularByteBuffer.getOutputStream().write(bytes, 0, bytes.length);
} catch (IOException e) {
handleInternalError("onCharacteristicChanged, Could not add data to outputStream");
}
} else {
log("onCharacteristicChanged, Unexpected characteristic");
}
}
};
private void enableNotifications() throws IllegalStateException {
boolean ok;
ok = this.mGatt.setCharacteristicNotification(outCha, true);
if (!ok) {
throw new IllegalStateException("gatt.setCharacteristicNotification unexpectedly returned false");
}
BluetoothGattDescriptor descriptor = outCha.getDescriptor(BleUtil.CLIENT_CHARACTERISTIC_CONFIG);
ok = descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
if (!ok) {
throw new IllegalStateException("descriptor.setValue() unexpectedly returned false");
}
ok = this.mGatt.writeDescriptor(descriptor);
if (!ok) {
throw new IllegalStateException("gatt.writeDescriptor() unexpectedly returned false");
}
}
private void setupTestService() throws IllegalStateException {
BluetoothGattService service = this.mGatt.getService(BleTestServer.SERVICE_UUID);
if (service == null) {
throw new IllegalStateException("No Test service found");
}
inCha(service);
outCha(service);
}
private void inCha(BluetoothGattService service) throws IllegalStateException {
inCha = service.getCharacteristic(BleTestServer.IN_UUID);
if (inCha == null) {
throw new IllegalStateException("No in characteristics found");
}
int properties = inCha.getProperties();
if (properties != BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) {
throw new IllegalStateException("unexpected in characteristics properties, " + properties);
}
}
private void outCha(BluetoothGattService service) throws IllegalStateException {
outCha = service.getCharacteristic(BleTestServer.OUT_UUID);
if (outCha == null) {
throw new IllegalStateException("No out characteristics found");
}
int properties = outCha.getProperties();
if (properties != BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
throw new IllegalStateException("unexpected outCha properties, " + properties);
}
}
private void createBleStreamChannel() {
createBleOutputStream();
byteChannel = new StreamChannel(circularByteBuffer.getInputStream(), bleOutputStream);
}
private void createBleOutputStream() {
bleOutputStream = new OutputStream() {
@Override
public void write(int oneByte) throws IOException {
write(new byte[]{(byte) oneByte}, 0, 1);
}
public void write(byte[] bytes) throws IOException {
write(bytes, 0, bytes.length);
}
public void write(byte[] bytes, int initialOffset, int length) throws IOException {
int bytesLeft = length;
int offset = initialOffset;
byte[] tempBytes = new byte[bytes.length - 4];
System.arraycopy(bytes, 4, tempBytes, 0, length-4);
log("Writing");
while (bytesLeft > 0) {
if (isClosed.get()) {
throw new IOException("BLE Output stream is closed");
}
int packetSize = effectiveMtu;
if (bytesLeft < effectiveMtu) {
packetSize = bytesLeft;
}
byte[] packet = new byte[packetSize];
System.arraycopy(bytes, offset, packet, 0, packetSize);
nextPacketToSend = packet;
writeEvent = new GattEvent();
addTask(MyGattTask.GattTask.GATT_TASK_SEND_DATA);
writeEvent.waitForIt(timeout);
if (writeEvent.hasHappened()) {
if (!writeEvent.isOk) {
throw new IOException(writeEvent.message);
}
}
writeEvent = null;
bytesLeft -= packetSize;
offset += packetSize;
}
log("write done");
}
};
}
private void log(String s) {
this.logOutput.print(s);
}
/**
* Event happened when channel has been established,
* when a timeout occurred, or when an error occurred.
*/
private static class GattEvent extends ToWaitFor {
public String message = "";
public String type = "";
public boolean isOk = false;
@Override
public boolean waitForIt(long timeout) {
boolean happened = super.waitForIt(timeout);
if (!happened) {
type = "timeout";
message = "timeout occurred";
isOk = false;
}
return happened;
}
}
/**
* Listens to event that happens to the BLE client.
*/
public interface Listener {
/**
* Reports the event that the client was disconnected.
*/
void disconnected();
}
}