Перейти к содержимому

Отправка сообщений в Telegram на ESP32 с использованием фреймворка ESP-IDF

Добрый день, уважаемые читатели! Продолжаем тему работы c Telegram API, и сегодня мы вновь поговорим об отправке сообщений в Telegram, но на этот раз для ESP32 с использованием фреймворка ESP-IDF. ESP32 по сравнению с ESP8266 предоставляет программисту гораздо больше возможностей и позволяет не особо заботится о количестве активных TLS-соединений в системе. На этот раз я не планирую специально писать демонстрационное приложение, а постараюсь объяснить работу на примере моей библиотеки reTgSend применительно к проекту Термостата на ESP32 и ESP-IDF. Но библиотеку эту легко адаптировать и под любое другое примеyение на ESP-IDF.


У некоторых читателей может возникнуть вопрос: а как быть тем, кто программирует свои ESP32 “по старинке” в привычном Arduino-окружении? На мой взгляд, у Вас есть два варианта:

  • Использовать решение, предложенное в предыдущей статье, посвященной отправке сообщений в Telegram на ESP8266. Но код придется немного “обработать напильником” , так как “сетевые” классы (WiFiClient например) для ESP32 находятся в других библиотеках и могут называться немного по другому. Но в целом и общем изложенная ранее технология работает на 100% – проверено.
  • Попытаться адаптировать подход, предложенный в данной статье, под фреймворк Arduino32 (так называется официальный фреймворк Arduino для ESP32). В принципе задачи, очереди и другие объекты, описанные в данной статье, должны доступны и из под Arduino32. Но, честно говоря, я не проверял и проверять пока нет времени.

 

Предупреждения:

1. Прежде чем мы с вами начнем обсуждать программную реализацию, Вы должны создать бота, получить его токен и chat id. Как это сделать, я рассказывал в прошлой статье: Создание (регистрация) telegram-бота для отправки уведомлений из устройств умного дома и не только. Если Вы ещё не читали и не знаете как это делается – начните с этой статьи.

2. В тексте статьи будут активно использоваться HTTP(S) запросы и всё, что с ними связано. Если для Вас это не очень знакомая тема, рекомендую предварительно ознакомиться со статьями HTTP запросы на ESP8266 и ESP32 и HTTPS, SSL/TLS соединения на ESP32 и ESP-IDF.

3. Предполагается, что у Вас уже имеется какой-либо проект для ESP32 и ESP-IDF, вы успешно подключили микроконтроллер к сети и синхронизировали реальное время. Эти аспекты выходят за рамки данной статьи и рассматриваться в данном конкретном случае не будут. На данном сайте Вы можете найти статьи и на эти темы.


Добавление корневого сертификата Telegram API в проект

Как я уже писал, Telegram API обрабатывает запросы только через защищенные соединения. Все запросы, отправленные на порт “незащищенный” порт HTTP (80) автоматически перенаправляются на порт HTTPS (443). То есть чтобы воспользоваться Telegram API, нам придется придется играть по их правилам. Что ж, в ESP-IDF это совсем не проблема. Но вначале нам придется “интегрировать” корневой сертификат ЦС Telegram API в наш проект.


Предупреждение 1. Если Вы читали статью про httpS-соединения на ESP32 и ESP-IDF, то наверняка должны помнить, что допускается несколько способов указания корневых сертификатов в проектах на ESP-IDF:

  • путем прямого указания расположения сертификата в коде прошивки, то есть его адрес (по сути это напоминает способ, используемый в Arduino-проектах)
  • путем добавления сертификата в глобальное хранилище сертификатов
  • можно также использовать готовый пакет сертификатов от Mozilla

Я чаще всего использую первый вариант (хотя настройки библиотеки допускают любые варианты). Данная глава написана исходя из первого способа.

Предупреждение 2. В настоящее время я использую Visual Studio Code + PlatformIO, поэтому все описанное справедливо для этой связки. Но, в принципе, для плагина Espressif IDE все должно быть только проще.

Предупреждение 3. Если Вы повторили мой проект Термостата на ESP32 и ESP-IDF, то действия, описанные в данной главе, повторять не нужно – всё уже сделано. Но почитать всё равно будет полезно, чтобы понять, как устроена библиотека.


