Введение
Перед тем, как мы двинемся дальше, хочу сделать три уточнения. Первое - это дать ссылку на проект, созданный нами в прошлой статье и доработанный нами в текущей. Для тех, кому не хватило сил дойти прошлый урок до конца.
Второе - это разъяснить ошибку, которая связана с проверкой разрешений checkPermission. Как я выяснил, эту ошибку генерирует инструмент под названием Lint, который помогает разработчикам изловить потенциальные проблемы ещё до того, как код скомпилируется. Вот здесь об этом можно почитать. Если в двух словах, то это встроенный инструмент проверки кода. Ищет потенциальные ошибки. В нашем случае он возмущался на то, что мы не проверяем критические разрешения, перед тем как начать/закончить сканирование эфира. Но дело в том, что у нас они проверяются, правда в другой функции. Поэтому, что бы убрать сообщение об ошибке, надо перед функцией написать строчку, отменяющую проверку - @SuppressLint("MissingPermission"). Тогда мы отключим сообщение о конкретно этой ошибке. На самом деле, такие же команды так же стоят у функций onScanResult() и listShow(). Если вы помните, то за основу моего урока был взят проект le_scan_classic_connect, в котором и использовались подавляющие инструкции @SuppressLint("MissingPermission"). Так что в нашем коде, перед функциями startScanning() и stopScanning(), их так же надо прописать.
Третье уточнение касается отладки приложения. Есть очень удобная штука, называется Logcat. Если вы посмотрите в среде AndroidStudio в левый нижний угол, то увидите там несколько различных закладок. Нас интересует закладка с кошкой. На рисунке внизу она открыта. Открыв эту закладу, во время исполнения нашей программы, мы увидим много строк с указанием времени, источника лога события и самого лога.
![Logcat Logcat](https://habrastorage.org/getpro/habr/upload_files/5f9/166/712/5f91667123421a2151f6613f9585bde7.jpg)
Из текста нашего приложения нетрудно увидеть, что сообщения в лог отправляют строчки, начинающиеся с 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-ов](https://habrastorage.org/getpro/habr/upload_files/f11/975/24d/f1197524d3c69e7e2d7fda2dd0597175.jpg)
В этой части кода мы будем обрабатывать обратные вызовы 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, необходимо сделать ещё кое что. Нужно разблокировать вот эту строчку в блоке инициализации.
![Инициализация обратных вызовов Инициализация обратных вызовов](https://habrastorage.org/getpro/habr/upload_files/e89/8e5/83b/e898e583be8124a6e73d301626c28d52.png)
А так же проинициализировать объект mBluetoothGatt. Кроме того, что бы наши обратные вызовы не раздражали инструмент статического анализа Lint, перед функцией onCreate(), необходимо вставить инструкцию @SuppressLint("MissingPermission"). Эти два дополнения подчеркнуты на рисунке ниже.
![Дополняем код новым содержанием Дополняем код новым содержанием](https://habrastorage.org/getpro/habr/upload_files/4eb/25a/fbf/4eb25afbf8f0ca16521f6259d6043224.png)
Теперь, когда все текущие изменения готовы, давайте попробуем запустить проект. В поведении самого приложения мы ничего нового не увидим. Однако давайте применим наши новые знания относительно Logcat. Загрузите проект в смартфон, откройте в Android Studio закладку Logcat и, после сканирования эфира, попробуйте присоединиться к BLE устройству. Делается это простым нажатием на строчку с одним из найденных устройств. Тогда функция нажатия deviceListView.setOnItemClickListener() сработает и мы попадем в обратный вызов BLE onConnectionStateChange(). Если наш гаджет позволяет к нему присоединяться, рассылая пакеты типа ADV_IND, тогда мы увидим примерно следующий лог событий.
![Лог присоединения к устройству Лог присоединения к устройству](https://habrastorage.org/getpro/habr/upload_files/d7a/78d/b61/d7a78db61cb59ea70aec3748dad6d199.png)
Внимательно рассмотрите его. Часть строк принадлежит самому BLE стеку, а другая часть - дело наших рук :-) В дальнейшем, отправляя в Logcat сообщения, мы сможем понять в каком состоянии находится наше приложение и исправлять возникающие ошибки. Давайте двигаться дальше. Для этого немного подправим наше приложение.
Чтение UUID сервисов и характеристик
Для работы с сервисами и характеристиками, нам необходимо их как то показывать на экране. Я сначала планировал делать вывод их в другое окно, но решил не усложнять. Давайте просто сдвинем наш listView вверх и добавим ещё один такой же элемент на нашу форму. Сразу привяжем его к краям экрана. Среда дает ему имя listView2.
![Добавление нового listView Добавление нового listView](https://habrastorage.org/getpro/habr/upload_files/aaa/940/c95/aaa940c9509bb5a28e5ed18e2efdaf84.png)
Мы же в нашем файле активности добавим такой заголовок. Все эти элементы нам нужны для управления новым элементом, который назовем листом сервисов и характеристик :-)
ListView listServChar;
ArrayAdapter<String> adapterServChar;
ArrayList<String> listServCharUUID = new ArrayList<>();
![Добавление листа сервисов и характеристик Добавление листа сервисов и характеристик](https://habrastorage.org/getpro/habr/upload_files/f72/890/997/f728909977fba4daa81a1d823ff97923.png)
В разделе инициализации обозначим новый элемент, а так же укажем функцию обработки нажатия на его строчки. Назовем её 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](https://habrastorage.org/getpro/habr/upload_files/7c6/c12/347/7c6c12347eda4d8b5506c40fc426c1fc.png)
Затем ниже функции 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);
}
}
//****************************************************************************************
Далее всё как обычно, соглашаемся с импортом новых классов.
![Обработка запросов сервисов и характеристик Обработка запросов сервисов и характеристик](https://habrastorage.org/getpro/habr/upload_files/50a/194/854/50a1948540e2db132bff739229aa37c9.png)
Теперь попытаемся запустить наше приложение. Если мы всё сделали правильно, у нас не должно быть ошибок. Для того, что бы увидеть все возможности нашей программы, включим антипотеряйку и запустим приложение.
![](https://habrastorage.org/getpro/habr/upload_files/d7d/ad7/d00/d7dad7d00b685c1006a4d74388c85775.png)
Давайте разберемся как получить данную картинку. После того как мы нажали кнопку 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](https://habrastorage.org/getpro/habr/upload_files/01d/9a3/2c6/01d9a32c660daba9cc78094752391c5e.png)
Вторая кнопка будет считывать из характеристики 0x2A07 сервиса 0x1804 значение уровня мощности выходного сигнала. На рисунке ниже видно, что он равен 0x07.
![Чтение бита из характеристики 0x2A07 Чтение бита из характеристики 0x2A07](https://habrastorage.org/getpro/habr/upload_files/9bf/1a5/c24/9bf1a5c24544ecec9803988f84c08c5b.png)
Итак приступим. Добавим на нашу форму ещё две кнопки и привяжем их к границам экрана. Я сделал эти кнопки квадратными. Просто так, что бы разнообразить наше приложение :-) Вы можете сделать так же, в настройках справа, в разделе style или оставить всё как есть. Что бы всё было красиво надо немного подвигать другие элементы экрана.
![Добавление новых кнопок Добавление новых кнопок](https://habrastorage.org/getpro/habr/upload_files/6bb/780/cb4/6bb780cb42b9a4d275163361106be9c9.png)
Теперь давайте обозначим их как 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();}
});
![Инициализируем новые кнопки Инициализируем новые кнопки](https://habrastorage.org/getpro/habr/upload_files/98f/bda/325/98fbda32545c9e26fc6c31e45d154cb6.png)
Здесь мы вводим новые функции 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()](https://habrastorage.org/getpro/habr/upload_files/c51/a80/ab7/c51a80ab753daa80184f3bf6ba7a44b0.png)
Добавим теперь и саму функцию 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;
![Добавляем новые элементы Добавляем новые элементы](https://habrastorage.org/getpro/habr/upload_files/c9d/ade/121/c9dade121c8c463a1a0ab69e71135ff2.png)
Что бы обращаться к характеристикам, нужно знать их 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;
}
![Сохраняем нужные нам атрибуты Сохраняем нужные нам атрибуты](https://habrastorage.org/getpro/habr/upload_files/c01/4da/99c/c014da99cba5db59351fd655b0995bb9.png)
Пора проверить как работает запись в характеристику. Запускаем iTAG, запускаем наше приложение. Нажмем на кнопку Start, а затем найдем в верхнем окне устройство iTAG и так же нажмем на него. В нижнем окне, через некоторое время, появится список атрибутов. Теперь нажмем на кнопку TX. В строке статуса появится надпись "Запись 1 успешна", а наша антипотеряйка начнет пищать. Нажмите на кнопку TX ещё раз - она замолчит. Ура, мы научились записывать данные в характеристику.
![Запись данных в характеристику Запись данных в характеристику](https://habrastorage.org/getpro/habr/upload_files/6f0/326/860/6f0326860001f2e58c227d6be84afdc5.png)
Теперь научимся считывать данные из характеристики. Мы уже предварительно многое подготовили для этого. Напишем две новые функции. Первая - это функция обратного вызова 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()](https://habrastorage.org/getpro/habr/upload_files/596/5a9/6f6/5965a96f6ac83126b3ddf1c9f2926a9b.png)
Вторая функция - запрос чтения характеристики. Заглушка этой функции у нас уже была. Теперь наполним её содержанием. Что бы Lint не ругался, заблокируем его.
@SuppressLint("MissingPermission")
public void CharactRx()
{
mBluetoothGatt.readCharacteristic(characteristicPower);
}
![Обработка нажатия кнопки RX Обработка нажатия кнопки RX](https://habrastorage.org/getpro/habr/upload_files/cc3/277/e12/cc3277e121dc299ab71b6fea749da22e.png)
Запускаем iTAG, наше приложение и делаем всё то же что и раньше, когда проверяли передачу данных. После получения из устройства списка сервисов и характеристик, нажимаем кнопку RX. В статусной строке должна появиться надпись об уровне сигнала, я подчеркнул её красным. Возможно для этого придется немного растянуть статусную строку, как я показал на рисунке.
![](https://habrastorage.org/getpro/habr/upload_files/471/b6d/1b5/471b6d1b509bfe41ebeb7fb084a5b08f.png)
.
Заключение
Вот и подошел к концу мой цикл статей об андроиде. Я постарался максимально подробно научить вас писать простые программы для управления BLE устройствами. Я думаю он пригодится многим программистам железа. Мне в своё время не удалось найти что то похожее, поэтому пришлось разбираться самому. Если есть какие то замечания - пишите. Но только по существу, а ещё лучше напишите сами хорошую статью. Этим вы и себе заработаете бонусы и поможете другим. Я обновил свой проект на GitHub-е с учетом сделанных доработок. Желаю всем успехов в профессиональном росте.
Печерских Владимир
Сотрудник Группы Компаний «Цезарь Сателлит»