Я давно хотел себе домой СО2 сенсор, но то денег не хватает, то жаба квакает… А тут мне пришла заказанная плата на базе ESP32-C6 и я решил попробовать собрать сам, благо различных Ардуино модулей был целый ящик. Если Вам интересно что у меня получилось добро пожаловать под кат. (длинная портянка и много картинок)
Самым первым делом для работы необходимо установить среду разработки. В моем случае это ESP-IDF. В качестве визуального редактора я использую Visual Studio Code. Он есть для всех платформ. Загружаем и устанавливаем его ссылка на дистрибутив.
Запустив VSCode устанавливаем в нем расширение ESP-IDF.
Внимание! Расширение не любит русских символов в пути установки, возможны проблемы в работе. Лучше устанавливать или в корень диска или в отдельную папку.
Подождем немного когда расширение полностью установится.
После установки нам нужно склонировать проект с Github по ссылке.
В окне VSCode выбираем с левой стороны пункт Source Control (1), нажимаем clone_repository (2), вставляем адрес репозитория (3), нажимаем enter и указываем папку куда сохранить.
Теперь немного отвлечемся на теорию. Без базовых основ теории, рассказывать дальше смысла не имеет.
В стандарте ZigBee каждое устройство описывается определенным типом.
Самым верхним уровнем идет профиль устройства. Он определяет назначение устройства. В нашем случае стандартный профиль id 0x0104.
А уже внутри профиля идет описание устройства.
- Самый первый параметр это endpoint — конечная точка, которая содержит в себе полное описание конечного устройства. Внутри одного эндпоинта может быть множество различных моделей, но не повторяющихся. Нельзя создать 2 реле внутри одного эндпоинта, но можно создать два разных эндпоинта в каждом из которых будет одно реле. И в сети ваше устройство будет определяться как несколько устройств в одном.
- Второй параметр это cluster — кластер. Кластер — это совокупность атрибутов и набора команд, которые предназначены для манипуляции этими атрибутами. Например кластер температуры, или кластер влажности.
- Последний параметр это attribute — атрибуты. Атрибуты являются значениями которые описывают текущее состояние кластера. Совокупность атрибутов всех кластеров, поддерживаемых устройством, определяет состояние устройства. Например в кластере температуры атрибут измеренного значения будет содержать непосредственно саму температуру.
В своем устройстве я буду использовать датчики температуры, влажности, давления и углекислого газа. Повторяющихся у меня нет, соответственно конечная точка будет одна.
Теперь давайте поймем какие кластеры мне нужны. Для этого есть официальная документация, которая полностью описывает все кластеры и их спецификацию, pdf онлайн Zigbee Cluster Library.
Каждый кластер в какой то степени уникален и имеет свой собственный идентификатор, и свой собственный формат данных.
Zigbee Cluster Library должна быть у вас под рукой, чтобы всегда знать какой тип данных принимает тот или иной кластер в свои атрибуты.
Это действительно минимально необходимый набор знаний, для разработки своих устройств. Остальную логику обрабатывает ZigBee Stack, условно «драйвер» ZigBee сети, конкретно в sdk espressif используется ZigBee Stack Zboss.
По самым базовым основам пробежались, приступим к работе непосредственно с кодом.
В качестве отправной точки я буду рассматривать свой собственный пример. В нем, вроде как, реализован минимально необходимый набор для получения работоспособного устройства.
Откроем его в VSCode. Через File — Open folder. необходимо выбрать папку со скачанным проектом.
ESP-IDF основан на freertos.
Главным исполняемым файлом в проекте является esp_zigbee_co2.c, откроем его. И прокрутим до самого низа, до старта приложения.
xTaskCreate(esp_zb_task, "Zigbee_main", 4096, NULL, 6, NULL);
Это интересующая нас часть, непосредственно задача которая описывает структуру устройства ZigBee, его кластеры и атрибуты кластеров. Для минимального устройства хватает ее и еще одной задачи.
Перейдем непосредственно к ней:
Первым делом инициализируется сам stack, с описанием и параметрами.
/* initialize Zigbee stack */
esp_zb_cfg_t zb_nwk_cfg = ESP_ZB_ZR_CONFIG();
esp_zb_init(&zb_nwk_cfg);
После инициализации начинается описание кластеров и их атрибутов. Рассмотрим на примере температуры. Кластеры измерения достаточно хитрые, и требуют заполнения 3-х атрибутов для своей работы.
/* Temperature cluster */
esp_zb_attribute_list_t *esp_zb_temperature_meas_cluster = esp_zb_zcl_attr_list_create(ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT);
esp_zb_temperature_meas_cluster_add_attr(esp_zb_temperature_meas_cluster, ESP_ZB_ZCL_ATTR_TEMP_MEASUREMENT_VALUE_ID, &undefined_value);
esp_zb_temperature_meas_cluster_add_attr(esp_zb_temperature_meas_cluster, ESP_ZB_ZCL_ATTR_TEMP_MEASUREMENT_MIN_VALUE_ID, &undefined_value);
esp_zb_temperature_meas_cluster_add_attr(esp_zb_temperature_meas_cluster, ESP_ZB_ZCL_ATTR_TEMP_MEASUREMENT_MAX_VALUE_ID, &undefined_value);
- Первым делом создаем структуру, указываем что нам необходимо создать список атрибутов, для конкретного кластера. В нашем случае ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT
- Начинаем перечислять атрибуты необходимые для работы, и указываем им значения по умолчанию. Measurment_value, min_value, max_value
Почему необходимы все 3 атрибута? В стандарте ZigBee 3.0 реализован механизм reporting. Устройство будет передавать ту же температуру не все время, а в случае если текущее измеренное значение будет отличаться от предыдущего, и прошел запрограммированный интервал времени. Хотя можно отправлять и просто принудительно.
После заполнения атрибутов кластера, необходимо создать список кластеров устройства. Кластеров ведь может быть множество, и стек должен об этом знать.
/* Create full cluster list enabled on device */
esp_zb_cluster_list_t *esp_zb_cluster_list = esp_zb_zcl_cluster_list_create();
esp_zb_cluster_list_add_temperature_meas_cluster(esp_zb_cluster_list, esp_zb_temperature_meas_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
esp_zb_cluster_list_add_humidity_meas_cluster(esp_zb_cluster_list, esp_zb_humidity_meas_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
esp_zb_cluster_list_add_pressure_meas_cluster(esp_zb_cluster_list, esp_zb_press_meas_cluster, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
esp_zb_cluster_list_add_time_cluster(esp_zb_cluster_list, esp_zb_server_time_cluster, ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE);
esp_zb_cluster_list_add_custom_cluster(esp_zb_cluster_list, custom_co2_attributes_list, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE);
esp_zb_cluster_list_add_ota_cluster(esp_zb_cluster_list, esp_zb_ota_client_cluster, ESP_ZB_ZCL_CLUSTER_CLIENT_ROLE);
- Создаем структуру содержащую список кластеров
- Добавляем кластеры в список, и указываем их роль. Роли так же указаны в документации, для каждого кластера Но у сенсоров в большинстве своем SERVER_ROLE
И самый последний шаг, создаем список конечных точек, заполняем его и передаем стеку для начала работы.
esp_zb_ep_list_t *esp_zb_ep_list = esp_zb_ep_list_create();
esp_zb_ep_list_add_ep(esp_zb_ep_list, esp_zb_cluster_list, SENSOR_ENDPOINT, ESP_ZB_AF_HA_PROFILE_ID, ESP_ZB_HA_SIMPLE_SENSOR_DEVICE_ID);
/* END */
esp_zb_device_register(esp_zb_ep_list);
Тут мы передаем непосредственно номер конечной точки(endpoint), выбранный профиль ( id 0x0104), и примерный тип устройства (simple sensor).
Заполнив этот минимально необходимый набор данных мы получим минимально работающее зигби устройство, но внутри каждого атрибута будут данные установленные при старте. И все что вы увидите на вашем сервере будет 0 или undefined. Перейдем к следующей части.
Заполнение данных внутри атрибутов
В моем случае заполнением данных занимается функция update_attribute. Она запускается отдельной задачей, можно было бы сделать через встроенные в систему таймеры, но пока сделал так.
xTaskCreate(update_attribute, "Update_attribute_value", 4096, NULL, 5, NULL);
Для заполнения значения температуры необходимо поместить в атрибут measured_value значение температуры (заполняем только measured_value, значение MIN и MAX заполняются стеком), оно должно быть uint16, и представлять из себя температуру умноженную на 100, грубо 23.6 градусов будет выглядеть как значение 2360. Все это описано в zigbee cluster library.
/* Write new temperature value */
esp_zb_zcl_status_t state_tmp = esp_zb_zcl_set_attribute_val(SENSOR_ENDPOINT, ESP_ZB_ZCL_CLUSTER_ID_TEMP_MEASUREMENT, ESP_ZB_ZCL_CLUSTER_SERVER_ROLE, ESP_ZB_ZCL_ATTR_TEMP_MEASUREMENT_VALUE_ID, &temperature, false);
/* Check for error */
if(state_tmp != ESP_ZB_ZCL_STATUS_SUCCESS)
{
ESP_LOGE(TAG, "Setting temperature attribute failed!");
}
Непосредственно само заполнение выполняется одной функцией, которой мы передаем набор параметров, конечную точку устройства, название кластера, роль кластера, название атрибута, само значение атрибута, и указываем что не надо проверять роль кластера.
На этом описание процедуры передачи температуры в стек заканчивается, все достаточно просто. Теперь в нашем устройстве не только описана температура, но и заполняются ее значения.
Важное уточнение. Для получения значений температуры на сервере необходимо выполнить биндинг на кластер! Без биндинга значения приходить не будут, даже если их просто заполнять.
Теперь вы можете открыть любой пример из официального SDK ссылка и на базе них сделать свое собственное устройство.
Теперь углубимся, немного, непосредственно в сам ZigBee-SDK от Espressif. Он основан на ZBOSS как мы помним.
В нашем проекте в каталоге main присутствует файл idf_component.yml.
По аналогии с Ардуино ( кто работал ) это менеджер библиотек, которые применяются в проекте.
## IDF Component Manager Manifest File
dependencies:
espressif/button: "^3.0.0"
espressif/esp-zboss-lib: "~0.6.0"
espressif/esp-zigbee-lib: "~0.9.3"
## Required IDF version
idf:
version: ">=5.1.0"
Сами библиотеки располагаются по адресу https://components.espressif.com/
В проектах ZigBee подключатся две основные библиотеки esp-zboss-lib и esp-zigbee-lib.
Пока проект только открыт, но ни разу не собран вы ни увидите в коде ни одну библиотеку, поэтому давайте выполним компиляцию проекта.
В самой нижней части окна, необходимо нажать клавишу Build_project.
В процессе компиляции начнут подтягиваться исходники всех подключенных библиотек. В эксплорере проекта появится новый каталог managed_components.
Теперь ответ на самый главный вопрос, который наверное у всех возник. Откуда я беру переменные с именами кластеров и атрибутов.
Так как SDK еще в процессе доработки, то к сожалению доступный список кластеров сильно ограничен, и список кластеров доступных для использования располагается тут
managed_components\espressif__esp-zigbee-lib\include\zcl\esp_zigbee_zcl_common.h
А список доступных атрибутов описан для каждого кластера отдельно, они лежат в каталоге
managed_components\espressif__esp-zigbee-lib\include\zcl\
Вспомним что изначально проект представляет из себя набор сенсоров температуры, влажности, давления и CO2. Если с первыми тремя никаких проблем нет, они полностью есть в SDK, то вот с CO2 возникло сложности. Кластер CO2 в SDK не реализован. В дальнейшем он может появится, но на текущем этапе он отсутствует. Как поступить в такой ситуации?
В SDK существует специальная функция по созданию кастомных кластеров (не описанных в документации), но есть ограничение они должны быть вида 0xFxxx другие варианты в SDK запрещены.
Давайте рассмотрим как создается кастомный кластер.
#define CO2_CUSTOM_CLUSTER 0xFFF2 /* Custom cluster used because standart cluster not working*/
/* Custom cluster for CO2 ( standart cluster not working), solution only for HOMEd */
const uint16_t attr_id = 0;
const uint8_t attr_type = ESP_ZB_ZCL_ATTR_TYPE_U16;
const uint8_t attr_access = ESP_ZB_ZCL_ATTR_MANUF_SPEC | ESP_ZB_ZCL_ATTR_ACCESS_READ_ONLY | ESP_ZB_ZCL_ATTR_ACCESS_REPORTING;
esp_zb_attribute_list_t *custom_co2_attributes_list = esp_zb_zcl_attr_list_create(CO2_CUSTOM_CLUSTER);
esp_zb_custom_cluster_add_custom_attr(custom_co2_attributes_list, attr_id, attr_type, attr_access, &undefined_value);
Для создания кастомного кластера СО2 мы полностью описываем характеристики его атрибута.
- Присваиваем номер атрибута, в моем случае атрибут будет 0
- Указываем тип данных атрибута, uint16
- Указываем разрешения для атрибута, что он специальный, его можно только читать и он может отсылать репорты
Через функцию
esp_zb_custom_cluster_add_custom_attr
заполняем атрибуты у нашего кастомного кластера.Теперь наша прошивка готова полностью. Соберем прошивку и загрузим на плату.
Финальная часть. Собранный девайс, и тест на реальных системах
Сам устройство я спаял на макетной плате и распечатал для него простенький корпус на 3д принтере.
После заливки прошивки и запуска, девайс будет искать доступную для подключения ZigBee сеть.
Давайте подключим его к Home Assistant c интеграцией ZHA.
Подключение произошло успешно и почти все работает, но из-за того что СО2 у нас в кастомном кластере, ZHA не может его распознать, так же ведет себя и Z2M. В репозитории официального SDK я создал issue ссылка и когда добавят кластер в официальную поддержку, то устройство будет подключаться везде, без каких либо проблем.
А пока мне пришел на помощь @voznemozhno со своим замечательным проектом HOMEd, простая и очень шустрая альтернатива ZHA и Z2M.
Больше всего HOMEd похож по функционалу на Z2M, как и Z2M он с помощью mqtt прокидывает в Home Assistant доступные ZigBee устройства.
Он добавил полную поддержку моей поделки в свой координатор.
И HOMEd умеет обновлять данный девайс через OTA
На этом все.
Полезные ссылки:
Группа разработчиков ZigBee девайсов
Группа по HOMEd
Комментарии (20)
ColdSUN
11.09.2023 07:29Разработчиков бы из Туи бить по голове распечаткой Zigbee Cluster Library, пока не перестанут городить кучу кастомных кластеров под стандартные вещи.
gudvinr
11.09.2023 07:29+1Проблема с новыми ESP в том, что почти нереально купить модули. Есть девкиты, но модулей фактически нет в рознице.
С девкитами особо каши не сваришь. Один-два уникальных девайса сделать можно, но какую-то мелкую серию для тестов или даже просто пара девайсов, допустим как у вас, для разных зон, это уже не очень удобно.
Из доступных ZB плат есть XT-ZB1, но они не очень сильно распространены в DIY
lenz1986 Автор
11.09.2023 07:29Я купил модуля на али ... особо проблем не видел.
gudvinr
11.09.2023 07:29+1У вас в статье девкит, а не модуль. Буквально несколько недель назад модулей на али не было вообще, а сейчас есть только одно предложение для H2, и то там они продаются поштучно, без возможности купить сразу несколько (что практически всегда серьезно снижает цену).
С C6 предложений может быть побольше, но C6 для конечных устройств вроде датчиков слишком жирный по возможностям.
lenz1986 Автор
11.09.2023 07:29Между ними разница по цене не сильно большая. И при закупке массово я бы смотрел на алибабе а не на алиэкспресс.
Heggi
ZHA распознала девайс без каких-либо дополнительных обработчиков?
Я тут баловался с прошивками от pvto, так ZHA без кастомного обработчика, настроенного на сигнатуру конкретного девайса, в упор работать не хотела.
После чего махнул рукой на zigbee в целом, т.к. писать кастомные обработчики для всяких поделок не хотелось совсем.
lenz1986 Автор
"но из-за того что СО2 у нас в кастомном кластере, ZHA не может его распознать, так же ведет себя и Z2M "
а температура, влажность и давление на скрине.
Heggi
Про СО2 понятно, я удивился что температура, влажность и давление заработали из коробки и без плясок с бубном.
Видимо я в своих экспериментах что-то не так делал.
lenz1986 Автор
в ZigBee SDK от ESP в этом плане все очень удобно, четко по документации. А ZHA очень любит работать четко по документации
Heggi
Это повод возобновить свои опыты но уже на другом чипе!
lenz1986 Автор
А вот так его видит Yandex
voznemozhno
До определенного момента в прошивках PTVO любые данные лежали в кластере Analog Input и различить их было фактически невозможно, не зная заведомо что там лежит. Сейчас разработчик PTVO перенес основные данные вроде температуры и влажности в соответствующие кластеры, поэтому программные шлюзы, умеющие "распознавать" устройства по списку кластеров, стали лучше дружить с PTVO.
Iv38
PTVO же умеет генерировать конвертеры для z2m вместе с прошивкой. Или вам принципиально именно ZHA использовать?
Heggi
ZHA принципиально, т.к. использую именно его и переделывать всё не горю желанием.