BLE под микроскопом. Андроид. Часть3

часть 1, часть 2

Введение

Перед тем, как мы двинемся дальше, хочу сделать три уточнения. Первое — это дать ссылку на проект, созданный нами в прошлой статье и доработанный нами в текущей. Для тех, кому не хватило сил дойти прошлый урок до конца.

Второе — это разъяснить ошибку, которая связана с проверкой разрешений checkPermission. Как я выяснил, эту ошибку генерирует инструмент под названием Lint, который помогает разработчикам изловить потенциальные проблемы ещё до того, как код скомпилируется. Вот здесь об этом можно почитать. Если в двух словах, то это встроенный инструмент проверки кода. Ищет потенциальные ошибки. В нашем случае он возмущался на то, что мы не проверяем критические разрешения, перед тем как начать/закончить сканирование эфира. Но дело в том, что у нас они проверяются, правда в другой функции. Поэтому, что бы убрать сообщение об ошибке, надо перед функцией написать строчку, отменяющую проверку — @SuppressLint («MissingPermission»). Тогда мы отключим сообщение о конкретно этой ошибке. На самом деле, такие же команды так же стоят у функций onScanResult () и listShow (). Если вы помните, то за основу моего урока был взят проект le_scan_classic_connect, в котором и использовались подавляющие инструкции @SuppressLint («MissingPermission»). Так что в нашем коде, перед функциями startScanning () и stopScanning (), их так же надо прописать.

Третье уточнение касается отладки приложения. Есть очень удобная штука, называется Logcat. Если вы посмотрите в среде AndroidStudio в левый нижний угол, то увидите там несколько различных закладок. Нас интересует закладка с кошкой. На рисунке внизу она открыта. Открыв эту закладу, во время исполнения нашей программы, мы увидим много строк с указанием времени, источника лога события и самого лога.

Logcat

Logcat

Из текста нашего приложения нетрудно увидеть, что сообщения в лог отправляют строчки, начинающиеся с Log.d и Log.e и они имеют разный цвет. В этом руководстве вы можете более подробно узнать как использовать этот инструмент отладки кода. Я же приведу краткий список его возможностей. В зависимости от расширения, цвет текста лога будет разный.

Log.e () — ошибки (error) красный
Log.w () — предупреждения (warning) коричневый
Log.i () — информация (info) зеленый
Log.d () — отладка (degub) голубой
Log.v () — подробности (verbose) черный

Обратные вызовы BluetoothGattCallback ()

Итак, перейдем наконец к нашему приложению. Продолжим наполнять его новыми функциями. Добавим внизу файла MainActivity следующий текст, отвечающий за BluetoothGattCallback.

    //************************************************************************************
    //                      C O N N E C T   C A L L   B A C K
    //************************************************************************************
    //    The connectGatt method requires a BluetoothGattCallback
    //    Here the results of connection state changes and services discovery would be delivered asynchronously.

    protected BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        //*********************************************************************************
        private volatile boolean isOnCharacteristicReadRunning = false;

        @SuppressLint("MissingPermission")
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            super.onConnectionStateChange(gatt, status, newState);
            String address = gatt.getDevice().getAddress();

            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (newState == BluetoothGatt.STATE_CONNECTED) {
                    Log.w(TAG, "onConnectionStateChangeMy() - Successfully connected to " + address);
                    boolean discoverServicesOk = gatt.discoverServices();
                    Log.i(TAG, "onConnectionStateChange: discovered Services: " + discoverServicesOk);
                } else if (newState == BluetoothGatt.STATE_DISCONNECTED) {
                    Log.w(TAG, "onConnectionStateChangeMy() - Successfully disconnected from " + address);
                    gatt.close();
                }
            } else {
                Log.w(TAG, "onConnectionStateChangeMy: Error " + status + " encountered for " + address);
            }
        }


    };
}

Я надеюсь уже никого не удивляет наличие новых классов, выделенных красным шрифтом? Поступаем как обычно — делаем импорт классов. Подводим мышку к красному шрифту и соглашаемся с предложенным импортом.

