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

Отправка изображений в Telegram с ESP32 без использования сторонних библиотек

Добрый день, уважаемый читатель!

В данной статье я расскажу, как отправить изображение (или файл) с устройства на базе ESP32 в канал или чат telegram. Причем сделаем мы это без использования сторонних библиотек: исключительно с использованием встроенного в ESP-IDF API ESP HTTP Client и максимально простым способом. Я уже рассказывал, как отправлять в telegram текстовые сообщения, теперь выполним то же самое, но для картинок, фотографий или файлов. В качестве источника изображений может выступать камера на ESP32-CAM или что-то ещё. Приведенный в статье пример вполне годиться не только для платформы ESP-IDF, но и для Arduino32, так как API ESP HTTP Client доступно и там и там.

В сети можно найти достаточно много примеров, где данная задача решается на платформе Arduino с помощью готовых сторонних библиотек-ботов. Но во-первых, я не давненько уже я не использую Arduino в своих проектах. Во-вторых я не стремлюсь использовать “полноценные” библиотеки для telegram-ботов с обратной связью по одной вполне очевидной причине – telegram api отправляет команды в виде JSON-пакетов, а парсинг JSON на микроконтроллере требует значительных ресурсов. Подключая внешнюю библиотеку бота к проекту, вы автоматически тянете в свой проект часть ненужного в большинстве случаев кода. Ну в третьих, и это самое важное, – я предпочитаю минимизировать использование сторонних библиотек в своих проектах. Точнее – я практически не использую их, за редким исключением. Причина проста – сторонний код может быть изменен в любой момент времени или заброшен автором, а не желаю зависеть от других авторов (таки да, и предпочитаю делать ошибки в коде самостоятельно, и потом героически их исправлять).

Между тем, примеров отправки изображений в telegram с использованием исключительно ESP-IDF не так уж и много. Точнее – почти совсем нет. Найденные в сети примеры в основном являются копипастами с одного сайта на другой, без попыток какого-либо анализа и устранения недостатков кода, поэтому они просто пестрят костылями и совершенно неразумным расходованием стека и памяти. Например в одном из примеров изображение из буфера, в котором оно храниться изначально, копируется в другой буфер и только после этого отправляется. При этом второй буфер расположен в стеке, что приводит к необходимости выделения задачи огромного стека. В другом примере автор вовсе отказался от использования esp http client, и сделал всё на сокетах, что имеет право на жизнь, но уж очень громоздко и сложно.

Пришлось брать быка за рога и разбираться самому.

 


Telegram API для отправки изображений

Для отправки изображений в telegram нам необходимо воспользоваться методом sendPhoto: core.telegram.org/bots/api#sendphoto. Метод может принимать следующие параметры:

Параметр Тип Обяз. Описание
business_connection_id String Optional Уникальный идентификатор бизнес-соединения, от имени которого будет отправлено сообщение
chat_id Integer or String Yes Уникальный идентификатор целевого чата или имя пользователя целевого канала (в формате @channelusername).
message_thread_id Integer Optional Уникальный идентификатор целевой ветки сообщений (темы) форума; только для супергрупп форума
photo InputFile or String Yes

Фото для отправки. Варианты отправки:

  • передайте file_id в виде строки, чтобы отправить фотографию, которая уже существует на серверах Telegram (рекомендуется),
  • передайте URL-адрес HTTP в качестве строки для Telegram, чтобы получить фотографию из Интернета,
  • загрузите новую фотографию, используя multipart/form-data. Размер фотографии должен быть не более 10 МБ. Ширина и высота фотографии в сумме не должны превышать 10000. Соотношение ширины и высоты должно быть не более 20.