В отличие от Arduino, в ESP-IDF сертификаты не нужно вставлять в код как строковые константы. Они прикрепляются к проекту как обычные файлы. При этом ESP-IDF предоставляет нам сразу несколько способов хранения сертификатов центров доверия:

  • использовать “свои” файлы сертификатов путем подключения их к проекту (разными способами),
  • подключить к проекту сразу целый набор корневых сертификатов от Mozilla и использовать опцию use_global_ca_store при подключении.

Лично меня “жаба душит” тащить в проект огромную кучу бесполезных в проекте сертификатов, поэтому я подключаю только необходимые в данном проекте.

Как добыть файл с нужным коревым сертификатом – я уже описывал здесь. Но в данном конкретном случае Вы можете просто скачать его из моего GitHub, воспользовавшись ссылкой: github.com/kotyara12/certs/blob/master/api_telegram_org.pem. Вам останется только “встроить” его в проект. 

 

Шаг 1. Выберите каталог, в котором вы будете размещать сертификаты, связанные с проектом или проектами.

Вы можете разместить файл с сертификатом либо в каком-либо подкаталоге вашего проекта, либо в подкаталоге “общих” библиотек, которые доступны сразу нескольким проектам. Я предпочитаю второй вариант. Это позволяет не повторять интеграцию в десятки проектов, а в случае необходимости замены сертификата это делается за 1 секунду. Поэтому я поместил файл сертификата здесь: c:\PlatformIO\libs\certs\api_telegram_org.pem. В каталоге c:\PlatformIO у меня находится все проекты для микроконтроллеров, а в папке libs находятся общие для всех проектов библиотечки. Именно от этого пути я буду отталкиваться в тексте статьи. Если вы предпочитаете другой вариант – соответственно исправляйте пути везде, где это потребуется.

 

Шаг 2. Добавляем ссылку на файл в систему сборки ESP-IDF.

Отрываем папку проекта, ищем каталог \src, а в нем файл CMakeLists.txt. Добавляем в конце строку “target_add_binary_data(${COMPONENT_TARGET} путь_к_файлу TEXT)” без кавычек, у меня это будет так:

# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.

FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
idf_component_register(SRCS ${app_sources})

target_add_binary_data(${COMPONENT_TARGET} C:/PlatformIO/libs/certs/isrg_root_x1.pem TEXT)
target_add_binary_data(${COMPONENT_TARGET} C:/PlatformIO/libs/certs/digi_cert.pem TEXT)
target_add_binary_data(${COMPONENT_TARGET} C:/PlatformIO/libs/certs/api_telegram_org.pem TEXT)

Как видите, в своих проектах я использую сразу три разных сертификата: isrg_root_x1.pem подходит для большинства сайтов, digi_cert.pem – для ThingSpeak, а api_telegram_org.pem говорит сам за себя.

 

Шаг 3. Добавляем ссылку на файл в PlatformIO.

Если вы используете родной плагин от Espressif, то этого уже достаточно. Однако, если вы используете PlatformIO, то этого будет не достаточно. Он не сможет начать сборку проекта, так как ничего “не знает” об этих файлах. Открываем файл platformio.ini и добавляем в него следующие строки:

[env]
...
; ---------------------------------------------------------------------------------------------
; Подключаемые файлы
; ---------------------------------------------------------------------------------------------

board_build.embed_txtfiles =
    ; Сертификат ISRG Root X1 (используется как корневой для MQTT, OTA и других серверов) действителен по 4 июня 2035 г. 14:04:38
    C:\PlatformIO\libs\certs\isrg_root_x1.pem
    ; Сертификат DigiCert High Assurance EV Root CA (используется как корневой для ThingSpeak и других серверов) действителен по 10 ноября 2031 г. 3:00:00
    C:\PlatformIO\libs\certs\digi_cert.pem
    ; Сертификат Telegram API действителен по 29 июня 2034 г. 20:06:20
    C:\PlatformIO\libs\certs\api_telegram_org.pem

На этом подключение файлов сертификатов к проекту ESP-IDF PlatformIO можно считать законченным.

Как получить содержимое подключенных файлов из кода программы

Подключенные к проекту файлы доступны из кода программы с ассемблерными идентификаторами:

  • _binary_%имя_вашего_файла_с_расширением%_start – указатель на начало содержимого файла (первый байт)
  • _binary_%имя_вашего_файла_с_расширением%_end – указатель на конец содержимого файла (последний байт)

То есть в нашем случае это будут:

  • _binary_api_telegram_org_pem_start – начало сертификата
  • _binary_api_telegram_org_pem_end – конец сертификата

