Дачный метеологгер жил на восьми метрах провода вдоль забора и писал CSV на карту. Однажды колодка на линии подсела, и в журнале начались срывы: библиотека возвращала nan, скетч честно пропускал такие строки, и я неделю смотрел на сплошной график, не зная, что под ним дыры. Когда я стал считать средние за сутки, выяснилось, что кривая тихо поплыла — пропущенные чтения сдвинули её без единого предупреждения. Тогда я впервые сформулировал для себя вопрос, который теперь задаю каждому: что ваша автоматика обязана делать, когда датчик замолчал?
Здесь соберём ответ целиком. Период опроса — константа с обоснованием: две секунды для DHT22, секунда для DHT11, и почему не наоборот. Срыв чтения — штатная ситуация, а не катастрофа: nan надо ловить и не пускать в арифметику. И главное — мягкая деградация: последнее честное значение с печатью возраста, счётчик сбоев и аварийная ветка, когда показания устарели окончательно. Почву под это мы готовили в уроке про интервал опроса и уроке про диагностику nan — там разобраны, почему чаще двух секунд нельзя и какие железные причины стоят за каждым срывом. Здесь — поведение автомата, а не поиск виноватых.
Период: константа, а не пауза между строками
Нижняя граница периода задана конструкцией сенсора: DHT11 обновляет показания примерно раз в секунду, DHT22 — раз в две. Опрос чаще этих значений не даёт свежих чисел — вы либо читаете то же самое, либо ловите сенсор на внутреннем пересчёте и получаете nan. Поэтому период живёт в скетче одной константой PERIOD = 2000 и проверяется таймером millis, а не размазанными delay: loop остаётся живым для кнопок, дисплея и реле, как мы привыкли с урока про millis вместо delay.
Вверх период крутят по задаче, и это нормально. Комнатной автоматике хватает опроса раз в пять-десять секунд — щелчки реле всё равно лимитирует ширина окна, а не темп чтений. Логгеру достаточно писать в журнал раз в минуту, опрашивая датчик раз в две: период опроса и период записи — разные константы, и смешивать их не стоит. Крайний полюс — батарейная станция, которая спит между опросами по четверть часа: там период диктует аккумулятор, а не сенсор, что мы делали в уроке про батарейный сон.
Пара практических привычек к константе периода. Первая: дайте ей имя, говорящее о единицах — PERIOD_MS, а не просто PERIOD, чтобы через полгода не гадать, секунды это или миллисекунды. Вторая: на время отладки период иногда растягивают до двадцати-тридцати секунд, чтобы руками успеть рассмотреть, как автомат живёт на устаревших показаниях, — а потом возвращают обратно. Константа в одном месте делает оба манёвра правкой одной строки вместо охоты за delay по всему скетчу.
У таймера millis есть собственный горизонт, о котором вспоминают раз в полтора месяца: счётчик переполняется через 49,7 суток, и все сравнения с ним проходят через ноль. Конструкция millis() - lastPoll < PERIOD на беззнаковой арифметике переживает переполнение без единой правки: вычитание в unsigned сворачивается корректно при любом моменте перехода. А вот запись вида millis() > lastPoll + PERIOD на границе лжёт — сложение уезжает за максимум, условие срывается, и автомат, проживавший сорок девять дней спокойно, вдруг решает, что время остановилось.
Мягкая деградация: старое число лучше nan
Когда обмен не состоялся, библиотека возвращает nan — «не число». Это вежливый отказ конкретного чтения, а не поломка сенсора. Реакций бывает три, и я прошёл их все. Наивная — пустить nan дальше: любое сравнение с nan даёт ложь, среднее за сутки превращается в nan, реле решает по мусору. Жёсткая — стоп по первому срыву: честно, но один сбой в неделю поднимает будильник среди ночи. Мягкая — держать последнее честное значение и его возраст: пользоваться свежими числами, помечать устаревшие, считать сбои подряд, а когда возраст выйдет за лимит — уходить в безопасный режим.
Мягкая деградация опирается на физику помещения: воздух в комнате не убегает за две секунды. Лимит возраста в шесть-восемь секунд для климата — это шесть-восемь опросов, в течение которых автоматика едет на замороженном, но реалистичном значении с честной пометкой в логе. Для инкубатора тот же лимит сжимают до пары периодов, потому что там цена просадки другая. Аварийная ветка при устаревании — не опция: нагреватель у порога замерзания, работающий по показаниям получасовой давности, опаснее выключенного. При этом ничего из этого не требует библиотек и классов — три переменные, два счётчика и одно сравнение.
#include <DHT.h> DHT dht(2, DHT22);const int RELAY = 7; const unsigned long PERIOD = 2000; // пол DHT22: раз в две секундыconst unsigned long MAX_AGE = 8000; // старше — показания недействительныconst int FAILS_TO_ALARM = 4; // сбоев подряд до тревожной строки unsigned long lastPoll = 0;unsigned long goodStamp = 0; // millis последнего честного чтенияfloat tGood = 0.0, hGood = 0.0;int failsRow = 0; void setup() { Serial.begin(9600); pinMode(RELAY, OUTPUT); digitalWrite(RELAY, LOW); dht.begin();} void loop() { if (millis() - lastPoll < PERIOD) return; lastPoll = millis(); float t = dht.readTemperature(); float h = dht.readHumidity(); if (isnan(t) || isnan(h)) { failsRow++; } else { tGood = t; hGood = h; goodStamp = millis(); failsRow = 0; } unsigned long age = (millis() - goodStamp) / 1000; bool svezhie = (goodStamp > 0) && (age <= MAX_AGE / 1000); Serial.print("t="); if (goodStamp == 0) { Serial.print("-"); } else { Serial.print(tGood, 1); } Serial.print(" vozrast="); Serial.print(age); Serial.print("s sboev="); Serial.print(failsRow); Serial.print(" -> "); if (goodStamp == 0) { Serial.println("zhdu pervoe chtenie"); } else if (!svezhie || failsRow >= FAILS_TO_ALARM) { digitalWrite(RELAY, LOW); // безопасное состояние Serial.println("ALARM: starye, nagrev stoit"); } else { Serial.println("normalno"); }}Разберём лог, который печатает этот скетч. Возраст — самая говорящая цифра строки: два-четыре секунды означают живую линию, растущий возраст — линию, которая умирает. Счётчик сбоев работает как индикатор монтажа: пара срывов в день на длинной линии — норма, десятки в день — контакт окислился или провод лёг на силовую трассу. В журнал из урока про кольцевой массив я добавил ровно две колонки — возраст и сбои, и логгер превратился из записной книжки в самодиагностику.
Заметьте асимметрию аварийной ветки: нагрев выключается, а не переключается. Без свежих показаний автоматика не имеет права греть: холодный ящик — обратимая беда, перегретый — нет. Безопасное состояние для каждого автомата своё — вентилятор оставить крутиться, реле разомкнуть, зуммер запищать — но оно обязано быть явно прописано, а не «само получится».
Отдельно про самый первый запуск. Пока ни одно честное чтение не прошло, goodStamp равен нулю, и автомат печатает zhdu pervoe chtenie: реле стоит, решения нет. Ветка выглядит скучной, но она ловит реальный сценарий — датчик забыли подключить или перепутали пин. Без неё автомат со старта считает возраст от нуля millis и решает по нулевому значению, а это худший возможный первый шаг термостата.
Аварийная ветка защищает решения автомата, но не сам скетч: если loop завис на delay или упёрся в глухой цикл, тревогу изнутри кода поднять уже некому. На платах AVR за этим следит аппаратный watchdog — таймер с потолком около восьми секунд, который перезагружает плату, если его не гладить вызовом wdt_reset в каждом проходе. Мягкая деградация и watchdog — два этажа одной защиты: первый переживает срывы линии, второй — зависший скетч, и вместе они закрывают худшие сценарии длинного прогона, когда к устройству никто не ходит неделями.
| Реакция на срыв чтения | Как выглядит в логе | Чем кончается |
|---|---|---|
| nan пускают в арифметику | Строки вроде t=nan, средние пропадают | Реле решает по мусору, журнал разваливается |
| Стоп по первому срыву | Одна сбойная строка — и всё стоит | Один сбой в неделю будит вас среди ночи |
| Мягкая деградация | Старое значение с возрастом и счётчиком сбоев | Автомат едет на честной пометке, лимит — аварийная ветка |
| Безопасный режим по лимиту возраста | Строка ALARM и реле в безопасном состоянии | Нагрузка стоит, пока показания не вернутся |
Период под задачу: от монитора до батарейки
Соберу периоды, к которым я пришёл на живых проектах. Две секунды — отладка и живой монитор, когда хочется видеть дыхание показаний. Пять-десять секунд — комнатная автоматика: окно реле всё равно решает реже, чем датчик успевает устать. Тридцать секунд — теплица и подвал, где всё меняется минутами. Минуты — журнал на SD. Четверть часа плюс сон — батарейная версия, и там период уже диктует не сенсор, а аккумулятор. Все эти цифры — не паспортные требования, а компромиссы между свежестью, шумом и сроком жизни железа.
Период и лимит возраста вместе образуют два контракта со временем: опрос не чаще, чем сенсор умеет, и решения не старше, чем воздух позволяет. Когда оба контракта прописаны в коде именованными константами, автомат становится предсказуемым при любой погоде на линии — собранный целиком такой автомат живёт в итоговом проекте раздела, с реле, LCD и той же аварийной веткой.
Проверить деградацию на живой плате проще, чем кажется: выньте сигнальный провод из колодки и смотрите в монитор. Возраст начнёт расти по две секунды за такт, счётчик сбоев — тикать, индикатор устаревания зажжётся, в логе появится строка ALARM, и реле уйдёт в безопасное состояние. Верните провод — и через два опроса лог вернётся к свежим значениям, не потеряв счётчик сбоев как историю. Пять секунд работы руками дают больше уверенности, чем сутки тихой работы, потому что вы своими глазами видели худший день вашей линии.
- 1. Какая нижняя граница периода опроса у DHT22?
- 2. Что возвращает библиотека, когда сенсор не ответил на обмен?
- 3. Почему подстановка нуля вместо срыва хуже пропуска строки?
- 4. Возраст последнего честного чтения вышел за лимит. Что делает скетч с мягкой деградацией?
- 5. Счётчик сбоев вырос с пары в день до десятков. Где искать?
- Период — паспорт сенсора: DHT11 раз в секунду, DHT22 раз в две; вниз его не крутят, вверх — по задаче.
- Период опроса и период записи в журнал — разные константы; смешивать их не стоит.
- nan — вежливый отказ чтения: ловится isnan и не пускается в арифметику никогда.
- Мягкая деградация: последнее честное значение + возраст + счётчик сбоев; за лимитом — безопасный режим.
- Счётчик сбоев — индикатор монтажа: пара в день норма, десятки — ищите контакт или наводку.
Частые вопросы по теме урока
- Какая нижняя граница периода опроса у DHT22?
- DHT22 обновляет показания раз в две секунды. Чаще вы прочитаете либо то же самое, либо nan в момент внутреннего пересчёта. Раз в две секунды — свойство сенсора, а не библиотеки
- Что возвращает библиотека, когда сенсор не ответил на обмен?
- Библиотека не поднимает тревогу — она тихо возвращает nan. Ловится сравнением isnan до любых вычислений и вывода. nan: обмен не состоялся, честного числа нет
- Почему подстановка нуля вместо срыва хуже пропуска строки?
- Ноль — правдоподобное значение, и его невозможно отличить от правды: мороз в июле проходит незамеченным, пока не пересчитают средние. Ноль рвёт средние и тренды и поднимает ложные тревоги
- Возраст последнего честного чтения вышел за лимит. Что делает скетч с мягкой деградацией?
- По лимиту возраста автомат уходит в аварийную ветку: реле в безопасное состояние, строка ALARM в лог. Греть по показаниям получасовой давности нельзя. Переходит в безопасное состояние и печатает об этом в лог
- Счётчик сбоев вырос с пары в день до десятков. Где искать?
- Счётчик сбоев — индикатор монтажа: рост означает окислившийся контакт, слабую подтяжку или провод, лёгший на силовую трассу. В железе: контакт, подтяжка, наводка на длинной линии