Обработка Callback-ов

Обработка Callback-ов

В этой части кода мы будем обрабатывать обратные вызовы BluetoothGatt стека. Под одной крышей BluetoothGattCallback могут находится различные Callback-и. Пока мы используем только onConnectionStateChange (), который отвечает за первоначальное подключение к BLE устройству. Сразу приведу полный список возможных Callback-ов. Как видим, мы можем перехватывать различные события — чтение и запись характеристик и дескрипторов. А так же некоторые другие события, например чтение уровня RSSI.

public void onServicesDiscovered (BluetoothGatt g, int stat) {}
public void onConnectionStateChange (BluetoothGatt g, int stat, int newState) {}
public void onCharacteristicRead (BluetoothGatt g, BluetoothGattCharacteristic c, int stat) {}
public void onCharacteristicWrite (BluetoothGatt g, BluetoothGattCharacteristic c, int stat) {}
public void onDescriptorRead (BluetoothGatt g, BluetoothGattDescriptor d, int stat) {}
public void onDescriptorWrite (BluetoothGatt g, BluetoothGattDescriptor d, int stat) {}
public void onReliableWriteCompleted (BluetoothGatt g, int stat) {}
public void onReadRemoteRssi (BluetoothGatt g, int rssi, int stat) {}
public void onCharacteristicChanged (BluetoothGatt g, BluetoothGattCharacteristic c) {}

Идем дальше. Что бы попадать в обработку обратных вызовов BluetoothGatt, необходимо сделать ещё кое что. Нужно разблокировать вот эту строчку в блоке инициализации.

Инициализация обратных вызовов

Инициализация обратных вызовов

А так же проинициализировать объект mBluetoothGatt. Кроме того, что бы наши обратные вызовы не раздражали инструмент статического анализа Lint, перед функцией onCreate (), необходимо вставить инструкцию @SuppressLint («MissingPermission»). Эти два дополнения подчеркнуты на рисунке ниже.

Дополняем код новым содержанием

Дополняем код новым содержанием

Теперь, когда все текущие изменения готовы, давайте попробуем запустить проект. В поведении самого приложения мы ничего нового не увидим. Однако давайте применим наши новые знания относительно Logcat. Загрузите проект в смартфон, откройте в Android Studio закладку Logcat и, после сканирования эфира, попробуйте присоединиться к BLE устройству. Делается это простым нажатием на строчку с одним из найденных устройств. Тогда функция нажатия deviceListView.setOnItemClickListener () сработает и мы попадем в обратный вызов BLE onConnectionStateChange (). Если наш гаджет позволяет к нему присоединяться, рассылая пакеты типа ADV_IND, тогда мы увидим примерно следующий лог событий.

Лог присоединения к устройству

Лог присоединения к устройству

Внимательно рассмотрите его. Часть строк принадлежит самому BLE стеку, а другая часть — дело наших рук:-) В дальнейшем, отправляя в Logcat сообщения, мы сможем понять в каком состоянии находится наше приложение и исправлять возникающие ошибки. Давайте двигаться дальше. Для этого немного подправим наше приложение.

Чтение UUID сервисов и характеристик

Для работы с сервисами и характеристиками, нам необходимо их как то показывать на экране. Я сначала планировал делать вывод их в другое окно, но решил не усложнять. Давайте просто сдвинем наш listView вверх и добавим ещё один такой же элемент на нашу форму. Сразу привяжем его к краям экрана. Среда дает ему имя listView2.

Добавление нового listView

Добавление нового listView

Мы же в нашем файле активности добавим такой заголовок. Все эти элементы нам нужны для управления новым элементом, который назовем листом сервисов и характеристик:-)

    ListView listServChar;
    ArrayAdapter adapterServChar;
    ArrayList listServCharUUID = new ArrayList<>();

Добавление листа сервисов и характеристик

Добавление листа сервисов и характеристик

