Штора на окне дачной веранды у меня полгода открывалась тумблером у двери, и меня это устраивало ровно до первого ливня: чтобы прикрыть окно, надо было выйти под дождь и дойти до двери. Пульт от машинки лежал на подоконнике без дела, приёмник оставался после прошлой связки — и через вечер штора слушала четыре кнопки из-за стола. Закрыто, приток, проветривание, открыто настежь: шторке не нужны сорок семь градусов, ей нужны позиции-щелчки, поэтому и скетч здесь построен не вокруг шагов ±15°, как в уроке про пульт и серво, а вокруг четырёх жёстких предустановленных углов. Полный ход от закрыто до настежь — 135°, при периоде тика 15 мс это ровно две секунды: рука успевает моргнуть, но глаз уже видит спокойное движение, а не молниеносный выстрел.
Что делаем: сначала сканируем коды своего пульта, потом раскладываем их по switch, и каждая кнопка назначает серво цель из набора 0-45-90-135. Между командами серво едет плавно, шагом один градус по таймеру на millis() — без рывков, которые у шторки на тросе превращаются в треск кулисы. Тот же каркас закрывает заслонку вентиляции теплицы, задвижку кормушки и шлагбаум из фанеры: меняются углы и механика на валу, код остаётся.
Ловушка номер один у меня была отформулирована на собственной шкуре: скетч с чужими кодами молчит так убедительно, что полчаса подозреваешь приёмник. Поэтому порядок жёсткий — сначала скан, потом логика. Сканер на десять строк и вся теория посылок лежат в уроке про приём кодов; здесь я повторю ровно то, что нужно для серво, и двинемся дальше.
Скан кодов: у каждого пульта своя азбука
Пульт шлёт посылку по протоколу NEC: байт адреса устройства, байт команды и инверсии обоих. Библиотека IRremote отдаёт декодированное сырьё целиком — IrReceiver.decodedIRData.decodedRawData, это тридцать два бита, где старшая половина адрес, а младшая команда с инверсией. Для собственных пультов хватает младших восьми бит, они же decodedIRData.command, но сырьё честнее для скана: по нему видно и адрес, и то, что пульты одной модели от разных партий шлют разные адреса при одинаковых командах.
Процедура скана занимает пять минут. Заливаете сканер из урока про приём кодов, открываете монитор порта, жмёте каждую кнопку дважды и выписываете таблицу: что нажали, что пришло. У симуляторного пульта CAR MP3 команды короткие — CH- это 70, CH это 71, CH+ это 69, EQ это 9; у настоящего пульта с того же маркетплейса decodedRawData выходит вида 0xFFA25D, и у соседнего экземпляра префикс будет другой. Переписывать коды из чужого гайда — лотерея, в которой я ровно один раз выиграл и больше не играю. Строка сканера в мониторе выглядит так: нажатие — адрес, команда и их инверсии; в свою таблицу пишите оба столбца: команда идёт в switch, адрес остаётся фильтром от чужих пультов в комнате.
Две вещи скан вспоминает заодно. Первая: после каждого decode() нужен IrReceiver.resume(), иначе приёмник отдаст одну посылку и замолчит — та же грабля, что и в любом ИК-скетче. Вторая: удержание кнопки шлёт повторы каждые 108-110 миллисекунд, и каждый повтор сканер покажет как новую строку. Для позиционного скетча это даже удобно: промахнулись мимо кнопки — следующие повторы всё равно приведут к той же цели.
#include <Servo.h>#include <IRremote.h> Servo zaslonka; // шторка, D10; приёмник на D11 const int POZ_ZAKRYTO = 0; // закрытоconst int POZ_PRITOK = 45; // утренний притокconst int POZ_VENTIL = 90; // проветриваниеconst int POZ_VSE = 135; // открыто настежь int tsel = POZ_ZAKRYTO; // куда едемint tek = POZ_ZAKRYTO; // где стоим, целые градусыunsigned long tHod = 0;const unsigned long T_HOD = 15; // шаг 1 градус каждые 15 мс void setup() { Serial.begin(9600); zaslonka.attach(10); zaslonka.write(tek); IrReceiver.begin(11, ENABLE_LED_FEEDBACK); Serial.println("gotov: CH- 0, CH 45, CH+ 90, EQ 135");} void loop() { if (IrReceiver.decode()) { // коды МОЕГО пульта — у вашего будут другие, сначала скан! switch (IrReceiver.decodedIRData.decodedRawData) { case 0xFFA25D: tsel = POZ_ZAKRYTO; break; // CH- case 0xFF629D: tsel = POZ_PRITOK; break; // CH case 0xFFE21D: tsel = POZ_VENTIL; break; // CH+ case 0xFF906F: tsel = POZ_VSE; break; // EQ } Serial.print("cel="); Serial.println(tsel); IrReceiver.resume(); // отпустили приёмник — без него тишина } if (tek != tsel && millis() - tHod >= T_HOD) { tHod = millis(); if (tsel > tek) tek++; else tek--; zaslonka.write(tek); }}Разбор короткий, потому что хитростей тут мало. Switch по decodedRawData — прямая привязка «код кнопки — позиция», ни одной ветки, которую можно перепутать. Углы живут в четырёх константах в шапке: механика шторки подрешёт их в первую неделю, и править придётся одну строку, а не искать литералы по телу скетча. Цель и текущий градус разделены сознательно: tsel меняет кнопка, tek ведёт таймер хода, и эти две переменные не обязаны совпадать ни в один момент.
Плавный ход — тот самый шаг один градус каждые 15 миллисекунд. Прямой write(tsel) долетел бы до позиции мгновенно, но для шторки на тросе это рывок, а для серво — старт с максимальным током: редуктор разгоняет весь ход разом. С шагом 135° проезжаются за две секунды, и глаз видит спокойную ленту, а не выстрел. Каркас тика я собрал по уроку про шаг по градусу на millis, а почему внутри loop не должно быть ни одного delay — разложено в уроке про millis вместо delay: между тиками скетч продолжает читать пульт без пауз.
write вызывается только при смене целого градуса — библиотека и так гонит импульсы пятьдесят герц, частые записи ничего не добавляют. Период тика — та же ручка скорости, что и шаг: 10 мс дают полный ход 135° за 1,35 с, 20 мс — ленивые 2,7 с. Я остановился на 15 мс как на компромиссе: быстрее глаз уже не различает градусы, медленнее — шторка отвечает с ощутимой задумчивостью. Печать cel= оставлена намеренно: по ней в мониторе видно, как цель щёлкает по кнопкам, а серво догоняет градус за градусом. Если в логе цель прыгает сама, без нажатий — приёмник ловит солнце или галогенку, и это вопрос направления окна, а не кода.
Штора, заслонка, калитка: механика четырёх позиций
Крайние углы подрезаются под механику, а не по красоте чисел. Мои 0 и 135 — рабочие пределы шторки: в физический упор качалке упираться не надо, я оставляю запас в пять градусов, иначе каждое открытие настежь бьёт механизмом о раму. Если серво тянет трос или кулису, угол превращается в ход троса, и тут легко ошибиться в другую сторону: 45° на роге — это совсем не 45 сантиметров троса, передаточное считается по геометрии качалки. Два положения и тумблер я разбирал в отдельном уроке — четыре кнопки это та же задача, только список позиций вырос, и пульт заменил поход к стене. Передаточное кулисы считается на бумаге за минуту: длина плеча качалки на валу серво, плечо рычага на оси шторы, отношение — и угол на роге умножается на него. У моей шторы отношение вышло 0,8, поэтому паспортные 45° превращались в 36° хода троса, и первый комплект углов пришлось поднимать.
Между командами серво можно усыпить detach(): она перестанет гудеть и греться в ожидании, а шторку в крайних положениях подопрут упоры или собственная фрикция кулисы. С вольным рогом, когда его можно повернуть рукой и что при этом учесть, мы разбирались в уроке про detach — для тяжёлой шторы режим не всегда подходит, просевший трос вернёт серво не туда, куда вы её отправили.
Четыре позиции — это уже интерфейс, а не движение. CH- всегда означает «домой», и рука запоминает кнопку за вечер; CH и CH+ — рабочие режимы между ними, EQ — форсаж. Я специально не вешаю на пятую кнопку промежуточный угол: позиций больше пяти с пульта уже не помнишь, и вместо запоминания начинается угадывание. Если нужен плавающий угол — это другой жанр, он живёт в связках с потенциометром и Serial из соседних уроков.
| Кнопка пульта | Команда в симуляторе | Угол | Роль в моей шторке |
|---|---|---|---|
| CH- | 70 | 0° | закрыто: ночной режим, штора лежит на упоре |
| CH | 71 | 45° | утренний приток: окно приоткрыто на ладонь |
| CH+ | 69 | 90° | проветривание: рабочий режим на весь день |
| EQ | 9 | 135° | открыто настежь: проветрить и высушить раму |
В симуляторе пульт лежит рядом с платой и шлёт короткие команды 70-71-69-9, серво на карточке доезжает до цели на глазах. На реальной плате тот же скетч сработает только после подмены констант на коды вашего скана — и это не недоработка, а главный урок главы: код кнопки принадлежит конкретному пульту, как ключ конкретной двери. Повторы при удержании здесь безвредны: пять посылок CH+ подряд пять раз назначат те же 90°, и плавный ход не дрогнет — повторная механика пультов работает на вас, а не против.
Пара слов о том, что в симуляторе не видно. Ход шторки на тросе под нагрузкой отъедает от заданных градусов на пару-тройку единиц — это нормально, трос тянется; крайние позиции я подрезал так, чтобы недоезд не оставлял щели. И ток: SG90 на рывке берёт до 250 мА, и от USB одной платы его хватает; если же шторка тяжёлая и вы поставили MG996R — от USB она не поедет совсем, и это уже предмет следующего урока.
Упражнение на закрепление — верните себе один угол, который пульт не отдаёт: заведите долгое нажатие. Повтор при удержании прилетает каждые 110 мс, и если между посылками скетч успевает засечь паузу длиннее полсекунды, её можно назначить «сверх-командой»: EQ на удержании — до 90° и назад. Второе упражнение — перенести позиции на заслонку вентиляции теплицы: там углы другие, но таблица кодов и скелет скетча не изменятся вовсе. Третье, для реальной комнаты: выключите свет и понажимайте пульт в полной темноте — если серво дёрнется сама, приёмник ловит фоновую засветку, и окно баночки стоит развернуть от источника или прикрыть диафрагмой из чёрной плёнки.
- Код кнопки — собственность конкретного пульта: сначала скан, потом switch
- decodedRawData — 32 бита сырьём (адрес + команда + инверсии), command — младшие 8 бит
- Четыре позиции = четыре константы в шапке: механика правится без троганья логики
- Плавный ход — 1° за тик по millis: при периоде 15 мс ход 90° занимает 1,35 с
- resume() после каждого decode() — иначе приёмник молчит после первой кнопки
- 1. Скетч из гайда переписан под CAR MP3, серво молчит на все кнопки. Первый шаг?
- 2. Что содержит decodedRawData на посылке NEC?
- 3. Почему write зовётся только при смене целого градуса?
- 4. Шаг 1° каждые 15 мс. За сколько серво проедет ход в 90°?
- 5. Что прилетит скетчу, если зажать кнопку на реальном пульте?
Частые вопросы по теме урока
- Скетч из гайда переписан под CAR MP3, серво молчит на все кнопки. Первый шаг?
- Коды кнопок у каждого пульта свои, даже у одинаковых моделей адреса гуляют от партии. Сканер на десять строк снимает вопрос за пять минут. Отсканировать свой пульт и выписать коды
- Что содержит decodedRawData на посылке NEC?
- NEC шлёт адрес, команду и инверсии обоих байтов — всё это и лежит в decodedRawData. Младшие 8 бит дублирует decodedIRData.command. 32 бита: адрес, команда и их инверсии
- Почему write зовётся только при смене целого градуса?
- Servo.h держит импульсы 50 Гц сама: повторная запись того же угла — пустой вызов. Пишем лишь на смене градуса, между тиками loop свободен. Библиотека сама гонит импульсы 50 Гц, лишние вызовы ничего не дают
- Шаг 1° каждые 15 мс. За сколько серво проедет ход в 90°?
- 90 шагов по 15 мс — это 1350 мс. Период тика и есть ручка скорости: 7 мс дадут 0,6 с, 30 мс — почти три секунды. Около 1,35 с
- Что прилетит скетчу, если зажать кнопку на реальном пульте?
- Удержание шлёт повторы примерно каждые 110 мс, библиотека отдаёт каждый как новое декодирование. Позиционному скетчу это безвредно: цель та же. Повторы каждые 108-110 мс как новые декодирования