© 2023 DiyTronic

Мигаем светодиодом через Bluetooth

В одном из комментариев меня упрекнули — дескать «всё суета, а ты вот попробуй светодиодом поморгать через bluetooth». Ну что — упрёк справедлив и вызов принят. Начинаю серию статей по программированию под zephyr. В данной статье будет код для bluetooth устройства с одним светодиодом, которым можно управлять.

Я слегка напряжён — каждый раз когда громко заявляю про серию статей возникают обстоятельства непреодолимой силы, по которым каждый раз эта серия заканчивается на первой же статье. Но тем-не менее попробую.

Создаём минимальное bluetooth устройство

Итак простейший код для включения bluetooth является просто вызовом функции bt_enable в которую передаётся функция которая вызовется после завершения инициализации bluetooth стека. В этой функции мы вызываем функцию bt_le_adv_start в которую передаём данные для адвертайзинга и для сканирования.

Вот такого кода вполне достаточно для создания BLE устройства. Я сознательно удалил все проверки на ошибки и вызовы отладочных функций, чтобы не загромождать код и чтобы было более понятно. На самом деле конечно проверка кодов ошибок возвращаемых функциями нужна.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <zephyr.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>

#define DEVICE_NAME "Zephyr LED test"
#define DEVICE_NAME_LEN (sizeof(DEVICE_NAME) - 1)

static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
};

// Имя видимое при сканированиии
static const struct bt_data sd[] = {
BT_DATA(BT_DATA_NAME_COMPLETE, DEVICE_NAME, DEVICE_NAME_LEN),
};

// Функция вызываемая по завершении инициализации bluetooth
static void bt_ready(int err)
{
bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
}

void main(void)
{
bt_enable(bt_ready);
while (1) {}
}

Собственно запустив вот такой нехитрый код на устройстве мы получаем вполне рабочее bluetooth устройство

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ bluetoothctl
....
[NEW] Device EE:DC:1F:BE:87:F0 Zephyr LED test
Agent registered

[bluetooth]# info EE:DC:1F:BE:87:F0
Device EE:DC:1F:BE:87:F0 (random)
Name: Zephyr LED test
Alias: Zephyr LED test
Appearance: 0x0341
Paired: no
Trusted: no
Blocked: no
Connected: no
LegacyPairing: no
UUID: Generic Access Profile (00001800-0000-1000-8000-00805f9b34fb)
UUID: Generic Attribute Profile (00001801-0000-1000-8000-00805f9b34fb)

[bluetooth]# connect EE:DC:1F:BE:87:F0
Attempting to connect to EE:DC:1F:BE:87:F0
[CHG] Device EE:DC:1F:BE:87:F0 Connected: yes
Connection successful
[CHG] Device EE:DC:1F:BE:87:F0 ServicesResolved: yes

Минутка теории

Вообще когда начинаешь разбираться с работой bluetooth стека документация выглядит откровенно пугающей. Куча терминов — все эти HCI, GATT, Advertizing и пр. по крайней мере мне так точно просто выносили мозг и погружали в депрессию. Однако разобравшись как следует в теме всё оказалось не так пугающе.

Итак к нашему великому счастью все нюансы соединения за нас обрабатывает zephyr и это прекрасно. Поэтому останавливаться на этом больше не буду. Перейдём к более насущной теме — обмену данными.

Итак для обмена данными в bluetooth стеке есть такое понятие как GATT — некая таблица аттрибутов, которую мы можем читать и если разрешат то и писать. На самом деле это прекрасно — никакого бардака и всё чётенько. Читаем и пишем только то что можно и только в заранее отведённые места.

Ну и соответственно чтобы как-то организовать взаимодействие и обмен данными с нашим устройством мы должны в нём создать такую таблицу и добавить в неё некий аттрибут в который мы и будем писать. А уже устройство должно следить за значением этого аттрибута и как-то реагировать — в нашем случае поджигать или гасить светодиод.

Переходим от теории к практике

Создаём GATT таблицу

