Гаражная сигнализация писала в монитор события честно и подробно: «створка открыта», «створка закрыта», «движение». Каждую неделю я сохранял окно монитора в файл — до того дня, когда понадобился ответ на простой вопрос: во сколько ночью открывались ворота. В журнале не было ни одного штампа; самая точная формулировка, которую он выдавал, — «пять строк выше записи про движение». Плата события помнила, момент — никогда.
Второй журнал я вёл умнее: штамповал строки счётчиком millis() — и продержался до первой грозы. Плата перезапустилась, счётчик обнулился, и в файле соседствуют «двенадцать миллионов секунд» и «четыре секунды», обе про один вечер; почему счётчик с последнего запуска не годится в метки, а часы с батарейкой годятся, я разводил по полочкам в уроке про millis и часы.
Журнал показаний и журнал событий — разные вещи. Телеметрия пишется по ритму — раз в минуту, раз в час, — и её штампы уже собирали с картой памяти в уроке про логгер на SD. События редки, непредсказуемы, и каждое заслуживает собственной строки с меткой времени. Самый дешёвый журнал для таких строк — Serial: не нужен ни модуль карты, ни файловая система, монитор пишет то, что шлёт плата, а сохранить текст можно копипастой или утилитой со стороны компьютера, которая слушает порт и пишет всё в файл.
Здесь собираем строку журнала по слоям: штамп времени из часов с батарейкой, уровень события, источник, текст. Разберём, зачем нулям в штампе своя работа (05 против 5), как одной командой переключать строку под Excel, и почему штамп честен к моменту записи, но не всегда — к моменту происшествия.
Анатомия строки: штамп, уровень, источник, текст
Строка журнала, которая не бесит через месяц, состоит из четырёх полей. Штамп — девятнадцать символов в формате 2026-01-15 09:07:03: год, месяц, день, затем время. Уровень — INFO для рутины и WARN для тревог: глазами файл с уровнями читается втрое быстрее, потому что поиск «где сломалось» сводится к поиску WARN. Источник — короткое имя узла: сейчас плата одна, а когда рядом появится вторая, смешанные журналы без имён распутывать мучительно. Дальше — сам текст события, коротко и по-человечески.
Штамп собирается из объекта DateTime одной функцией: снимок now() берётся один раз в начале обработки события, поля переводятся в строку с ведущими нулями. Паддинг тут не косметика. Без него 9:07:03 превращается в 9:7:3 — колонки едут, глаз спотыкается на каждой строке. Хуже того, сортировка строкой разваливается: в списке «9:7» и «10:6» первый окажется позже второго, хотя на часах всё наоборот. Ведущий ноль делает все значения поля одной ширины — и обычная текстовая сортировка становится хронологической. Функция на три строки решает это раз и навсегда, и переезжает из скетча в скетч без правок.
Мелочь, которая портит целые файлы: два вызова rtc.now() в одной строке. Между двумя вызовами 09:07:03 соседствует поле 09:07:04, а на границе минуты журнал получает строку, которой не было ни в какую секунду. Один снимок в переменную, дальше из него читаются все шесть полей: год для имени файла, час для фильтра «ночь или день» — покуда снимок один, любые нарезки согласованы между собой.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;const int KNOPKA = 2;bool csv = false; // 0 — человекочитаемый, 1 — CSVunsigned long poslTeler = 0; String pad2(int v) { if (v < 10) return "0" + String(v); return String(v);} String stamp() { // один снимок now() на строку DateTime now = rtc.now(); String s = ""; s += now.year(); s += "-"; s += pad2(now.month()); s += "-"; s += pad2(now.day()); s += " "; s += pad2(now.hour()); s += ":"; s += pad2(now.minute()); s += ":"; s += pad2(now.second()); return s;} void logi(String uroven, String kto, String tekst) { String razdel = csv ? ";" : " | "; // локаль Excel: запятая/точка с запятой Serial.print(stamp()); Serial.print(razdel); Serial.print(uroven); Serial.print(razdel); Serial.print(kto); Serial.print(razdel); Serial.println(tekst);} void setup() { Serial.begin(9600); pinMode(KNOPKA, INPUT_PULLUP); rtc.begin(); rtc.adjust(DateTime(F(__DATE__), F(__TIME__))); logi("INFO", "garazh", "start posle podklyucheniya");} void loop() { if (digitalRead(KNOPKA) == LOW) { // дверь открыли logi("WARN", "garazh", "stvorka otkryta"); delay(300); // грубый антидребезг для примера } if (millis() - poslTeler >= 10000UL) { // телеметрия раз в 10 с poslTeler = millis(); logi("INFO", "garazh", "temp chipa " + String(rtc.getTemperature(), 1)); }}Экспорт: из монитора в таблицу
Экспорт начинается с удержания строк целиком. Монитор передаёт байты через буфер UART на 64 байта, и если скетч шлёт журналы быстрее, чем порт их отделяет, соседние строки склеиваются или рвутся — устройство буфера и его границы разобраны в уроке про буфер UART. Для редких событий хватает и 9600 бод: строка журнала в двадцать пять байт уходит за двадцать пять миллисекунд. Скорость 115200 не спасёт от склейки, если шлёть по строке на каждый круг loop, — лечится ритм, а не боды. Правило у меня такое: одна строка — одна логическая запись, а не побайтовый поток, и пауза между записями всегда больше времени их печати.
Режим CSV я включаю командой из монитора и держу за одну роскошь: журнал открывается таблицей без ручного разрезания. Здесь есть грабля с национальностью: Excel в русской локали ожидает разделителем точку с запятой, в английской — запятую; пришлёте запятую русской локали — вся строка ляжет в одну колонку. Поэтому в скетче разделитель живёт в одной переменной и переключается командой csv: попробовали, увидели, как ваш Excel отреагировал, зафиксировали свой вариант. Штамп при этом остаётся ISO — он сортируется как текст, а столбец времени в таблице сортируется сам собой.
Теперь честная оговорка про точность. Штамп ставится в момент записи строки, а не в момент происшествия: событие могло полежать в буфере UART или подождать delay в цикле — и штамп отметит, когда о нём узнала плата. Для двери гаража опоздание в доли секунды неважно; для момента, который важен точнее, добавляю в строку доли секунды из millis() % 1000 — с оговоркой, что millis() считает от включения, и его «миллисекунды минуты» верны, только если секунды штампа сверены с реальными. Машине, которой журнал скармливается скрипту, чаще полезнее числовая метка — секунды от 1970 года из now.unixtime(): с ними интервалы считаются вычитанием, и вся арифметика таких меток разложена в уроке про unixtime; людям — строка с уровнями, и хороший журнал умеет печатать оба варианта. Тревожную часть я всегда шлю первой строкой — она важнее телеметрии.
| Вариант строки журнала | Пример | Слабое место |
|---|---|---|
| Человекочитаемый с уровнями | 2026-01-15 09:07:03 | WARN | garazh | stvorka otkryta | Excel его не режет по колонкам |
| CSV под русскую локаль | 2026-01-15 09:07:03;WARN;garazh;stvorka otkryta | в английской локали колонка одна |
| CSV под английскую локаль | 2026-01-15 09:07:03,WARN,garazh,stvorka otkryta | в русской локали колонка одна |
| Штамп с долями секунды | 09:07:03.482 — из millis() % 1000 | верен при сверке секунд с реальными |
Три действия дадут больше, чем абзац теории. Нажмите кнопку пару раз — подождите пять секунд и нажмите снова: в журнале появятся и WARN события, и INFO телеметрии, штампы встанут по секундной стрелке модуля. Подведите время кнопками на модуле часов — следующая строка получит новый штамп: часы с батарейкой отвечают за метки целиком, скетч их не пересчитывает. И отправьте csv 1 — та же история ляжет колонками, кавычек и настроек не потребуется; обратная команда csv 0 вернёт строку для глаз. Если нажать кнопку десять раз подряд, вы получите десять строк WARN с одинаковым текстом и разными штампами — это и есть тест на честность журнала.
Обратите внимание на флаг bylaNazhata: строка пишется на переходе отпущено-нажато, а не на каждом круге, пока держите кнопку. В code-блоке для платы для простоты стоял delay(300) — в живом проекте такой антидребезг замораживает журнал на треть секунды, и очередное событие, пришедшее за это время, штампуется с опозданием: ровно та цена, о которой шла речь в таблице выше. Переход по фронту из симулятора — это тот же приём, что ловит кнопки по изменению состояния в уроке про первый запуск монитора: журнал честен настолько, насколько честен момент его записи.
- 1. Почему снимок rtc.now() берётся один раз на строку журнала?
- 2. Что ломается в журнале без ведущих нулей, когда минуты показывают 9 и 10?
- 3. Какой разделитель ждёт Excel в русской локали?
- 4. Событие пробыло в буфере UART полсекунды. Какой штамп оно получит?
- 5. Что даёт millis() % 1000 в строке журнала?
- Строка журнала — четыре поля: штамп ISO, уровень INFO/WARN, источник, текст; уровни ускоряют поиск тревог втрое
- Паддинг нулей — не красота: фиксированная ширина полей делает текстовую сортировку хронологической
- Один снимок now() на строку: два вызова на границе секунды дают строку из разных моментов
- Разделитель CSV зависит от локали Excel: русская ждёт точку с запятой, английская — запятую; переключение одной командой
- Штамп отмечает момент записи, не происшествия: очередь печати и delay между строками его оттягивают
Serial-журнал — самый дешёвый способ получить метки времени, но у него жёсткая граница: он живёт, пока жив монитор. Как только журнал нужен без компьютера рядом, строка переезжает с Serial на карту — механика записи и close() там своя, я разбирал её в уроке про логгер на SD. А если событие обязано пережить десятки тысяч перезапусков и вести счёт сам себе, посмотрите на кольцевой журнал в EEPROM: штампы те же, носитель — без движущихся частей.
Частые вопросы по теме урока
- Почему снимок rtc.now() берётся один раз на строку журнала?
- Между двумя вызовами now() может смениться секунда, и в одной строке окажутся поля из разных моментов. Один снимок в переменную — и все шесть полей согласованы. Два снимка могут попасть на разные секунды — штамп развалится
- Что ломается в журнале без ведущих нулей, когда минуты показывают 9 и 10?
- Поля разной ширины рушат и колонки, и сортировку текстом: девятка встанет после десятки. Паддинг делает каждое поле фиксированной ширины. Строковая сортировка ставит 9:7 позже 10:6
- Какой разделитель ждёт Excel в русской локали?
- Русская локаль Excel режет по точке с запятой, английская — по запятой. Отсюда команда csv в скетче: разделитель подстраивается под приёмник. Точку с запятой
- Событие пробыло в буфере UART полсекунды. Какой штамп оно получит?
- Штамп честен к моменту записи строки. Опоздание в доли секунды на 9600 бод незаметно, но delay между печатями растягивает его до полусекунды. Момент чтения из буфера, когда строка печаталась
- Что даёт millis() % 1000 в строке журнала?
- Остаток даёт доли секунды, но привязан к моменту включения. Значение верно, только если секунды штампа сверены с реальными часами. Доли секунды — с оговоркой, что счёт идёт от включения