Метео-будка на даче научилась показывать температуру на экране — и на этом остановилась: данные жили только внутри неё. Следующим шагом напрашивался сервер: панель с историей за месяц, графики и уведомление на телефон, когда в теплице минус. Для отдачи данных GET не годится в принципе — он просит у сервера, а не отдаёт ему. Первая моя попытка была наивной: я подставил показания прямо в адрес — /api.php?temp=21.4&hum=58 — и сервер их даже принимал. Пока не вылезли две беды: каждый такой запрос с ключом в URL падал в лог сервера на видном месте, а некоторые сервисы длинные адреса молча обрезали, и половина показаний терялась без следа. Как задаёт вопросы плата, мы разобрали в базовом уроке про HTTP-запросы, здесь же — как отдаёт.
Правильный инструмент для отдачи — POST с телом. В этом уроке соберём телеметрический скетч целиком: формируем тело из показаний датчика, ставим правильный Content-Type, отправляем http.POST(), разбираем код ответа — 200, 201, 422 — и дочитываем подтверждение сервера. Отдельно разберёмся, почему POST — это создание записи, а не её обновление, и чего это стоит при плохой связи.
Анатомия POST: тело вместо адреса
POST-запрос выглядит так: строка POST /api/v1/data HTTP/1.1, заголовки — среди них два новых, Content-Type и Content-Length, — пустая строка, а дальше тело: сами данные. Content-Type сообщает серверу формат: application/x-www-form-urlencoded — «ключ=значение&ключ2=значение2», как в адресах, только в теле; application/json — полноценный JSON-объект. Content-Length — длина тела в байтах; на реальной плате её считает сам HTTPClient, руками ничего складывать не нужно, и в этом одна из причин брать обёртку вместо сырого сокета.
В HTTPClient POST делается в три строки после begin: addHeader с Content-Type, затем int code = http.POST(body) — библиотека отправит тело и вернёт код ответа, как после GET, — и ответ сервера дочитывается тем же getString(). Схема ровно та же, что разобрана в уроке про WiFiClient и HTTPClient: меняется запрос-строка и появляется тело. Сеть, само собой, должна быть уже подключена — цикл ожидания WL_CONNECTED переезжает из урока в урок без изменений, а если хотите, чтобы плата пережила смерть роутера, соединяйте его с автоматом восстановления из урока про реконнект: телеметрия с перезагрузкой сети — типичный сценарий дачи.
#include <WiFi.h>#include <HTTPClient.h> const char* ssid = "DomaSetka";const char* password = "superParol";String url = "http://server.example.com/api/v1/data";const char* apiKey = "a1b2c3d4e5"; void setup() { Serial.begin(115200); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }} void loop() { float temp = 21.4; // здесь показания вашего датчика int hum = 58; String body = "temp=" + String(temp, 1) + "&hum=" + String(hum) + "&device=dacha1"; HTTPClient http; http.begin(url); http.addHeader("Content-Type", "application/x-www-form-urlencoded"); http.addHeader("X-API-Key", apiKey); int code = http.POST(body); // тело уходит, возвращается код ответа if (code == 200 || code == 201) { Serial.print("Записано, ответ сервера: "); Serial.println(http.getString()); } else { Serial.print("Сервер ответил: "); Serial.println(code); } http.end(); // освободили соединение в любом случае delay(60000); // раз в минуту — не чаще}Разбор. Тело собирается из переменных: String(temp, 1) печатает число с одним знаком после запятой — 21.4, без артефактов вроде 21.400001, которыми грешит сырая конвертация float в строку. Ключ API уехал из адреса в заголовок X-API-Key: URL светится в логах сервера и прокси, заголовки — гораздо реже. Точного стандарта тут нет — у каждого сервиса свой заголовок: где-то X-API-Key, где-то Authorization: Bearer, поэтому перед первым запросом загляните в документацию API и проверьте имя заголовка по букве. Код 201 Created значит, что сервер создал новую запись, 200 — рабочий вариант без создания; полную таблицу смотрите ниже. Ответ сервера после POST дочитывается getString() — обычно там короткий JSON со статусом и номером записи, по которому плата понимает, что показание принято.
Про частоту — отдельное слово, потому что тут я видел все виды фанатизма. delay(60000) в скетче — это одно показание в минуту, тысяча четыреста сорок записей в сутки с одного датчика; для домашней теплицы хватает и одной в пять минут, а при изменении так часто данные всё равно дублируют друг друга. Между запросами HTTPClient можно создавать заново в каждом проходе loop, как в скетче: соединение короткоживущее, begin/end парные, и подводных камней меньше. Мой баланс для дачи: три минуты обычного режима, минута при выходе за пороги — серверу спокойнее, и история всё равно читаема. И если сервер даёт код ответа не 2xx, я печатаю тело ответа тоже — в нём обычно написано, что именно серверу не понравилось, и это экономит вечер.
Ещё один нюанс, который я понял дорого. POST — не обновление: два одинаковых POST подряд — это две записи в базе. Отсюда ловушка ретраев: если timeout истёк, а пакет всё-таки доехал, сервер записал показание, и повторный POST создаст второе. Лечится идемпотентностью: серверу передают поле с меткой времени или счётчиком измерения, и повтор с той же меткой он молча игнорирует. На народных мониторах погоды дубликаты видны невооружённым глазом — одинаковые показания с разницей в секунду; это почти всегда чужой скетч без метки, а не магнитные бури.
| Код | Что случилось | Что делать плате |
|---|---|---|
| 200 / 201 | Записано; 201 — создана новая запись | Дочитать ответ getString() |
| 400 / 422 | Тело не разобралось: не тот формат, поле или имя заголовка | Сверить Content-Type, имена полей и имя ключа-заголовка |
| 401 / 403 | Ключ не принят | Проверить X-API-Key или Authorization |
| -1 | До сервера не добрались вообще | WiFi, адрес, таймаут — как в GET |
JSON-тело и куда расти
Современные API чаще ждут JSON, чем форму: тело выглядит как {"device":"dacha1","temp":21.4,"hum":58} — в фигурных скобках и с кавычками, строка на полсотни символов. В скетче меняются две вещи: Content-Type становится application/json, а тело собирается строкой или, правильнее, библиотекой ArduinoJson — она расставит кавычки и запятые за вас и не сломает синтаксис на одном пропущенном символе. Сборка документов — тема урока про serializeJson, а полный цикл «запрос → разбор ответа с фильтрами» живёт в уроке про разбор JSON. Моё правило по границе: два поля — можно собрать строкой, пять и больше или вложенные массивы — только ArduinoJson, рукастанье с кавычками в String того не стоит.
Если показания идут чаще раза в минуту — присмотритесь к MQTT. В уроке про устройство протокола MQTT плата держит постоянное соединение с брокером и шлёт по сто байт вместо пачки HTTP-заголовков на каждое измерение. Арифметика простая: POST раз в минуту — это миллисекунды соединения и пара сотен байт накладных расходов, POST каждые пять секунд — уже тысячи байт заголовков в минуту только ради двух чисел. Для одного POST в минуту HTTP честнее и проще: не нужен отдельный брокер, отладка идёт в Serial. А вот пять датчиков по минуте уже заметно грузят и сеть, и сервер — на этой развилке я когда-то потерял два вечера, поэтому оставляю её размеченной.
С симулятором — оговорка наперёд: движок эмулирует только GET, вызов http.POST() в виртуальной сети не поддержан. Поэтому в скетче ниже POST-часть заменена честной моделью: плата формирует тело, печатает в Serial запрос целиком, как он ушёл бы в сеть — строку метода, Content-Type, длину тела, пустую строку и само тело, — а затем делает контрольный GET на тестовый URL, чтобы показать живой обмен. На реальной плате между печатью тела и контрольным GET стоит http.POST(body) из скетча выше — остальное совпадает дословно. Такой «протокол на бумаге» — кстати, мой любимый способ отладки формата: пока сервер не принял тело, его глазами сервера видно сразу. И обратите внимание на Content-Length в логе: движок печатает настоящую длину собранной строки — так же сам считает её и HTTPClient на живой плате.
- POST = тело вместо адреса: строка метода, Content-Type, Content-Length, пустая строка, данные
- http.POST(body) возвращает код ответа; ответ сервера дочитывается тем же getString()
- Формат тела и Content-Type меняются парой: form-urlencoded или application/json
- 201 Created — запись создана; повторный POST создаёт дубликат, ретраи лечатся меткой времени
- Чаще раза в минуту — уже MQTT: постоянное соединение дешевле пачки HTTP-заголовков
- 1. Чем POST принципиально отличается от GET?
- 2. Забыли addHeader с Content-Type и отправили строку полей. Что получите?
- 3. Что возвращает http.POST(body)?
- 4. Timeout истёк, вы повторили POST. Что стало с показанием в базе?
- 5. Когда для тела POST стоит переходить на ArduinoJson?
Теперь у платы полный набор ролей: она подключается к сети, ходит за данными GET-ом и отдаёт свои POST-ом. Дальше по разделу — JSON по HTTP: ответы реальных API приходят упакованными, и ArduinoJson разберёт их без indexOf-самодеятельности — фильтры вытащат из килобайтного ответа только температуру и время, сэкономив память. Потом будут веб-сервер с маршрутами, mDNS с адресами по имени и время из NTP. А для серверной части держите правило из середины урока: один POST в минуту — HTTP, пять датчиков по минуте — MQTT. На этой развилке я в своё время потратил два вечера не на тот путь — вам она достаётся уже размеченной.
Частые вопросы по теме урока
- Чем POST принципиально отличается от GET?
- GET просит, POST отдаёт: данные уходят в теле с заголовками Content-Type и Content-Length, а код ответа возвращается как обычно. Данные едут в теле запроса, а не в адресе
- Забыли addHeader с Content-Type и отправили строку полей. Что получите?
- Без Content-Type сервер угадывает формат — и чаще всего отказывает кодом 400/422. Формат тела и заголовок проверяются парой. Скорее всего 400 или 422: сервер не понял формат тела
- Что возвращает http.POST(body)?
- Как и GET, POST возвращает int — код ответа. Ответ сервера потом дочитывается getString(). Код ответа сервера
- Timeout истёк, вы повторили POST. Что стало с показанием в базе?
- Каждый POST — новая запись. Ретраи лечат меткой времени или счётчиком: повтор с той же меткой сервер игнорирует. Удвоилось: POST не обновление, а создание записи
- Когда для тела POST стоит переходить на ArduinoJson?
- Два поля собираются строкой без боли. Пять и больше или вложенные массивы — ArduinoJson, который не сломает синтаксис. С трёх-пяти полей и при вложенных структурах