Всё готово, можно настраивать безопасное подключение. 

 


Отправляем запрос к Telegram API

В ESP-IDF за HTTP(S) запросы отвечает библиотека esp_http_client. Для работы с ней необходимо её подключить:

#include "esp_http_client.h"

Эта библиотека предоставляет полный функционал для работы с HTTP-подключениями, в том числе и для TLS/SSL. Да, в Arduino можно отправить HTTP-запрос, используя только WiFiClient, но в отличие от WiFiClient она не только передает данные, но и “берет на себя” формирование всех служебных заголовков и анализ ответов сервера, вам остается только правильно настроить соединение. А если сравнивать с аналогичным Arduino-вским клиентом HTTPClient, то esp_http_client предлагает нам гораздо более продвинутый функционал. Чем мы и воспользуется. Отправлять данные будем методом POST с использованием JSON-пакета. 

Шаг 1. Генерируем JSON-пакет с данными.

Для этого воспользуемся системной функцией vsnprintf и следующим шаблоном JSON-пакета:

"{\"chat_id\":%s,\"parse_mode\":\"HTML\",\"disable_notification\":%s,\"text\":\"%s\r\n"}"

куда мы должны подставить:

  • идентификатор чата
  • режим звуковых уведомлений
  • текст сообщения

Сделать это можно разными способами. Например это может выглядеть так:

char* buffer_json = malloc_stringf(API_TELEGRAM_TMPL_MESSAGE, chat_id, tgNotifyApi(tgMsg), message);
if (buffer_json == nullptr) {
  ESP_LOGE(logTAG, "Failed to create json request to Telegram API");
  return ESP_ERR_NO_MEM;
};

где:

char * malloc_stringf(const char *format, ...) 
{
  char *ret = nullptr;
  if (format != nullptr) {
    // get the list of arguments
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);
    // calculate length of resulting string
    int len = vsnprintf(nullptr, 0, format, args1);
    va_end(args1);
    // allocate memory for string
    if (len > 0) {
      ret = (char*)malloc(len+1);
      if (ret != nullptr) {
        memset(ret, 0, len+1);
        vsnprintf(ret, len+1, format, args2);
      } else {
        ESP_LOGE(tagHEAP, "Failed to format string: out of memory!");
      };
    };
    va_end(args2);
  };
  return ret;
}

Что делает эта функция: в первый раз vsnprintf вызывается с пустым указателем на выходной буфер строки (в результате возвращается необходимая длина буфера), затем выделаем в куче блок памяти необходимого размера и только затем повторно вызываем vsnprintf уже с указателем на захваченную память.

В результате мы должны получить готовый к отправке JSON-пакет. Чем мы далее и займемся.

Шаг 2. Теперь нам нужно настроить подключение.

Выглядит это сложно и громоздко, но на деле – проще пареной репы.

// Объявляем локальную переменную
esp_http_client_config_t cfgHttp;
memset(&cfgHttp, 0, sizeof(cfgHttp));

// Используем POST-запрос
cfgHttp.method = HTTP_METHOD_POST;
// Указываем хост: API_TELEGRAM_HOST = "api.telegram.org"
cfgHttp.host = API_TELEGRAM_HOST;
// Указываем порт: API_TELEGRAM_PORT = 443
cfgHttp.port = API_TELEGRAM_PORT;
// Указываем URI запроса: API_TELEGRAM_SEND_MESSAGE = "/bot" + CONFIG_TELEGRAM_TOKEN + "/sendMessage"
cfgHttp.path = API_TELEGRAM_SEND_MESSAGE;
// Задаем таймаут API_TELEGRAM_TIMEOUT_MS = 60000
cfgHttp.timeout_ms = API_TELEGRAM_TIMEOUT_MS;
// Настраиваем SSL через api_telegram_org_pem_start
cfgHttp.transport_type = HTTP_TRANSPORT_OVER_SSL;
cfgHttp.cert_pem = api_telegram_org_pem_start;
cfgHttp.use_global_ca_store = false;
cfgHttp.skip_cert_common_name_check = false;
// Отключаем асинхронный режим выполнения запроса
cfgHttp.is_async = false;

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

Шаг 3. Отправляем запрос на сервер

Для собственно отправки запроса к API необходимо написать вот такой код:

esp_err_t ret = ESP_FAIL;
// Создаем http_client
esp_http_client_handle_t client = esp_http_client_init(&cfgHttp);
if (client) {
  // Указываем тип POST-данных
  esp_http_client_set_header(client, "Content-Type", "application/json");
  // Добавляем JSON в тело запроса
  esp_http_client_set_post_field(client, buffer_json, strlen(buffer_json));
  // Запускаем на выполнение
  ret = esp_http_client_perform(client);
  if (ret == ESP_OK) {
    // Запрашиваем код возврата API
    int retCode = esp_http_client_get_status_code(client);
    // Все хорошо, сообщение отправлено
    if (retCode == HttpStatus_Ok) {
      ret = ESP_OK;
      ESP_LOGD(logTAG, "Message sent: %s", tgMsg->message);
    // Слишком много запросов, нужно подождать
    } else if (retCode == HttpStatus_Forbidden) {
      ret = ESP_ERR_INVALID_RESPONSE;
      ESP_LOGW(logTAG, "Failed to send message, too many messages, please wait");
    // Ошибка
    } else {
      ret = ESP_ERR_INVALID_ARG;
      ESP_LOGE(logTAG, "Failed to send message, API error code: #%d!", retCode);
    };
  } else {
    rlog_e(logTAG, "Failed to complete request to Telegram API, error code: 0x%x!", ret);
  };
  // Не забываем удалить клиента после использования
  esp_http_client_cleanup(client);
};

Не забудьте также после отправки сообщения удалить и сформированный JSON из кучи. Полный код функции отправки tgSendApi(tgMessageItem_t* tgMsg) вы можете найти здесь.

Из всего этого можно написать функцию отправки сообщения, например, esp_err_t tgSendApi(tgMessage_t* tgMsg). Из-за громоздкого написания я не буду приводить весь код здесь (но мы уже разобрали отдельные её части подробно), но вы можете найти её целиком здесь. А что такое tgMessage_t – будет рассказано чуть-чуть ниже… Не переключайтесь, а у нас пока рекламная пауза.

 


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

Всё это хорошо работает, но мы должны помнить, что мы работаем в многозадачной ОС FreeRTOS, и дергать из разных задач одну и ту же функцию отправки сообщений – не лучшая идея. Ведь вполне может случится так, что две разных задачи вздумают одновременно отправить разные сообщения и из этого точно ничего хорошего не выйдет. Даже если не случится конфликта внутри вашей программы, API таких издевательств точно не потерпит. Гораздо правильнее выделить для целей отправки сообщений специально заточенную под это дело задачу. А мы будем отправлять этой самой задаче только непосредственно текст необходимых сообщений. Тогда и интервалы запросов к API можно контролировать, и ошибки обрабатывать корректнее.

Итак, отправлять сообщения в Telegram будет специальная задача. Для того, что бы отправить задаче сообщения – прикрутим к ней входящую очередь. Пока очередь пуста – задача в полной отключке и не потребляет ресурсов (кроме собственного стека). Как только в очереди появляются данные – задача просыпается и отправляет запрос к API. Классика жанра.

Шаг 1. Вначале нужно подключить библиотеки и объявить необходимые переменные:

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h" 

TaskHandle_t _tgTask;
QueueHandle_t _tgQueue = nullptr;

#if CONFIG_TELEGRAM_STATIC_ALLOCATION
StaticQueue_t _tgQueueBuffer;
StaticTask_t _tgTaskBuffer;
StackType_t _tgTaskStack[CONFIG_TELEGRAM_STACK_SIZE];
uint8_t _tgQueueStorage [CONFIG_TELEGRAM_QUEUE_SIZE * TELEGRAM_QUEUE_ITEM_SIZE];
#endif // CONFIG_TELEGRAM_STATIC_ALLOCATION

Здесь предусмотрены два варианта создания задачи – динамический (в куче) и статический (в BSS). Я предпочитаю статический метод – ведь задача эта работает всё время, и выгружать её из памяти не требуется (но можно иногда приостановить на время). Если в настройках проекта макрос CONFIG_TELEGRAM_STATIC_ALLOCATION установлен в 1, то используется статический метод, иначе – динамический.

Шаг 2. Передача строк через очередь

