banner
Object Detector and MQTT in Zephyr RTOS. Part I.

В статье рассмотрен исходный код (in c language) проекта MQTT написаного и опубликованного разработчиками Nordic Semiconductor в GitHub. Для чего это потребовалось? Исходная задача состоит в том, чтобы связать, программу Object Detector и программу управления промышленным оборудованием посредством протокола MQTT. Под промышленным оборудованием понимются различные типы устройств, снимающих информацию с датчиков и управляющих приводами. Понятно, что прежде чем делать новый проект неплохо взглянуть на уже готовый.
Проект MQTT был скомпилирован под OS Windows 10 с использованием nRF Connect SDK v2.9.0, затем запущен на плате nRF7002 DK. С целью отладки в качестве второго клиента использована программа Desktop MQTTX. Она эмулирует функции программы Object Detector. При проверке взаимодействия между программами данные отправлялись с Desktop MQTTX, принимались платой nRF7002 DK и отображались в терминале VS Code. И наоборот, данные отправлялись программой на плате, принимались и отображались в Desktop MQTTX. Во второй части (Part 2) статьи будет описана программа Object Detector с доработанной возможностью обмениваться данными по протоколу MQTT с клиентом на плате nRF7002 DK. В качестве брокера для связи между клиентами использовался broker.emqx.io
В сети есть несколько статей по теме MQTT на Zephyr RTOS. Эти статьи дополняют официальную документацию. Например, NRF7002 MQTT Client Example. Статья эта может быть полезена тем, что содержит образец настроек Wi-Fi и MQTT в файле prj.conf. Подробное описание процедуры запуска проекта MQTT и модулей можно найти в статье от Nordic Semiconductor.
Распознавание объектов и управление оборудованием - это суть очень разные по типу задачи. Такие задачи могут быть разнесены по разным процессорам и разным OS. Для такого решения есть несколько причин. Так, RTOS оперирует временем отклика, измеряемым в микросекундах или миллисекундах. И главное в RTOS то, что время отклика и время выполнения кода не превышают допустимых значений. У обычных OS, в которых работает программа Object Detector, время реакции на событие может быть очень разным. Object Detector не только классифицирует объект, но и обеспечивает GUI с пользователем. На это уходят значительное время и ресурсы процессора. Вероятно для задач распознавания объектов и управления оборудованием лучше использовать отдельные процессоры с разными OS. Связь между задачами можно реализовать посредством протокола, подходящего для конкретного случая.
Короткое резюме. Для управления оборудованием выбрана Zephyr RTOS и отладочный комплект nRF7002 DK для оценки и разработки. Для программы Object Detector подойдет любая OS (Windows, macOS, Linux и так далее) с установленными пакетами Qt/C++, OpenCV, PostgreSQL, CMake, и соответвующими compilers and linkers. В качестве протокола связи выбран MQTT.
Это двадцать четвертая статья из цикла "Real-Time Object Recognition". Первые двадцать три опубликованы здесь: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22 и 23.
1. Список программ в проекте.
Для установки инструментария Zephyr настоятельно рекомендую воспользоваться инструкцией.
2. Список исходных файлов проекта.
3. Threads and Zephyr bus.
Threads.
Общие понятия многопоточных приложений в Zephyr RTOS изложены в Nordic Developer Academy в Lesson 7. А это ссылки на описание потоков и API-интерфейс в документации.
Задачи модулей проекта MQTT: Trigger, Sampler, Transport, Network работают в отдельных потоках. Потоки статически определяются и инициализируются в файлах этих модулей макросами K_THREAD_DEFINE(...).
Zephyr bus.
Zephyr bus это программная шина, позволяющая потокам взаимодействовать друг с другом по схеме «многие ко многим». Вот ссылки на описание шины и API-интерфейс в документации.
Zephyr bus реализована посредством инструментария в виде каналов ZBUS_CHAN_DEFINE(...), сообщений ZBUS_MSG_INIT(...), подписчиков ZBUS_SUBSCRIBER_DEFINE(...), слушателей ZBUS_LISTENER_DEFINE и других макросов, структур и функций.
В проекте MQTT каналы определяются в файле common/message_channel.c. Общая схема взаимодействия модулей в проекте описана в этом документе.
4. Common.
В файлах этого модуля определяются каналы для сообщений между потоками. Откройте в браузере файл message_channel.c. В строках 13-43 находятся макросы ZBUS_CHAN_DEFINE(...). Эти макросы определяют каналы сообщений между потоками. В состав макросов входит параметр ZBUS_OBSERVERS(...). В общем случае это список наблюдателей. Посмотрите, например, на текст в строках 13-19. Здесь создается канал для связи между потоками в модулях Trigger и Sampler. Сами потоки определяются и инициируются в файлах trigger.c, transport.c, sampler.c, networks.c одноименных модулей.
5. Trigger.
В документации на проект назначение модуля указывается так: "Отправляет сообщения по триггерному каналу с интервалом, установленным параметром CONFIG_MQTT_SAMPLE_TRIGGER_TIMEOUT_SECONDS, и при нажатии кнопки."
Откройте файл trigger.c. Более конкретно назначение модуля Trigger может звучать так: инициация событий и публикация сообщений в канале TRIGGER_CHAN. Работа модуля начинается с создания потока в строках 58-60 исходного текста. После статического определения и инициализации потока, управление передается функции static void trigger_task(void), которая переходит в бесконечный цикл while (true) - смотрите строку 52. Интервал времени между сообщениями определяется константой CONFIG_MQTT_SAMPLE_TRIGGER_TIMEOUT_SECONDS. Публикации формируются в функции static void message_send(void) вызовом функции zbus_chan_pub(...). Публикация может быть инициирована обработчиком событий в функции static void button_handle(...) при нажатии на кнопку в nRF7002 DK.
6. Sampler.
Назначение этого модуля в документации на проект сформулировано следующим образом:
  1. Производится выборка данных при каждом получении сообщения по каналу запуска;
  2. Отобранная полезная нагрузка отправляется по каналу полезной нагрузки.
