BLE под микроскопом. Андроид. Часть3
часть 1, часть 2
Введение
Перед тем, как мы двинемся дальше, хочу сделать три уточнения. Первое — это дать ссылку на проект, созданный нами в прошлой статье и доработанный нами в текущей. Для тех, кому не хватило сил дойти прошлый урок до конца.
Второе — это разъяснить ошибку, которая связана с проверкой разрешений checkPermission. Как я выяснил, эту ошибку генерирует инструмент под названием Lint, который помогает разработчикам изловить потенциальные проблемы ещё до того, как код скомпилируется. Вот здесь об этом можно почитать. Если в двух словах, то это встроенный инструмент проверки кода. Ищет потенциальные ошибки. В нашем случае он возмущался на то, что мы не проверяем критические разрешения, перед тем как начать/закончить сканирование эфира. Но дело в том, что у нас они проверяются, правда в другой функции. Поэтому, что бы убрать сообщение об ошибке, надо перед функцией написать строчку, отменяющую проверку — @SuppressLint («MissingPermission»). Тогда мы отключим сообщение о конкретно этой ошибке. На самом деле, такие же команды так же стоят у функций onScanResult () и listShow (). Если вы помните, то за основу моего урока был взят проект le_scan_classic_connect, в котором и использовались подавляющие инструкции @SuppressLint («MissingPermission»). Так что в нашем коде, перед функциями startScanning () и stopScanning (), их так же надо прописать.
Третье уточнение касается отладки приложения. Есть очень удобная штука, называется Logcat. Если вы посмотрите в среде AndroidStudio в левый нижний угол, то увидите там несколько различных закладок. Нас интересует закладка с кошкой. На рисунке внизу она открыта. Открыв эту закладу, во время исполнения нашей программы, мы увидим много строк с указанием времени, источника лога события и самого лога.
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-ов
В этой части кода мы будем обрабатывать обратные вызовы 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 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
Затем ниже функции 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);
}
}
//****************************************************************************************
Далее всё как обычно, соглашаемся с импортом новых классов.
Обработка запросов сервисов и характеристик
Теперь попытаемся запустить наше приложение. Если мы всё сделали правильно, у нас не должно быть ошибок. Для того, что бы увидеть все возможности нашей программы, включим антипотеряйку и запустим приложение.
Давайте разберемся как получить данную картинку. После того как мы нажали кнопку 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.
Запись байта в характеристику 0×2A06
Вторая кнопка будет считывать из характеристики 0×2A07 сервиса 0×1804 значение уровня мощности выходного сигнала. На рисунке ниже видно, что он равен 0×07.
Чтение бита из характеристики 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 ()
Добавим теперь и саму функцию 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 ()
Вторая функция — запрос чтения характеристики. Заглушка этой функции у нас уже была. Теперь наполним её содержанием. Что бы Lint не ругался, заблокируем его.
@SuppressLint("MissingPermission")
public void CharactRx()
{
mBluetoothGatt.readCharacteristic(characteristicPower);
}
Обработка нажатия кнопки RX
Запускаем iTAG, наше приложение и делаем всё то же что и раньше, когда проверяли передачу данных. После получения из устройства списка сервисов и характеристик, нажимаем кнопку RX. В статусной строке должна появиться надпись об уровне сигнала, я подчеркнул её красным. Возможно для этого придется немного растянуть статусную строку, как я показал на рисунке.
.
Заключение
Вот и подошел к концу мой цикл статей об андроиде. Я постарался максимально подробно научить вас писать простые программы для управления BLE устройствами. Я думаю он пригодится многим программистам железа. Мне в своё время не удалось найти что то похожее, поэтому пришлось разбираться самому. Если есть какие то замечания — пишите. Но только по существу, а ещё лучше напишите сами хорошую статью. Этим вы и себе заработаете бонусы и поможете другим. Я обновил свой проект на GitHub-е с учетом сделанных доработок. Желаю всем успехов в профессиональном росте.
Печерских Владимир
Сотрудник Группы Компаний «Цезарь Сателлит»