Настой удобрений для рассады держится тридцать шесть часов, карантин нового аквариума — две недели, проветривание погреба — четверть часа. В каждом случае я хочу одного: записать момент начала, узнать момент готовности и в любой день спросить «сколько осталось». Первую версию я написал по-наивному: хранил час готовности и прибавлял к часу длительность. Тридцать шесть часов поверх девяти вечера дали шестьдесят один час — а дальше скетч честно искал 61-й час суток, которого не бывает. Полночь, перенос на завтра, високосный февраль — каждый перенос надо бы дописывать руками, и каждый я когда-то обязательно забыл бы.
Всё это уже решено в библиотеке: у RTClib есть класс TimeSpan — длительность в четырёх числах, с которой момент складывается одной строкой, а полночь и месяцы проходят сами, потому что внутри это просто секунды. Разберу конструктор и методы, потом соберу обратный отсчёт с дедлайном, а заодно честно скажу, где TimeSpan заменяется простой unix-арифметикой — в том числе потому, что симулятор на этом сайте TimeSpan не эмулирует.
Полезно заранее разложить задачи арифметики времени на три сорта, потому что у них один инструмент, но разные вопросы. Сдвиг: к известному моменту прибавить длительность и получить дедлайн. Остаток: от дедлайна вычесть текущий момент и узнать, сколько осталось. Длительность: из двух отметок сделать интервал — сколько длилось проветривание или насос. TimeSpan закрывает все три, unix-секунды — тоже: выбор внутри урока будет не между «правильно и неправильно», а между «библиотечная запись» и «та же запись в четырёх делениях».
TimeSpan: интервал в четырёх числах
Конструктор принимает ровно четыре аргумента в порядке дни, часы, минуты, секунды: TimeSpan(1, 12, 0, 0) — это сутки и половина, те самые тридцать шесть часов. Порядок — самая частая оплошность новичка: хочется написать «часы, минуты, секунды», и TimeSpan(2, 0, 0) внезапно означает не два часа, а два дня. Я сам в первый раз получил сдвиг на два дня вместо двух часов и час искал, куда делись сутки, — с тех пор в спорных местах пишу все четыре аргумента с нулями: TimeSpan(0, 2, 0, 0).
Методы отдают компоненты: days(), hours(), minutes(), seconds(). Подвох в том, что это компоненты остатка, а не полные величины. Для тридцати шести часов days() вернёт 1, а hours() — 12, не 36: сутки уже забрал days(). Если вам нужны «итого часов», считайте самому: totalseconds() / 3600 — метод totalseconds() отдаёт всю длительность одним числом в секундах. Это же число — валюта для любых сравнений и накоплений: интервалы складываются и вычитаются в секундах без заботы о переносах.
Арифметика собирается из трёх операций. DateTime плюс TimeSpan даёт новый момент — так рождается дедлайн: DateTime deadline = now + TimeSpan(1, 12, 0, 0). Разность двух DateTime даёт TimeSpan — так рождается остаток: TimeSpan left = deadline - now. А TimeSpan складывается с TimeSpan, когда длительности копятся. Всё это честно переходит через полночь, конец месяца и февраль: тридцать шестой час сам найдёт следующие сутки, и писать переносы руками не нужно. В симуляторе на этом сайте TimeSpan не поддержан движком — поэтому здесь я делаю ту же арифметику через unixtime, а TimeSpan оставляю в код-блоках для платы.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; const int RELAY = 7; // сигнал готовности через релеint gotovo = 0;DateTime deadline; // дедлайн переживает loop(), потому и глобальный void setup() { Serial.begin(9600); rtc.begin(); DateTime now = rtc.now(); deadline = now + TimeSpan(1, 12, 0, 0); // выдержка 36 часов Serial.print("Nacalo: d="); Serial.print(now.day()); Serial.print(" h="); Serial.print(now.hour()); Serial.print(" m="); Serial.println(now.minute());} void loop() { DateTime now = rtc.now(); if (now < deadline) { TimeSpan left = deadline - now; Serial.print("Ost: "); Serial.print(left.days()); // 1 Serial.print(" d "); Serial.print(left.hours()); // 12, а не 36! Serial.print(" h "); Serial.print(left.minutes()); Serial.println(" m"); } else { if (gotovo == 0) { // фронт готовности gotovo = 1; digitalWrite(RELAY, HIGH); Serial.println("GOTOVO"); } } delay(60000); // печать раз в минуту}Обратный отсчёт в симуляторе: unix-арифметика
В сим-скетче ту же службу несут секунды с 1970 года. Дедлайн — это unixtime начала плюс длительность в секундах: dedlain = t0 + 150. Остаток — вычитание: left = dedlain - t, где t — рабочее время. А разложение остатка на дни, часы, минуты и секунды — это деления с остатком, те самые, что я разбирал в уроке про массивы и журналы на примере перевода секунд: дни — это left / 86400, потом остаток делится на 3600, из него берётся минутная доля, и последнее деление отдаёт секунды. Цепочка на четыре строки заменяет весь API TimeSpan — и я честно пользуюсь ею на плате, когда TimeSpan под рукой не нужен. Для контроля возьмите 129600 секунд: делениями получится ровно 1 d 12 h — те же тридцать шесть часов, что в конструкторе TimeSpan(1, 12, 0, 0).
Разница между платой и симом остаётся прежней: на плате рабочее время даёт rtc.now() в каждом проходе loop(), а в симуляторе часы застывают на моменте запуска, поэтому скетч ведёт время от якоря t0 плюс millis()/1000 — паттерн настоящего класса RTC_Millis из той же библиотеки. Секунды при этом тикают по-настоящему: за счёт виртуального времени симулятора остаток падает на глазах. Фронт готовности считается флагом из урока про свет по расписанию: строка GOTOVO и гудок зуммера выходят один раз, а не каждые две секунды после дедлайна. И проверка ввода та же: parseInt() после available(), мусор отбрасывается — подробности в уроке про чтение чисел из монитора.
- Запустите и посчитайте строки: остаток падает на 2 секунды за печать, разложение сходится с цельным числом слева.
- Дождитесь GOTOVO: строка и гудок выходят один раз, хотя loop() крутится дальше — это фронт на флаге.
- Введите 3600 — отсчёт перезапустится и покажет 0 d 1 h 0 m 0 s: проверка разложения на компоненты.
- Введите 90000 — увидите 1 d 1 h 0 m 0 s: сутки честно переехали в разряд дней.
- Введите мусорное слово — скетч молчит: парсинг даёт ноль, а ноль отбракован проверкой на больше нуля.
| Вызов | Что возвращает | Пример |
|---|---|---|
| TimeSpan(1, 12, 0, 0) | длительность: дни, часы, минуты, секунды | 36 часов |
| left.totalseconds() | вся длительность одним числом в секундах | 129600 |
| left.days() / hours() / minutes() | компоненты с вычетом старших разрядов | 1 d 12 h, а не 36 h |
| now + TimeSpan(...) | новый DateTime — дедлайн | момент через 36 часов |
| deadline - now | TimeSpan — остаток | 1 d 11 h 59 m |
Три приёма из практики, которые ложатся поверх скетча. Первый — накопление наработки: сколько суммарно отработал насос за сезон. Каждое срабатывание добавляет в счётчик long длину интервала в секундах, печать — тем же разложением на дни и часы. Запас у знакового long огромный: два миллиарда секунд — это под семьдесят лет наработки, за которую вы этот счётчик пять раз перепрошьёте. Второй — сравнение выдержек между собой: «какой дедлайн ближе» — это обычное сравнение секунд, без календаря.
Третий приём самый ценный — он про то, что дедлайн может оказаться в прошлом ещё при старте. Пока плата была выключена, выдержка не остановилась: удобрение настоялось, карантин прошёл. Скетч с проверкой now < deadline при включении сразу увидит отрицательный остаток и сразу поднимет флаг готовности — и это правильное поведение, а не сбой: с дедлайном, сохранённым в EEPROM, отсчёт переживает выключение, а с переменной в ОЗУ — нет. Именно поэтому в проектах с выдержкой я пишу дедлайн в энергонезависимую память и после загрузки первым делом сравниваю его с текущим временем.
И последняя развилка, чтобы не изобретать велосипеды: интервалы короче нескольких минут я считаю millis()-таймерами из урока про millis() вместо delay() — они дешёвые и не требуют железа. Когда речь о часах и днях, millis() уже не спасает: плата не переживёт без сброса столько, а часы DS3231 — запросто. Граница в моём коде лежит примерно на паре часов: меньше — millis(), больше — RTC с TimeSpan или unix-арифметикой.
- TimeSpan(дни, часы, минуты, секунды) — порядок жёсткий; в спорных местах пишу все четыре аргумента с нулями
- days()/hours()/minutes()/seconds() — компоненты остатка: для 36 часов hours() вернёт 12; итого — totalseconds()/3600
- DateTime + TimeSpan — дедлайн; DateTime - DateTime — остаток; полночь и месяцы переезжают сами, внутри секунды
- В симе TimeSpan не эмулируется: та же арифметика делениями на 86400/3600/60 с остатком %
- Флаг на фронте готовности: GOTOVO и гудок один раз, а не каждую секунду после дедлайна
- Дедлайн в EEPROM как unsigned long переживает перезагрузку — выдержка продолжается честно
- 1. TimeSpan(2, 0, 0) — это какая длительность?
- 2. left — TimeSpan на 36 часов. Что вернёт left.hours()?
- 3. Как получить момент «через 36 часов» от текущего?
- 4. Что делает DateTime минус DateTime?
- 5. Остаток отсчёта — 90000 секунд. Сколько покажет разложение left / 86400 и (left % 86400) / 3600?
Теперь у расписания есть обе половины: окна по часам суток из первого урока и длительности со сдвигами из этого. Дальше по разделу эти же часы встают в более сложные приборы — будильник с LCD уже написан в соседнем уроке, а если часы захочется сверять по сети, то первый шаг — подключение ESP32 к Wi-Fi и NTP-сервер: unixtime оттуда и от DS3231 складываются в одну арифметику без переделок.
Частые вопросы по теме урока
- TimeSpan(2, 0, 0) — это какая длительность?
- Порядок аргументов — дни, часы, минуты, секунды: два с половиной аргумента не заданы, значит два дня. Полная форма TimeSpan(0, 2, 0, 0) дала бы два часа. Два дня
- left — TimeSpan на 36 часов. Что вернёт left.hours()?
- hours() отдаёт компонент после вычета полных суток: days() забрал 1, осталось 12 часов. Полные часы считаются делением totalseconds() на 3600. 12
- Как получить момент «через 36 часов» от текущего?
- Сложение DateTime с TimeSpan даёт новый момент, и перенос через полночь происходит сам. Прибавление к полю hour() даёт 45-й час суток, а к unixtime прибавляют секунды — 129600. now + TimeSpan(1, 12, 0, 0)
- Что делает DateTime минус DateTime?
- Разность двух моментов — это длительность, то есть TimeSpan. Его totalseconds() даёт интервал в секундах, а компоненты раскладываются методами. Возвращает TimeSpan — длительность между моментами
- Остаток отсчёта — 90000 секунд. Сколько покажет разложение left / 86400 и (left % 86400) / 3600?
- 90000 / 86400 — это 1, остаток 3600 секунд делится на 3600 и даёт 1 час. Цепочка делений с остатком повторяет разложение TimeSpan в unix-арифметике. 1 день и 1 час