Откройте файл sampler.c. Если говорить более детально, то назначение модуля состоит в том, чтобы слушать канал TRIGGER_CHAN и по приходу сообщения отправить отформатированную строку #define FORMAT_STRING "Hello MQTT! Current uptime is: %d" в канал PAYLOAD_CHAN. В строках 56-58 определяется и инициализируется поток sampler_task_id. Внутри потока запускается функция static void sampler_task(void). Эта функция слушает канал TRIGGER_CHAN. По получении сообщения запускается функция static void sample(void), в ней в канал PAYLOAD_CHAN передается строка c полезным сообщением.
7. Network.
Назначение этого модуля в документации на проект указано следующим образом:
  1. После загрузки устройство автоматически подключается к Wi-Fi или LTE, в зависимости от платы и примерной конфигурации;
  2. Отправляет сообщения о состоянии сети по сетевому каналу.
Откройте файл network.c. В строках 109-111 исходного кода определен и инициирован новый поток с именем network_task_id. Во вновь созданном потоке запускается функция static void network_task(void), что находится в строках 67-107. В строках 72-73 определяется функция обратного вызоыа l4_event_handler для протокола IPv4. В строках 76-77 определяется функция обратного вызова connectivity_event_handler для Wi-Fi. Сами функции:
static void l4_event_handler(struct net_mgmt_event_callback *cb, uint32_t event, struct net_if *iface) и
static void connectivity_event_handler(struct net_mgmt_event_callback *cb, uint32_t event, struct net_if *iface)
находятся в строках 28-54 и 56-65 соответственно. Затем функциями conn_mgr_all_if_up(true) и conn_mgr_all_if_connect(true) выполняется настройка сетевого интерфейса и подключение к сети.
8. Transport.
Назначение этого модуля в документации на проект сформулировано так:
  1. Обрабатывает соединение MQTT;
  2. Автоматически подключается и поддерживает соединение MQTT, пока доступна сеть;
  3. Получает сообщения о состоянии сети по сетевому каналу;
  4. Публикует сообщения, полученные по каналу полезной нагрузки, в настроенной теме MQTT.
