
В этой части мы соберём прошивку для ESP и подключимся к интеграции.
-
Часть 2. Аппаратная часть (вы здесь)
-
Часть 3. Теория по аддонам Home Assistant + Установка ZigBridge
Аппаратная часть
Для реализации задачи был выбран микроконтроллер ESP32-C6, который имеет на борту модуль для работы с Zigbee. В среднем такой стоит до 600 рублей на разных площадках. Конкретно в моём случае используется ESP32-C6 Mini.
Внешний вид платы
Программная часть
Интеграция с Home Assistant будет самой сложной и объемной частью. Вся интеграция будет упакована в Docker контейнер, основной язык был выбран Python, фреймворк Flask. Для ESP основной язык будет С, поскольку за среду разработки мы берём ESP-IDF.
Меньше слов, больше дела, а поэтому…
Подготовка окружения
Ну что же, начнем с подготовки рабочей среды для написания кода под ESP, а именно с установки ESP-IDF. Все дальнейшие действия я буду делать только в Visual Studio Code.
Пропустим установку VS Code и приступим к установке расширения, через которое всё и будет работать. Достаточно перейти в раздел Extensions и вбить в поиск «ESP-IDF» или перейти по ссылке
После установки расширения скачиваем проект из репозитория и открываем проект в VS Code, ждем инициализации и видим такое окно (возможно откроется главное меню, тогда в нём надо нажать кнопку «Configure extension»):
Выбираем вариант Express, ставим галочку «Show all tags» и выбираем из списка релиз версии 5.2.1, далее выбираем пути для сохранения дистрибутивов и жмём Install (При выборе места сохранения стоит учесть, что дистрибутив IDF имеет вес в несколько Гб).
После завершения установки окружение готово к работе.
Основная часть прошивки
Не будем рассматривать всю прошивку целиком, иначе это займет очень много времени, перейдем сразу к ключевым моментам работы с ESP.
Схематично логика работы выглядит следующим образом
Функция инициализации
код
void app_main(void){ esp_zb_platform_config_t config = { .radio_config = ESP_ZB_DEFAULT_RADIO_CONFIG(), .host_config = ESP_ZB_DEFAULT_HOST_CONFIG(), }; esp_console_repl_t *repl = NULL; esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); /* Prompt to be printed before each line. * This can be customized, made dynamic, etc. */ repl_config.prompt = PROMPT_STR ">"; repl_config.max_cmdline_length = 128; esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl)); //hw_config.tx_gpio_num = 12; //hw_config.rx_gpio_num = 13; //ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl)); // Регистрация обработчика команды "init" esp_console_cmd_t initCommand = { .command = "init", .help = "Добавить числа в список", .hint = "<число1> <число2> ...", .func = &initCommandHandler, }; esp_console_cmd_t restartCommand = { .command = "rst", .help = "Перезагрузка устройства", .hint = NULL, .func = &restartCommandHandler, }; ESP_ERROR_CHECK(esp_console_cmd_register(&restartCommand)); ESP_ERROR_CHECK(esp_console_cmd_register(&initCommand)); ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_zb_platform_config(&config)); light_driver_init(LIGHT_DEFAULT_OFF); xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL); ESP_ERROR_CHECK(esp_console_start_repl(repl)); vTaskDelay(pdMS_TO_TICKS(1000)); // Пауза в 1 секунду}
В данной функции у нас размечаются возможные к использованию интерактивные команды, которыми мы и будем управлять нашей ESP, а именно команда rst и init. В аргумент .func указывается ссылка на выполняемую функцию.
esp_console_cmd_t initCommand = { .command = "init", .help = "Добавить числа в список", .hint = "<число1> <число2> ...", .func = &initCommandHandler, }; esp_console_cmd_t restartCommand = { .command = "rst", .help = "Перезагрузка устройства", .hint = NULL, .func = &restartCommandHandler, }; ESP_ERROR_CHECK(esp_console_cmd_register(&restartCommand)); ESP_ERROR_CHECK(esp_console_cmd_register(&initCommand));
Стоит обратить внимание, что в зависимости от варианта исполнения платы, доступ к серийному выводу USB в разных версиях платы делается по-разному, конкретно в случае с версией Mini порт доступен через JTAG.
esp_console_dev_usb_serial_jtag_config_t hw_config = ESP_CONSOLE_DEV_USB_SERIAL_JTAG_CONFIG_DEFAULT(); //возможен как JTAG, так и UART ESP_ERROR_CHECK(esp_console_new_repl_usb_serial_jtag(&hw_config, &repl_config, &repl));
Так же мы должны явно указать максимальную длину вводимой команды. Я указал 256, но если не планируется использовать все каналы, то размер можно и сократить.
repl_config.max_cmdline_length = 256;
И непосредственно запуск главного цикла.
light_driver_init(LIGHT_DEFAULT_OFF); //оставил инициализацию бортового светодиода, если кому-то надо (: xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 5, NULL);
Запуск главной функции
Первыми же строками в главной функции esp_zb_task() мы создаем семафор и заставляем его ожидать результата выполнения функции обработчика команды init, иначе ESP сразу попытается инициализироваться без данных о каналах. clustersSemaphore это глобальная переменная и будет доступна из любой функции.
clustersSemaphore = xSemaphoreCreateBinary(); if (clustersSemaphore == NULL) { ESP_LOGE("APP", "Ошибка: не удалось создать семафор"); // Обработка ошибки return; }
Тем временем, в функции обработки init команды мы ожидаем сообщения init и разбираем ту последовательность номеров каналов, которую передает интеграция. Функция addCluster создает сущность кластера, которая в рамках Zigbee и является набором атрибутов для управления устройством (в нашем случае это лампочка).
static int initCommandHandler(int argc, char **argv) { for (int i = 1; i < argc; i++) { // Преобразование строки в число и добавление в список int val = atoi(argv[i]); addCluster(val); } // Вывод содержимого списка в консоль (для тестирования) clusters_t *current = head; while (current != NULL) { printf("%d", current->val); if (current->next != NULL) { printf(", "); } current = current->next; } printf("\n"); if (head != NULL) { xSemaphoreGive(clustersSemaphore); } return ESP_OK;}static void addCluster(int val) { clusters_t *newCluster = (clusters_t *)malloc(sizeof(clusters_t)); if (newCluster == NULL) { ESP_LOGE("APP", "Ошибка: не удалось выделить память для нового элемента списка"); return; } newCluster->val = val; newCluster->next = NULL; if (head == NULL) { // Если список пуст, новый элемент становится головой и хвостом head = newCluster; tail = newCluster; } else { // Иначе добавляем новый элемент в конец списка tail->next = newCluster; tail = newCluster; }}
Как только мы завершаем обработку входящего набора каналов, пропускаем через семафор основную функцию и инициализируем непосредственно устройства по каналам. Всю портянку рассматривать небудем, в двух словах — Сначала мы инициализируем Zigbee стек, задавая все необходимые параметры, чтобы после запуска в эфир наша плата говорила «Я лампочка с такими‑то кластерами, у меня такой‑то производитель, модель и версия».
Код инициализации устройств
// Если список clusters пуст, ждем его заполнения if (xSemaphoreTake(clustersSemaphore, portMAX_DELAY) == pdTRUE) { /* initialize Zigbee stack */ esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZED_CONFIG(); esp_zb_init(&zb_nwk_cfg); /* basic cluster create with fully customized */ set_zcl_string(manufacturer, "kallibr44"); set_zcl_string(model, "emulated_light"); set_zcl_string(firmware_version, "0.0.1"); uint8_t dc_power_source; dc_power_source = 4; uint16_t undefined_value; undefined_value = 0x8000; /* identify cluster create with fully customized */ uint8_t identyfi_id; identyfi_id = 0; esp_zb_cluster_list_t *esp_zb_cluster_list = esp_zb_zcl_cluster_list_create(); //esp_zb_cluster_list_t *esp_zb_cluster_switch = esp_zb_zcl_cluster_list_create(); esp_zb_attribute_list_t *esp_zb_identify_cluster = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_IDENTIFY); esp_zb_identify_cluster_add_attr(esp_zb_identify_cluster, ESP_ZB_ZCL_CMD_IDENTIFY_IDENTIFY_ID, &identyfi_id); /* Basic cluster data*/ esp_zb_attribute_list_t *esp_zb_basic_cluster = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_BASIC); esp_zb_basic_cluster_add_attr(esp_zb_basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MANUFACTURER_NAME_ID, manufacturer); esp_zb_basic_cluster_add_attr(esp_zb_basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_MODEL_IDENTIFIER_ID, model); esp_zb_basic_cluster_add_attr(esp_zb_basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_SW_BUILD_ID, firmware_version); esp_zb_basic_cluster_add_attr(esp_zb_basic_cluster, ESP_ZB_ZCL_ATTR_BASIC_POWER_SOURCE_ID, &dc_power_source); /** важно указать именно номер 4, тогда координатор сможет чаще опрашивать наше устройство, поскольку не ожидает от него глубокого сна*/ esp_zb_on_off_light_cfg_t light_cfg = ESP_ZB_DEFAULT_ON_OFF_LIGHT_CONFIG(); //esp_zb_ep_list_t *esp_zb_on_off_light_ep = esp_zb_on_off_light_ep_create(HA_ESP_LIGHT_ENDPOINT, &light_cfg); esp_zb_attribute_list_t *zb_light_cfg = esp_zb_on_off_cluster_create(&light_cfg); esp_zb_cluster_list_add_basic_cluster(esp_zb_cluster_list,esp_zb_basic_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); esp_zb_cluster_list_add_identify_cluster(esp_zb_cluster_list,esp_zb_identify_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); esp_zb_cluster_list_add_on_off_cluster(esp_zb_cluster_list,zb_light_cfg,ESP_ZB_ZCL_CLUSTER_SERVER_ROLE); //esp_zb_ep_list_update_ep(esp_zb_on_off_light_ep, esp_zb_cluster_list, 1, ESP_ZB_AF_HA_PROFILE_ID, ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID); esp_zb_ep_list_t *esp_zb_ep_list = esp_zb_ep_list_create(); clusters_t *current = head; while (current != NULL) { esp_zb_endpoint_config_t ep_config = { .endpoint = current->val, .app_device_id = ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID, .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID }; esp_zb_ep_list_add_ep(esp_zb_ep_list, esp_zb_cluster_list,ep_config); //printf("Обработка значения: %d\n", current->val); current = current->next; } // Освобождение памяти при завершении программы clusters_t *current_free = head; while (current_free != NULL) { clusters_t *next = current_free->next; free(current_free); current_free = next; } esp_zb_device_register(esp_zb_ep_list); esp_zb_core_action_handler_register(zb_action_handler); esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); ESP_ERROR_CHECK(esp_zb_start(false)); esp_zb_main_loop_iteration(); }
Вся магия происходит в конце этой функции, а именно в зоне создания итоговых конфигураций: мы берём наш список каналов (глобальная переменная head) и начинаем штамповать эти конфигурации перед инициализацией, после чего, передаем на запуск собранный массив конфигов.
//esp_zb_ep_list_update_ep(esp_zb_on_off_light_ep, esp_zb_cluster_list, 1, ESP_ZB_AF_HA_PROFILE_ID, ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID); esp_zb_ep_list_t *esp_zb_ep_list = esp_zb_ep_list_create(); clusters_t *current = head; while (current != NULL) { esp_zb_endpoint_config_t ep_config = { .endpoint = current->val, .app_device_id = ESP_ZB_HA_ON_OFF_LIGHT_DEVICE_ID, .app_profile_id = ESP_ZB_AF_HA_PROFILE_ID }; esp_zb_ep_list_add_ep(esp_zb_ep_list, esp_zb_cluster_list,ep_config); //printf("Обработка значения: %d\n", current->val); current = current->next; } // Освобождение памяти при завершении программы clusters_t *current_free = head; while (current_free != NULL) { clusters_t *next = current_free->next; free(current_free); current_free = next; } esp_zb_device_register(esp_zb_ep_list); esp_zb_core_action_handler_register(zb_action_handler); esp_zb_set_primary_network_channel_set(ESP_ZB_PRIMARY_CHANNEL_MASK); ESP_ERROR_CHECK(esp_zb_start(false)); esp_zb_main_loop_iteration();
В итоге данных манипуляций, в сети появляется один физический девайс, который говорит, что я в себе содержу еще (список каналов с команды init) устройств-лампочек. Для zigbee сети, эти виртуальные «лампочки» являются независимыми девайсами.
Заключительная часть прошивки
И напоследок рассмотрим обработку входящего события от координатора. За это отвечает функция zb_action_handler, которую мы зарегистрировали выше в инициализации устройств.
static esp_err_t zb_attribute_handler(const esp_zb_zcl_set_attr_value_message_t *message){ esp_err_t ret = ESP_OK; bool light_state = 0; ESP_RETURN_ON_FALSE(message, ESP_FAIL, TAG, "Empty message"); ESP_RETURN_ON_FALSE(message->info.status == ESP_ZB_ZCL_STATUS_SUCCESS, ESP_ERR_INVALID_ARG, TAG, "Received message: error status(%d)", message->info.status); ESP_LOGI(TAG, "Received message: endpoint(%d), cluster(0x%x), attribute(0x%x), data size(%d)", message->info.dst_endpoint, message->info.cluster, message->attribute.id, message->attribute.data.size); if (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF) { if (message->attribute.id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID && message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL) { light_state = message->attribute.data.value ? *(bool *)message->attribute.data.value : light_state; ESP_LOGI(TAG, "|{'cl':%d,'st':%d}|",message->info.dst_endpoint, *(bool *)message->attribute.data.value); light_driver_set_power(light_state); } } return ret;}
Каждое входящее событие проходит через эту функцию с передачей объекта message, который содержит исчерпывающую информацию о входящем сообщении. Сначала сообщение проходит встроенные фильтры через ESP_RETURN_ON_FALSE(), а далее мы проверяем, что входящее сообщение относится именно к тому, что мы ожидаем, а именно отсев кластера по имени ON_OFF (message->info.cluster == ESP_ZB_ZCL_CLUSTER_ID_ON_OFF), после отсеиваем атрибут кластера ON_OFF и то, что он имеет булево значение (message->attribute.id == ESP_ZB_ZCL_ATTR_ON_OFF_ON_OFF_ID &&
message->attribute.data.type == ESP_ZB_ZCL_ATTR_TYPE_BOOL) и когда мы убедились, что получили именно то, что хотели, отправляем результат в JSON формате на серийный порт (ESP_LOGI(TAG, «|{‘cl’:%d,’st’:%d}|»,message->info.dst_endpoint, (bool )message->attribute.data.value);)
Сборка и запуск прошивки
После того, как вы изучили и, если было необходимо, изменили код, нужно настроить взаимодействие с платой.

Пройдемся по пунктам меню:
-
Выбранная версия ESP-IDF
-
Режим работы с ESP. Необходимо выбрать UART, если прошиваете через usb.
-
COM порт с платой. Для изменения просто необходимо нажать на эту кнопку и выбрать из выпадающего списка нужный порт.
-
Модель ESP, под которую будет компилироваться прошивка.
-
Очистка кэша проекта. Полезно, когда изменения «залипают», отрицательно не влияет на работу, рекомендуется, если хочется сделать чистую сборку
-
Сборка проекта.
-
Прошивка ESP.
-
«Всё в одном» — по одной кнопке происходит сборка прошивки, загрузка в память ESP и открытие окна мониторинга.
Думаю тут не стоит на чем-то останавливаться, достаточно лишь сказать что для запуска ESP нам достаточно нажать кнопку под цифрой 8, предварительно выбрав нужный COM порт. На этом подготовка аппаратной части готова.
В следующей части разберёмся с интеграцией, её настройкой и релизом, для возможности установки её в Home Assistant.
ссылка на оригинал статьи https://habr.com/ru/articles/947514/