Кнопка включения поливалки жила у меня на кухонной полке, а ведро с насосом — на балконе. Первый вариант связи был честно-примитивный: два провода на двадцать метров через дверной проём, digitalRead на другом конце. Работало через раз — длинная линия ловила наводки, кнопка дребезжала, плюс нельзя было переставить ведро без переклейки проводов. В ящике в это время лежали ИК-диод и приёмник из набора, и за вечер вторая версия стала беспроводной: первая плата светит в приёмник второй, вторая исполняет команды.
То, что между платами летает, и есть свой протокол: заранее договорённый словарь кадров. Библиотека IRremote умеет обе стороны диалога — IrSender и IrReceiver, мы знакомились с ними в уроке про установку и API четвёртой версии. А сам формат кадра — адрес отправителя плюс код действия — достаётся бесплатно из протокола NEC, который мы разложили по битам в уроке про NEC: библиотека сама проверит кадр по инверсии и сама разложит его на поля.
План урока: словарь из двух байт, передача со повторами и паузами, приёмник с фильтром по адресу и подтверждением приёма, и граница канала — сколько метров такая пара реально берёт и почему ей не нужна общая земля.
Первый вопрос, который стоит закрыть до кода: зачем вообще NEC, если можно слать упрощённые кадры — только свои два байта без всяких преамбул. Упрощённый кадр короче в четыре раза, и на бумаге это выглядит как победа. Но его пришлось бы слать самому: таймером формировать несущую 38 кГц, самому мерить паузы, самому проверять целостность — по сути, писать третий протокол с нуля, со всеми граблями таймеров. Библиотека при этом отдаёт NEC бесплатно: sendNEC и decode уже проверены тысячами проектов, кадр несёт адрес с командой и проверяет себя инверсией. Мнение с границей: сырые кадры через sendRaw имеют смысл, когда на приёмнике стоит нестандартный приёмник или нужен кадр чужого устройства бит в бит — для пары своих плат NEC честнее и быстрее в разработке.
Словарь: адрес устройства плюс код действия
Протокол начинается не с кода, а со словаря, записанного на бумажке. Два байта: адрес устройства и код действия. У меня вышло так: 0x10 — светильник, 0x11 — поливалка, 0x12 — будущие шторы; действия — 0x01 включить, 0x02 выключить, 0x03 переключить. Адрес работает фильтром: приёмник светильника игнорирует кадры с адресом 0x11, даже если их шлёт ваша же вторая плата. Это ровно тот приём, которым телевизор не реагирует на пульт люстры: сравнение одного байта до любого switch.
Передача выглядит скромно: IrSender.sendNEC(адрес, команда, число повторов). Диод — через резистор 100-150 Ом на пин 3 Uno, железо описано в уроке про установку. Третий аргумент — повторы силами библиотеки: ноль даёт один кадр, единица-тройка добавляют столько же повторных посылок. Поверх этого я ставлю свою паузу: между сериями команд — 150-200 мс, чтобы приёмник успел отработать кадр и освободить буфер. Окно глухоты между decode и resume мы измеряли в уроке про разбор кадра: серия из трёх посылок подряд в это окно не влезает, часть кадров приёмник честно проглотит.
Подтверждение приёма — третья часть словаря, о которой забывают чаще всего. Дешёвый вариант — бип: приёмник пищит 50 мс на каждый принятый кадр, и рука сама чувствует, что команда дошла. Серьёзный вариант — обратный кадр: приёмник шлёт sendNEC со своим адресом и кодом-галочкой, передатчик ждёт его секунду и повторяет команду при молчании. Для отладки хватает третьего пути — строки addr= и cmd= в Serial, но в постоянном проекте бип живёт с поливалкой до сих пор.
#include <IRremote.hpp> const int BTN_LAMP = 2; // светильник: вкл/выклconst int BTN_PUMP = 4; // поливалка: старт const uint16_t ADDR_LAMP = 0x10;const uint16_t ADDR_PUMP = 0x11;const uint8_t CMD_TOGGLE = 0x03;const uint8_t CMD_START = 0x01; void setup() { Serial.begin(9600); pinMode(BTN_LAMP, INPUT_PULLUP); pinMode(BTN_PUMP, INPUT_PULLUP); IrSender.begin(); Serial.println("Peredatchik gotov");} void loop() { if (digitalRead(BTN_LAMP) == LOW) { IrSender.sendNEC(ADDR_LAMP, CMD_TOGGLE, 2); // кадр + 2 повторных Serial.println("slal: lamp toggle"); delay(400); } if (digitalRead(BTN_PUMP) == LOW) { IrSender.sendNEC(ADDR_PUMP, CMD_START, 2); Serial.println("slal: pump start"); delay(400); }}Приёмник зеркален: IrReceiver.begin() на пине 2, в decode-блоке сначала сравнение decodedIRData.address со своим адресом, и только потом разбор команды. Кадр NEC проверяет себя сам: команда едет в паре с инверсией, и битые посылки библиотека отсеивает до вашего switch — это первый слой помехоустойчивости, доставшийся бесплатно. Второй слой — повторы передатчика: если один кадр съело окно глухоты или засветка, следующий дойдёт. Третий слой — дедупликация на приёмнике: повторная посылка той же команды в течение 300 мс исполняется один раз, остальное молча пропускается.
Дедупликация — то место, где я обманулся сам. Повторы передатчика и автоповтот удержания NEC — разные вещи: у повторной посылки полный кадр с преамбулой, и флаг IRDATA_FLAGS_IS_REPEAT на ней не поднимается. Фильтр повторов из урока про управление светом здесь бессилен — нужен свой таймер: команда и время её приёма, всё лишнее за 300 мс — мимо. Для команд типа «включить» лишняя посылка безвредна — действие идемпотентно; для «переключить» дедуп обязателен, иначе свет дёргается.
#include <IRremote.hpp> const int IR_PIN = 2;const int LAMP_PIN = 9;const int BUZZ_PIN = 8; const uint16_t ADDR_LAMP = 0x10;const uint8_t CMD_TOGGLE = 0x03; unsigned long lastMs = 0;bool lampOn = false; void setup() { Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); pinMode(LAMP_PIN, OUTPUT); pinMode(BUZZ_PIN, OUTPUT); Serial.println("Priemnik svetilnika: zhdem adres 0x10");} void loop() { if (IrReceiver.decode()) { uint16_t addr = IrReceiver.decodedIRData.address; uint8_t cmd = IrReceiver.decodedIRData.command; if (addr == ADDR_LAMP) { bool dubl = (millis() - lastMs < 300); if (cmd == CMD_TOGGLE && !dubl) { lampOn = !lampOn; digitalWrite(LAMP_PIN, lampOn); digitalWrite(BUZZ_PIN, HIGH); delay(50); digitalWrite(BUZZ_PIN, LOW); Serial.print("cmd=toggle lamp="); Serial.println(lampOn); } lastMs = millis(); } IrReceiver.resume(); }}Границы канала: метры, земля, честные обещания
Пара плат живёт в тех же метрах, что пульт с телевизором: три-пять метров с типовым диодом через 150 Ом при прямой видимости, дальше — по геометрии из урока про дальность: один отскок от потолка режет дистанцию вдвое, через стену — ноль. Отсюда планировка: передатчик ставят там, где стоит человек с кнопкой, приёмник — в прямой видимости или под работающим отражением, и обе оптики смотрят друг в друга в пределах надёжного конуса.
Зато по электричеству канал свободнее проводного. Линии digitalRead требуют общей земли — без неё два провода шумят, мы разбирали это в уроке про общий провод. ИК-каналу земля не нужна вовсе: связь оптическая, гальванически развязанная, платы могут сидеть на разных блоках питания и даже в разных розетках. Двадцать метров провода из моей первой версии заменяют две макетки без единого соединения между ними — и переезд ведра на другой балкон перестаёт быть проектом.
Честные обещания: ИК не гарантирует доставку. Кадр мог не дойти, и приёмник об этом не узнает, пока не получит обратный ACK. Для кнопки поливалки хватает бипа: нажал — щёлк — дошло. Для счётчиков и показаний датчиков, где потеря недопустима, берите провод или радио — оптика для потоков данных не предназначена. Мнение с границей: свой протокол на NEC отлично закрывает домашние сценарии «нажал — исполнилось» и становится шатким, как только очередь из десятков сообщений в секунду набирает темп.
И про скорость диалога, чтобы вы не ждали от канала лишнего. Полный кадр NEC тянется около 68 мс, серия из трёх посылок с паузами — четверть секунды: команда доезжает за мгновение по меркам человека, но за вечность по меркам процессора. Диалог «команда — ACK» у меня в проекте занимал 200-300 мс на круг — для кнопки света незаметно, для опроса датчика раз в сто миллисекунд уже не годится. Отсюда правило: ИК-канал для событий, а не для потоков — нажатие, тревога, разовая команда. Всё, что чаще раза в секунду, уезжает на провод или радио.
| Слой защиты | Что делает | Числа |
|---|---|---|
| Инверсия в кадре NEC | библиотека отсеивает битые посылки до вашего кода | команда и её инверсия в каждом кадре |
| Повторы передатчика | дублируют команду против засветки и окна глухоты | 2-3 посылки, пауза между сериями 150-200 мс |
| Дедупликация приёмника | не даёт повторам исполниться трижды | та же команда игнорируется 300 мс |
| Подтверждение | человек или скетч видит доставку | бип 50 мс, ACK-кадр или строка в Serial |
- Свой протокол — это словарь на бумажке: адрес устройства плюс код действия; кадр NEC несёт оба байта и инверсию для самопроверки
- Передача: IrSender.sendNEC(адрес, команда, повторы) на пине 3; приёмник сравнивает decodedIRData.address и только потом разбирает command
- Повторы передатчика — не автоповтот NEC: флаг IS_REPEAT молчит, дедупликацию делает приёмник — 300 мс на команду
- ИК-каналу не нужна общая земля: платы живут на разных блоках питания без единого провода между ними
- Дальность пары — 3-5 м при прямой видимости; подтверждение доставки — бип 50 мс или ACK-кадр, для потоков данных оптика не годится
- 1. Две платы общаются только по ИК. Нужна ли между ними общая земля?
- 2. Что несёт кадр в своём протоколе на базе NEC?
- 3. Приёмник получил одну и ту же команду трижды за 200 мс. Что это и что делать?
- 4. Какой вызов отправляет команду с адресом устройства на передатчике?
- 5. Самое дешёвое подтверждение приёма для человека — это…
Протокол в кармане: словарь, фильтр адреса, повторы с паузами, бип-галочка. Дальше три дороги: нагружать канал — добавить кнопок и устройств в словарь, считать канал честно — метки и счётчик из урока про дальность, или построить к протоколу приёмку — стенд проверки пультов из следующего урока проверит и ваш самодельный передатчик, и чужие пульты с барахолки. Мой совет: бумажку со словарём вклейте в крышку проекта — через год без неё код превращается в шифровку.
Частые вопросы по теме урока
- Две платы общаются только по ИК. Нужна ли между ними общая земля?
- Связь идёт светом: ни одного провода между платами не требуется, каждая живёт на своём блоке питания. Общая земля нужна проводным линиям вроде digitalRead через кабель. Не нужна: канал оптический и гальванически развязан
- Что несёт кадр в своём протоколе на базе NEC?
- Словарь — два байта: адрес устройства и код действия. Целостность кадра проверяет инверсия команды, встроенная в протокол NEC, а не отдельная CRC. Адрес устройства и код действия
- Приёмник получил одну и ту же команду трижды за 200 мс. Что это и что делать?
- Повторные посылки идут полными кадрами с преамбулой, флаг IS_REPEAT на них не поднимается. От троекратного исполнения спасает таймер дедупликации на приёмнике. Повторы передатчика ради надёжности — исполнить один раз, дедупликация 300 мс
- Какой вызов отправляет команду с адресом устройства на передатчике?
- Передача — IrSender.sendNEC с тремя аргументами: адрес, код действия, повторы. Диод ставится на пин 3 Uno через резистор 100-150 Ом. IrSender.sendNEC(адрес, команда, число повторов)
- Самое дешёвое подтверждение приёма для человека — это…
- Бип 50 мс на каждый кадр — копеечная деталь и мгновенная обратная связь: рука чувствует доставку. Обратный ACK-кадр — вариант для автоматических проверок. Короткий бип на приёмнике при каждом принятом кадре