Добрый день, уважаемый читатель!
В данной статье я расскажу, как отправить изображение (или файл) с устройства на базе 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 |
Фото для отправки. Варианты отправки:
|
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. Благодарю за внимание.
💠 Полный архив статей вы найдете здесь
Пожалуйста, оцените статью:
Здравствуйте! Вашу информацию вижу давно, никак не решусь сделать.
Благодарю Вас! Мне понравилось много информации нужной.
Можно сделать бота на айфоне?
Здравствуйте!
Если у вас как построить бота в телеграмм для рекламы?
Буду признательна!