caption String Optional Подпись к фото (также может использоваться при повторной отправке фотографий по file_id), Максимальная длина сообщения 1024 символа после анализа объектов.
parse_mode String Optional Режим парсинга текста в подписи к фотографии. Подробнее см. в разделе «Параметры форматирования».
caption_entities Array of MessageEntity Optional Список специальных объектов в формате JSON, которые появляются в заголовке и которые можно указать вместо parse_mode.
has_spoiler Boolean Optional Укажите значение True, если фотография должна быть покрыта анимацией спойлера.
disable_notification Boolean Optional Отправляет сообщение без звука. Пользователи получат уведомление без звука.
protect_content Boolean Optional Защищает содержимое отправленного сообщения от пересылки и сохранения
reply_parameters ReplyParameters Optional Описание сообщения, на которое нужно ответить
reply_markup InlineKeyboardMarkup or ReplyKeyboardMarkup or ReplyKeyboardRemove or ForceReply Optional Дополнительные возможности интерфейса. Сериализованный объект JSON для встроенной клавиатуры, настраиваемой клавиатуры ответа, инструкций по удалению клавиатуры ответа или принудительному ответу пользователя.

Исходя из вышеизложенного, для отправки изображения нам понадобятся как минимум следующие поля:

  • chat_id – идентификатор чата
  • photo – собственно данные фото
  • caption – подпись к фото (при необходимости)
  • parse_mode – режим разбора caption (html или markdown, при необходимости)

Все это должно быть передано telegram API POST-запросом под адресу https://api.telegram.org/botTOKEN/sendPhoto в виде multipart/form-data. JSON-пакет, как это было в случае с текстовыми сообщениями, уже не подходит. Вид передаваемого блока данных должен выглядеть примерно так:

POST /botTOKEN/sendPhoto/
HOST: api.telegram.org:443
Content-Type: multipart/form-data; boundary=mpb-bla-bla-bla
Content-Length: XXXXXXXX

--mpb-bla-bla-bla
Content-Disposition: form-data; name="chat_id"

CHAT_ID
--mpb-bla-bla-bla
Content-Disposition: form-data; name="caption"

ПОДПИСЬ <b>К ФОТО</b>
--mpb-bla-bla-bla
Content-Disposition: form-data; name="parse_mode"

html
--mpb-bla-bla-bla
Content-Disposition: form-data; name="photo"; filename="esp32-cam.jpg"
Content-Type: image/jpeg

ДВОИЧНЫЕ_ДАННЫЕ
--mpb-bla-bla-bla--
<пустая строка как признак конца сообщения>

где:

  • TOKEN – секретный токен вашего бота
  • CHAT_ID – идентификатор чата или канала
  • ПОДПИСЬ <b>К ФОТО</b> – текстовое сообщение, прикрепленное к фото, например в HTML-формате
  • ДВОИЧНЫЕ_ДАННЫЕ – собственно двоичные данные

О том, что такое токен и chat_id, вы можете узнать из другой статьи. Осталось все это реализовать, используя стандартный для ESP32 http – клиент. 

 


Отправляем изображение

Прежде всего напомню, что telegram API принимает запросы только по HTTPS-соединению. Поэтому нам потребуется импортировать в программу корневой сертификат сервера. Как это сделать, можно почитать здесь и здесь, в данной статье я не буду заострять на этом внимания.

Прежде всего, я оформил все необходимые поля в виде макросов (можно также объявить их не макросами, а static const char*, кроме API_TELEGRAM_BOUNDARY – но на суть это никак не повлияет):

#define API_TELEGRAM_BOUNDARY           "mpb-bla-bla-bla"
#define API_TELEGRAM_CONTENT_TYPE       "multipart/form-data; boundary=" API_TELEGRAM_BOUNDARY "\r\n"
#define API_TELEGRAM_PHOTO_PARSE_MODE   "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n"
#define API_TELEGRAM_PHOTO_CHAT_ID      "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\n%s\r\n" API_TELEGRAM_PHOTO_PARSE_MODE
#define API_TELEGRAM_PHOTO_CAPTION      "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\n%s\r\n"
#define API_TELEGRAM_PHOTO_IMAGE        "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"
#define API_TELEGRAM_PHOTO_TAIL         "\r\n--" API_TELEGRAM_BOUNDARY "--\r\n\r\n"

