Кнопка и конечный автомат состояний

Кнопка кажется простой, но за ней прячется важная идея программирования — конечный автомат состояний.

"Нажата" и "отпущена" — это два состояния. Переходы между ними и есть основа любой логики управления, от лифта до игрового персонажа.

Кнопка подключается между GPIO-пином и землёй. Когда её нажимают, цепь замыкается, и пин читает сигнал. Библиотека gpiozero включает встроенный подтягивающий резистор, чтобы в отпущенном состоянии пин не "болтался".

Ты уже встречал конечные автоматы, просто не знал, что они так называются. Светофор, который циклично меняет цвета. Лифт, который "стоит", "едет вверх" или "едет вниз". Персонаж в игре, который "бежит", "прыгает" или "падает", и не может прыгать дважды в воздухе. Всё это — наборы состояний и правил перехода между ними. Кнопка — самый маленький такой автомат: всего два состояния, "нажата" и "отпущена". Поняв логику на кнопке, ты потом легко масштабируешь её на сложные системы.

Как работает под капотом

   GPIO4 (физ. 7) o------+
                         |
                      [кнопка]
                         |
   GND (физ. 6)    o------+

Что физически происходит, когда ты жмёшь кнопку? Внутри неё две металлические пластинки, которые в покое не касаются друг друга — цепь разомкнута, тока нет. Нажатие соединяет пластинки, цепь замыкается, и пин "видит" соединение с землёй. Тут возникает вопрос: а что читает пин, когда кнопка отпущена и никуда не подключена? Без помощи он читал бы случайный электрический шум — то 0, то 1. Чтобы этого не было, нужен подтягивающий резистор, который "притягивает" пин к известному уровню, когда кнопка не нажата. gpiozero включает его за тебя автоматически.

   отпущена                 нажата
   +-----------+            +-----------+
   | пластины  |            | пластины  |
   | разомкнуты|            | сомкнуты  |
   +-----------+            +-----------+
   цепь разорвана  -->      цепь замкнута
   пин = "не нажата"        пин = "нажата"

На Pi код кнопки тоже про железо, поэтому он идёт как пример для платы, не для браузера:

from gpiozero import Button

button = Button(4)        # GPIO4

button.wait_for_press()
print("Кнопку нажали!")

А вот логику поведения кнопки удобно описать как конечный автомат (state machine): у системы есть состояния, а события (нажатие/отпускание) переводят её между ними. Эту логику можно запустить прямо в браузере. Попробуй сам ▶

# Конечный автомат: лампа переключается по нажатию кнопки
state = "OFF"                       # начальное состояние

# имитируем поток событий с кнопки
events = ["press", "release", "press", "release", "press"]

for event in events:
    if event == "press":
        # на каждое нажатие меняем состояние лампы
        state = "ON" if state == "OFF" else "OFF"
        print(f"Нажатие -> лампа теперь {state}")
    else:
        print(f"Отпускание (состояние {state} не меняется)")

print("Итог:", state)

Заметь: отпускание ничего не меняет — реагируем только на нажатие. Это типичная логика выключателя "нажми-переключи".

Автомат не обязан быть из двух состояний. Усложним пример: пусть кнопка выбирает режим яркости по кругу — выключено, тускло, ярко и снова выключено. Это уже три состояния, но принцип тот же: текущее состояние плюс событие дают новое состояние. Такую логику тоже удобно отладить на чистом Python, прежде чем трогать железо:

# Автомат на три состояния: режим лампы по кругу
order = ["OFF", "DIM", "BRIGHT"]
index = 0

for press in range(5):              # пять нажатий подряд
    index = (index + 1) % len(order)
    print(f"Нажатие {press + 1} -> режим {order[index]}")

Частые ошибки

  • Дребезг контактов. Механическая кнопка при нажатии "дребезжит" — gpiozero сам сглаживает это параметром bouncetime.
  • Реагируют на каждое изменение. Если переключать состояние и на нажатие, и на отпускание — лампа будет вести себя странно.
  • Забыли про подтяжку. Без подтягивающего резистора пин читает случайный шум; gpiozero включает её автоматически.
  • Бесконечный цикл "съедает" процессор. Если опрашивать кнопку вечным циклом без паузы, Pi греется зря — лучше использовать события when_pressed.
  • Путают "нажата сейчас" и "только что нажали". Состояние пина (нажата/отпущена) и событие перехода (момент нажатия) — это разные вещи, и автомат должен реагировать именно на переход.

Best practices

  • Описывай поведение через состояния и переходы — код станет понятнее.
  • Используй события when_pressed вместо вечного цикла опроса, когда возможно.
  • Отлаживай автомат на списке событий, как выше, прежде чем переносить на железо.
  • Храни список состояний в одном месте (список или словарь) — добавить новый режим будет легко.
  • Рисуй автомат на бумаге: кружки-состояния и стрелки-переходы. Картинка часто понятнее кода.

Итоги. Кнопка — это два состояния и переходы между ними, а подтягивающий резистор спасает пин от случайного шума. Класс Button(4) читает нажатие на Pi, а логику переключения удобно моделировать конечным автоматом на чистом Python — хоть на два состояния, хоть на три. Дальше соберём светофор из нескольких светодиодов.

Проверьте себя
1. Что такое конечный автомат состояний?
AРобот
BМодель, где у системы есть состояния, а события переводят её между ними
CТип резистора
DКоманда Linux
2. Что такое дребезг контактов кнопки?
AЗвук кнопки
BКратковременные ложные срабатывания из-за вибрации контактов при нажатии
CПоломка кнопки
DСлишком сильное нажатие