Ловушка, в которую я попал вторым же проектом с часами: хотелось посчитать, сколько проработал насос между двумя показаниями. Время я тогда хранил строками вида «9:15» и «10:00» и сравнивал их как текст — и к утру получил разброд: строковое сравнение уверенно считает, что 9:15 больше, чем 10:00, потому что девятка больше единицы. Потом добавились ведущие нули, потом дата — и всё это ползло по коду, как трещина по стеклу. Лечится одно: время не хранят и не сравнивают строками, для этого у объекта DateTime есть числа, операторы и unix-время.
В этом уроке раскладываем DateTime по винтикам: из чего состоит снимок момента, как достать каждое поле, как получить секунды с 1970 года и как сдвинуть момент на полтора часа одной строкой. Всё это — над уже работающим скетчем из урока про установку RTClib и первый скетч.
Второй слой той же ловушки — хранение. Строка «31.12.2025» не сортируется, не вычитается и не переживает смену формата: поменяете точки на дефисы — и половина парсинга поедет. Числа и unix-время лишены всех этих болячек, поэтому в моих проектах строки живут только на самом краю — в момент печати человеку.
Снимок момента и его поля
DateTime — маленький объект-значение. Вызов rtc.now() копирует текущий момент из чипа в переменную, и дальше это фото: часы идут, а ваш снимок застыл. Заведите переменную один раз в начале loop() — и все проверки внутри цикла увидят согласованную картину, даже если секунда тикнёт посреди вычислений. Это дешёвое правило убирает целый класс плавающих багов, когда половина решения принимается по 9:59, а половина — по 10:00. Сам цикл при этом не должен тормозить: держите опрос часов частым, а тяжёлую работу разносите таймерами из урока про millis() вместо delay().
Поля достаются методами-геттерами: year() возвращает полный год (2025), month() — месяц от 1 до 12, day() — день от 1 до 31, hour() — час от 0 до 23, minute() и second() — минуты и секунды от 0 до 59. Седьмой геттер, dayOfTheWeek(), возвращает день недели числом от 0 до 6, где 0 — воскресенье; календарь библиотека считает сама, из даты. Типы честные: у года 16 бит, у остальных полей по 8 — поэтому в int они влезают с запасом, а в byte месяц и день помещаются идеально.
Числа хороши для машины, а человеку нужен формат. Имя дня недели по-русски собирается массивом из семи строк по индексу dayOfTheWeek() — тот же приём, что в уроке про массивы для журналов; короткое имя месяца делается вторым массивом из двенадцати строк. Ведущие нули — простым сравнением с десятью, как в print2 из прошлого урока.
Ещё один нюанс, который экономит вечер: поля DateTime — это методы, а не свойства. Скобки обязательны: now.hour() — вызов, а now.hour — чтение несуществующего свойства, и компилятор ответит сообщением, которое новичка скорее запутает, чем направит. Привычка простая: ставьте скобки сразу — у снимка времени нет ни одного поля, доступного без вызова.
Unixtime: секунды с 1970 года
Второй способ посмотреть на тот же момент — метод unixtime(): он возвращает количество секунд, прошедших с полуночи 1 января 1970 года. Число грубое на вид, но для машинной логики незаменимое: разность двух моментов — это обычное вычитание, сортировка журналов — сортировка чисел, а обратная сборка делается конструктором DateTime от одного числа. Часовой пояс в это значение не зашит — считается по UTC, поэтому для сравнений и разностей он и не важен, а для показа человеку вы и так форматируете поля сами.
Про запас хода: unixtime() у RTClib отдаёт беззнаковое 32-битное число, которое кончится в 2106 году. Опасность не в самой библиотеке, а в ваших переменных: сохраните значение в знаковый long — и после января 2038 старший бит превратит его в отрицательное. Моё правило: держать unixtime в типе unsigned long и никогда не скармливать его функциям, ждущим signed-аргументы. Для бытовых Arduino-проектов этого хватает с большим запасом.
Именно unixtime я прошиваю в каждый столбец журнала рядом с человеческой датой: строку читает человек, число — скрипт анализа. Связка экономит часы работы, когда через полгода надо быстро ответить, с какой минуты поливалка начала лить лишнее.
Живой пример из того же дачного логгера: два числа подряд — 1767225540 и 1767226800 — вычитаются в уме: 1260 секунд, двадцать одна минута. Ни календаря, ни часовых поясов, ни форматов — просто арифметика второго класса, которая одинаково работает и между соседними минутами, и между записями годичной давности.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; void setup() { Serial.begin(9600); rtc.begin();} void loop() { DateTime now = rtc.now(); Serial.print("unix="); Serial.println(now.unixtime()); // момент через полтора часа от текущего DateTime later = now + TimeSpan(0, 1, 30, 0); long diff = later.unixtime() - now.unixtime(); Serial.print("diff="); Serial.println(diff); // 5400 if (now < later) { Serial.println("now < later: sravnenie rabotaet"); } delay(5000);}Сравнения и TimeSpan
Для DateTime в библиотеке определены обычные операторы сравнения: ==, !=, <, >, <=, >=. Будильник перестаёт быть набором из трёх вложенных if по часам, минутам и секундам и становится одной строкой: if (now >= alarmTime) — сработать. Именно этот приём с флагом и сбросом разобран в уроке про часы с будильником; здесь важно, что сравниваются сами объекты, а не их строковые представления — поэтому ловушка с 9:15 против 10:00 там невозможна в принципе.
Интервалы задаёт класс TimeSpan: конструктор принимает дни, часы, минуты и секунды — TimeSpan(0, 1, 30, 0) это полтора часа. Сложение DateTime с TimeSpan даёт новый момент: DateTime later = now + TimeSpan(0, 1, 30, 0). Вычитание двух DateTime возвращает TimeSpan той же природы: (later - now).totalseconds() даст 5400. Мнение с границей: для разностей внутри суток мне хватает операторов и полей, а когда интервалы длиннее суток или их надо копить в счётчике — я сразу перехожу на unixtime, там арифметика примитивна и не подводит.
Сдвиг через TimeSpan заменяет и ручные переносы через полночь: момент «через три часа после 23:00» честно станет следующим днём, потому что внутри это всё те же секунды. Писать такие переносы руками — гарантированный источник багов на границе суток: они редкие, потому и живучие.
Парные числа из полей DateTime удобно гнать в графики: вывод вида h=14 mi=5 s=9 без проблем читается Serial Plotter'ом — пары имя=значение не дают рядам перепутаться. А сами снимки времени пригодятся в любом проекте с памятью: смотрите урок про счётчик включений на EEPROM — туда же ложится и последний момент срабатывания будильника.
| Вызов | Возвращает | Пример |
|---|---|---|
| now.year() | год целиком, 16 бит | 2025 |
| now.month() | месяц, 1-12 | 3 |
| now.day() | день месяца, 1-31 | 8 |
| now.hour() / minute() / second() | час 0-23, минуты и секунды 0-59 | 14, 5, 9 |
| now.dayOfTheWeek() | день недели 0-6, где 0 — воскресенье | 6 |
| now.unixtime() | секунды с 1970-01-01, беззнаковое 32-битное | 1767225540 |
| now + TimeSpan(д, ч, м, с) | новый DateTime, сдвинутый на интервал | момент через 1 ч 30 мин |
- rtc.now() отдаёт снимок-фото момента: снимок не меняется, пока часы тикают — половина решений по 9:59 невозможна
- Поля: year() (2025), month() 1-12, day() 1-31, hour() 0-23, minute()/second() 0-59, dayOfTheWeek() 0-6 с нулём-воскресеньем
- unixtime() — секунды с 1970: разности и сортировка становятся вычитанием; держите значение в unsigned long
- Сравнивайте объекты операторами ==, <, > — строки для времени не годятся: 9:15 больше 10:00 как текст
- TimeSpan(дни, часы, минуты, секунды) двигает момент: now + TimeSpan(0,1,30,0) — через полтора часа
- Имена дней и месяцев — массивами строк по индексу геттера, ведущие нули — сравнением с десятью
- 1. Что вернёт now.month() в декабре?
- 2. Что хранит unixtime()?
- 3. Как получить момент через полтора часа от текущего?
- 4. Что вернёт now.dayOfTheWeek() в воскресенье?
- 5. Чем опасно хранить unixtime в знаковом long?
Теперь у часов есть интерфейс: поля для показа человеку, unix-время для арифметики, операторы для сравнений. Дальше по разделу логично собрать на этом каркасе прибор — будильник с дневным циклом уже ждёт в соседнем уроке, а хранить пороги срабатывания удобно в энергонезависимой памяти. Когда надоест выставлять время руками, эти же часы можно синхронизировать по сети: первый шаг — подключение ESP32 к Wi-Fi, а дальше в том разделе дойдём и до NTP.
Частые вопросы по теме урока
- Что вернёт now.month() в декабре?
- Месяцы в RTClib нумеруются по-человечески: 1 — январь, 12 — декабрь. Нулевого месяца нет. 12
- Что хранит unixtime()?
- Это unix-время: целое число секунд с полуночи 1 января 1970. Разность двух значений — длительность между моментами. Секунды с 1 января 1970 года
- Как получить момент через полтора часа от текущего?
- Сложение DateTime с TimeSpan даёт новый момент. Прибавление к часу легко вылезает за 23, а строки для арифметики не годятся. now + TimeSpan(0, 1, 30, 0)
- Что вернёт now.dayOfTheWeek() в воскресенье?
- Нумерация RTClib: 0 — воскресенье, 6 — суббота. Имена дней подставляйте массивом по этому индексу. 0
- Чем опасно хранить unixtime в знаковом long?
- Беззнаковое 32-битное число живёт до 2106 года, а знаковая переменная ломается в 2038-м. Держите unixtime в unsigned long. 31-й бит знака: после января 2038 значение уйдёт в минус