Затем настраиваем http-соединение:

// Настраиваем параметры соединения
esp_http_client_config_t config;
memset(&config, 0, sizeof(config));
config.host = "api.telegram.org";
config.port = 443;
config.path = "/bot" CONFIG_TELEGRAM_TOKEN "/sendPhoto";
config.method = HTTP_METHOD_POST;
config.transport_type = HTTP_TRANSPORT_OVER_SSL;
config.cert_pem = api_telegram_org_pem_start;
config.keep_alive_enable = true;
config.is_async = false;

Обратите внимание: я установил признак keep_alive_enable, дабы сервер не пытался закрыть соединение сразу же после получения первых заголовков. Но сильно на это рассчитывать не стоит, так как даже при keep_alive_enable среднестатистический сервер держит открытым неактивное соединение около 15 секунд.

Следующим шагом формируем необходимые поля char_id и caption в куче:

// Форматируем служебное поле chat id
char* chat_id_buf = nullptr;
if (chat_id) chat_id_buf = malloc_stringf(API_TELEGRAM_PHOTO_CHAT_ID, chat_id);

// Форматируем служебное поле caption (подпись к снимку)
char* caption_buf = nullptr;
// Преобразуем время создания снимка в строку
char dts[20]; // DD.MM.YYYY HH:NN:SS + \0
memset(dts, 0, sizeof(dts));
time_t now = time(nullptr);
struct tm tinfo;
localtime_r(&now, &tinfo);
strftime(dts, sizeof(dts), "%d.%m.%Y %H:%M:%S", &tinfo);
// Форматируем подпись к снимку
if (caption) {
  char* caption_full = malloc_stringf("<b>" CONFIG_DEVICE_CAPTION "</b>: %s\r\n<code>%s</code>", caption, dts);
  if (caption_full) {
    caption_buf = malloc_stringf(API_TELEGRAM_PHOTO_CAPTION, caption_full);
    free(caption_full);
  };
} else {
  caption_buf = malloc_stringf(API_TELEGRAM_PHOTO_CAPTION, dts);
};

Примечание: для формирования строк в куче я активно использую функцию malloc_stringf из библиотеки rStrings. Не забывайте удалить их после использования!

Затем нам необходимо посчитать длину всего сообщения, которое будет передано:

// Вычисляем размер передаваемых данных
size_t len_image = strlen(API_TELEGRAM_PHOTO_IMAGE) + fb->len + strlen(API_TELEGRAM_PHOTO_TAIL);
if (chat_id_buf) len_image += strlen(chat_id_buf);
if (caption_buf) len_image += strlen(caption_buf);

Здесь предполагается, что длина изображения в байтах храниться в fb->len.

Теперь уже можно начинать собственно соединение и передачу данных. Открываем соединение:

esp_http_client_handle_t http_client = esp_http_client_init(&config);
if (http_client) {
    // Указываем тип передаваемых данных
    esp_http_client_set_header(http_client, "Content-Type", API_TELEGRAM_CONTENT_TYPE);
    
    // Открываем соединение (в этот момент передаются заголовки)
    esp_err_t err = esp_http_client_open(http_client, len_image);
    ...

Обратите внимание – в данном случае я вместо esp_http_client_perform или esp_http_client_connect использовал функцию esp_http_client_open.

В чем их отличие?

  • esp_http_client_perform выполняет весь полный цикла обмена с сервером на основе предварительно заполненных заголовков и данных. В данном случае нам это не подходит, так как гораздо удобнее передавать картинку прямиком из буфера, который был сформирован камерой.
  • esp_http_client_connect просто устанавливает соединение и более ничего не происходит. После этого теоретически можно было бы передавать заголовки “вручную” с помощью esp_http_client_write, но увы – это не работает. Дело в том, что esp_http_client_read и esp_http_client_write работают только в том случае, если заголовки уже переданы.
  • esp_http_client_open открывает соединение и передает несколько обязательных служебных заголовков HTTP-запроса, в том числе тип запроса, тип содержимого и его длину (Content-Length и Content-Type) и т.д. Поэтому “вручную” их формировать уже не нужно, что облегчает нам задачу. После этого “разблокируются” функции esp_http_client_read и esp_http_client_write.

.После esp_http_client_open соединение открыто и готово к передаче собственно данных. Для этого воспользуется функцией записи esp_http_client_write:

// Отправляем идентификатор чата
if (chat_id_buf) {
  esp_http_client_write(http_client, chat_id_buf, strlen(chat_id_buf));
  free(chat_id_buf);
};

// Отправляем подпись к фото
if (caption_buf) {
  esp_http_client_write(http_client, caption_buf, strlen(caption_buf));
  free(caption_buf);
};

// Отправляем изображение (напрямую, без использования промежуточных буферов)
esp_http_client_write(http_client, API_TELEGRAM_PHOTO_IMAGE, strlen(API_TELEGRAM_PHOTO_IMAGE));
esp_http_client_write(http_client, (const char*)fb->buf, fb->len);
esp_http_client_write(http_client, API_TELEGRAM_PHOTO_TAIL, strlen(API_TELEGRAM_PHOTO_TAIL));

Вот собственно, почти и всё, без особых премудростей и лишних буферов.

Останется только прочитать код ответа API и закрыть соединение.

// Читаем заголовки ответа сервера и парсим код ответа
esp_http_client_fetch_headers(http_client);
int ret_code = esp_http_client_get_status_code(http_client);
rlog_i(logTAG, "Telegram API code: %d", ret_code);

// Закрываем соединение
esp_http_client_close(http_client);

 

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

extern const char api_telegram_org_pem_start[] asm("_binary_api_telegram_org_pem_start");
extern const char api_telegram_org_pem_end[]   asm("_binary_api_telegram_org_pem_end");  

#define API_TELEGRAM_BOUNDARY           "mpb-bla-bla-bla"
#define API_TELEGRAM_CONTENT_TYPE       "multipart/form-data; boundary=" API_TELEGRAM_BOUNDARY "\r\n"
#define API_TELEGRAM_PHOTO_PARSE_MODE   "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"parse_mode\"\r\n\r\nhtml\r\n"
#define API_TELEGRAM_PHOTO_CHAT_ID      "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"chat_id\"\r\n\r\n%s\r\n" API_TELEGRAM_PHOTO_PARSE_MODE
#define API_TELEGRAM_PHOTO_CAPTION      "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"caption\"\r\n\r\n%s\r\n"
#define API_TELEGRAM_PHOTO_IMAGE        "--" API_TELEGRAM_BOUNDARY "\r\nContent-Disposition: form-data; name=\"photo\"; filename=\"esp32-cam.jpg\"\r\nContent-Type: image/jpeg\r\n\r\n"
#define API_TELEGRAM_PHOTO_TAIL         "\r\n--" API_TELEGRAM_BOUNDARY "--\r\n\r\n"


void tgSendImage(camera_fb_t * fb, const char* chat_id, const char* caption) 
{
  rlog_i(logTAG, "Send photo to Telegram...");

  // Настраиваем параметры соединения
  esp_http_client_config_t config;
  memset(&config, 0, sizeof(config));
  config.host = "api.telegram.org";
  config.port = 443;
  config.path = "/bot" CONFIG_TELEGRAM_TOKEN "/sendPhoto";
  config.method = HTTP_METHOD_POST;
  config.transport_type = HTTP_TRANSPORT_OVER_SSL;
  config.cert_pem = api_telegram_org_pem_start;
  config.keep_alive_enable = true;
  config.is_async = false;

  // Форматируем служебное поле chat id
  char* chat_id_buf = nullptr;
  if (chat_id) chat_id_buf = malloc_stringf(API_TELEGRAM_PHOTO_CHAT_ID, chat_id);

  // Форматируем служебное поле caption (подпись к снимку)
  char* caption_buf = nullptr;
  // Преобразуем время создания снимка в строку
  char dts[20]; // DD.MM.YYYY HH:NN:SS + \0
  memset(dts, 0, sizeof(dts));
  time_t now = time(nullptr);
  struct tm tinfo;
  localtime_r(&now, &tinfo);
  strftime(dts, sizeof(dts), "%d.%m.%Y %H:%M:%S", &tinfo);
  // Форматируем подпись к снимку
  if (caption) {
    char* caption_full = malloc_stringf("<b>" CONFIG_DEVICE_CAPTION "</b>: %s\r\n<code>%s</code>", caption, dts);
    if (caption_full) {
      caption_buf = malloc_stringf(API_TELEGRAM_PHOTO_CAPTION, caption_full);
      free(caption_full);
    };
  } else {
    caption_buf = malloc_stringf(API_TELEGRAM_PHOTO_CAPTION, dts);
  };

  // Вычисляем размер передаваемых данных
  size_t len_image = strlen(API_TELEGRAM_PHOTO_IMAGE) + fb->len + strlen(API_TELEGRAM_PHOTO_TAIL);
  if (chat_id_buf) len_image += strlen(chat_id_buf);
  if (caption_buf) len_image += strlen(caption_buf);
  
  // Инициализируем соединение
  esp_http_client_handle_t http_client = esp_http_client_init(&config);
  if (http_client) {
    // Указываем тип передавамых данных
    esp_http_client_set_header(http_client, "Content-Type", API_TELEGRAM_CONTENT_TYPE);
    
    // Открываем соединение (в этот момент передаются заголовки)
    esp_err_t err = esp_http_client_open(http_client, len_image);
    if (err == ESP_OK) {
      // Отправляем идентификатор чата
      if (chat_id_buf) {
        esp_http_client_write(http_client, chat_id_buf, strlen(chat_id_buf));
        free(chat_id_buf);
      };
      
      // Отправляем подпись к фото
      if (caption_buf) {
        esp_http_client_write(http_client, caption_buf, strlen(caption_buf));
        free(caption_buf);
      };

      // Отправляем изображение (напрямую, без использования промежуточных буферов)
      esp_http_client_write(http_client, API_TELEGRAM_PHOTO_IMAGE, strlen(API_TELEGRAM_PHOTO_IMAGE));
      esp_http_client_write(http_client, (const char*)fb->buf, fb->len);
      esp_http_client_write(http_client, API_TELEGRAM_PHOTO_TAIL, strlen(API_TELEGRAM_PHOTO_TAIL));

      // Читаем заголовки ответа сервера и парсим код ответа
      esp_http_client_fetch_headers(http_client);
      int ret_code = esp_http_client_get_status_code(http_client);
      rlog_i(logTAG, "Telegram API code: %d", ret_code);

      // Закрываем соединение
      esp_http_client_close(http_client);
    } else {
      rlog_e(logTAG, "Failed to open HTTPS connection: %d (%s)", err, esp_err_to_name(err));
    };

    // Освобождаем ресурсы
    esp_http_client_cleanup(http_client);
    http_client = nullptr;
  };
}

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

 


В качестве демонстрации приведу фото, полученное описанным выше способом с камеры OV2640 без какой-либо обработки. У меня имеется 2 экземпляра OV2640, оба выдают очень темное изображение в комнатных условиях. Регулировать яркость изображения можно, но в небольших пределах (-2 … +2), но об этом как-нибудь расскажу в другой раз.

На этом разрешите откланяться, с вами был Александр aka kotyara12. Благодарю за внимание.

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


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

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

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