Откройте в новой вкладке файл transport.c. В строках 387-389 определяется и инициируется поток stransport_task_id, который запускает функцию static void transport_task(void). К рассмотрению кода этой функции мы вернемся чуть позже. А сейчас обратите внимание на то, что в этом модуле используется State Machine Framework (SMF) с State Machine Framework API. Напомню, что SMF это конечный автомат с четко определенными состоянимями и условиями для переключения состояний. Структура для хранения состояний static struct s_object{...} объявлена в строках 52-64. Идея использования SMF состоит в том, чтобы определить возможные состояния автомата, затем зарегистрировать функции обратного вызова, которые будут выполняться при изменении состояния. В строке 41 декларируются внутренние состояния автомата enum module_state { MQTT_CONNECTED, MQTT_DISCONNECTED }. В строках 300-306 строится таблица состояний static const struct smf_state state[] для [MQTT_DISCONNECTED] и [MQTT_CONNECTED]:
  • disconnected_entry;
  • disconnected_run;
  • connected_entry;
  • connected_run;
  • connected_exit.
Что соответствует функциям обратного вызова:
  • static void disconnected_entry(void *o). Строки 220-230;
    Функция выполняется, когда модуль переходит в отключенное состояние.
  • static void disconnected_run(void *o). Строки 233-252;
    Функция выполняется, когда модуль находится в отключенном состоянии.
  • static void connected_entry(void *o). Строки 255-269;
    Функция выполняется, когда модуль переходит в подключенное состояние.
  • static void connected_run(void *o). Строки 272-290;
    Функция выполняется, когда модуль находится в подключенном состоянии.
  • static void connected_exit(void *o). Строки 293-298.
    Функция выполняется, когда модуль выходит из подключенного состояния.
Посмотрите внимательно на то, какие работы выполняются при вызове каждой из этих функций. Например, внутри функции static void connected_entry(void *o) отменяются все текущие действия по подключению, когда модуль переходит в подключенное состояние, затем выполняется функция subscribe() - это подписка на тему. А в функции static void connected_run(void *o) выполняется публикация сообщения с использованием функции publish(...).
Вернемся к описанию основной функции static void transport_task(void). В строках 314-321 определена структура struct mqtt_helper_cfg cfg, а в строке 333 выполнена функция инициации mqtt_helper_init(&cfg). Что такое MQTT Helper? Из документации узнаем, что это вспомогательная библиотека MQTT, которая служит для упрощения использования этого протокола в Zephyr. Обратите внимание на то, что значениям структуры соответствуют функции обратного вызова MQTT Helper:
  • static void on_mqtt_connack(enum mqtt_conn_return_code return_code, bool session_present). Строки 70-75;
  • static void on_mqtt_disconnect(int result). Строки 77-82;
  • static void on_mqtt_publish(struct mqtt_helper_buf topic, struct mqtt_helper_buf payload). Строки 84-90;
  • static void on_mqtt_suback(uint16_t message_id, int result). Строки 92-101.
Таким образом, в коде модуля Transport определяются два типа функций обратного вызова. Один тип функций для конечного автомата (SMF). Второй тип функций обратного вызова для MQTT Helper.
Основной цикл while (!zbus_sub_wait(&transport, &chan, K_FOREVER)) функции static void transport_task(void) находится в строках 343-385. В первой строке кода прграммма ждет уведомление от канала. Если это канал NETWORK_CHAN функция zbus_chan_read(&NETWORK_CHAN, &status, K_SECONDS(1)) прочитает данные из канала. Затем оператором _obj.status = status; изменяется статус структуры. Вызовом функции smf_run_state(SMF_CTX(&s_obj)) будет запущена одна итерация конечного автомата.
Переходы состояний автомата и вызовы функций MQTT Helper графически изображены на рисунке.
9. Учебные курсы от Nordic Semiconductor.
Евгений Вересов.
10.02.2025 года.