Уходя из мастерской, я делаю одно и то же: гашу лампу над столом, опускаю шторку на окне и выключаю вытяжной вентилятор. Три прибора, три пульта, три раза поискать глазами выключатель в темноте. Когда третья вещь превратилась в ритуал, я перестал терпеть: одна кнопка пульта — и цепочка исполняется сама. Свет гаснет, серводвигатель прикрывает шторку, реле отщёлкивает вентилятор. Обратно — зеркальная цепочка с другой кнопки. Это и есть макрос-кнопка: пульт как пультовый рубильник «ушёл — пришёл».
Кирпичи по отдельности раздел уже клал: сценарий ночника с циклами по кнопкам — в уроке про сцены циклом, а команды реле, хранимые во флеш-памяти, — в отдельном уроке, к нему вернёмся ниже. Здесь я собираю из кирпичей то, чего там не было: конечный автомат, который держит цепочку шагов на millis и при этом не перестаёт слушать пульт — макрос можно прервать другой кнопкой в любой момент. Форму автоматов с состояниями и переходами раздел брал в уроке про автомат состояний, и моя переменная шага — прямой её родственник.
Единственное жёсткое правило урока произнесу сразу: в цепочке не должно быть ни одного delay. Макрос на delay() выглядит рабочим первые две минуты и умирает ровно тогда, когда нужен — в момент, когда вы хотите его прервать. Почему — ниже, с числами.
Автомат макроса: шаги по millis
Устройство умещается в три переменные. Номер шага: минус один — макрос спит, ноль и выше — идёт исполнение. Время входа в шаг: millis() на момент, когда шаг начался. И окно шага: 500 миллисекунд, между действиями. Loop крутится без задержек; если макрос спит, автомат молчит и пропускает пульт как ни в чём не бывало. Услышал стартовую кнопку — записал номер шага и время. Идёт макрос, и время входа просрочено на 500 мс — исполнил следующий шаг, снова записал время. Три действия — три шага, четвёртый срабатывает как закрытие: автомат объявляет макрос исполненным и уходит в спячку.
Почему именно конечный автомат, а не список с задержками: шаг — это состояние, а не позиция в очереди, и состояние можно менять из любой точки loop. Прервать, заморозить, перезапустить, добавить шаг посреди цепочки — всё это правки одной переменной, а не переписывание структуры вызовов. А ещё автомат честно переживает каждую итерацию loop, и в каждую итерацию decode() успевает спросить эфир: приёмник не перестаёт работать ни на один миллисекунд макроса. Именно поэтому серводвигатель из урока про плавный серво-шаг встаёт в цепочку без конфликтов — та же дисциплина millis, тот же живой loop.
Экономику loop стоит прочувствовать числами. Пустой loop на Uno крутится десятки тысяч раз в секунду; даже нагруженный decode() оставляет сотни опросов эфира на каждый шаг макроса. Против этого фона один шаг — микроскопическая работа: digitalWrite, пара байт в Serial, запись millis() — и автомат снова спит до следующего окна. То есть 500 мс между шагами — не пауза, а 500 мс непрерывного слушания эфира, и кнопка прерывания ловится не «между шагами», а в любой микросекунде макроса. Когда вы в следующий раз соблазнитесь delay(500) — помните, что вы покупаете не паузу, а глухоту ценой полсекунды за каждый шаг.
Про числа. Окно 500 мс — не догма, а старт: реле щёлкает за 10 мс, лампе хватит 50, серводвигателю на 90 градусов комфортно 300-500. Мой макрос из трёх шагов длится полторы секунды — между «нажал» и «всё закрыто». Удлините окна до секунды — получите театральную паузу; сожмите до 100 мс — реле и серво начнут щёлкать вразнобой, и на слух это выглядит поломкой, хотя электрически всё честно.
// Макрос-кнопка: EQ запускает цепочку, VOL- прерывает в безопасное#include <IRremote.hpp>#include <Servo.h> const int IR_PIN = 2;const int LED_PIN = 12; // "свет" над столомconst int SERVO_PIN = 9; // шторка окнаconst int RELAY_PIN = 7; // вентилятор вытяжки const int CMD_MACRO = 9; // EQ - макрос "ушёл"const int CMD_ABORT = 7; // VOL- - стоп, всё назадconst unsigned long STEP_MS = 500; Servo damper;int step = -1; // -1: спит; 0..3: номер шагаunsigned long stepAt = 0; void setup() { pinMode(LED_PIN, OUTPUT); pinMode(RELAY_PIN, OUTPUT); damper.attach(SERVO_PIN); damper.write(0); Serial.begin(9600); IrReceiver.begin(IR_PIN, ENABLE_LED_FEEDBACK); Serial.println("makros gotov: EQ - start, VOL- - abort");} void loop() { if (IrReceiver.decode()) { int cmd = IrReceiver.decodedIRData.command; if (cmd == CMD_MACRO && step == -1) { // старт только из покоя step = 0; stepAt = millis(); Serial.println("makros 'ushel': start"); } else if (cmd == CMD_ABORT && step != -1) { // прерывание в любой момент digitalWrite(RELAY_PIN, LOW); digitalWrite(LED_PIN, LOW); damper.write(0); step = -1; Serial.println("abort: vse v bezopasnoe sostoyanie"); } IrReceiver.resume(); return; // кадры обрабатываем сразу } if (step != -1 && millis() - stepAt >= STEP_MS) { if (step == 0) { digitalWrite(LED_PIN, LOW); Serial.println("shag 1/3: svet gasim"); } else if (step == 1) { damper.write(90); Serial.println("shag 2/3: shtorka opuskaetsya"); } else if (step == 2) { digitalWrite(RELAY_PIN, LOW); Serial.println("shag 3/3: ventilyator vyklyuchen"); } else { step = -1; Serial.println("makros zavershen"); return; } step++; stepAt = millis(); }}| Кнопка | Макрос | Шаги по 500 мс |
|---|---|---|
| EQ | «ушёл» | свет гаснет → серво 90°, шторка опущена → реле выкл, вентилятор стоит |
| OK | «пришёл» | реле вкл → серво 0°, шторка поднята → свет зажигается |
| CH | «проветрить» | серво 45°, шторка приоткрыта → реле вкл → серво 0°, шторка закрыта |
| VOL- | стоп | немедленно: реле выкл, серво 0°, свет выкл — безопасное состояние |
Прерывание макроса другой кнопкой
Прерывание — это не «остановить и забыть», а «остановить и вернуть всё в безопасное состояние». Смысл в порядке действий: реле выключить первым — вентилятор не должен крутиться, пока серводвигатель ползёт к нулю; серводвигатель вернуть в открытое положение; свет — по вкусу, в моей мастерской он гаснет: abort-кнопка означает «я передумал уходить» только наполовину. Если в цепочке серво двигается плавно по градусам, а не одним рывком, прерывание обязано вернуть рог тем же плавным шагом — иначе механизм шторки получает удар на скорости, которую вы ему не рассчитывали.
Вторая семантика — защита от двойного старта. В скетче старт принимается только из состояния покоя: второе нажатие EQ во время макроса игнорируется. Без этой проверки бегущий макрос перезаписывает stepAt на середине, серводвигатель замирает на полпути, а вы ищете плавающий глюк, которого в коде формально нет. Если макросов становится несколько — EQ «ушёл», OK «пришёл», макрос-сцены для чтения из урока про сценарии ночника, — держите один автомат и одну переменную шага на всех: два параллельных автомата на одних реле дерутся за пины и ломают друг другу состояние, а один автомат просто выбирает таблицу шагов по номеру кнопки.
Безопасное состояние стоит записать один раз и держать одной функцией, а не копипастить в ветки: «реле выкл, серво в исходный угол, свет по вкусу». Вызывается она из трёх мест — прерывание, завершение макроса и setup(). Третий пункт — тот, о котором забывают: плата могла перезагрузиться посреди макроса, и после восстановления питания цепочка не «продолжится» — она просто не начнётся, зато ни одно реле не зависнет включённым на весь рабочий день. Три строки в setup(), которые дешевле любого разговора с электриком, — это тот же принцип, что таймаут тишины у робота из первого урока: штатное поведение при нештатной ситуации продумывается заранее, а не после первого случая.
Что делать, когда шагов становится двадцать? Таблица шагов уезжает во флеш-память — массив пар «действие, задержка» в PROGMEM, и автомат читает её по индексу, роняя расход ОЗУ до нуля; раскладка разобрана в уроке про команды реле и PROGMEM.
Симулятор даёт полную картину автомата, потому что все три исполнителя у него на борту: светодиод вспыхивает на первом шаге, серводвигатель уезжает на 90 градусов на втором, реле отщёлкивает на третьем, и всё это с живым логом номеров шагов. Единственное, чего в симе не видно, — та самая причина, ради которой всё затевалось: настоящая шторка на настоящем серводвигателе опускается с шорохом, и шаг в 500 мс подобран под этот шорох, а не под скорость библиотеки. На реальной плате проверьте окна руками: если реле щёлкает раньше, чем серводвигатель дошёл до угла, окно первого шага не трогайте — расставьте порядок шагов так, чтобы медленный исполнитель шёл первым.
Когда цепочек станет несколько, окна перейдут из константы в данные: шаг с серводвигателем получит свои 700 мс, реле — 200, свет — 100, и таблица шагов превратится в пары «действие, окно». Автомат при этом не меняется ни на строчку: меняется только то, что он читает. Дальше можно обучать цепочки на лету, записывая пойманные кнопки в EEPROM, как в уроке про обучаемый пульт, — я дошёл до этого на второй зимовке мастерской, когда цепочек стало четыре, а править скетч под каждое новое реле надоело.
Отдельно про выбор кнопки-стартера — мелочь, которая решает удобство на годы. Макрос запускается в темноте, на ходу, без взгляда на пульт, поэтому кнопка должна нащупываться пальцем вслепую: у моего пульта это крупные VOL и стрелки каналов, а мелкие цифры в центре вслепую ловятся через раз. Стартовые кнопки держите на краях панели, стоп — под большим пальцем, и не назначайте макрос на кнопки, которыми пользуетесь в остальных приборах: одна ложная громкость с соседнего пульта — и мастерская внезапно проветривается. Раскладку я пару раз перебирал, и каждый перебор занял меньше минуты, чем один поиск заветной кнопки в темноте.
- 1. Почему шаги макроса нельзя делать через delay()?
- 2. Что хранит автомат макроса между шагами?
- 3. Как правильно прервать макрос другой кнопкой?
- 4. Макрос из трёх шагов с окном 500 мс. Сколько длится от нажатия до конца?
- 5. Шагов в макросе стало двадцать. Что делать со списком действий?
- Макрос-кнопка — конечный автомат: номер шага и millis() входа, окно 500 мс, спячка под минус единицей.
- Ни одного delay в цепочке: они делают приёмник глухим, а кнопку прерывания — бесполезной до конца макроса.
- Прерывание = безопасное состояние немедленно: реле выкл первым, серво к исходному углу, шаг в минус один.
- Защита от двойного старта: EQ во время макроса игнорируется, иначе шаги перезаписываются на середине.
- Шаги растут — таблица уезжает в PROGMEM; один автомат на все цепочки, прерывание общее.
Частые вопросы по теме урока
- Почему шаги макроса нельзя делать через delay()?
- За миллисекунды delay() loop не спрашивает эфир, resume не срабатывает, кадры падают мимо. Макрос на delay() глух весь, и прервать его нельзя — кнопка стопа дожидается конца цепочки в очереди. во время delay() приёмник не вызывается: кадры теряются, кнопка прерывания мертва до конца цепочки
- Что хранит автомат макроса между шагами?
- Две переменные: номер шага (минус один — покой) и stepAt. Разность millis() - stepAt против окна 500 мс решает, пора ли исполнять следующий шаг. номер текущего шага и millis() момента входа в него
- Как правильно прервать макрос другой кнопкой?
- Прерывание — это «остановить и вернуть в безопасное»: реле выкл первым, серво к исходному углу, шаг в минус один. Ждать конца цепочки — значит, что кнопка стопа не работает как стоп. немедленно вернуть все пины в безопасное состояние и обнулить шаг
- Макрос из трёх шагов с окном 500 мс. Сколько длится от нажатия до конца?
- Три шага по 500 мс — полторы секунды исполнения. Окно подобрано под медленного исполнителя: реле щёлкает за 10 мс, серво на 90 градусов комфортно проезжает за 300-500 мс. полторы-две секунды: три шага по 500 мс
- Шагов в макросе стало двадцать. Что делать со списком действий?
- Таблица в PROGMEM хранит пары «действие, задержка» во флеш-памяти, автомат читает по индексу — ОЗУ остаётся свободным. Двадцать delay() подряд снова делают макрос глухим. уехать в PROGMEM таблицей «действие + окно» и читать по индексу