Несмотря на кажущуюся простоту задачи, здесь есть небольшая проблема. Дело в том, что пересылать огромные массивы данных через очередь настоятельно не рекомендуется. Тем более, если заранее не известен размер этих данных, как в случае со нашими сообщениями – строками переменной длины. Поэтому для отправки сообщения заранее неизвестной длины необходимо предварительно разместить текст сообщения в heap (куче), а в очередь отправки передается только указатель на эти данные. После отправки сообщения наша задача должна будет сама освободить память, занимаемую переданным ей сообщением.

Как вариант можно использовать заведомо большой статический буфер, но с ним свои сложности: во первых потребуется большой буфер, который относительно редко используется; а во вторых – не получится поместить в очередь сразу несколько сообщений, что легко реализуется при динамическом размещении.

Кроме того, поскольку мы можем отправлять сообщения в разные группы или чаты, необходимо передать идентификатор чата. Поэтому давайте объявим новый тип данных:

typedef struct {
  char* message;
  uint64_t chat_id;
} tgMessage_t;

Эту запись (структуру) уже можно безболезненно пихать в очередь задачи, так как она является данными с фиксированным размером в памяти. Здесь хранится указатель на текст сообщения и идентификатор чата.

Как вариант, можно заранее определить несколько типов чатов, например: главный (для всех пользователей) и служебный (только отладочные сообщения); а идентификаторы чата задать где-то через константы (настройки проекта):

typedef enum {
  TGC_MAIN,
  TGC_SERVICE
} tgChatType_t;

typedef struct {
  char* message;
  tgChatType_t chat_type;
} tgMessage_t;

Так будет немного лучше с точки зрения экономии памяти под очередь – ведь теперь идентификатор чата занимает только 1 байт вместо 8.

Далее мы создаем строку сообщения в памяти любым способом, я предпочитаю свою любимую функцию (моя прелесть) malloc_stringf() (см. выше) и прикрепляем её к записи tgMessage_t. А затем всё вот это отправляем в очередь:

tgMessage_t tgMsg;
tgMsg.chat_type = TGC_MAIN;
tgMsg.message = malloc_stringf("Привет %s!", "мир");

if (xQueueSend(_tgQueue, &tgMsg, pdMS_TO_TICKS(1000)) == pdPASS) {
  return true;
} else {
  ESP_LOGE("Failed to adding message to queue [ %s ]!", tgTaskName);
  return false;
};

Но делать каждый раз это не слишком удобно, поэтому давайте напишем ещё одну функцию, которая будет форматировать сообщение и помещать его в очередь:

