© 2026 DiyTronic

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

«Попробуй объяснить другому - поймёшь сам.»

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

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

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

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

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

#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 устройство

$ 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 есть набор макросов. В частности для нашего случая создадим следующие:

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 сервиса.

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.

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) выглядит так:

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, которая как раз под такие вещи и заточена. Но в данном случае и таааак сойдёт.

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

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

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

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

Источники

Комментарии