Два логгера висели у меня в теплице на одной шине: один писал в журнал срабатывания полива, второй — открывания форточки. Через месяц я захотел простой ответ: форточку открывают ПОСЛЕ полива или во время? Журналы лежали строками вида «16:05» — и я полез сравнивать их как текст. Анализ сходу объявил 9:15 более поздним временем, чем 10:00, потому что девятка в строке больше единицы. Потом я вспомнил, что уже наступал на эти же грабли, — и перевёл оба журнала в секунды с 1970 года, после чего вопрос «что раньше» стал вопросом первого класса начальной школы.
В этом уроке — второй слой той же работы: сравнивать два момента честно, строить из отметок хронологию и отвечать на вопрос «сколько между ними». Сами поля объекта и смысл unixtime лежат в уроке про разбор DateTime — там же про держание значения в unsigned long. Здесь я лезу дальше: знак разности, границы, и один подвох беззнакового вычитания, который на плате превращает «90 секунд назад» в 136 лет.
Почему именно секунды с 1970 года, а не поля по отдельности? Одна шкала для всех моментов: в неё укладываются и события сегодняшней смены, и записи годичной давности, без календаря и часовых поясов. Разность двух чисел — интервал, знак разности — направление, сортировка журнала — сортировка чисел. Поля хороши для человека и для окна по часам суток, но для хронологии между днями они проваливаются: попробуйте руками сравнить «15 января, 16:05» с «9 января, 23:50» через три вложенных if — и вы поймёте, почему в моих журналах unixtime идёт отдельным столбцом рядом с человеческой датой.
Операторы сравнения: шесть штук и одно молчание
Для DateTime в RTClib определены обычные операторы: ==, !=, <, >, <=, >=. Библиотека сравнивает моменты целиком — год, месяц, день, час, минуту, секунду — и отдаёт true или false. Будильник из урока про часы с будильником перестаёт быть гирляндой из вложенных if и становится одной строкой: if (now >= alarmTime) — звенеть. Именно эти операторы мне нужны на плате, и там они работают как положено.
А вот в симуляторе на этом сайте операторы сравнения DateTime не эмулируются: проверка a < b молча уходит в ветку «нет» при любом раскладе — не падает, не ругается, просто врёт. Поэтому все сим-скетчи этого урока построены на другом инструменте — разности unixtime, — и это не костыль, а тот же механизм, которым операторы и устроены внутри: у обоих снимков берут секунды с 1970 и вычитают. Запомните это молчание как свойство симулятора, а на плате пользуйтесь операторами без опаски.
Разность снимает и вопрос «раньше или позже», и вопрос «на сколько». Если b - a больше нуля — момент b был позже; меньше нуля — раньше; ровно ноль — это один и тот же момент с точностью до секунды. Величина разности — интервал в секундах, готовый для печати, графика или сравнения с порогом. Одно вычитание отвечает на оба вопроса сразу — за это я и держу журналы в unixtime.
С парой == и != есть тонкость, о которую я дважды стучался: два вызова rtc.now() подряд вернут два разных объекта, даже если секунда не успела тикнуть, — внутри них разные снимки с разницей в долю секунды. На плате библиотека сравнивает поля, и для двух снимков одной секунды == честно даст true, а вот в самодельных структурах сравнивать объекты по частям я не советую: надёжнее переводить оба момента в unixtime и сравнивать числа — и в коде понятнее, и от representation-ловушек свободно.
Строим хронологию из трёх отметок
Хронология из отметок собирается теми же разностями. Три события — старт, срабатывание насоса, открывание форточки — дают три парных интервала: от старта до насоса, от насоса до форточки, от старта до форточки. Если каждый следующий момент позже предыдущего, разности положительны и складываются в общую длину: 90 секунд плюс 120 — те же 210 секунд от старта до конца. Это дешёвая самопроверка: если пара разностей не сходится в сумму, в журнале дыра или запись вне порядка.
Порядок трёх отметок удобно проверять одной строкой: if (start < nasos && nasos < fortochka) — хронология подтверждена. Для больших журналов сортировка по unixtime — это обычная сортировка чисел, без парсинга дат; я именно так перестраиваю CSV перед разбором, сам формат журнала на карте разобран в уроке про логирование на SD-карту. Числа с условием: разность в знаковом long спокойно живёт в пределах плюс-минус 68 лет, то есть для журналов, которые ведутся годами от одного RTC, запаса хватает с избытком — даже при отметке каждую секунду разности останутся в пределах пары миллионов, далеко от границ типа.
И про ввод, раз уж сим-скетч интерактивный: число секунд в симуляторе я читаю Serial.parseInt() после проверки available() — по правилам из урока про чтение чисел из монитора. Мусорная строка парсится в ноль и отбрасывается проверкой на больше нуля: скетч молчит, а не обнуляет свой дедлайн.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; void setup() { Serial.begin(9600); rtc.begin(); DateTime start = rtc.now(); DateTime nasos = start + TimeSpan(0, 0, 1, 30); // +1 мин 30 с DateTime fortochka = start + TimeSpan(0, 0, 3, 30); // +3 мин 30 с if (start < nasos && nasos < fortochka) { Serial.println("Hronologiya OK: start - nasos - fortochka"); } else { Serial.println("Hronologiya slomana!"); } long d1 = (nasos - start).totalseconds(); // 90 long d2 = (fortochka - nasos).totalseconds(); // 120 long d3 = (fortochka - start).totalseconds(); // 210 Serial.print("d1="); Serial.println(d1); Serial.print("d2="); Serial.println(d2); Serial.print("d3="); Serial.println(d3); Serial.print("summa d1+d2="); Serial.println(d1 + d2); // 210, сходится с d3} void loop() { }В скетче выше спрятаны три привычки, которые я теперь копирую без изменений. Первая — разности в знаковом long: totalseconds() у TimeSpan на плате отдаёт как раз long, и минус остаётся минусом. Вторая — сумма как самопроверка: 90 плюс 120 обязаны дать 210, и строка summa d1+d2 печатается не для красоты, а для ловли дыр в журнале. Третья — вся хронология собирается в setup() один раз: это снимок процесса, а не живой монит; живой остаток до события — это уже loop() и рабочее время, как в сим-скетче ниже.
И одно «но», которое я вынес на дорогой цене: сравнивать можно только моменты одной эпохи. Если второй логгер стоял на RTC без батарейки и чип сбросился, его отметки начнутся с 1 января 2000 года — это unixtime 946684800, на двадцать шесть лет раньше честных записей. Хронология сложится, сумма сойдётся, а вывод будет врать: «форточка открылась через год до полива». Поэтому в шапке каждого пишущего скетча у меня стоит lostPower() — порядок проверок из урока про установку RTClib и первый скетч, и в журнал рядом с unixtime печатается человеческая дата: год 2000 в чужой записи виден глазами за секунду.
| Способ сравнить два момента | Где работает | Что говорит | Где ломается |
|---|---|---|---|
| Операторы DateTime | плата (в симе молчат) | true/false: кто раньше | в симуляторе — всегда «нет» |
| Разность unixtime в long | плата и сим | знак — направление, величина — интервал в секундах | в unsigned на плате минус оборачивается |
| Строки «16:05» | нигде | ничего надёжного | 9:15 больше 10:00 как текст |
| Минута суток h*60+m | плата и сим | порядок внутри одних суток | через полночь и между датами не сравнит |
- Запустите и сверьте шапку: 90, 120 и 210 секунд — два коротких интервала сходятся в общий от старта до форточки.
- Введите 30: скетч ответит «раньше дедлайна» — разность положительная.
- Введите 240, когда proshlo уже перевалило за сотню: вердикт окажется «позже» — событие отсчитывается от текущего момента, а не от старта, и это самая поучительная проверка.
- Введите 300: вердикт «позже», и минус разворачивается в положительное число перед печатью.
- Дождитесь, когда proshlo перевалит за 240: строка «до дедлайна» печатается минусом — в симе это честно, на плате в unsigned оно бы обернулось в гигантское число.
- Операторы ==, !=, <, >, <=, >= сравнивают DateTime целиком — на плате; в симуляторе они молчат, и там работают unixtime-разности
- Одно вычитание отвечает на два вопроса: знак разности — «раньше или позже», величина — интервал в секундах
- unixtime храню в unsigned long, а разности считаю в знаковом long — беззнаковый минус на плате оборачивается в ~136 лет
- Хронология трёх отметок проверяется суммой интервалов: 90 + 120 обязаны дать 210
- Сортировка журнала по unixtime — сортировка чисел без парсинга дат
- parseInt() после available() с отбраковкой нуля: мусор из монитора не обнуляет дедлайн
- 1. В симуляторе if (a < b) для двух DateTime всегда уходит в «нет». Почему?
- 2. Разность unixtime получилась -90. Что это значит?
- 3. Почему разность двух unixtime я считаю в знаковом long, хотя сами значения держу в unsigned long?
- 4. Интервалы соседних событий — 90 и 120 секунд. Общий интервал от первого до третьего события?
- 5. В журнал пришло «9:15» и «10:00» строками. Что не так со строковым сравнением?
Сравнения закрыли вопрос «кто раньше». Остаётся второй половина арифметики времени: не вычитать моменты друг из друга, а прибавлять к моменту длительность — «через три часа», «через сутки», «выдержка 36 часов». Для этого в RTClib есть отдельный класс TimeSpan со своими граблями, и он разобран в следующем уроке про TimeSpan и арифметику времени. Отмеченные моменты, если их надо пережить перезагрузку, ложатся в энергонезависимую память — приём показан в уроке про счётчик включений на EEPROM.
Частые вопросы по теме урока
- В симуляторе if (a < b) для двух DateTime всегда уходит в «нет». Почему?
- Движок симулятора не поддерживает операторы сравнения DateTime: проверка молча возвращает ложь. На плате операторы работают, а в симе ту же службу несут unixtime-разности. Операторы сравнения DateTime в симе не эмулируются
- Разность unixtime получилась -90. Что это значит?
- Знак разности — направление: минус значит, что вычитаемый момент позже уменьшаемого, а событие было на 90 секунд раньше. Величина без знака — интервал. Событие было 90 секунд назад
- Почему разность двух unixtime я считаю в знаковом long, хотя сами значения держу в unsigned long?
- В C++ вычитание беззнаковых остаётся беззнаковым: «90 секунд назад» превращается в 4294967206 — около 136 лет. Знаковый long сохраняет честный минус. Беззнаковое вычитание на плате оборачивает минус в ~4,29 млрд секунд
- Интервалы соседних событий — 90 и 120 секунд. Общий интервал от первого до третьего события?
- Интервалы транзитивных событий складываются: 90 + 120 = 210. Несходящаяся сумма — верный признак дыры в журнале или записи вне порядка. 210
- В журнал пришло «9:15» и «10:00» строками. Что не так со строковым сравнением?
- Строковое сравнение идёт по кодам символов слева направо, без знания о формате времени. Доверять порядок моментам можно только числам: полям, unixtime или операторам. «9:15» больше «10:00», потому что девятка больше единицы