Лог за июль я открыл в августе и наткнулся на собственную лень: строки вида 24.7,24.7,0 — какая колонка температура, какая влажность, что за ноль в конце? Второй удар был похуже: посреди недели я добавил в конец строки четвёртое поле, и в одном файле легли два формата — старые строки по три колонки, новые по четыре. Скрипт, который ждал четыре колонки в каждой строке, сломался на первой же старой записи, и неделя данных превратилась в тыкву. Разбор занял вечер вместо минуты: файл резали на два куска, а заказчик тем временем смотрел на пустой график. Обе беды из одного корня: состав колонок я придумывал в момент записи, а не до него.
Теперь у меня правило: колонки, их порядок и имена проектируются до первой записи, как схема проводки до пайки. В базовом уроке про CSV мы уже писали шапку и лог раз в минуту; здесь пойдём глубже — какие колонки я закладываю в типовой логгер, зачем в логе версия прошивки и почему запятая внутри данных — самый дорогой символ формата. Лог — это не только для скриптов: его чаще всего открывают люди, и люди читают глазами.
Четыре колонки, которые переживут месяц на полке
Мой типовой набор: t_ms, raw, status, fw. t_ms — миллисекунды от старта через millis(): часы реального времени стоят денег и ног на плате, а большинству задач нужно не календарное время, а интервалы между событиями. raw — показание без обработки: аналоговое значение 0-1023 или число с датчика; сырые данные позже пересчитываются любым коэффициентом, а вот пропущенного уже не добудешь — так писали и в первом уроке про логирование. Для каждого поля у меня вопрос-фильтр: пригодится ли оно через месяц в сыром виде? Если да — отдельная колонка; если нет — не пишем вовсе, карта не мусорка. humidity, если датчик умеет, — пятой колонкой в новом файле, а не через точку с запятой в raw. status — код события: 0 норма, 1 порог, 2 тревога; коды короче слов и не содержат запятых. fw — версия прошивки: одна цифра, которая через месяц экономит вечер.
Зачем колонка версии: как только меняется обработка, старые и новые строки начинают врать друг о друге. Я однажды поправил коэффициент пересчёта и неделю не мог понять, почему график дёргается на границе — строки до перепрошивки считались старым коэффициентом. Теперь фильтр по fw отрезает чужие строки одним движением. Версию я поднимаю при каждом изменении, которое влияет на данные, — даже при смене частоты записи; и при откате прошивки на объекте старые строки снова честные, фильтр покажет это сам. Арифметика объёма: строка из четырёх колонок — примерно 22-24 байта с переводом строки; при записи раз в минуту это 1440 строк или около 33 КБ в сутки, мегабайт за месяц. Карта на 2 ГБ при таком темпе пустеет дольше пяти лет, и это с запасом на более частые записи.
| Колонка | Что пишет | Пример | Зачем |
|---|---|---|---|
| t_ms | миллисекунды от старта | 3720000 | интервалы без RTC, сортировка по времени |
| raw | сырое показание датчика | 612 | пересчёт задним числом любым коэффициентом |
| status | код события 0/1/2 | 1 | фильтр тревог без текста и запятых |
| fw | версия прошивки | 12 | отсекает строки другого формата обработки |
Заголовок — первая строка файла: t_ms,raw,status,fw. Пишется один раз при создании файла, проверкой SD.exists — механику повторных шапок после каждой перезагрузки разобрали в базовом уроке, там же про импорт в русский Excel. Имена колонок я делаю равными именам переменных в скетче: через месяц не приходится вспоминать, что значит поле t2, — файл и код говорят на одном языке, и скрипт читает данные по имени, а не по номеру столбца. И никаких пробелов после запятых в шапке: Excel переживёт, а скрипт с жёстким сплитом по запятой — нет.
Читаемость глазами — критерий, который я недооценивал годами: откройте лог в Блокноте. Если через минуту непонятно, что происходит в устройстве, колонки спроектированы плохо, сколько бы скриптов их ни ели. И порядок колонок — контракт на весь файл: нужны новые данные — заводите новый файл с новой шапкой и версией fw на единицу больше, а не переименовываете поля посреди старого. Лог с исправными колонками потом приятно читать и обратно в скетч — побайтовым циклом из урока про available и read или построчно. Мой тест: покажите лог человеку, который не видел устройство, — если он угадал, что случилось ночью, колонки удались.
Разделитель: запятая, кавычки и точка с запятой
Канонический разделитель CSV — запятая, и числовые логи я пишу именно ею: файл без переделок едят pandas, Python и любая телеметрия. Русский Excel по умолчанию ждёт точку с запятой и запятую в дробях — это проходили в базовом уроке, вместе с импортом через «Данные → Из текста/CSV». Здесь про другое: если текстовое поле всё-таки нужно и запятая в нём возможна, у поля два щита — двойные кавычки вокруг значения либо смена разделителя на точку с запятой целиком для файла. Смешивать разделители в одном файле нельзя: читалка выберет один, и колонки поедут у всех строк одинаково криво. Правило с кавычками каскадное: внутри поля, взятого в кавычки, сама кавычка удваивается — ещё один довод в пользу числовых кодов.
Дроби пишу точкой, потому что Serial.print(float) ставит её сам, а для парсера 24.7 и 24,7 — разные вещи. Комментарии в CSV не существуют: строка с решёткой — просто битая запись. Карту кодов статусов я держу в комментариях скетча или отдельным readme.txt на карте — файл лога остаётся чистой таблицей, которую читает всё подряд без настроек. Точка с запятой — легальный разделитель, если лог пишется для одного Excel: pandas тоже умеет его принимать. Но я держу канон, чтобы не помнить, где какой файл. И дробные сырые значения я не пишу вовсе: raw целым числом 0-1023 точнее отображает АЦП, чем пересчитанные вольты с двумя знаками после запятой.
#include <SPI.h>#include <SD.h> const int chipSelect = 10;const int FW = 12; // версия прошивки - колонка в логеconst char *fileName = "log.csv"; unsigned long lastLog = 0;unsigned long startedAt = 0; void setup() { Serial.begin(9600); if (!SD.begin(chipSelect)) { Serial.println("Нет карты"); while (true) { } } startedAt = millis(); if (!SD.exists(fileName)) { // шапка - один раз, в новом файле File f = SD.open(fileName, FILE_WRITE); f.println("t_ms,raw,status,fw"); f.close(); } lastLog = millis();} void loop() { if (millis() - lastLog >= 60000UL) { lastLog = millis(); int raw = analogRead(A0); int status = 0; // 0 - норма, 1 - выше порога, 2 - тревога if (raw > 600) { status = 1; } if (raw > 900) { status = 2; } File f = SD.open(fileName, FILE_WRITE); if (f) { f.print(millis() - startedAt); f.print(","); f.print(raw); f.print(","); f.print(status); f.print(","); f.println(FW); f.close(); // упало питание - потеряли одну строку } }}В скетче шапка создаётся вместе с файлом, а каждая строка собирается пятью print-вызовами: числа по колонкам, запятые между ними, перевод строки в конце. Пятая колонка не понадобилась — и правильно: колонки дешевле добавить в новый файл с новой шапкой и fw на единицу больше, чем чинить смешанный формат старого. Запись раз в минуту держится на millis-планировщике из урока про millis вместо delay — delay здесь заморозил бы опрос датчика между записями. Значение пишется сразу после замера: между чтением A0 и строкой в файле — миллисекунды, и запись честно описывает состояние устройства в момент замера. Проверка колоночной дисциплины бесплатна: сравните шапку и первую строку данных — количество запятых должно совпадать байт в байт.
Дальше по маршруту: конфиг с порогами для такого логгера разобран в уроке про readStringUntil и конфиг. Полный цикл обслуживания карты — открытие, запись, закрытие и что теряется при сбое питания — собран в уроке про запись и close. Первый логгер, с которого всё началось, живёт в самом начале раздела. Старые логи при смене формата не удаляйте: новая шапка, новый файл, а архив остаётся с честной fw в каждой строке.
- 1. Зачем в CSV-логе отдельная колонка с версией прошивки?
- 2. Текстовое поле может содержать запятую. Как не сломать CSV?
- 3. Сколько примерно места займёт лог из 4 колонок при записи раз в минуту за месяц?
- 4. Почему статус события пишут кодом 0/1/2, а не словами «норма» и «тревога»?
- 5. Когда писать строку заголовка t_ms,raw,status,fw?
- Колонки проектируются до первой записи: состав, порядок и имена — контракт на весь файл
- Типовой набор t_ms,raw,status,fw: интервалы без RTC, сырые данные для пересчёта, код статуса, версия прошивки для фильтра
- Шапка — первая строка с именами колонок, пишется один раз при создании файла проверкой SD.exists
- Строка из 4 колонок — 22-24 байта; раз в минуту это 33 КБ в сутки — карта на 2 ГБ пустеет дольше пяти лет
- Запятая внутри значения рвёт колонки: кавычки — канон, числовые коды — практика автора; смешивать разделители в файле нельзя
- Новые поля — новый файл с новой шапкой и fw на единицу больше, а не переименование полей посреди старого лога
Частые вопросы по теме урока
- Зачем в CSV-логе отдельная колонка с версией прошивки?
- После смены коэффициента или формата старые и новые строки врут друг о друге. Фильтр по fw отрезает строки другого формата одним движением — автор урока потерял на этом неделю. Чтобы отфильтровать строки, записанные другой обработкой
- Текстовое поле может содержать запятую. Как не сломать CSV?
- Канон CSV берёт поле с запятой в двойные кавычки. Практика автора честнее: кодировать статусы числами — запятых нет, колонки не едут, парсер проще. Взять значение в двойные кавычки или кодировать поле числом
- Сколько примерно места займёт лог из 4 колонок при записи раз в минуту за месяц?
- Строка из четырёх колонок — 22-24 байта с переводом строки, 1440 строк в сутки — около 33 КБ, за месяц около мегабайта. Карта на 2 ГБ при таком темпе пустеет дольше пяти лет. Около 1 МБ — примерно 33 КБ в сутки
- Почему статус события пишут кодом 0/1/2, а не словами «норма» и «тревога»?
- Код экономит байты каждой строки и не содержит запятых — колонки не едут. Соответствие кодов и событий хранят в комментариях скетча или readme на карте. Код короче, а запятых в нём не бывает
- Когда писать строку заголовка t_ms,raw,status,fw?
- Безусловная шапка при каждом старте накидывает десятки строк-заголовков после перезагрузок, и столбцы едут. Пишем один раз при создании файла — проверкой SD.exists. Один раз, при создании файла, проверкой SD.exists