Собственно даже по умолчанию у любого устройства есть GATT таблица (возможно это zephyr добавляет — я это не рыл). Как минимум в ней есть имя и appearance (что-то вроде некоевого типа устройства). Аттрибуты GATT таблицы группируются в сервисы — вот тут (https://www.bluetooth.com/specifications/gatt/services) можно посмотреть список стандартных сервисов и аттрибутов. Сервисы в свою очередь группируются в профили.

Соответственно я создам сервис с одним аттрибутом. Ну и все элементы GATT имеют свой идентификатор UUID. Часть из них зарезервирована, а остальные можно использовать по своему усмотрению. Для создания UUID-ов в Zephyr есть набор макросов. В частности для нашего случая создадим следующие:

UUID-ы для сервиса и аттрибута
1
2
3
4
5
6
7
static struct bt_uuid_128 led_service_uuid = BT_UUID_INIT_128(
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

static struct bt_uuid_128 led1_uuid = BT_UUID_INIT_128(
0xf1, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12);

Далее в примере выше мы определяли массив ad для адвертайзера. В него нужно добавить UUID сервиса.

Прописываем advertizing аттрибуты
1
2
3
4
5
6
static const struct bt_data ad[] = {
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
0xf0, 0xde, 0xbc, 0x9a, 0x78, 0x56, 0x34, 0x12,
0x78, 0x56, 0x34, 0x12, 0x78, 0x56, 0x34, 0x12),
};

Ну и в хук, который вызывается после инициализации bluetooth стека добавим инициализацию GATT.

Регистрация GATT
1
2
3
4
5
static void bt_ready(int err)
{
bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad), sd, ARRAY_SIZE(sd));
bt_gatt_service_register(&led_svc); // вот это добавлено
}

А передаём мы в функцию bt_gatt_service_register как раз ссылку на нашу таблицу, на создании которой далее остановлюсь подробнее.

Итак создание таблицы аттрибутов (GATT) выглядит так:

Создание GATT таблицы
1
2
3
4
5
6
7
8
9
static struct bt_gatt_attr led_attrs[] = {
/* Vendor Primary Service Declaration */
BT_GATT_PRIMARY_SERVICE(&led_service_uuid),
BT_GATT_CHARACTERISTIC(&led1_uuid.uuid, BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE),
BT_GATT_DESCRIPTOR(&led1_uuid.uuid, BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT,
read_led1,
write_led1,
&led1_value)
};

Как видно из таблицы мы просто вызываем ряд макросов, в которые первым параметром передаём UUID, а затем набор параметров. Для сервиса у нас никаких параметров нет, у аттрибута (CHARACTERISTIC) только задаём права доступа, включая запись конечно., а вот в DESCRIPTOR кроме всего прочего передаём адреса функций для чтения и для записи этого аттрибута, ну и последним параметром адрес переменной, в которой будет храниться значение аттрибута. Её надо заранее объявить, впрочем как и функции.

Сами функции большого интереса не представляют посмотреть их можно в полной версии кода программы.

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

Проверка

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

Использовал я нордиковскую программу «nRF Connect». Итак смотрим как выглядит наше устройство:

Если нажать кнопку «Connect» то происходит подключение и мы можем прочитать список сервисов и аттрибутов:

Виден наш сервис и аттрибут. Видны они как Unknown т. к. являются нестандартными. Так-же видно значение аттрибута и кнопочки для чтения и записи.

Ну и отправляя значение отличное от нуля (обязательно вводить 2 знака, даже если у нас незначащий нуль) мы видим как на плате загорается светодиод. Соответственно вводя нулевое значение мы гасим светодиод.

Итоги

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

Вообще так размышляя на тему стоило бы управление светодиодом и работу с bluetooth внести в разные процессы, а взаимодействие сделать в виде обмена сообщениями. Тем более, что пишем-то мы под RTOS, которая как раз под такие вещи и заточена. Но в данном случае и таааак сойдёт.

Так-же осталась нераскрыта тема авторизации для доступа к записи атрибута, которая делается довольно просто.

Хоте записать видео, но что-то как-то пока ума не приложу как в один кадр засунуть экран, устройство и смартфон.

Полный код программы

Ниже привожу полный код программы:

Источники

Комментарии