Погребной логгер из урока про DHT-логгер писал температуру в data.csv раз в десять секунд, и график за месяц вышел красивый — ровная линия с парой бугров. Один бугор сидел ровно в вечер пятницы, с восьми до половины девятого. Что это было — потеплело на улице, дверь держали открытой или сосед заходил за банками — данные молчат: в CSV есть температура, а событий в ней нет. Бумажка на стене со строчкой «19:52 — открыли люк» — инструмент так себе: листы теряются, время в них пишут примерно, а сводить рукописи с CSV приходится отдельно и вручную.
Лечится кнопкой-маркером. Нажал — и в тот же log.csv легла строка вида sec,MARK,name=3: sec — те же секунды от старта платы, что и у строк данных, name — номер нажатия, растёт с каждым разом. После выгрузки строка ищется одним действием: фильтр по слову MARK в таблице, поиск по слову в любом редакторе. Я ищу метку и читаю график в окне плюс-минус пара минут вокруг неё. Один вечер настройки — и лог перестал быть сухой температурой: в нём записано, где начался эксперимент, где открыли окно, где случилось то самое.
Собираем полный скетч: DHT22 на D2 держит поток данных каждые десять секунд, кнопка на D3 втыкает метку с фронтом и антидребезгом, LED на D12 подтверждает вспышкой, что строка легла. Каркас — из урока про DHT-логгер, механика фронта — та же, на которой в уроке про старт-стоп кнопкой открывались сессии записи; разница в том, что здесь лог пишется всегда, а кнопка не управляет записью, а оставляет след в ней. Метки времени от часов реального времени — тема отдельного урока про штампы RTC; здесь считаем миллис-ом и честно оговариваем его границы.
Метка едет в тот же файл, что и данные
Главная мысль метода: MARK пишется в тот же CSV, тем же open-print-close, что и данные. Искушение завести отдельный events.txt велико и вредно. Два файла — это два независимых хвоста буфера: при сбое питания они теряются вразнобой, умирают одни события при живых данных или наоборот. Плюс ручная сводка потом: два файла в одну таблицу не сложатся сами. Одна строка в общем потоке лишена всех этих бед — она едет в той же очереди, встаёт между соседними измерениями и доезжает вместе с ними.
Формат строки я держу таким: первая колонка sec — общий ключ сортировки, дальше слово MARK и номер. Строки данных при этом выглядят как sec,temp,hum — колонок у метки меньше, но CSV переживает такие гибриды: таблица строится по шапке, а строчка с текстом во второй колонке просто не попадает в числовой столбец температуры. Excel и LibreOffice едят это спокойно, pandas — после параметра. Если сводка строгая, кладите метку в отдельные колонки с пустыми полями: sec,mark,name и sec,temp,hum у меня оба варианта жили, первый — короче в записи.
Миллис-метка вместо часов — компромисс с ясными границами. Пока плата живёт без перезагрузки, секунды от старта идеально совпадают с осью данных: у строк данных и у меток один и тот же startedAt, сортируй да строй. После reset счётчик обнулится, и в файле появится второй sec=0 — логгерам, которые переживают отключения, нужен RTC-штамп из урока про метки времени: там же разобрано, как часы и карта уживаются на разных шинах в одном скетче. Для настольного эксперимента «включил — записал — выключил» миллис-а честна до конца.
Фронт, антидребезг, LED: кнопка как прибор
#include <SPI.h>#include <SD.h>#include <DHT.h> const int chipSelect = 10;const int BTN = 3; // кнопка между D3 и GNDconst int LED = 12; // вспышка-подтверждение меткиDHT dht(2, DHT22); const unsigned long INTERVAL = 10000UL;unsigned long lastLog = 0;unsigned long startedAt = 0; int marks = 0; // счётчик меток name=Nbool prevBtn = HIGH;unsigned long lastPress = 0; void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); pinMode(LED, OUTPUT); dht.begin(); if (!SD.begin(chipSelect)) { Serial.println("SD error"); while (true) { } } startedAt = millis(); lastLog = millis();} void loop() { unsigned long sec = (millis() - startedAt) / 1000; // поток данных: строка CSV каждые 10 секунд if (millis() - lastLog >= INTERVAL) { lastLog = millis(); float t = dht.readTemperature(); float h = dht.readHumidity(); File f = SD.open("log.csv", FILE_WRITE); if (f) { f.print(sec); f.print(","); f.print(t, 1); f.print(","); f.println(h, 1); f.close(); } } // маркер: фронт HIGH -> LOW плюс окно 200 мс bool nowBtn = digitalRead(BTN); if (prevBtn == HIGH && nowBtn == LOW && millis() - lastPress > 200) { lastPress = millis(); marks = marks + 1; File f = SD.open("log.csv", FILE_WRITE); if (f) { f.print(sec); f.print(",MARK,name="); f.println(marks); f.close(); digitalWrite(LED, HIGH); delay(60); // вспышка-подтверждение digitalWrite(LED, LOW); Serial.print("mark name="); Serial.println(marks); } } prevBtn = nowBtn;}Фронт и окно. prevBtn хранит прошлое состояние пина, интересен только переход HIGH → LOW, а окно 200 мс режет дребезг — механические контакты звенят первые 5-20 мс после замыкания. Цифра не с потолка: осознанное повторное нажатие человек быстрее 150-200 мс не делает, поэтому одно окно закрывает сразу две беды — звон контактов и второе нажатие «на всякий случай». Для старт-стопа ту же цифру мы ставили в уроке про управление записью; здесь она защищает не сессию, а чистоту меток.
LED-вспышка на 60 мс — деталь, которую хочется выбросить, а не стоит. Без подтверждения человек жмёт второй и третий раз, и лог обрастает дублями, которые потом вычищаешь руками: из CSV метки не удаляются до самой выгрузки. Вспышка отвечает «легло» быстрее, чем палец успевает нажать снова. Сознательная цена — delay(60), в который скетч слеп; на фоне паузы самой записи это не принципиально, а двойные метки в моём погребе кончились именно после появления диода.
Пауза записи в момент нажатия — частый вопрос. Метка встаёт в 512-байтный буфер библиотеки и физически уезжает на карту вместе с сектором, до которого ей может быть и сто миллисекунд — ровно те паузы, что мы замеряли миллис-ом в уроке про скорость записи. Для человеческого тайминга «нажал — вспышка» это неощутимо, а порядок в файле метка получает честный: она и данные проходят через одну очередь буфера, никого не обгоняя. Отдельная очередь для меток не нужна — и вредна: вернёмся к двум хвостам из прошлого абзаца.
| Способ фиксации события | Что ляжет в файл | Когда мой выбор |
|---|---|---|
| Кнопка-маркер (этот урок) | sec,MARK,name=N в общий CSV | события ловит человек: начало опыта, открыли люк, сняли показания |
| Метка по условию в скетче | та же строка MARK в ветке if (t > 30) | событие формализуемо порогом; в момент пика до кнопки не успеть |
| RTC-штамп в каждой строке | дата-время из DS3231 вместо sec | логгер переживает перезагрузки и переезды между объектами |
| Отдельный файл events.txt | метки в стороне от данных | не использую: два хвоста буфера теряются вразнобой при сбое |
Вторая строка таблицы — автоматическая метка — стоит пары строк без нового скетча: в ветке условия вызывается тот же f.print(sec); f.print(",MARK,name="); f.println("t>30"). Так в лог оставляют след события, до которого кнопкой не дотянуться: пик температуры, обрыв датчика, сработавшее реле. У меня насос в теплице включается реле, и скетч пишет строку sec,MARK,name=PUMP: после выгрузки поиск по MARK показывает все включения с точностью до секунды, и для ответа на вопрос «сколько раз за ночь гоняли воду» не нужен ни датчик тока, ни наблюдатель. Разобрать такие строки в таблицу потом помогает урок про strtok и atoi — метка с текстовым полем ест тот же разбор, что и числа.
Дозируйте метки. Одна на осмысленное действие — «начал опыт», «поменял банку» — работает; метка каждые полминуты превращает хвост файла в рощу MARK, среди которой снова ничего не видно, только искать теперь дольше. Мой ориентир: за вечер эксперимента их набирается пять-десять, и каждая занесена в блокнот со своим номером. Если событий много и все важные — это уже не маркер, а данные, и им нужна своя колонка в CSV, а не строка-исключение. Номер name при этом не сбрасывается до reset платы: если логгер переживёт перезагрузку, счётчик пойдёт с единицы — ещё один аргумент за RTC-штамп в длинных проектах.
Симулятор: метка среди строк данных
- Метка едет в тот же log.csv, что и данные: одна очередь, один хвост буфера, при сбое теряются вместе — отдельный events.txt я не веду
- Формат sec,MARK,name=N: общий ключ sec сортируется вместе с данными, name — короткий номер для заметок в блокноте
- Кнопка — это фронт HIGH → LOW плюс окно 200 мс: дребезг 5-20 мс и второе нажатие отсекаются одним сравнением millis()
- LED-вспышка 60 мс — подтверждение «легло»; без неё лог обрастает дублями, которые вычищаются руками
- Миллис-метка честна до первой перезагрузки; переживает питание — RTC-штамп из урока про метки времени
- 1. Куда пишется строка MARK при нажатии кнопки?
- 2. Почему окно антидребезга 200 мс, а не 30 мс?
- 3. Нажатие пришлось на момент физической записи сектора. Что со строкой MARK?
- 4. Зачем LED-вспышка на 60 мс после записи метки?
- 5. Когда миллис-метки перестают быть честными?
Частые вопросы по теме урока
- Куда пишется строка MARK при нажатии кнопки?
- Метка и данные проходят через одну очередь буфера и доезжают вместе; отдельный файл даёт два независимых хвоста, которые при сбое теряются вразнобой, плюс ручную сводку потом. в тот же log.csv, что и данные, той же командой open-print-close
- Почему окно антидребезга 200 мс, а не 30 мс?
- Короткое окно гасит звон контактов, но не спасает от второго нажатия «на всякий случай» — оно даст дубль метки. Двести миллисекунд закрывают обе беды и не тормозят человека. дребезг укладывается в первые 20 мс, а осознанное повторное нажатие человек не делает быстрее 150-200 мс — окно режет и его
- Нажатие пришлось на момент физической записи сектора. Что со строкой MARK?
- Метка проходит через ту же очередь 512-байтного буфера, что и данные. Физическая запись сектора занимает 7-8 мс с пиками до 100 — для человека это неощутимо, порядок в файле честный. метка встанет в буфер и уедет на карту вместе с сектором — до 100 мс спустя
- Зачем LED-вспышка на 60 мс после записи метки?
- Без подтверждения рука жмёт повторно — в логе появляются дубля, которые вычищаются вручную. Вспышка отвечает быстрее, чем палец успевает нажать снова. человек знает, что метка легла, и не жмёт второй раз из осторожности
- Когда миллис-метки перестают быть честными?
- Переполнение millis() разности переживают корректно, а вот reset обнуляет счёт целиком. Логгерам, живущим между включениями, нужен штамп от часов реального времени. после первой перезагрузки платы — отсчёт начнётся с нуля, и в файле появится второй sec=0