часть 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<String> adapterServChar;
    ArrayList<String> 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<BluetoothGattService> services = gatt.getServices();
            runOnUiThread(() -> {

                for (int i = 0; i < services.size(); i++) {
                    BluetoothGattService service = services.get(i);
                    List<BluetoothGattCharacteristic> 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);
    }
}
//****************************************************************************************

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

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

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

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

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

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

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

Вторая кнопка будет считывать из характеристики 0x2A07 сервиса 0x1804 значение уровня мощности выходного сигнала. На рисунке ниже видно, что он равен 0x07.

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

Итак приступим. Добавим на нашу форму ещё две кнопки и привяжем их к границам экрана. Я сделал эти кнопки квадратными. Просто так, что бы разнообразить наше приложение :-) Вы можете сделать так же, в настройках справа, в разделе 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-е с учетом сделанных доработок. Желаю всем успехов в профессиональном росте.

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

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

Комментарии (0)