Кухонный таймер на макетке прожил без пульта ровно один день: выставлять минуты кнопкой на самой плате оказалось неудобно, и я прицепил ИК-приёмник — то, как пульт светит невидимым светом и почему его ловит баночка приёмника, мы уже разбирали в уроке про ИК-сигнал. VOL+ прибавляла минуту, VOL- убавляла. А потом я сделал то, что каждодневно делает телевизор: зажал VOL+ и стал ждать, пока минуты набегут до тридцати. Не набежало ничего — скетч прибавил одну минуту и замер: он реагировал только на новое нажатие, а удержание для него отсутствовало. Пульт при этом был ни при чём: телевизор в той же руке честно ехал по громкости от той же кнопки.
Вторая история случилась в ту же неделю. Я откопал в закладках скетч десятилетней давности про пульты, запустил его со своим приёмником — и в мониторе вместо команд полезли строки с числом 4294967295, он же 0xFFFFFFFF. Кнопки при этом работали: телевизор реагировал, громкость менялась. Кадры доезжали, а старая библиотека видела в них какое-то магическое число вместо команды.
Обе истории про одно и то же: серию кадров, которой NEC отвечает на удержание. Здесь разберём, что пульт шлёт, пока палец на кнопке, как это видит библиотека четвёртой версии, откуда в старых скетчах берётся 0xFFFFFFFF и как построить обработку, где выключатель срабатывает один раз, а регулятор едет плавно — как громкость у телевизора.
Серия кадров: что летит, пока палец на кнопке
Пока кнопку держат, NEC живёт по расписанию. Первое нажатие — полный кадр на 32 бита, те самые 50–85 мс, которые мы считали в уроке про анатомию кадра NEC. Дальше пульт молчит до отметки 108 мс от начала полного кадра, а потом начинает шлёпать кадры-повторы: короткая шапка из 9 мс вспышки и 2,25 мс паузы, каждые 108 мс — примерно девять штук в секунду. Отпустили кнопку — серия обрывается на последнем повторе, следующее нажатие снова стартует полным кадром.
Цифры честные для классического NEC, но с оговоркой, которую я усвоил на одном пульте от ресивера: он слал повтор не через 108 мс, а уже через 40 мс после конца основного кадра — и делал так даже после короткого щелчка. Расписание у каждого производителя своё, поэтому в скетчах я не привязываюсь к паузам и не жду «ровно 108 миллисекунд»: единственный надёжный признак повтора — пометка в самом кадре, а не секундомер в loop().
Самое важное про устройство кадра-повтора: в нём нет ни адреса, ни команды, ни инверсий — только шапка. По воздуху не летит ни один бит данных, всё сообщение сводится к «ту же кнопку всё ещё держат». Отсюда вопрос, на который наступает каждый второй: откуда decode() берёт команду, если в кадре её физически нет? Библиотека четвёртой версии отвечает так: декодер копирует адрес и команду из последнего полного кадра и поднимает в поле flags бит IRDATA_FLAGS_IS_REPEAT. Снаружи повтор выглядит почти как обычный кадр: decodedIRData.command держит прежнее значение, протокол прежний, и отличить его можно только по флагу. Мы уже вешали эту проверку на свет в уроке про команды и повторы — там флаг отделял первое нажатие от удержания, чтобы ночник не мигал. Здесь копнём на уровень ниже: не «как запретить повторы», а «как их приручить».
Из «в повторе нет данных» следует практическое предупреждение: если скетч проспал полный кадр в начале серии, вся серия унаследует команду последнего разобранного кадра — а тот может оказаться чужим или битым. Стреляет редко: полный кадр длинный, 50–85 мс, пропустить его можно, только уйдя из loop() в delay() на десятки миллисекунд. Но знать про это стоит — иначе однажды поймёте, что повторы «застряли» на старой команде, и потратите вечер на пульт, который не виноват.
Чтобы увидеть серию своими глазами, я собрал детектор: он печатает команду, признак повтора и номер кадра внутри серии. Одна кнопка — одна серия, и в логе видна вся механика: новый кадр обнуляет счётчик, повторы крутят его вверх.
#include <IRremote.hpp> const int IR_PIN = 2;int seriesCount = 0; // номер кадра внутри текущей серии void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); Serial.println("Zhmite i derzhite knopku");} void loop() { if (IrReceiver.decode()) { byte cmd = IrReceiver.decodedIRData.command; bool isRepeat = IrReceiver.decodedIRData.flags & IRDATA_FLAGS_IS_REPEAT; if (!isRepeat) { Serial.print("--- novaya seriya, cmd="); Serial.println(cmd); seriesCount = 0; } else { seriesCount = seriesCount + 1; Serial.print("repeat "); Serial.print(seriesCount); Serial.print(" cmd="); Serial.println(cmd); } IrReceiver.resume(); }}Пара чисел к скетчу. Строка лога в двадцать символов при 9600 бод занимает около 20 мс — в паузу 108 мс она влезает с запасом, поэтому детектор не глотает повторы. А вот дамп длительностей на каждый повтор уже не поместится: помним про окно глухоты между decode() и resume() из урока про разбор кадра — всё, что пульт шлёт внутри этого окна, теряется.
Возможный вопрос: почему бы не сравнивать команду с прошлой — cmd != lastCmd — вместо флага? На двух разных кнопках это сработает, но второе нажатие той же кнопки подряд неотличимо от повтора: и там, и там команда 21. Флаг — единственный честный признак, потому что он ставится декодером по самому факту шапки без данных. В симуляторе ниже мы обойдёмся сравнением, и я оговорю, чем эта замена отличается от настоящей.
Паттерн обработки: выключатель против регулятора
Каждой команде в карте я заранее задаю режим: переключатели — свет, режим, старт-стоп — слушают только кадры без флага повтора; регуляторы — громкость, скорость, позиция — все кадры подряд. Так удержание превращается из побочного эффекта протокола в элемент интерфейса, ровно как у телевизора: клик переключает канал, а удержание плавно едет по громкости. Действие «только на новом кадре» записывается одной строкой: if (cmd == X && !isRepeat), а шаг регулятора выполняется на каждом decode() без дополнительных условий.
Скорость регулировки складывается из двух ручек: шага за кадр и частоты кадров. При периоде 108 мс серия даёт около девяти кадров в секунду: шаг 1 — девять единиц в секунду, шаг 2 — восемнадцать. Для громкости от 0 до 30 я беру шаг 2 — весь путь за полторы секунды удержания. Для минут таймера это уже бешеная скорость: плюс девять минут за секунду — так и до ужина промотаешь. Поэтому для грубых величин я прореживаю повторы: кадр засчитывается, только если с прошлого зачтённого прошло не меньше 250 мс — выходит четыре шага в секунду. Паузу меряю millis(), а почему delay() в loop() для этого не годится, объясняет урок про millis вместо delay.
И обратный случай, о котором забывают: если регулятор слишком нервный, я не уменьшаю шаг, а игнорирую первые два повтора серии — палец ещё на полпути, и ранняя чувствительность только мешает. Границу подбираю под конкретный пульт: на тугих кнопках люфт съедает первые повторы, на мягких — нет. Проверка простая: держите кнопку до упора и смотрите по детектору, сколько кадров успело пройти, пока рука «дожимала».
| Кадр серии | Что по воздуху | Что видит decode() | Как обрабатывать |
|---|---|---|---|
| Первый (полный) | 32 бита: адрес, инверсия, команда, инверсия | command и address заполнены, флаг повтора опущен | как новое нажатие: toggle, старт, печать имени |
| Повтор | шапка 9 мс + 2,25 мс, битов данных нет | command скопирован из прошлого полного кадра, флаг IS_REPEAT поднят | только регуляторам: шаг за кадр, при нужде прореженный millis-паузой |
| Пауза 108 мс | тишина между повторами | decode() возвращает false | окно для печати и медленной логики |
| Конец серии | тишина после отпускания | кадров нет | следующее нажатие стартует новым полным кадром |
Симулятор: плавная громкость на кадрах пульта
- Удержание кнопки NEC — это серия: полный кадр 50–85 мс, затем кадры-повторы 9+2,25 мс каждые 108 мс, около девяти в секунду
- В кадре-повторе нет ни адреса, ни команды: декодер копирует их из последнего полного кадра и поднимает бит IRDATA_FLAGS_IS_REPEAT в flags
- Расписание пультов гуляет: встречаются повторы и через 40 мс после кадра — доверяйте флагу, а не секундомеру
- В старых версиях IRremote повтор приезжал сырым словом 0xFFFFFFFF в results.value; в 4.x он разобран, как обычный кадр
- Выключатели слушают только новые кадры, регуляторы — все подряд; скорость задают шаг за кадр и millis-прореживание
- 1. Что физически передаёт кадр-повтор NEC?
- 2. Откуда в IRremote 4.x берётся command у повторного кадра?
- 3. В мониторе старого скетча сыплются строки 0xFFFFFFFF. Что это?
- 4. Сколько повторных кадров в секунду шлёт классический NEC при периоде 108 мс?
- 5. Таймер кухни: удержание VOL+ мотает минуты. Зачем я прореживаю повторы паузой на millis в 250 мс?
Теперь серия кадров читается как открытая книга: полный кадр запускает, повторы едут с флагом, отпускание обрывает серию. В следующем уроке повесим на кнопки настоящие дела — соберём карту кодов для светодиода и RGB, где каждая кнопка получает свою роль. Заодно поговорим о главной грабле пультовых проектов: таблицы кодов из чужих статей не переносятся на ваш пульт.
Частые вопросы по теме урока
- Что физически передаёт кадр-повтор NEC?
- Повтор — сообщение «ту же кнопку всё ещё держат». Ни адрес, ни команда по воздуху не едут: декодер берёт их из последнего полного кадра. Только шапку: 9 мс вспышки и 2,25 мс паузы, без битов данных
- Откуда в IRremote 4.x берётся command у повторного кадра?
- В повторе данных нет, поэтому библиотека подставляет значения прошлого полного кадра и помечает кадр флагом — оттуда и прежняя команда. Декодер копирует его из последнего полного кадра и ставит флаг IS_REPEAT
- В мониторе старого скетча сыплются строки 0xFFFFFFFF. Что это?
- До версии 3 повтор отдавался сырым словом в results.value без расшифровки. В 4.x тот же кадр разобран: command заполнен, флаг в flags. Признак умирающего приёмника
- Сколько повторных кадров в секунду шлёт классический NEC при периоде 108 мс?
- 1000 мс делить на 108 мс — примерно девять кадров в секунду. Отсюда скорость регулятора: шаг 2 даёт восемнадцать единиц в секунду. Около девяти
- Таймер кухни: удержание VOL+ мотает минуты. Зачем я прореживаю повторы паузой на millis в 250 мс?
- Прореживание оставляет четыре шага в секунду: минуты набегают с комфортом. Пауза меряется millis(), потому что delay() заморозил бы приём. Девять кадров в секунду дают плюс девять минут за секунду удержания — слишком быстро