В разделе инициализации обозначим новый элемент, а так же укажем функцию обработки нажатия на его строчки. Назовем её CharactRxData (). Она позволит нам читать данные.

        listServChar = findViewById(R.id.listView2);
        adapterServChar = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1);
        listServChar.setAdapter(adapterServChar);
        listServChar.setOnItemClickListener((adapterView, view, position, id) -> {
            String uuid =  listServCharUUID.get(position);
            CharactRxData(uuid);
        });

Инициализация listServChar

Инициализация listServChar

Затем ниже функции onConnectionStateChange () добавим ещё одну функцию обратного вызова — onServicesDiscovered () и функцию обработки нажатия на список listServChar.

        //************************************************************************************
        @SuppressLint("MissingPermission")
        @Override
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            super.onServicesDiscovered(gatt, status);
            final List services = gatt.getServices();
            runOnUiThread(() -> {

                for (int i = 0; i < services.size(); i++) {
                    BluetoothGattService service = services.get(i);
                    List characteristics = service.getCharacteristics();
                    StringBuilder log = new StringBuilder("\nService Id: \n" + "UUID: " + service.getUuid().toString());

                    adapterServChar.add("     Service UUID : " + service.getUuid().toString());
                    listServCharUUID.add(service.getUuid().toString());

                    for (int j = 0; j < characteristics.size(); j++) {
                        BluetoothGattCharacteristic characteristic = characteristics.get(j);
                        String characteristicUuid = characteristic.getUuid().toString();

                        log.append("\n   Characteristic: ");
                        log.append("\n   UUID: ").append(characteristicUuid);

                        adapterServChar.add("Charact UUID : " + characteristicUuid);
                        listServCharUUID.add(service.getUuid().toString());
                    }
                    Log.d(TAG, "\nonServicesDiscovered: New Service: " + log);
                 }
            });
        }
    };
    //************************************************************************************
    public void CharactRxData(String uuid)
    {
        Log.d(TAG, "UUID : " + uuid);
        textViewTemp.setText(uuid);
    }
}
//****************************************************************************************

Далее всё как обычно, соглашаемся с импортом новых классов.

Обработка запросов сервисов и характеристик

Обработка запросов сервисов и характеристик

Теперь попытаемся запустить наше приложение. Если мы всё сделали правильно, у нас не должно быть ошибок. Для того, что бы увидеть все возможности нашей программы, включим антипотеряйку и запустим приложение.

d7dad7d00b685c1006a4d74388c85775.png

Давайте разберемся как получить данную картинку. После того как мы нажали кнопку Start, в верхнем окне надо найти нашу антипотеряйку. У неё будет имя iTAG. Нажимаем на данную строчку. Наше приложение попробует присоединиться к этому гаджету и считать с него сервисы и характеристики. Это действие небыстрое и происходит оно в функции обратного вызова onServicesDiscovered (). Что бы наше приложение не зависало, мы исполняем его в отдельном потоке, используя инструкцию runOnUiThread (). Всё, что удалось считать с устройства, будет записано во втором окне. Если теперь нажать в нем любую строчку, UUID сервиса или характеристики будет записано в textViewTemp. Это ещё не конечная точка нашего приложения, но не убедившись в том, что мы правильно считываем UUID устройства, дальше идти нет смысла. В функции onServicesDiscovered (), мы так же записываем новые строчки с UUID в Logcat. Перейдите на эту закладку и убедитесь в этом сами. Картинка там получается красивая :-)

Запись и чтение характеристик

Давайте сейчас немного остановимся и сформулируем наши следующие шаги. Итак, мы уже можем присоединяться к устройству iTAG и считывать какими сервисами и характеристиками он обладает. Но мы пока не умеем с ними работать. Если посмотреть в замечательной программе nRF Connect на наш iTAG, то мы увидим, что у него есть сервис 0×1802 — Immediate Alert. А если его раскрыть, то там есть характеристика 0×2A06, позволяющая записывать в неё некоторые значения. Если записать 0×01, как на рисунке ниже, наша антипотеряйка начнет пищать. Если записать 0×00, она замолчит. Поэтому я хочу добавить в наше приложение две кнопки. Первая будет инвертировать битовый флаг и записывать его в характеристику 0×2A06.

Запись байта в характеристику 0x2A06

Запись байта в характеристику 0×2A06

Вторая кнопка будет считывать из характеристики 0×2A07 сервиса 0×1804 значение уровня мощности выходного сигнала. На рисунке ниже видно, что он равен 0×07.

Чтение бита из характеристики 0x2A07

Чтение бита из характеристики 0×2A07

Итак приступим. Добавим на нашу форму ещё две кнопки и привяжем их к границам экрана. Я сделал эти кнопки квадратными. Просто так, что бы разнообразить наше приложение:-) Вы можете сделать так же, в настройках справа, в разделе style или оставить всё как есть. Что бы всё было красиво надо немного подвигать другие элементы экрана.

Добавление новых кнопок

Добавление новых кнопок

Теперь давайте обозначим их как TX и RX и проинициализируем. В заголовке напишем

    Button txButton;
    Button rxButton;

А в поле инициализации

        txButton = findViewById(R.id.button3);
        txButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {CharactTx();}
        });
        rxButton = findViewById(R.id.button4);
        rxButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {CharactRx();}
        });

Инициализируем новые кнопки

Инициализируем новые кнопки

Здесь мы вводим новые функции CharactTx () и CharactRx (), обработку которых напишем ниже по тексту. В первой, мы будем формировать пакет на передачу в характеристику 0×2A06. А принимать ответ от стека мы будем в функции обратного вызова onCharacteristicWrite (). В данном случае, это будут статусы, показывающие была произведена запись или нет. Кроме того в строку textViewTemp будем писать что мы отправили в характеристику. Разместим эту функцию сразу после onServicesDiscovered ().

  @Override
  public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
      super.onCharacteristicWrite(gatt, characteristic, status);

      if (status == BluetoothGatt.GATT_SUCCESS) {
          textViewTemp.setText("Запись " + Integer.toString(alertStatus) + " успешна");
          Log.i(TAG, "onCharacteristicRead: Write characteristic: UUID: " + characteristic.getUuid().toString());
      } else if (status == BluetoothGatt.GATT_READ_NOT_PERMITTED) {
          Log.e(TAG, "onCharacteristicRead: Write not permitted for " + characteristic.getUuid().toString());
      } else {
          Log.e(TAG, "onCharacteristicRead: Characteristic write failed for " + characteristic.getUuid().toString());
      }
  }

Функция обратного вызова onCharacteristicWrite()

Функция обратного вызова onCharacteristicWrite ()

Добавим теперь и саму функцию CharactTx (), а вместо CharactRx () поставим заглушку.

    //************************************************************************************
    public boolean CharactTx()
    {
        if ((mBluetoothGatt == null) || (serviceAlert == null) || (characteristicAlert == null)) {
            return false;
        }
        alertStatus ^= 0x01;
        byte[] alert = new byte[1];
        alert[0] = (byte)alertStatus;

        characteristicAlert.setValue(alert);
        characteristicAlert.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

        if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_CONNECT}, 0);
        }
        mBluetoothGatt.writeCharacteristic(characteristicAlert);
        return true;
    }
    //************************************************************************************
    public void CharactRx()
    {
    }

Однако что бы всё заработало, нам снова придется добавить ряд элементов. В заголовок после инициализации кнопок введем новые элементы, а именно — флаг переключения тревоги и глобальные переменные сервисов и характеристик. Одни используем для передачи, другие для чтения.

    int alertStatus = 0;
    BluetoothGattService serviceAlert, servicePower;
    BluetoothGattCharacteristic characteristicAlert, characteristicPower;

Добавляем новые элементы

Добавляем новые элементы

Что бы обращаться к характеристикам, нужно знать их UUID и другие параметры. Проще всего их прочитать и сохранить в глобальных переменных, когда мы запрашивает весь список атрибутов. Для этого опять и снова вносим изменения в код :-) На этот раз в функцию onServicesDiscovered (). Когда мы увидим наши характеристики 0×2A06 и 0×2A07, мы сохраняем их значения. Проверять надо полный 128-ми битный UUID. На рисунке ниже я подчеркнул 16-ти битный UUID в составе полного 128-ми битного.

    if (characteristicUuid.equals("00002a06-0000-1000-8000-00805f9b34fb")) {
        characteristicAlert = characteristic;
        serviceAlert = service;
    }
    if (characteristicUuid.equals("00002a07-0000-1000-8000-00805f9b34fb")) {
        characteristicPower = characteristic;
        servicePower = service;
    }

Сохраняем нужные нам атрибуты

Сохраняем нужные нам атрибуты

Пора проверить как работает запись в характеристику. Запускаем iTAG, запускаем наше приложение. Нажмем на кнопку Start, а затем найдем в верхнем окне устройство iTAG и так же нажмем на него. В нижнем окне, через некоторое время, появится список атрибутов. Теперь нажмем на кнопку TX. В строке статуса появится надпись «Запись 1 успешна», а наша антипотеряйка начнет пищать. Нажмите на кнопку TX ещё раз — она замолчит. Ура, мы научились записывать данные в характеристику.

Запись данных в характеристику

Запись данных в характеристику

Теперь научимся считывать данные из характеристики. Мы уже предварительно многое подготовили для этого. Напишем две новые функции. Первая — это функция обратного вызова onCharacteristicRead (). Напишем её сразу после onCharacteristicWrite (). В обоих случаях, при чтении и записи данных, я использовал буферный подход. Что бы в будущем, когда будете обрабатывать больше одного байта, не надо было ничего переделывать.

public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {

    super.onCharacteristicRead(gatt, characteristic, status);
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.i(TAG, "onCharacteristicRead: Read characteristic: UUID: " + characteristic.getUuid().toString());

        byte[] valueInputCode = new byte[characteristic.getValue().length];
        System.arraycopy(characteristic.getValue(), 0, valueInputCode, 0, characteristic.getValue().length);

        StringBuffer sb1 = new StringBuffer();
        for (int j = 0; j < valueInputCode.length; j++) {
            sb1.append(String.format("%02X", valueInputCode[j]));
        }
        Log.i(TAG, "onCharacteristicRead: Value: " + sb1);
        textViewTemp.setText("Уровень мощности = " + sb1.toString());

    } else if (status == BluetoothGatt.GATT_READ_NOT_PERMITTED) {
        Log.e(TAG, "onCharacteristicRead: Read not permitted for " + characteristic.getUuid().toString());
    } else {
        Log.e(TAG, "onCharacteristicRead: Characteristic read failed for " + characteristic.getUuid().toString());
    }
}

Функция обратного вызова onCharacteristicRead()

Функция обратного вызова onCharacteristicRead ()

Вторая функция — запрос чтения характеристики. Заглушка этой функции у нас уже была. Теперь наполним её содержанием. Что бы Lint не ругался, заблокируем его.

    @SuppressLint("MissingPermission")
    public void CharactRx()
    {
        mBluetoothGatt.readCharacteristic(characteristicPower);
    }

Обработка нажатия кнопки RX

Обработка нажатия кнопки RX

Запускаем iTAG, наше приложение и делаем всё то же что и раньше, когда проверяли передачу данных. После получения из устройства списка сервисов и характеристик, нажимаем кнопку RX. В статусной строке должна появиться надпись об уровне сигнала, я подчеркнул её красным. Возможно для этого придется немного растянуть статусную строку, как я показал на рисунке.

.

Заключение

Вот и подошел к концу мой цикл статей об андроиде. Я постарался максимально подробно научить вас писать простые программы для управления BLE устройствами. Я думаю он пригодится многим программистам железа. Мне в своё время не удалось найти что то похожее, поэтому пришлось разбираться самому. Если есть какие то замечания — пишите. Но только по существу, а ещё лучше напишите сами хорошую статью. Этим вы и себе заработаете бонусы и поможете другим. Я обновил свой проект на GitHub-е с учетом сделанных доработок. Желаю всем успехов в профессиональном росте.

Печерских Владимир

Сотрудник Группы Компаний «Цезарь Сателлит»

© Habrahabr.ru