bool tgSendMsg(tgChatType_t chat, const char* msgText, ...)
{
  if (_tgQueue) {
    tgMessage_t tgMsg;
    tgMsg.chat_type = chat;
    tgMsg.message = nullptr;

    // Выделяем память под форматированное сообщение и создаем его
    va_list args;
    va_start(args, msgText);
    uint16_t len = vsnprintf(nullptr, 0, msgText, args);
    tgMsg.message = (char*)esp_calloc(1, len+1);
    if (tgMsg.message) {
      vsnprintf(tgMsg->message, len+1, msgText, args);
    } else {
      ESP_LOGE(logTAG, "Failed to allocate memory for message text");
      va_end(args);
      return false;
    };
    va_end(args);

    // Помещаем сообщение в очередь отправки
    if (xQueueSend(_tgQueue, &tgMsg, pdMS_TO_TICKS(1000)) == pdPASS) {
      return true;
    } else {
      ESP_LOGE("Failed to adding message to queue [ %s ]!", tgTaskName);
      return false;
    };
}

У на получилась основная “интерфейсная” функция отправки сообщения, которая должна использоваться из других задач.

Примечание: Если вы создадите очередь достаточного размера (например CONFIG_TELEGRAM_QUEUE_SIZE = 2, 4, 8 или 16), то в ней могут хранится сразу несколько сообщений, дожидаясь своей очереди на отправку.

Обратите внимание: “снаружи” задачи отправки сообщений мы выделили память под сообщение в куче, но ещё не освободили её! Вместо этого мы перекладываем ответственность за освобождение памяти на хрупки плечи задачи отправки сообщений. 

Шаг 3. Создаем функцию задачи по отправке сообщений

Теперь нам потребуется написать главную функцию задачи и запустить её (задачу). Берем в руки карандашик и ваяем:

void tgTaskExec(void *pvParameters)
{
  tgMessage_t tgMsg;
  esp_err_t errSend;
  while (true) {
    if (xQueueReceive(_tgQueue, &inMsg, portMAX_DELAY) == pdPASS) {
      ESP_LOGI(logTAG, "New message received: %s", inMsg.message);
      // Получили сообщение, пытаемся отправить
      errSend = tgSendApi(&inMsg);
      if (errSend == ESP_OK) {
        ESP_LOGI(logTAG, "Message was successfully sent");
      } else {
        ESP_LOGE(logTAG, "Failed to send message: %d", errSend);
      };
      // Не забываем удалить из памяти пересылаемое сообщение
      if (tgMsg.message) free(tgMsg.message);
      tgMsg.message = nullptr;
    };
  };
  vTaskDelete(NULL);
}

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

Шаг 4. Запускаем задачу на выполнение

bool tgTaskCreate() 
{
  if (!_tgTask) {
    if (!_tgQueue) {
      #if CONFIG_TELEGRAM_STATIC_ALLOCATION
      _tgQueue = xQueueCreateStatic(CONFIG_TELEGRAM_QUEUE_SIZE, TELEGRAM_QUEUE_ITEM_SIZE, &(_tgQueueStorage[0]), &_tgQueueBuffer);
      #else
      _tgQueue = xQueueCreate(CONFIG_TELEGRAM_QUEUE_SIZE, TELEGRAM_QUEUE_ITEM_SIZE);
      #endif // CONFIG_TELEGRAM_STATIC_ALLOCATION
      if (!_tgQueue) {
        ESP_LOGE("Failed to create a queue for sending notifications to Telegram!");
        return false;
      };
    };
    
    #if CONFIG_TELEGRAM_STATIC_ALLOCATION
    _tgTask = xTaskCreateStaticPinnedToCore(tgTaskExec, "tg_send", CONFIG_TELEGRAM_STACK_SIZE, nullptr, CONFIG_TASK_PRIORITY_TELEGRAM, _tgTaskStack, &_tgTaskBuffer, CONFIG_TASK_CORE_TELEGRAM); 
    #else
    xTaskCreatePinnedToCore(tgTaskExec, tgTaskName, CONFIG_TELEGRAM_STACK_SIZE, nullptr, CONFIG_TASK_PRIORITY_TELEGRAM, &_tgTask, CONFIG_TASK_CORE_TELEGRAM); 
    #endif // CONFIG_TELEGRAM_STATIC_ALLOCATION
    if (!_tgTask) {
      vQueueDelete(_tgQueue);
      ESP_LOGE("Failed to create task for sending notifications to Telegram!");
      return false;
    }
    else {
      ESP_LOGI("Task [ %s ] has been successfully started", tgTaskName);
      return true;
    };
  };
}

где макросы препроцессора, обозначающие:

  • CONFIG_TELEGRAM_STATIC_ALLOCATION – использовать статический метод размещения очереди и задачи
  • CONFIG_TELEGRAM_QUEUE_SIZE – размер очереди сообщений, у меня равно 16 (я выравниваю размеры очереди до степени двойки, хотя, наверное, это не обязательно)
  • TELEGRAM_QUEUE_ITEM_SIZE – размер элемента очереди, это просто sizeof(tgMessage_t)
  • CONFIG_TELEGRAM_STACK_SIZE – размер стека задачи, у меня сейчас это 3584 байт
  • CONFIG_TASK_PRIORITY_TELEGRAM – приоритет задачи, тут все зависит от баланса приоритетов других задач
  • CONFIG_TASK_CORE_TELEGRAM – ядро задачи отправки, у меня всегда равно 1 (для двухядерных конфигураций, разумеется)

Запускать задачу стоит только после того, как ваше устройство успешно подключилось к сети WiFi (или через GPRS / Ethernet), и получило время с NTP-сервера. Кроме того, постоянно держать задачу запущенной не всегда хорошо. Если по каким – либо причинам доступ в интернет отсутствует, имеет смысл приостановить задачу, а вместе с ней и запросы к API, а после восстановления доступа – запустить вновь. Сделать это можно с помощью рассылки внутри FreeRTOS специальных служебных событий и создания специальных обработчиков. Но, наверное, это тема для отдельной статьи.

 


Что можно улучшить?

Приведенный выше пример будет отлично работать в устройствах, которые всегда online. Но, допустим, вы проектируете самосдельную охранно-пожарную сигнализацию, которая, кроме всего прочего, умеет работать от резервного источника питания. В этом случае не лишним будет контролировать наличие основного сетевого питания. Казалось бы – что может быть проще? При пропадании сетевого напряжения просто отправим сообщение в Telegram, и …. его не получим. По той простой причине, что с большой долей вероятности сетевое оборудование будет тоже offline (не у вас так у провайдера). Казалось бы, очередь отправки частично решает проблему. Но, как правило, очередь достаточно быстро заполняется сообщениями и часть сообщений все равно теряется.

Я нашел решение в виде создания системы приоритетов и создания ещё одной, внутренней, очереди сообщений. Опишу просто идею реализации, код вы всегда сможете найти в библиотеке reTgSend. При поступлении нового сообщения оно сразу же изымается из очереди задачи и записывается во внутреннюю очередь сообщений. Если внутренняя очередь заполнена полностью, то в ней ищется сообщение с наименьшим приоритетом, и если таковое находится, то оно будет удалено, а на его место записано новое. Этот способ позволяет отбросить неважные сообщения (типа “Потеряно подключение к wifi точке доступа“) и оставить только существенные. Которые вы получите после восстановления основного питания и доступа в сеть интернет, разумеется, но всё-таки получите!


Библиотека reTgSend

В заключение хочу привести описание “публичных” функций для моей библиотечки reTgSend, если вдруг вы захотите её использовать.

Для управления собственно задачей предусмотрено несколько функций:

  • tgTaskCreate() – создает и запускает задачу клиента Telegram (точно так же, как это и было описано выше)
  • tgTaskSuspend() – приостанавливает задачу, но не выгружает её из памяти, например при отключении от сети
  • tgTaskResume() – заново запускает приостановленную ранее задачу
  • tgTaskDelete() – останавливает и полностью удаляет задачу из памяти (я не использую, но пусть будет)

Для отправки сообщения предусмотрена всего одна функция:

bool tgSendMsg(msg_options_t msgOptions, const char* msgTitle, const char* msgText, ...);

где:

  • msgOptions – опции сообщения: тип чата, приоритет и звук уведомлений
  • msgTitle – заголовок сообщения (не обязателен)
  • msgText – шаблон или текст отправляемого сообщения
  • ... – дополнительные аргументы для форматирования сообщения

Опции представляют собой тип данных, объявленный здесь:

/**
 * Notification options
 * bits: xKKKPPPA
 *    7: x   - reserved
 *  5-6: KKK - msg_kind_t
 *  1-4: PPP - message_priority_t
 *    0: A   - alert / sound notification
 * */
typedef uint8_t msg_options_t;

То есть по сути это однобайтовое целое число, в которым “зашифрован” тип чата, приоритет и тип уведомления. Поскольку пользоваться этой “странной” конструкцией не архиудобно, в библиотеке предусмотрена функция-макрос:

#define tgSend(msgKind, msgPriority, msgNotify, msgTitle, msgText, ...);

В которой можно напрямую указать все параметры сообщения.

Параметры библиотеки, оформленные в виде макросов препроцессора, можно найти в файле def_tg_task.h. Вы сами можете посмотреть, макросов там не сильно много, и все они снабжены подробными комментариями. 

Примечание: если вы заходите использовать эту библиотеку в ваших проектах, то следует учитывать, что она тянет за собой еще несколько моих же библиотек. От некоторых из них (например reLog и reEvents) – можно легко избавиться, слегка переделав код. Да, признаю, это не самая правильная структура файлов внутри библиотек, и в будущем она будет обязательно переделываться (если время и здоровье позволят). Но на момент написания этой статьи что есть, то есть. И тем не менее это работает на все 100%.

Ну а на этом тему отправки сообщений в Telegram можно считать исчерпанной. Мы с вами рассматривали только весьма одностороннее использование Telegram. На самом деле Telegram API намного сложнее и интереснее, и в рамках данной статьи не будет рассматриваться. Кроме того, для себя я решил, что не стоит налаживать двухсторонний обмен данным с Telegram непосредственно с ESP32 (по нескольким объективным причинам). Вместо этого я планирую когда-нибудь написать шлюз “telegram-mqtt” для создания многоуровнего меню управления умным домом непосредственно из Telegram чатов.

 


Ну этом позвольте откланяться, с вами был ваш Александр ака kotyara12. Вопросы или комментарии вы можете оставить под статьей или в telegram-чате https://t.me/k12chat

🔶 Полный архив статей вы найдете здесь


Пожалуйста, оцените статью:
[ 0 из 5, всего 0 оценок ]

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *