События 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();
    }
}

0 ответов

Другие вопросы по тегам