На щите теплицы у меня висел механический счётчик моточасов — красивый, со шкалой. Толку оказалось ноль: он мотал всё, что висело на его линии, и не различал, качала помпа по расписанию или её крутили вручную. А вопрос стоял конкретный: насос по паспорту выдаёт 600 литров в час — сколько он накачал за сутки и не начал ли молотить дольше положенного, потому что забился фильтр? Ответ даёт не новое железо, а часы, которые уже стоят на плате: две метки unixtime и вычитание.
Метки времени я разбирал в уроке про unixtime и эпоху — там про 1970 год и про то, почему время можно хранить одним числом. Метка unixtime живёт в одном числе даже тогда, когда часы показывают полночь: скетчу не надо помнить, какое сегодня число, — достаточно взять ещё одну метку. Считайте это продолжением той истории: метки перестали быть темой и стали инструментом. Здесь практика: разница двух меток — длительность события в секундах, готовая для накопления и печати. Арифметику TimeSpan и прибавление часов к DateTime трогать не буду — она в уроке про TimeSpan; вычитание двух целых чисел проще, быстрее и не тянет объектов.
Одно число вместо трёх
Соблазн посчитать длительность по полям — из часов вычесть часы, из минут минуты, из секунд секунды — разбивается о перенос: на границе 23:59 и 00:01 разница минут отрицательная, начинается танец с заёмными шестьюдесятью. Unixtime снимает весь этот танец: rtc.now().unixtime() отдаёт одно 32-битное число секунд, и длительность — просто разница. Полночь, конец месяца, високосный февраль учтены внутри числа автоматически. В 32 бита помещается до января 2038 года — для счётчика наработки запас в полтора поколения гаджетов.
Но одна разница даёт длительность только одного включения. Дневная статистика — это сумма: наработка за сегодня равна сумме секунд, когда реле было включено. Копить удобнее дельтами: в каждом проходе loop прибавлять t - prevT, где t — текущий unixtime, а prevT — метка прошлого прохода. Тогда включение, пересёкшее полночь, честно разваливается: часть секунд уходит вчерашней копилке, часть — сегодняшней. Одна разница старт-стоп так не умеет — она приписала бы всё дню выключения.
Числа, с которыми вы останетесь наедине: сутки — это 86400 секунд, час — 3600, минута — 60. Форматирование сводки делается остатками по модулю, без библиотек: часы — целое от деления на 3600, минуты — остаток от деления на 3600, поделённый на 60, секунды — остаток на 60. Храните наработку в сырых секундах — для вычислений это единственная честная валюта, — а печатайте в двух видах: секунды для расчётов и чч:мм:сс для глаз. Готовая печать без String лежит в функции pechatHMS скетча ниже: на Uno у String своя цена фрагментации памяти, и в приборе, который должен работать месяцами, эта цена непозволительна.
#include <Wire.h>#include <RTClib.h> RTC_DS3231 rtc; const int BTN = 2; // ручной пуск насосаconst int RELAY = 7;bool nasosOn = false;bool btnByl = true; // прошлое состояние кнопки (INPUT_PULLUP)unsigned long startedAt = 0; // метка старта текущего включенияunsigned long segodnya = 0; // наработка за сегодня, секunsigned long prevT = 0; // метка прошлого прохода loopint lastDay = -1; void pechatHMS(unsigned long sec) { // печать чч:мм:сс без String Serial.print(sec / 3600); Serial.print(":"); if ((sec % 3600) / 60 < 10) Serial.print("0"); Serial.print((sec % 3600) / 60); Serial.print(":"); if (sec % 60 < 10) Serial.print("0"); Serial.println(sec % 60);} void setup() { Serial.begin(9600); pinMode(BTN, INPUT_PULLUP); pinMode(RELAY, OUTPUT); digitalWrite(RELAY, LOW); rtc.begin(); prevT = rtc.now().unixtime(); lastDay = rtc.now().day(); Serial.println("Кнопка - насос, сводка за сутки - в полночь.");} void loop() { DateTime now = rtc.now(); unsigned long t = now.unixtime(); if (now.day() != lastDay) { // полночь: сводка и сброс if (lastDay != -1) { Serial.print("Итог суток, наработка "); pechatHMS(segodnya); } lastDay = now.day(); segodnya = 0; } bool knopka = (digitalRead(BTN) == LOW); if (knopka != btnByl) { // фронт кнопки: нажали или отпустили btnByl = knopka; if (knopka && !nasosOn) { nasosOn = true; startedAt = t; digitalWrite(RELAY, HIGH); Serial.println("Насос включён вручную"); } if (!knopka && nasosOn) { nasosOn = false; digitalWrite(RELAY, LOW); Serial.print("Выключен. Это включение длилось "); pechatHMS(t - startedAt); // справка, в копилку идут дельты } } if (nasosOn && t > prevT) { segodnya += (t - prevT); // копим только реально прошедшие секунды } prevT = t; delay(200);}В скетче два слоя, и их полезно не смешивать. Событийный: фронт кнопки включает и выключает насос, на выключении печатается длительность включения одной разницей. Эта справка верна всегда — даже если включение пересекло полночь, разница старт-стоп честно покажет общие минуты, просто целиком припишет их дню выключения. Копилочный: в каждую сводку за сутки попадают дельты t - prevT, набранные только в секунды, когда реле было включено. Справка при выключении — та самая цифра, что закроет спор о лишних получасах. Двойной учёт тут исключён конструкцией: справка печатается, но в segodnya ничего не добавляет.
Второй слой — типы, и это главная ловица урока. На Uno int — 16 бит со знаком, максимум 32767. Длительность в секундах больше девяти часов шести минут в int не влезает: число уезжает в минус, и Serial рапортует, что насос работал минус две минуты. Все переменные времени объявлены unsigned long — 32 бита без знака, 4,29 миллиарда секунд, запас до 2106 года. Экономия двух байт на типе стоит ночи отладки, проверено.
Отдельно про источник включения. Здесь насос заводится фронтом кнопки на INPUT_PULLUP, и счётчику безразлично, кто дал команду — человек или расписание: копилка молча считает секунды, когда реле включено. Фронт тут не украшение: сравнение knopka != btnByl ловит сам факт смены состояния, и дребезг контактов лишь множит переключения, не портя сумму. Захотите разделить ручные минуты и плановые — добавьте вторую копилку и в момент включения запоминайте источник команды: для разбирательства, кто именно накачал лишнее, этого хватает с запасом.
Сводка за сутки и полночь
Смена суток ловится одной строкой: now.day() != lastDay. Порядок внутри критичен: сначала печать накопленного, потом присвоение и обнуление — иначе напечатаете нули и будете думать, что насос простаивал. Ложной полночи после перезагрузки не будет: lastDay инициализируется текущим днём в setup, так что первое же сравнение честное. На плате без сна цикл не останавливается и сводка напечатается в полночь без вашего участия — если вы, конечно, не усыпили его сами. Если статистику надо не печатать, а копить по дням, пишите итог в EEPROM при каждой полуночи — одна-две записи в сутки, ресурс ячейки в порядке. Схему суточного архива на 24 слота я разбирал в уроке про суточный архив, а метки событий с печатью времени — в уроке про журнал в Serial: связка «метка + наработка» — готовый чёрный ящик для спора с поставщиком насоса.
Итоги по дням кладите в EEPROM слотами: семь дней по четыре байта — 28 ячеек, запись в полночь, чтение по запросу. unsigned long раскладывается в четыре байта побайтно, и это редкий случай, когда байтовая арифметика в скетче оправдана целиком. Воскресная сводка после недели работы — уже статистика: среднесуточная наработка покажет, поливаете вы сад или медленно строите болото, и даст точную цифру для разговора с тем, кто обещал экономный полив.
Считанные часы превращаются в литры умножением: наработка полчаса при паспорте 600 л/ч — 300 литров за сутки. Дальше цифра работает как диагностика. Расписание не менялось, а наработка выросла в полтора раза — фильтр забился, напор упал, насос добирает объём дольше положенного. Обратная картина — помпа молотит свои десять минут, но качает меньше — говорит о течи на всасе. Контрольный пример: десять минут при 600 л/ч — это ровно 100 литров за сеанс; если бочка на 200 литров опустошается за два сеанса, счётчик и паспорт сходятся — системе можно верить. Команды «стат», «сброс» и пороговые оповещения добавьте через Serial: разбор чисел из порта разобран в уроке про parseInt, и на этом счётчик превращается в прибор с кнопками.
| Способ мерить | Что учитывает | Где ломается |
|---|---|---|
| Разница millis() | непрерывную работу при живом питании | перезагрузка стирает счёт; переполнение на 49,7 сутках |
| Часы минус часы, минуты минус минуты | кажется простым на первый взгляд | перенос полночи даёт отрицательные минуты и заёмные 60 |
| Разница unixtime | полночь, месяцы и високосные — внутри одного числа | 32 бита кончаются в январе 2038 года |
| Механический счётчик моточасов | работает без кода и питания | не разделяет дни и не скажет, что именно качал |
- 1. Почему разница двух unixtime не ломается на полночи?
- 2. Насос работал 10 часов, длительность считали в int на Uno. Что увидите в Serial?
- 3. Зачем копить наработку дельтами t - prevT, а не одной разницей старт-стоп?
- 4. Зависит ли разница unixtime от перевода часов на час?
- 5. Паспорт насоса 1200 л/ч, наработка за сутки 30 минут. Сколько накачано?
- unixtime — одно 32-битное число секунд: разница меток даёт длительность без переносов полночи и месяцев
- Дневную наработку копим дельтами t - prevT: включение через полночь честно разваливается на двое суток
- Все переменные времени — unsigned long: int на Uno ломается после 9 часов 6 минут
- Полночная сводка: смена now.day() — сначала печать, потом сброс
- Наработка, умноженная на паспортный расход, превращается в литры и в диагностику забитого фильтра
Частые вопросы по теме урока
- Почему разница двух unixtime не ломается на полночи?
- unixtime — счёт секунд от одной точки, без полей и переносов. Полночь, смена месяца и високос уже сидят внутри числа, поэтому разница меток — просто длительность, в какой бы день они ни были взяты. Оба числа — секунды одной непрерывной шкалы, вычитание про полночь не знает
- Насос работал 10 часов, длительность считали в int на Uno. Что увидите в Serial?
- 36000 секунд больше предела в 32767 — 16-битное знаковое число заворачивает в минус. Все переменные времени должны быть unsigned long, включая startedAt и саму копилку. Мусорное отрицательное число: int на Uno 16-битный, предел 32767
- Зачем копить наработку дельтами t - prevT, а не одной разницей старт-стоп?
- Разница старт-стоп приписывает всё время дню выключения. Дельты прибавляются тому дню, когда секунды реально прошли, — статистика за сутки остаётся честной. Включение через полночь честно разнесётся по двум суткам
- Зависит ли разница unixtime от перевода часов на час?
- Перевод правит показания часов, а шкала секунд одна для всех меток. При вычитании сдвиг сокращается — в следующем уроке этим свойством мы ещё воспользуемся. Нет: обе метки сдвигаются вместе, разница та же
- Паспорт насоса 1200 л/ч, наработка за сутки 30 минут. Сколько накачано?
- Полчаса — это 0,5 часа: 1200 x 0,5 = 600 литров. Именно такие пересчёты и делают счётчик наработки практическим прибором, а не забавой. 600 литров