Когда чайник, уперевшись в необходимость отойти от простой последовательности действий, задаёт на хабре вопрос типа "как сделать вот это?", ему с вероятностью 70% отвечают "погугли конечные автоматы" и 30% "используй finite state machine" в зависимости от страны работодателя профессионала. На следующий вопрос "а как?" отправляют в гугл. Идёт такой чайник, что только закончил мигать светодиодом и вытер пот со лба, что учил в школе немецкий и всю жизнь работал бульдозеристом в этот гугл и видит там статьи типа Википедия про конечные автоматы с формулами и в которых понятны только предлоги. Так как я тоже чайник, но до бульдозера работал программистом 30 лет назад, наступив на множество граблей по мере освоения программирования микроконтроллеров, решил написать эту статью простым языком для начинающих. Итак, задача стоит, например, научить ардуину понимать нажатие кнопки типа клик, нажатие и удержание, то есть, короткий тык в кнопку, длинный и очень длинный. Любой начинающий может сделать это в функции loop() и гордиться этим, но как быть, если кнопок несколько? А если при этом надо не мешать выполнению другого кода? Я написал библиотеку SmartButton для Arduino, чтобы потом не возвращаться к теме кнопок. Здесь же напишу, как она работает. Нам мешает жить функция delay(), я её поборол при помощи МКА и написал свой класс SmartDelay. Я уже сделал новый класс (SmartDelayMs) порождённый от SmartDelay, там всё то же самое, но в миллисекундах. В данной статье я не использую эту библиотеку, просто хвастаюсь. Чтобы не утомлять читателя повторно рассказом о том, как плохо пользоваться функцией delay() и как жить без неё, я рекомендую сначала прочитать мою старую статейку Замена delay() для неблокирующих задержек в Arduino IDE. Я немножко повторю основные тезисы здесь ещё раз ниже по мере написания кода. Так или иначе, но у вас есть объекты. Вы можете называть их кнопками, дисплеями, светодиодными лентами, роботами и измерителем уровня воды в бачке. Если ваш код выполняется не в одну ниточку последовательно, а по каким-то событиям, вы храните состояние объектов. Вы можете называть это как угодно, но это факт. Для кнопки есть, например, состояния "нажата" и "отпущена". Для робота, например: стоит, едет прямо, поворачивает. Количество этих состояний конечно. Ну, в нашем случае, да. Добавим состояния "клик", "нажатие" и "удержание". Уже пять состояний, которые нам надо различать. Причём, интересны нам лишь последние три. Этими пятью состояниями живёт кнопка. В страшном внешнем мире происходят события, которые от кнопки в общем-то никак не зависят: тыкания в неё пальцем, отпускания, удержания на разное время итп. Назовём их событие.  Итак, мы имеем объект "кнопка" у которого конечное количество состояний и на него действуют события. Это и есть конечный автомат (КА). В теории КА список возможных событий называют словарём, а события словами. Я дико извиняюсь, я это проходил 30 лет назад и плохо помню терминологию. Вам же не терминология нужна, правда?  В данной статье мы напишем замечательный код, который будет переводить наш КА из состояния в состояние в зависимости от событий. Он и является собственно машиной конечных автоматов (МКА). Не обязательно всё ваше устройство запихивать в один огромный КА, можно разбить на отдельные объекты, каждый из которых обслуживается своей МКА и даже находится в отдельном файле кода. Многие свои объекты вы сможете поместить на GitHub в виде готовых библиотек ардуины и использовать потом не вникая в их реализацию. Если ваш код не требует переносимости и повторного использования, можно всё лепить в один большой файл, что и делают как раз начинающие. Основой проектирования МКА является таблица. Для кнопки это будет выглядеть вот так: Зачем так сложно? Надо же описать все возможные состояния и учесть все возможные события. События: Время вы можете поставить своё, как вам удобно. Чтобы не менять потом в разных местах цифры, лучше сразу заменить их буквами :) Состояния: Итак, переводим русский язык на язык C++ Обращу внимание, что и события и состояния указаны не как целая переменная типа byte или int, а как enum. Такая запись не столь любима профессионалами так как не даёт экономить биты столь дорогой в микроконтроллерах памяти, но, с другой стороны очень наглядна. При использовании enum нас не заботит каким числом закодирована каждая константа, мы пользуемся словами, а не числами. enum Имя {Константа, Константа, Константа};
Так мы создаём тип данных enum Имя, у которого есть набор фиксированных значений из тех, что в фигурных скобках. На самом деле, конечно же, это числовой целый тип, но значение констант нумеруется автоматически.
Можно указать значения и вручную:
enum Имя {КонстантаА=0, КонстантаБ=25}; Для объявления переменной такого нового типа можно написать так:
enum Имя Переменная; В примере с кнопкой: Давайте уже заполним таблицу. Значком -> я обозначу переход в новое состояние. Для реализации таблицы мы сделаем функцию void doEvent(enum event e). Эта функция получает событие и выполняет действие как описано в таблице. Настало время ненадолго покинуть нашу уютную кнопку и окунуться в ужасный внешний мир функции loop(). Итак, имея под рукой генератор событий, можно приступить к реализации МКА.  На самом деле, события могут генерироваться не только по подъёму уровня сигнала на ноге контроллера и по времени. События могут передаваться одним объектом другому. Например, вы хотите, чтобы нажатие одной кнопки автоматически "отпускало" другую. Для этого достаточно вызвать её doEvent() с параметром Release. МКА — это же не только кнопки. Превышение порога температуры является событием для МКА и вызывает включение вентилятора в теплице, к примеру. Датчик неисправности вентилятора меняет состояние теплицы на "аварийное" и так далее.  Это первое, что приходит в голову начинающему. На самом деле, это неплохой вариант, если в таблице совсем мало заполненных клеток и действия сводятся только к смене состояний.  Плюсы: Минусы: Другая крайность — это таблица переходов или функций. Я про такой подход писал в статье Обработка нажатий кнопок для Arduino. Скрестить ООП и МКА.. В двух словах, вы создаёте для каждой разной клетки таблицы отдельную функцию и делаете таблицу из указателей на них. typedef позволяет определить свой тип данных и частенько этим удобно пользоваться. Например, enum event можно определить так: В программе после этого можно писать: Я определил указатель на функцию, которая принимает один аргумент типа enum event и ничего не возвращает: Так я сделал новый тип данных MKA. Значок & переводится на русский как "адрес" или "указатель". Таким образом, MKA это не значение функции, а указатель на неё. Я его сразу же заполнил указателями на функции, что выполняют действия. Если действия нет, я ставлю "указатель в никуда" NULL. Функция doEvent проверяет указатель из таблицы в координатах "текущее состояние" x "случившееся событие" и если для этого есть указатель на функцию — вызывает её с параметром "событие". Более подробно про указатели и адресную арифметику в языке C можно нагуглить в книжках типа "Язык Си для чайников". Плюсы: Минусы: Минус оказался таким жирным, что я после написания Обработка нажатий кнопок для Arduino. Скрестить ООП и МКА. и первого же применения кода в деле выпилил это всё нафиг. В итоге я пришёл к золотой середине "switch { switch {}}". Самый распространённый, средний вариант — это использование вложенных операторов switch. На каждое событие как case оператора switch надо писать switch с текущим состоянием. Получается длинно, но более-менее понятно. Умный компилятор всё-равно построит таблицу переходов, как описано в плане Б выше, но эта таблица будет в флеше, в коде и не будет занимать столь дорогую нам память переменных. Плюсы: Минусы: На самом деле, можно использовать план-В и план-А, то есть, switch по событиям, но if внутри по состояниям. На мой вкус, switch и на то и на то понятнее и удобнее, если потребуется что-то потом поменять. В табличке мы не учитывали наличие других МКА сосредоточившись на нашем обработчике событий кнопки. На практике же требуется не только радоваться тому, что кнопка работает, но и выполнять другие действия. У нас есть два важных места для вставки хорошего кода: Лучше всего, чтобы не портить красоту, написать свои функции типа offClick() и onClick(), в которых будет обрабатываться уже эти события, возможно, что они передадут его другому МКА :D  Я обернул всю логику работы МКА кнопки в класс C++. Это удобно, так как не отвлекает от написания основного кода, все кнопки работают самостоятельно, мне не надо изобретать имена кучи переменных. Только это совсем другая история и я могу написать, как оформлять ваши МКА в классы и делать из них библиотеки для Arduino. Пишите в комментариях пожелания. В итоге, в теории, после прочтения этой статьи должно появиться желание заменить в любимом ардуиновом скетче свой нечитаемый глючный говнокод на красивый структурированный и с использованием МКА. Обратите внимание, что в функции loop(), которая в нашем случае лишь генерит события, нет ни одной задержки. Таким образом, после приведённого кода можно писать что-то ещё: генерить события для других МКА, выполнять ещё какой-то свой код, не содержащий delay(); Я очень надеюсь, что эта статья помогла вам понять, что такое МКА и как их реализовать в вашем скетче. Сисадмин