Мой первый метеологгер писал в CSV каждые десять секунд, как учили лучшие уроки интернета. Через неделю в файле было шестьдесят тысяч строк, а на вопрос «бывало ли в кладовке жарко в четверг днём» график отвечал стеной пикселей: цифр полно, ответа нет. Проблема была не в железе и не в библиотеке — в расписании. Частота записи сама по себе ничего не измеряет; измеряет её соответствие вопросу, ради которого журнал вообще заводился. Десятисекундная пила не отвечает ни на один вопрос про климат — она отвечает только на вопрос, жив ли датчик — и на этот вопрос журнал отвечает без карты вообще: первый запуск я всегда веду в Serial, и карта подключается, когда вопросы созрели.
Второй урок той недели был про качество пробы. Каждые десять секунд в файл уезжало одиночное чтение DHT22 со всеми его сюрпризами: один сквозняк на линии — и в графике вырастал шип до нуля, который потом вычищаешь вручную. В этом уроке собираю версию, которая живёт у меня теперь: записи по расписанию с интервалом под задачу, каждая строка — медиана всплеска из трёх проб, шапка файла пишется ровно один раз, пропуски не рвут сетку и не выдумывают значения. Сторона карты — формат CSV, режимы открытия и close после каждой записи — уже разобрана в уроке про CSV-логгер DHT; здесь смотрим на прибор глазами датчика: что писать, когда и какого качества. Интервал при этом живёт одной константой наверху скетча — менять его можно хоть ежемесячно, каркас от этого не трогается.
Расписание: интервал и всплеск пробы
Начнём с арифметики всплеска, потому что она ограничивает расписание сверху. Медиана из трёх проб требует разнести их на 2,1 секунды — правило интервала опроса никто не отменял, и механику выбора между средним и медианой мы считали в уроке про медиану и выборку. Три пробы с шагом — почти семь секунд на серию, а значит интервал записи короче пятнадцати секунд не имеет смысла: скетч будет жить в постоянном опросе, датчик не отдохнёт, а в журнале всё равно останутся те же честные точки. Мой рабочий слот — минута: всплеск занимает седьмую его часть, остальное время плата свободна, и это не расточительство: между слотами можно спокойно читать кнопки, обновлять дисплей и отвечать в Serial.
Второе ограничение — объём, и тут цифры утешительные. Строка с меткой секунд, двумя значениями и переводом строки весит около двадцати байт; минутный интервал даёт 1440 строк и примерно 29 КБ в сутки. Карте даже на 2 ГБ хватит на столетие — упираешься не в объём, а в собственное терпение листать. Таймер слота обязан стоять на millis, а не на delay — разбор кооперативного цикла показывает, что delay слепит логгер на весь интервал: ни кнопки, ни Serial, ни реакции на мир между записями. Таблица ниже — мои рабочие точки, посчитанные от двадцатибайтной строки. Столбец sec при этом честен внутри одного запуска — ограничение и лекарство от него обсудим в конце урока.
Минуту я выбрал не из таблицы, а из эксперимента: две недели слот в десять секунд соседствовал со слотом в минуту, и графики отвечали на одни и те же вопросы одинаково — но минутный читался сразу, а десятисекундный приходилось сглаживать. Правило вышло простое: если вопрос журнала формулируется часами — «жарило ли днём», «подсыхал ли подвал» — интервал больше минуты; если минутами — «как остывает чай», «что сделал сквозняк» — тридцать секунд и медиана, а одиночные пробы оставьте отладке.
| Задача лога | Интервал | Проба | Строк в сутки | Объём в сутки |
|---|---|---|---|---|
| подвал, гараж, кладовка | 10 минут | одиночное чтение | 144 | ≈3 КБ |
| комната, кабинет | 1 минута | медиана из 3 проб | 1440 | ≈29 КБ |
| теплица, инкубатор | 30 секунд | медиана из 3 проб | 2880 | ≈58 КБ |
| отладка и первый запуск | 10 секунд | одиночное чтение | 8640 | ≈173 КБ |
#include <SPI.h>#include <SD.h>#include "DHT.h" const int CS_PIN = 10;const int LED = 12; // сторож серии пропусковDHT dht(2, DHT22); const unsigned long INTERVAL = 60000UL; // слот раз в минутуconst unsigned long PAUSE = 2100UL; // шаг между пробамиconst int N = 3; // проб во всплескеconst int MISS_LIMIT = 5; // пропусков подряд -> LED unsigned long lastSlot = 0;unsigned long startedAt = 0;int rows = 0;int missed = 0;float pr[N]; void setup() { Serial.begin(9600); dht.begin(); pinMode(LED, OUTPUT); if (!SD.begin(CS_PIN)) { Serial.println("SD begin failed: format, pitanie ili provoda"); while (true) { } } startedAt = millis(); if (!SD.exists("data.csv")) { // шапка только для нового файла File f = SD.open("data.csv", FILE_WRITE); if (f) { f.println("sec,temp,hum"); f.close(); Serial.println("shapka zapisana odin raz"); } } lastSlot = millis();} void loop() { if (millis() - lastSlot < INTERVAL) return; // остальное время плата свободна lastSlot = millis(); int ok = 0; // всплеск: три пробы с шагом 2,1 с for (int i = 0; i < N; i++) { float v = dht.readTemperature(); if (!isnan(v) && v > -40.0 && v < 80.0) { pr[ok] = v; ok++; } if (i < N - 1) delay(PAUSE); } float h = dht.readHumidity(); unsigned long sec = (millis() - startedAt) / 1000UL; if (ok >= 2) { // медиана валидных проб for (int i = 1; i < ok; i++) { float key = pr[i]; int j = i - 1; while (j >= 0 && pr[j] > key) { pr[j + 1] = pr[j]; j--; } pr[j + 1] = key; } float med; if (ok == 3) med = pr[1]; else med = (pr[0] + pr[1]) / 2.0; File f = SD.open("data.csv", FILE_WRITE); if (f) { f.print(sec); f.print(","); f.print(med, 1); f.print(","); if (!isnan(h)) f.print(h, 0); // сбой влажности - пустое поле, не ноль f.println(); f.close(); rows++; missed = 0; digitalWrite(LED, LOW); Serial.print("row="); Serial.print(rows); Serial.print(" med="); Serial.println(med, 1); } else { Serial.println("open failed: karta, pitanie ili CS"); } } else { missed++; // слот провалился целиком Serial.print("slot missed, podryad: "); Serial.println(missed); if (missed >= MISS_LIMIT) digitalWrite(LED, HIGH); }}Пропуски заслуживают отдельного абзаца, потому что здесь легче всего наобум наврать в журнал. Если весь всплеск провалился — все три пробы вернули мусор — я не пишу в файл ни ноль, ни последнее удачное значение: строка просто не пишется, и в сетке остаётся честная дыра. Метка секунд делает дыру видимой: между 3600 и 3720 пропало полторы минуты, и график это покажет пустотой, а не фальшивым плато. Через месяц дыры даже полезны: по ним видно, когда линия была подозрительна, задолго до полного отвала. Пять провалов подряд — повод звать человека: светодиод загорается, и первым делом проверяешь питание модуля карты и линию датчика, потому что валится не одна проба, а серия целиком.
Отдельный вопрос — humidity. Случай, когда температура читается, а влажность — нет, бывает, и вписывать вместо неё ноль нельзя: ноль процентов влажности — физическая бессмыслица, которая поедет в графики. Поэтому в скетче пустое поле: две запятые подряд, Excel покажет разрыв, статистика не испорчена. Поле не заполняется — значит поле не измерялось; правило скучное, но журналы с ним переживут месяцы без ручной чистки. Я однажды потерял так неделю статистики инкубатора: нули влажности вычистить из истории оказалось дольше, чем один раз поправить скетч.
Сводку дня я печатаю по отдельному таймеру раз в сутки: строка rows= и missed= за двадцать четыре часа. Стоит она одной переменной, а отвечает на вопрос, который иначе вылезает через месяц: а половина строк не потерялась ли? Десяток пропусков за сутки — норма для длинной линии; сотня — уже диагноз, и вы узнаете об этом утром, а не когда график разложится.
Куда растёт лог
Первое усовершенствование после недели работы — календарный штамп вместо секунд от старта: метка sec честна внутри одного запуска, но после перезагрузки отсчёт начинается заново, и в файле встречаются две записи с sec=5, разделённые неделей. Модуль часов на шине решает это полностью — урок про штампы из RTC добавляет в строку дату и время и переживает любые перезагрузки. Второе — черновик в оперативной памяти: кольцевой журнал держит последние десятки показаний и отвечает на вопрос «что случилось пять минут назад» без чтения карты — карта при этом отдыхает, а вы успеваете принять решение, писать ли событие подробно. И одна мелочь, которая сэкономит вечер: в русской локали электронных таблиц запятая — разделитель десятичных, и значение 24,1 развалится в CSV на две колонки; точка с запятой в качестве разделителя полей лечит это навсегда.
На этом кластер замыкается, и звенья складываются в цепочку: медиана из предыдущих уроков делает каждую пробу чистой, верно выбранный датчик даёт верную физику, а расписание из этого урока превращает чистые пробы в журнал, на который можно ссылаться словами «в четверг днём жарило — вот строка». Дальше журнал расцветает по собственной воле: графики в таблице, экспорт в электронные таблицы, пороги и сигналы поверх истории. Скелет же — слот, всплеск, шапка один раз, честные дыры — я за годы не переделал ни разу.
- Расписание = интервал записи плюс качество пробы: медиана всплеска из трёх проб с шагом 2,1 с.
- Всплеск занимает почти семь секунд — интервал короче пятнадцати не имеет смысла: скетч живёт в постоянном опросе.
- Минутный слот — 1440 строк и около 29 КБ в сутки; шапка CSV пишется один раз по проверке SD.exists.
- Провал всплеска — дыра в сетке, а не выдуманное число; пять пропусков подряд зажигают сторожевой LED.
- Файл открывается и закрывается на каждой строке: десятки миллисекунд против потери всего файла при выдёргивании питания.
- 1. Почему слот записи не имеет смысла короче пятнадцати секунд?
- 2. Сколько примерно даст лог с минутным интервалом, если строка весит около 20 байт?
- 3. Весь всплеск провалился — все три пробы вернули мусор. Что попадёт в файл?
- 4. Зачем шапку CSV писать через проверку SD.exists только для нового файла?
- 5. Светодиод сторожа загорелся: пять слотов подряд провалились. Первое действие?
Частые вопросы по теме урока
- Почему слот записи не имеет смысла короче пятнадцати секунд?
- Три пробы с шагом 2,1 секунды — почти семь секунд серии. Ужимать интервал ниже пятнадцати секунд — значит жить в постоянном опросе без выигрыша в качестве журнала. Всплеск из трёх проб занимает почти семь секунд — датчик не успевает отдыхать между сериями
- Сколько примерно даст лог с минутным интервалом, если строка весит около 20 байт?
- В сутках 1440 минут — столько же строк, 1440 × 20 байт ≈ 29 КБ. Даже крошечная карта запасена на годы: объём здесь никогда не становится ограничением. Около 29 КБ
- Весь всплеск провалился — все три пробы вернули мусор. Что попадёт в файл?
- Дыра с меткой секунд честнее любого выдуманного числа: график покажет разрыв, а не фальшивое плато. Пять провалов подряд — сигнал проверить питание карты и линию датчика. Ничего: строка не пишется, в сетке остаётся дыра
- Зачем шапку CSV писать через проверку SD.exists только для нового файла?
- Без проверки каждый запуск платы добавит строку шапки к существующему журналу — и в середине журнала встанет мусорная строка. exists различает новый файл и продолжение старого. Чтобы каждая перезагрузка не дописывала sec,temp,hum в середину накопленных строк
- Светодиод сторожа загорелся: пять слотов подряд провалились. Первое действие?
- Целиком провалившиеся серии — это электрика, а не статистика: питание модуля, контакт линии, длина провода. Форматирование и урезание интервала ничего не починят. Проверить питание модуля карты и линию датчика — валится серия целиком