Зарядка телефона на подоконнике, гирлянда на балконе, вентилятор в гараже — все они живут по одному сценарию: утром их включают, а выключить забывают. Умная розетка из магазина решает это за тысячу рублей и отдельным приложением; у меня же на столе лежали Uno, реле-модуль за сто пятьдесят и пульт из набора, и не хватало только одного решения — пусть срок выбирает кнопка. Так родился таймер розетки: цифра включает нагрузку на выбранное время, повтор цифры пересчитывает срок заново, OK гасит всё немедленно.
Реле по кодам кнопок мы уже дёргали: в уроке про панель команд каждая кнопка закрывает за собой свою нагрузку — это рубильник без памяти о времени. Таймер — надстройка другого сорта: между командой и реле встаёт отсчёт, и вся механика держится на millis, а не на delay. Планировщик без пауз разобран в уроке про millis, форма повторных кадров NEC — в уроке про repeat-кадры; здесь я собираю из этих кирпичей прибор, а не приём.
Одна оговорка о доверии, чтобы не возвращаться к ней в конце с порицанием. ИК-таймер по определению прибор с мягким контрактом: пульт потерялся, батарейка села, плату моргнуло светом — нагрузка гаснет. Для зарядки это фича, а для утюга — недопустимая беспечность. Список того, что я не вешаю на такой таймер, ниже, сразу после скетча.
Раскладка сроков и отсчёт без delay
Раскладка созревала неделю. Кнопка «1» даёт тридцать минут — срок для зарядки телефона, «3» — час для гирлянды, «4» — два часа для вентилятора в жару. Повторное нажатие той же цифры пересчитывает срок от текущего момента, а не складывает сроки: два щелчка по «1» дают не шестьдесят минут, а свежие тридцать — предсказуемость важнее гибкости. OK гасит реле немедленно и обнуляет отсчёт. Вся логика ложится в три переменные: флаг running, момент старта и выбранный срок. Сравнивается со сроком пройденный путь — разница millis() и момента старта; оба слагаемых беззнаковые, поэтому корректность сохраняется даже после переполнения millis на отметке 49,7 суток.
Цифровые кнопки NEC при удержании шлют тот же кадр-повтор каждые 108 миллисекунд, что и громкость: флаг IRDATA_FLAGS_IS_REPEAT обязан отсекаться до логики, иначе зажатая «1» пересчитывала бы срок девять раз в секунду, и срок не кончился бы никогда. Как устроены повторы внутри кадра — история урока по ссылке выше; фильтр флагом в связке с millis-окном разобран в уроке про фильтр повторов. Здесь беру тот же приём без изменений. Скетч целиком:
// Таймер розетки: цифры задают срок, OK гасит немедленно#include <IRremote.hpp> const int IR_PIN = 11;const int RELAY_PIN = 7;const int LED_PIN = 9; // ШИМ вне Таймера 2: берите 9 или 10 // У модулей с оптопарой вход чаще активен LOW: поменяйте местами,// если ваше реле щёлкает от HIGHconst int RELAY_ON = HIGH;const int RELAY_OFF = LOW; const int CMD_30MIN = 22; // кнопка «1»const int CMD_60MIN = 13; // кнопка «3»const int CMD_120MIN = 12; // кнопка «4»const int CMD_OFF = 67; // OK const unsigned long TERM_30 = 1800000UL;const unsigned long TERM_60 = 3600000UL;const unsigned long TERM_120 = 7200000UL; bool running = false;unsigned long startedAt = 0;unsigned long term = 0; void setup() { Serial.begin(9600); digitalWrite(RELAY_PIN, RELAY_OFF); // выключающее значение ДО pinMode pinMode(RELAY_PIN, OUTPUT); pinMode(LED_PIN, OUTPUT); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); Serial.println("timer: 1/3/4 = 30/60/120 min, OK = off");} void loop() { if (IrReceiver.decode()) { bool isRepeat = IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT; int cmd = IrReceiver.decodedIRData.command; if (!isRepeat) { unsigned long novo = 0; if (cmd == CMD_30MIN) novo = TERM_30; if (cmd == CMD_60MIN) novo = TERM_60; if (cmd == CMD_120MIN) novo = TERM_120; if (cmd == CMD_OFF) { running = false; digitalWrite(RELAY_PIN, RELAY_OFF); Serial.println("off"); } else if (novo > 0) { startedAt = millis(); // срок отсчитывается заново term = novo; running = true; digitalWrite(RELAY_PIN, RELAY_ON); Serial.print("on: "); Serial.print(novo / 60000UL); Serial.println(" min"); } } IrReceiver.resume(); } if (running && millis() - startedAt >= term) { // путь больше срока running = false; digitalWrite(RELAY_PIN, RELAY_OFF); Serial.println("srok vysel: off"); } if (running) { // дыхание: отсчёт видно через комнату static int out = 0; static int dir = 1; static unsigned long last = 0; if (millis() - last >= 15) { last = millis(); out = out + dir; if (out >= 255) dir = -1; if (out <= 0) dir = 1; analogWrite(LED_PIN, out); } } else { analogWrite(LED_PIN, 0); }}Строки порядка в setup() здесь дороже, чем кажутся. У модулей с оптопарой вход почти всегда активен уровнем LOW: оптодиод зажигается, когда пин прижат к земле, и реле щёлкает. Пока пин не настроен, он висит в воздухе, и поведение входа отдаётся на милость подтяжки самого модуля: у части плат она к питанию, у части — к земле, отсюда классика «реле щёлкает при каждой прошивке». Лекарство — записать выключающее значение до pinMode(): у обратного триггера это HIGH, и в скетче пары RELAY_ON и RELAY_OFF вынесены в константы, чтобы смена модуля стоила одной строки.
Ток входа оптопары — сущие копейки, 15–20 миллиампер на канал, и Uno тянет её пином без вопросов; катушку реле питает сам модуль. Джампер JD-VCC отделяет питание катушки от логики: снятый джампер и отдельные пять вольт на JD-VCC изолируют пин от индуктивных выбросов катушки — на большой нагрузке это уже не педантичность, а жизнь контактов. Светодиод на девятом пине дышит, пока реле замкнуто: глаз видит отсчёт через комнату, без монитора.
Сроки — не догма, а отправная точка под мою нагрузку. Тридцать минут для зарядки взялись из вечернего ритуала: телефон доезжает до полного за сорок, но с запасом в десять я сплю спокойнее. Для помпы аквариума срок сжался бы до пятнадцати, для увлажнителя вырос до четырёх часов — таблица сроков правится в трёх константах, и это вся цена пересборки. Прятать сроки в EEPROM настроек, чтобы менять их пультом, я не стал: интерфейс из трёх кнопок честнее меню.
Граница доверия: что не вешать на ИК
Мой список запретов короткий и жёсткий. Утюг, плитка и всё, что греется до пожара, — нет независимо от тока: контракт таймера строится на отказе, и я не могу гарантировать, что отказ наступит именно тогда, когда нужно. Обогреватель на две тысячи ватт тянет 9 ампер — клеммы реле на 10 ампер живут впритык, а пусковой ток компрессорных моделей рвёт контакты за сезон. Честная территория таймера — зарядки, гирлянды, вентиляторы, помпа аквариума: нагрузки, для которых погаснуть в любой момент безопасно и скучно.
Сравнение с соседями по разделу здесь короткое. Панель команд из урока про реле и PROGMEM решает «что включить», таймер — «на сколько включить», и в моём коридоре они живут рядом, не мешая друг другу. Если захочется, чтобы срок переживал сброс питания, понадобится EEPROM — приём слотов показан в уроке про обучаемый пульт. Но для розетки я EEPROM сознательно не пишу: после моргания света зарядка должна остаться погашенной, а не продолжить отсчёт в пустой комнате.
| Кнопка | Что делает | Почему так |
|---|---|---|
| «1» (22) | включает реле на 30 минут | срок зарядки телефона на ночь |
| «4» (12) | включает реле на 120 минут | вентилятор в жару или увлажнитель |
| повтор той же цифры | пересчитывает срок заново от текущего момента | предсказуемо: сроки не копятся |
| OK (67) | гасит реле немедленно и сбрасывает отсчёт | выход из сценария без ожидания |
После недели жизни с таймером вскрылось внезапное: чаще всего я жму не цифру срока, а OK. Гирлянда погасла, зарядка дотянулась — и рука сама ищет кнопку сброса, чтобы реле не щёлкало впустую до конца срока. Раскладку я потому и держу на разных кнопках, что отмену хочется находить вслепую: в тёмном коридоре выпирающий OK у пульта из набора ловится пальцем сразу.
Белый список по адресу пульта сюда тоже просится: соседский пульт от люстры говорит по-NEC на том же адресе 0x00, и его «вверх» встанет в мою раскладку сроков. Фильтр по decodedIRData.address и разбор чужих кадров — в уроке про помехи и белый список; для коридорной розетки я фильтр оставил: лишние десять строк против сюрпризов от гостей с пультами.
Соблазн собрать тот же таймер на delay() реален, и я его отпрограммировал один раз — хватает надолго: пауза в тридцать минут стопорит loop, приёмник перестаёт слушать пульт, и отменить срок до его конца невозможно в принципе. Delay-версия умеет только «включил и забыл»; миллис-версия на том же железе остаётся на связи каждую миллисекунду, и именно поэтому вся логика уместилась в один свободный loop без единой блокировки.
И про хвост отсчёта. Дыхание идёт с периодом полторы секунды — глаз различает его на периферии зрения и не путает с гирляндой рядом. За последнюю десятую часть срока я мигать не учу: предупреждение перед выключением на свету выглядит приветливо, а в спальне стало бы лишним световым событием. Тихий доезд в ноль — решение в пользу сна, и спорить с ним не хочется даже ради эффектности.
- 1. Почему отсчёт ведут через разницу millis() - startedAt, а не сравнение millis() с дедлайном в будущем?
- 2. Реле-модуль щёлкает при подаче питания, хотя в setup() ничего не включалось. Кто виноват?
- 3. Что случится с раскладкой сроков, если не гасить флаг IRDATA_FLAGS_IS_REPEAT?
- 4. Что делает повторное нажатие той же цифры срока?
- 5. Почему я не вешаю утюг на ИК-таймер, хотя реле 10 А тянет его ток?
- Срок хранится парой «момент старта + длительность», сравнивается пройденный путь — переполнение millis не страшно.
- Флаг IRDATA_FLAGS_IS_REPEAT гасится до логики: удержание цифры иначе пересчитывало бы срок каждые 108 мс.
- Активный LOW у реле-модуля с оптопарой: выключающее значение пишется до pinMode(), пары ON/OFF — в константах.
- Повтор цифры пересчитывает срок заново, а не суммирует; OK гасит нагрузку немедленно.
- Территория таймера — зарядки, гирлянды, вентиляторы; греющиеся приборы с обратным контрактом не вешаются никогда.
Частые вопросы по теме урока
- Почему отсчёт ведут через разницу millis() - startedAt, а не сравнение millis() с дедлайном в будущем?
- Сравнение абсолютных времён ломается при переполнении millis: дедлайн уезжает за 2^32, и момент его «не наступает». Разница пройденного пути и срока в беззнаковой арифметике остаётся верной через любой перенос. разница двух беззнаковых моментов корректна и после переполнения millis на 49,7 сутках
- Реле-модуль щёлкает при подаче питания, хотя в setup() ничего не включалось. Кто виноват?
- У модулей с оптопарой триггер чаще обратный: реле срабатывает от LOW. Непрошитый пин в Z-состоянии дёргается за подтяжкой модуля. Лечение: выключающее значение записать до pinMode(), у обратного триггера — HIGH. вход активен от LOW, а пин до pinMode() висит в воздухе
- Что случится с раскладкой сроков, если не гасить флаг IRDATA_FLAGS_IS_REPEAT?
- NEC-пульт при удержании шлёт кадр-повтор каждые 108 мс. Без фильтра каждое повторение пересчитывает срок заново — таймер становится вечным. Флаг отсекается до любой логики команд. зажатая цифра продлевала бы срок девять раз в секунду, и срок не кончился бы
- Что делает повторное нажатие той же цифры срока?
- Продление пересчитывает старт от millis() и заново записывает срок — суммы не происходит. Два щелчка по «1» дают свежие тридцать минут, а не час: предсказуемость интерфейса дороже гибкости. начинает выбранный срок заново от текущего момента
- Почему я не вешаю утюг на ИК-таймер, хотя реле 10 А тянет его ток?
- Таймер безопасен для нагрузок, которым погаснуть в любой момент скучно и неопасно: зарядки, гирлянды, вентиляторы. Греющееся до пожара оборудование требует обратного контракта, которого у одностороннего ИК нет. контракт строится на отказе: любой сброс гасит нагрузку, и гарантий нужного момента нет