Логгер в щитке писал исправно, но каждый взгляд на данные стоил получаса: отключить питание, снять крышку, подковырять карту из разъёма push-push, который слабеет с каждым извлечением, вставить в ноутбук, посмотреть две цифры, собрать всё обратно. На четвёртый раз я понял, что извлекаю карту чаще, чем чищу фильтр в аквариуме, и решил лечить это сетью: пусть плата сама отдаёт лог по WiFi, а я листаю его из браузера телефона.
Arduino Uno для такой задачи не годится — ей нечем держать WiFi. А вот ESP32 соединяет всё в одной плате: WiFi и шина SD живут там бок о бок, и базу мы уже закладывали в уроке про первый WiFi на ESP32. Схема незамысловатая: логгер пишет на карту как обычно, а отдельный обработчик по запросу GET /log отдаёт файл прямо в браузер. Не облако, не база данных — один GET и текст на экране. Бонус схемы: раз лог живёт на карте, копий не нужно — читаешь откуда угодно, хоть с дивана, хоть из другого города, лишь бы плата и телефон были в одной сети.
Здесь разберём, какие пины занимает SD на ESP32 и чем это отличается от Uno; из чего состоит HTTP-ответ и от чего зависит, покажет браузер текст или предложит скачать; и соберём скетч веб-сервера, отдающего log.csv. Отдельно пройдём по граблям: файл на десятки мегабайт, таймауты браузера и пустые ответы. Если логгер у вас ещё не пишет, начните с первого урока раздела — веб-серверу нужна готовая карта.
ESP32 и SD: та же шина, другие пины
На Uno SD-карта забирает аппаратное SPI — MOSI 11, MISO 12, SCK 13 и CS 10, мы раскладывали это в уроке про распиновку SD по SPI. На ESP32 та же шина, но пины свои: у VSPI это SCK 18, MISO 19, MOSI 23 и CS 5. Достаточно вызвать SD.begin(5), а если скетч уже где-то конфигурирует SPI — задайте всё явно: SPI.begin(18, 19, 23, 5) перед SD.begin. С уровнями проще, чем на Uno: ESP32 сам трёхвольтовый, и карте его логика родная. Модуль с AMS1117 и делителями подключается без сюрпризов; голую карту напрямую по-прежнему нельзя. Файл на ESP32 открывается путём со слэшем: /log.csv — путь выглядит как на Linux, и без начального слэша SD.open создаст файл не там, где вы его будете искать.
Питание я на ESP32 проверяю дважды. При старте WiFi плата потребляет пиками до полампера, и просевшее питание SD-модуля даёт ровно те симптомы, что и на Uno: SD error при заведомо живой карте. Отдельный стабилизатор на модуль или короткие толстые провода решают; анатомия модуля и его аппетиты разобраны в уроке про устройство модуля SD. На макетке SD и WiFi живут вместе спокойно при коротких проводах до модуля и отдельном питании — иначе каждый старт WiFi будет звёздочкой на карте ошибок инициализации.
HTTP-ответ — обычный текст, и порядок в нём строгий: строка статуса (HTTP/1.1 200 OK), затем заголовки, по одному на строку, затем пустая строка-разделитель, затем тело. Браузер судит о теле по заголовку Content-Type: text/plain — просто текст, показывай как есть; на text/csv многие браузеры норовят скачать файлом, поэтому для листания лога с телефона я ставлю text/plain. Без Content-Type часть браузеров показывает кашу или качает файл — одна строка заголовка снимает все следующие вопросы. А пустую строку между заголовками и телом я однажды потерял: страница открывалась, текста не было, и полчаса я смотрел в Content-Type, который был ни при чём.
В скетче грязную работу делает библиотека WebServer: server.on("/log", handleLog) регистрирует обработчик на путь, server.handleClient() в каждом обороте loop разбирает входящие соединения. Внутри обработчика — открытие файла, заголовки, отдача тела. В модели симулятора ниже тот же ответ печатается в Serial: строки те же, только уезжают не по WiFi, а в монитор. Пути и здесь различаются: обработчик открывает /log.csv, а если сессий несколько — путь у каждой ровно один: /run1.csv, /run2.csv.
#include <WiFi.h>#include <WebServer.h>#include <SPI.h>#include <SD.h> const char* ssid = "home"; // ваша сетьconst char* pass = "password";const int CS_PIN = 5; // VSPI: SCK 18, MISO 19, MOSI 23 WebServer server(80); void handleLog() { File f = SD.open("/log.csv", FILE_READ); if (!f) { server.send(404, "text/plain", "log.csv not found"); return; // пустых 200 не оставляем } server.setContentLength(f.size()); // браузер знает, сколько ждать server.send(200, "text/plain", ""); uint8_t buf[512]; while (f.available()) { size_t n = f.read(buf, sizeof(buf)); // кусок = сектор server.sendContent((const char*)buf, n); delay(1); // дышим: WiFi-стек и соседние задачи } f.close();} void setup() { Serial.begin(115200); SPI.begin(18, 19, 23, 5); if (!SD.begin(CS_PIN)) { Serial.println("SD error"); while (true) { } } WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { delay(500); } Serial.println(WiFi.localIP()); // этот адрес открыть в браузере server.on("/log", handleLog); server.begin();} void loop() { server.handleClient();}Чтение и отправка идут кусками по 512 байт — ровно сектор, минимальная порция, которую карта отдаёт за одно обращение. f.read(buf, sizeof(buf)) кладёт кусок в буфер, sendContent уезжает по WiFi, и так до конца файла. delay(1) в цикле — не украшение: он отдаёт квант времени WiFi-стеку и соседним задачам ESP32, без него длинная отдача сыплется сбоями и таймаутами. Content-Length выставляется из f.size() до начала отправки: браузер заранее знает, сколько байт ждать, и показывает прогресс вместо вечного вращения индикатора. Чанк в 512 байт по WiFi улетает за единицы миллисекунд, то есть мегабайтный файл занимает секунды — но только пока цикл отправки не зажат чем-то ещё.
Почему не отдать всё одним вызовом? Файл на 60 мегабайт в строку не влезет ни в RAM, ни в String — памяти на ESP32 хватает на буфер и ни байтом больше. Поэтому чанки — единственный способ для файлов крупнее пары сотен килобайт, и он же самый бережный: буфер один, переиспользуется, карты касается каждый сектор по одному разу. Скорость карты здесь не главное: упираемся в WiFi и в обработчик, поэтому Class 4 хватает с запасом — как и на Uno. Кстати о занятости: обработчик, который читает карту без пауз, прижимает и логгер на этой же плате — та же история, что в уроке про скорость записи и потерянные строки, только теперь пауза мешает не Serial, а вашей собственной записи.
| Способ достать лог | Что нужно | Когда выбираю |
|---|---|---|
| Вынуть карту к ноутбуку | Разобрать корпус, руки и кардридер | Разовая выгрузка раз в месяц |
| Дамп файла в Serial | Ноутбук с IDE рядом с платой | Отладка на столе |
| GET /log с ESP32 по WiFi | Сеть рядом, корпус не трогаем | Живой объект: лог смотрят с телефона |
Грабли больших файлов
Лог за полгода легко вырастает до десятков мегабайт, и мобильный браузер обрывает такое скачивание по своему таймауту раньше, чем вы успеваете пролистать. Моё решение — хвост: обработчик принимает параметр вроде /log?tail=200 и перед отправкой делает f.seek(f.size() - tail), отдаёт последние килобайты. Полный файл качается по требованию отдельной ссылкой, а глазами листают хвост — ровно как в просмотрщике логов на больших серверах. Сессии из урока про кнопку старт-стоп здесь помогают сами: каждая сессия — отдельный файл, и вместо одного гиганта качается файл нужного дня. Параметр читается через server.arg: WebServer отдаёт значение по имени, и вся математика seek-а вешается на него одной строкой.
Второй пункт — дисциплина loop: server.handleClient() должен вызываться в каждом обороте, без delay(5000) посреди цикла. Затесался большой delay — сервер отвечает раз в пять секунд, и таймауты возвращаются уже по его вине. Развитие темы веба на той же плате: HTML-страница прямо из скетча разобрана в уроке про WebServer и HTML, а управление светодиодом из браузера — тема отдельного урока раздела esp32. Сам логгер, который кладёт файл на карту, собирался в первом уроке раздела — веб-сервер встраивается к нему без единого изменения схемы записи. Одна оговорка: читать и писать один и тот же файл одновременно не стоит — у меня такой файл дважды разваливался в ноль, поэтому для веб-выдачи держите отдельную карту или хотя бы отдельный файл.
- 1. Какие пины занимает SD-карта на ESP32 по умолчанию?
- 2. Что указать в ответе, чтобы браузер показал текст лога, а не предложил скачать?
- 3. Почему файл отдают чанками по 512-1024 байта?
- 4. Браузер показывает пустую страницу, файл на карте есть. Первая проверка?
- 5. Лог вырос до 60 МБ, браузер обрывает скачивание. Что сделать?
- SD на ESP32 занимает VSPI: SCK 18, MISO 19, MOSI 23, CS 5; SPI.begin задавайте явно перед SD.begin
- HTTP-ответ по порядку: статус, Content-Type: text/plain, пустая строка, тело файла
- Файл крупнее сотни килобайт отдавайте чанками по 512 байт с delay(1) в цикле — иначе таймауты и сбои
- Content-Length из f.size() до отправки: браузер знает, сколько ждать, и показывает прогресс
- Большой лог листайте хвостом: seek на последние килобайты по параметру tail, полный файл — по отдельной ссылке
Частые вопросы по теме урока
- Какие пины занимает SD-карта на ESP32 по умолчанию?
- Шина та же SPI, но у VSPI на ESP32 свои ножки: SCK 18, MISO 19, MOSI 23, CS 5. SPI.begin можно задать явно перед SD.begin. SCK 18, MISO 19, MOSI 23, CS 5
- Что указать в ответе, чтобы браузер показал текст лога, а не предложил скачать?
- По Content-Type браузер решает, показывать тело или качать. text/plain показывается как текст; text/csv многие браузеры скачают файлом. Content-Type: text/plain
- Почему файл отдают чанками по 512-1024 байта?
- f.read кладёт ровно сектор в переиспользуемый буфер, delay(1) между чанками отдаёт квант WiFi-стеку — длинная отдача не валит соединение. Чанк — один сектор, стек успевает дышать: меньше шансов на таймаут и сбой
- Браузер показывает пустую страницу, файл на карте есть. Первая проверка?
- Классика: обработчик отправил 200 без тела, потому что файл не открылся. Сначала проверка if (!f) с 404, и только потом заголовки и чанки. Открылся ли файл: if (!f) и статус 404 вместо пустого 200
- Лог вырос до 60 МБ, браузер обрывает скачивание. Что сделать?
- Мобильный браузер обрывает длинное скачивание по таймауту. Отдача последних килобайт через f.seek по параметру — листают хвост, полный файл качают отдельно. Отдать хвост: seek на последние килобайты по параметру tail