Полив на таймере я перевёл с millis на часы ради одного обещания: включать насос ровно в 18:30, когда жара сошла. Первые сутки всё сработало. Вторые — нет: узел к тому моменту получил вторую работу, логировать температуру раз в десять минут, цикл стал редким и тяжёлым, и будильная минута проскочила между двумя пробуждениями. Условие «час равен восемнадцати и минута равна тридцати» было безупречным и бесполезным: оно ловит ровно одну минуту из шестидесяти, и если цикл в неё не попал — события просто не было. Полив, которого не было, я обнаружил на утро по сухой земле: железо честно отработало своё расписание, расписание честно не сработало.
Ловушку равенства внутри одной минуты мы уже закрывали флагом в уроке про часы с будильником — там loop() бегал часто, и флаг гасил шестьдесят повторов. Здесь случай зеркальный: цикл редкий, и та же минута может пройти мимо целиком. Формулировку «сработать в 18:30» придётся переписать с равенства на границу — и заодно получить бесплатно ответы на два соседних вопроса: «сколько осталось» и «на сколько опоздали».
В этом уроке три инструмента: момент как больше-или-равно, штамп дня, который гасит повторы, и разность unixtime, отвечающая на оба вопроса сразу — «сколько осталось» и «на сколько опоздали». Операторы DateTime и беззнаковые ловушки разности разобраны в уроке про сравнение DateTime — здесь только прикладная сборка для реального узла.
Момент: не равенство, а граница
Момент 18:30 — это не точка, которую надо поймать, а граница, которую надо пересечь. Проверка «сейчас позже или ровно в 18:30» истинна в 18:30:00 и остаётся истинной в 19:00 и в 23:00. Для частого цикла разницы с равенством почти нет; для редкого она спасает событие: опоздали с пробуждением на девять минут — дело всё равно сделается при первом проходе после границы. С формальной стороны это сравнение двух чисел, и никакой магии: момент и текущее время приводятся к одной единице и сравниваются как целые.
Цена границы — повторы: условие остаётся истинным до полуночи, и без защиты насос включался бы каждый проход. Защита — штамп дня: переменная помнит, когда дело последний раз делалось, и пропуск может быть только один раз в сутки. Сравните с флагом из урока про LCD-будильник: там флаг жил внутри одной минуты и сбрасывался в ветке else; здесь живёт целые сутки и сбрасывается сам, когда меняется дата.
Границу собирают из полей текущей даты: DateTime(now.year(), now.month(), now.day(), 18, 30, 0) — «сегодня в 18:30». Сравнивать удобно в unix-форме: now.unixtime() >= moment.unixtime() — одно число против одного числа, без побитовых сюрпризов календаря. А разность тех же чисел печатает опоздание в секундах — полезная цифра для лога: по ней видно, жив ли цикл узла. Опоздание в десять секунд — норма, опоздание в десять минут — повод проверить, чем занят loop в остальные девять.
Сравнение в unix-форме у меня и потому, что оно переживает смену дня: в 00:01 проверка «сегодня в 18:30» честно скажет, что момент ещё впереди — целые сутки до цели. Если бы я сравнивал по полям часа и минуты, первая минута нового дня выглядела бы как «уже позже», и штамп дня бы не спас: дело сделалось бы сразу после полуночи. Unix-форма вместе со сборкой границы из полей даты закрывает этот промах начисто.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc;int poslednijDen = -1; // штамп дня: когда насос уже включался void setup() { Serial.begin(9600); rtc.begin();} void loop() { DateTime now = rtc.now(); // граница «сегодня в 18:30» из полей текущей даты DateTime moment(now.year(), now.month(), now.day(), 18, 30, 0); bool vremyaPrishlo = now.unixtime() >= moment.unixtime(); if (vremyaPrishlo && poslednijDen != now.day()) { poslednijDen = now.day(); // раз в сутки, не чаще long opozdanie = now.unixtime() - moment.unixtime(); Serial.print("nasos vkluchen, opozdanie "); Serial.print(opozdanie); Serial.println(" s"); // ... включить насос, отработать по счётчику, выключить } delay(1000);}Штамп дня — не единственная защита, и полезно знать её соседей. Флаг внутри минуты из урока про LCD-будильник живёт в памяти платы и сбрасывается в else — так живут частые циклы, когда loop() пробегает сотни раз за будильную минуту. Штамп живёт сутки и переживает редкие циклы. А для задачи «не делай два раза при пропадании питания» есть энергонезависимая память: счётчик запусков в EEPROM переживает и перезагрузку, и выключенную розетку.
Проверено на живых узлах: полив из этого урока прожил сезон, и разность unixtime со штампом ни разу не подвели; кормушка из соседнего урока раздела использует ту же связку с другим приёмом фильтра дня, а в итоговой теплице обе защиты живут рядом: клапан — по границе, журнал — по штампу. Выбор простой: частый цикл — флаг, редкий — граница со штампом, а когда сомневаетесь — ставьте границу: она превращает пропущенное событие в опоздавшее, а это всегда лучше потерянного.
Разность и остаток: одно число вместо календаря
Вопрос «сколько осталось до запуска» отвечает та же разность: moment.unixtime() - now.unixtime() — секунды одним числом. Печать в часах и минутах — деление на 3600 и остаток по модулю 60; никаких календарных правил в скетче нет, високосный февраль и переводы часов уже зашиты в пересчёт библиотеки. Если интервал длиннее суток и его надо складывать с другими, берите TimeSpan — механику разборов я показывал в уроке про арифметику TimeSpan.
Пара строк печати для узла. Остаток до запуска печатаю в двух формах: сырые секунды в лог для отладки и часы с минутами для человека — ostatok / 3600 и (ostatok % 3600) / 60. Целочисленное деление здесь работает на вас: остальное отбрасывается само, и округление ни к чему, потому что цифра обновится на следующем пробуждении. А если остаток ушёл в минус — значит момент уже позади, и та же величина со знаком плюс становится опозданием: одна переменная, две роли, нулевая арифметика.
Сама форма unixtime с её границами 2038 и 2106 годов разобрана в уроке про эпоху — для расписаний внутри одного года запас многократный. Выбирайте единицу по задаче: минуты — для расписаний, секунды — для «мгновенно после старта», сутки — для «раз в день». Порядок двух событий проверяют операторами DateTime, но обе отметки обязаны прийти из одного источника: rtc.now() и millis() — разные вселенные, и склейка у них отдельная, в уроке про millis против часов.
| Задача | Чем сравнивать | Почему именно так |
|---|---|---|
| Событие в назначенную минуту при редком цикле | граница через unixtime плюс штамп дня | равенство ловит одну минуту и теряет событие между пробуждениями |
| Сколько осталось до запуска | разность unixtime | одно число в секундах, без календарных правил |
| Интервалы длиннее суток | TimeSpan | перенос через полночь и дни ведёт библиотека |
| Кто из двух событий раньше | операторы DateTime | обе отметки обязаны прийти из rtc.now() |
Симулятор: редкий цикл в чистом виде
Настоящий powerDown движок не держит — он про это уже честно говорилось в сценарии с двумя будильниками, — зато редкий цикл изображается точно: скетч проверяет расписание раз в десять секунд, между проверками «спит» задержкой и печатает каждое пробуждение. Это та же физика, что у узла на батарейке: между пробуждениями минуты проходят целиком. Часы живые, на I2C — A4 и A5 — и подводятся кнопками виджета, так что «будильную минуту» вы проматываете за секунды.
Доведите время виджета до 18:30 кнопками плюс-минус час и минута. Первый проход после границы включит насос с печатью опоздания, все следующие за тот же день пройдут мимо — штамп дня работает. Разность до момента печатается на каждом пробуждении и на глазах тает по мере подводки. Отдельно проверьте край: заведите время на минуту назад от момента и дошагивайте по одной минуте — срабатывание должно случиться ровно на проходе, первым после 18:30, а не раньше.
- 1. Цикл просыпается раз в десять минут. Почему проверка hour == 18 и minute == 30 ненадёжна?
- 2. Условие «сейчас позже 18:30» остаётся истинным до полуночи. Что гасит повторы?
- 3. Сколько осталось до 18:30 — что возвращает разность moment.unixtime() - now.unixtime()?
- 4. Узел проспал ровно месяц и проснулся пятнадцатого, а штамп был «пятнадцатое». Что надёжнее now.day()?
- 5. Нужно узнать, какое из двух событий случилось раньше. Чем сравнивать?
- Момент — граница, а не равенство: now.unixtime() >= moment.unixtime() ловит событие даже при опоздании на минуты
- Повторы гасит штамп дня; надёжный штамп — unixtime / 86400, потому что day() циклится каждые 31 день максимум
- Разность unixtime даёт остаток и опоздание одним числом в секундах; интервалы с переносом через сутки ведёт TimeSpan
- Опоздание стоит печатать в лог: по нему видно, жив ли цикл узла и не просачивается ли расписание
- Операторы DateTime сравнивают отметки из одного источника; rtc.now() и millis() — разные вселенные
Частые вопросы по теме урока
- Цикл просыпается раз в десять минут. Почему проверка hour == 18 и minute == 30 ненадёжна?
- Равенство истинно ровно 60 секунд; редкий цикл может пройти мимо будильной минуты целиком, и события не будет. Граница «позже или ровно» ловит момент при любом опоздании. Потому что она ловит одну минуту из шестидесяти и проскакивает между пробуждениями
- Условие «сейчас позже 18:30» остаётся истинным до полуночи. Что гасит повторы?
- Переменная-штамп помнит день последнего срабатывания и пропускает всё остальное до смены даты. Это родственник флага из урока про LCD-будильник, но живёт он сутки, а не минуту. Штамп дня: дело делается один раз в сутки
- Сколько осталось до 18:30 — что возвращает разность moment.unixtime() - now.unixtime()?
- Unixtime — секунды с эпохи, разность двух отметок даёт интервал в секундах. Часы и минуты из него достаются делением на 3600 и остатком. Секунды одним числом
- Узел проспал ровно месяц и проснулся пятнадцатого, а штамп был «пятнадцатое». Что надёжнее now.day()?
- day() повторяется каждый месяц, и спящий месяц превращается в пропуск события. Деление unixtime на 86400 даёт монотонный номер дня — бесплатный и не циклящийся штамп. unixtime / 86400 — номер дня с эпохи, не циклится
- Нужно узнать, какое из двух событий случилось раньше. Чем сравнивать?
- У DateTime есть перегруженные операторы сравнения — им и порядок, и равенство. Главное правило: обе отметки из одного источника времени, склейка rtc.now() с millis() ломает вывод. Операторами DateTime, обе отметки из rtc.now()