Корпус прибора напечатан, и в нём просверлено ровно одно отверстие под кнопку. Меню с кнопками вверх и вниз из соответствующего урока сюда не встанет — там минимум три кнопки, а базовый курсор разобран ещё раньше, в уроке про меню и курсор. Шилд с пятью кнопками на A0 (разбор лестницы резисторов) тоже мимо: габариты. Остаётся одна кнопка — и её хватает на полноценное меню из трёх пунктов с правкой значений.
Кнопка перестаёт быть выключателем и становится каналом, у которого два измерения: факт нажатия и его длина. Короткое — листать пункты. Длинное — войти в правку выбранного. В правке короткие нажатия меняют значение, длинное — выход с записью. Четыре жеста, ноль лишних деталей, автомат из двух состояний. Я пришёл к этому, когда четвёртый провод от кнопок оторвался внутри корпуса на горячем клею, — и с тех пор так делаю на всех приборах с одним экраном и тремя настройками.
Разбор ниже: пороги короткого и длинного, ловушка двойного срабатывания, автоповтор с разгоном для быстрого прогона значения и таймаут, который вытащит меню из правки, даже если кнопка залипла в кармане. Код — на параллельном LCD с раскладкой шилда: движок симулятора исполняет его живьём. Раскладка (8, 9, 4, 5, 6, 7) — паспорт классического шилда: на макетке это шесть проводов, на шилде — вообще ничего паять не надо.
Одна кнопка — три события
Пороги у меня такие: короче 400 мс — короткое, длиннее 900 мс — длинное. Между ними мёртвая зона, где не происходит ничего. Зазор в полсекунды — не перестраховка: рука держит нажатие с разбросом, и без мёртвой зоны попытка войти в правку раза в три заканчивалась случайным листанием. Пороги подбираются под свою кнопку за минуту: Serial печатает длительность каждого нажатия, десять попыток — и числа на столе. Границы зоны проверяю на себе: держу кнопку, глядя на числа, и выбираю пороги так, чтобы длинное не требовало сосредоточенности — интерфейс, который надо ловить, никто использовать не станет.
Дребезг никуда не делся: 20-30 мс подпрыгиваний на замыкании и отпускании, как на счётчике с антидребезгом. Здесь хватает фронта с фильтром по времени: решение принимается по длительности удержания, а не по количеству фронтов. Отдельная механика — автоповтор в правке: держишь кнопку — значение бежит, первые повторы по 300 мс, дальше по 120. Без автоповтора прогон порога с нуля до девяноста — это девяносто нажатий, после которых меню перестаёшь уважать.
Короткое нажатие детектится на отпускании — и только если длинное не сработало. На фронте нажатия его ловить нельзя: в момент касания неизвестно, что задумал палец. Таблица ниже — паспорт жестов, моя шпаргалка при разводке любого нового корпуса; она же годится при переносе меню на другой дисплей, где меняется печать, но не жесты.
| Жест | В меню | В правке значения |
|---|---|---|
| Короткое, до 400 мс | листает пункты по кругу | шаг значения вверх |
| Длинное, от 900 мс | вход в правку пункта | выход с записью значения |
| Удержание после длинного | ничего | автоповтор: 3 шага по 300 мс, дальше по 120 мс |
| Пауза 10 с в правке | ничего | возврат в меню без записи |
Автомат состояний и таймаут
Автомат — два состояния и один таймер. MENU: короткое листает, длинное переводит в EDIT. EDIT: короткое шагает значение, автоповтор ускоряет, длинное выходит с записью, десять секунд бездействия — выход без записи. Никаких delay: все интервалы на millis, иначе автоповтор и таймаут не уживутся в одном loop — это прямо следует из урока про millis вместо delay. Состояние — переменная из двух значений, логика — четыре if. Третий уровень (подменю, правка разряда) — тот же каркас с ещё одним состоянием, на нём стоит вложенное меню настроек.
Две вещи выносят меню за пределы урока. Первая — память: значения пунктов после перезагрузки живут в EEPROM, схема сохранения разобрана в уроке про меню настроек с EEPROM; здесь запись ставится только на выходе длинным нажатием, а не на каждом шаге — ресурс ячеек не бесконечный. Вторая — другой дисплей: те же состояния переносятся на OLED один в один, вкладки на одной кнопке разобраны в отдельном уроке; меняется печать, автомат остаётся.
Про механику нажатий отдельно: пороги и флаги короткого-длинного уже разобраны в этой серии до байтов, здесь они собираются в интерфейс. У автомата отрастает работа на обоих экранах, а таймаут становится страховкой, а не украшением. Если после сборки жесты кажутся вялыми, вернитесь к печать длительностей в Serial: интерфейс начинается с чисел, а не с ощущений.
#include <LiquidCrystal.h>#include <EEPROM.h> LiquidCrystal lcd(8, 9, 4, 5, 6, 7); const int btn = 2;const int led = 12; // горит в режиме правки int item = 0; // 0..2int rejim = 0; // 0 - меню, 1 - правкаint porog = 45; // пункт 1: 0-90, шаг 5int skorost = 3; // пункт 2: 0-5int svet = 1; // пункт 3: 0/1 int prevBtn = HIGH;unsigned long pressT = 0;int longDone = 0;unsigned long repT = 0;int repN = 0;unsigned long bezdeistvie = 0; int znachOf(int i) { if (i == 0) { return porog; } if (i == 1) { return skorost; } return svet;} void znachSet(int i, int v) { if (i == 0) { porog = v; } if (i == 1) { skorost = v; } if (i == 2) { svet = v; }} int shagOf(int i) { if (i == 0) { return 5; } return 1;} int maxOf(int i) { if (i == 0) { return 90; } if (i == 1) { return 5; } return 1;} void printItem(int i) { if (i == 0) { lcd.print("POROG "); } if (i == 1) { lcd.print("SKOROST"); } if (i == 2) { lcd.print("SVET "); }} void pechatMenu() { lcd.setCursor(0, 0); lcd.print("MENYU "); lcd.print(item + 1); lcd.print("/3 "); lcd.setCursor(0, 1); lcd.print("> "); printItem(item); lcd.print(" "); lcd.print(znachOf(item)); lcd.print(" ");} void pechatPravka() { lcd.setCursor(0, 0); lcd.print("PRAVKA: "); lcd.setCursor(0, 1); lcd.print("> "); printItem(item); lcd.print(" "); lcd.print(znachOf(item)); lcd.print(" ");} void shag() { // короткое: листать или шагать if (rejim == 0) { item = item + 1; if (item > 2) { item = 0; } pechatMenu(); } else { int v = znachOf(item) + shagOf(item); if (v > maxOf(item)) { v = 0; } znachSet(item, v); pechatPravka(); } bezdeistvie = millis();} void vhod() { // длинное: войти или выйти с записью if (rejim == 0) { rejim = 1; digitalWrite(led, HIGH); pechatPravka(); } else { rejim = 0; digitalWrite(led, LOW); EEPROM.put(item * 2, znachOf(item)); // запись - только на выходе Serial.print("SAVED P"); Serial.print(item + 1); Serial.print("="); Serial.println(znachOf(item)); pechatMenu(); } bezdeistvie = millis();} void setup() { Serial.begin(9600); pinMode(btn, INPUT_PULLUP); pinMode(led, OUTPUT); lcd.begin(16, 2); EEPROM.get(0, porog); // настройки переживают перезагрузку EEPROM.get(2, skorost); EEPROM.get(4, svet); if (porog < 0 || porog > 90) { porog = 45; } pechatMenu();} void loop() { unsigned long now = millis(); int nowBtn = digitalRead(btn); if (prevBtn == HIGH && nowBtn == LOW) { pressT = now; longDone = 0; } if (nowBtn == LOW && longDone == 0 && now - pressT >= 900) { longDone = 1; repN = 0; repT = now; vhod(); } if (nowBtn == LOW && longDone == 1 && rejim == 1) { unsigned long period = 300; // автоповтор с разгоном if (repN >= 3) { period = 120; } if (now - repT >= period) { repT = now; repN = repN + 1; shag(); } } if (prevBtn == LOW && nowBtn == HIGH) { if (longDone == 0 && now - pressT >= 30) { // короткое на отпускании shag(); } longDone = 0; } prevBtn = nowBtn; if (rejim == 1 && now - bezdeistvie >= 10000) { // таймаут правки rejim = 0; digitalWrite(led, LOW); Serial.println("TIMEOUT - BEZ ZAPISI"); pechatMenu(); }}Порог длинного нажатия в симуляторе вынесен на потенциометр не для красоты: это самый быстрый способ почувствовать цену мёртвой зоны. При 400 мс правка открывается мгновенно, но случайные длинные случаются; при 1200 короткие нажатия испортить трудно, а вход ощущается тугим. Мой рабочий выбор для приборной кнопки — 900 мс: на границе комфорта и предсказуемости.
Для трёх-четырёх настроек каркаса хватает с запасом. Когда пунктов станет больше шести, одной кнопки мало уже не по логике, а по времени: добираться до последнего пункта устанешь, даже с автоповтором. Дальше — либо группировка, либо вторая кнопка. Код при этом не меняется: автомат просто получает ещё один уровень.
Рост автомата стоит планировать заранее, потому что он неизбежен. Первая надстройка — четвёртое состояние после записи: светодиод мигает два раза, пункт подтверждён. Вторая — сервисное меню по секретному жесту: пять коротких нажатий подряд из корня, и автомат получает второй этаж. Обе надстройки ложатся на тот же каркас без delay: состояния в одной переменной, паузы на millis, таймаут продолжает работать страховкой.
Флаги вместо состояний — путь, по которому я ходил и не советую: pressed, editing, saved, timeout — через месяц связка флагов ловит сочетания, которых вы не планировали. Одна переменная rejim со значениями 0-3 держит автомат в узде: любая комбинация либо разрешена, либо недостижима. Это принцип светофора: зелёный и красный одновременно не горят, потому что их не четыре бита, а один номер.
И про кнопку в звонке: короткое и длинное работают и там. Будильник из предыдущего урока ждал отбой со сном — этот автомат даёт ту же грамматику. Собранные вместе, три урока кластера складываются в прибор: часы с будильником, меню настройки на одной кнопке, прогресс долгой калибровки на экране — и ни одного лишнего провода. А одно отверстие в корпусе остаётся одним: панель сверлилась один раз и навсегда.
Один практический момент про пороги и уставшие кнопки. Тактовая кнопка после тысячи нажатий начинает держать контакт дольше: то, что вчера было 350 мс, сегодня идёт 420 — и попадает в мёртвую зону. Если интерфейс стал вялым на старой плате, начните с повторной калибровки порогов: Serial с печатью длительностей всё ещё в скетче, и десять нажатий вернут числа на место.
Отдельный вопрос — очень длинное удержание. Автоповтор не должен бежать вечно: после двадцати повторов я останавливаю бег и жду отпускания, иначе значение улетает в максимум и приходится возвращать его обратно. Тот же предохранитель стоит в пунктах с шагом в пять: девяносто единиц при периоде 120 мс пробегаются за одиннадцать секунд, и пальцу легко проскочить нужное место. Остановка по счётчику — три строки кода, а возвращает управление рукам.
- 1. Кнопку держали 1,2 секунды. Что должен засчитать автомат?
- 2. Зачем между 400 и 900 мс мёртвая зона, где ничего не происходит?
- 3. Кнопка залипла в кармане, пока прибор в режиме правки. Что спасает интерфейс?
- 4. Где правильно детектить короткое нажатие?
- 5. Что даёт автоповтор с разгоном?
- Пороги 400/900 мс с мёртвой зоной между ними: рука держит нажатие с разбросом
- Короткое детектится на отпускании и только если длинное не сработало — иначе двойной жест
- Автоповтор с разгоном: три шага по 300 мс, дальше по 120 мс — прогон значения без 90 нажатий
- Таймаут 10 с возвращает меню из правки: залипшая кнопка не оставляет прибор в EDIT
- Запись настроек — на выходе длинным нажатием, не на каждом шаге: ресурс EEPROM не бесконечный
Частые вопросы по теме урока
- Кнопку держали 1,2 секунды. Что должен засчитать автомат?
- Удержание дольше порога длинного — ровно одно событие LONG. Флаг longDone не даёт отпусканию засчитаться вторым, коротким жестом. Только длинное
- Зачем между 400 и 900 мс мёртвая зона, где ничего не происходит?
- Без зазора нажатие на границе срабатывало бы то как короткое, то как длинное. Мёртвая зона делает жест предсказуемым ценой полсекунды ожидания. Чтобы не гадать на границе: рука держит нажатие с разбросом, зона съедает неопределённость
- Кнопка залипла в кармане, пока прибор в режиме правки. Что спасает интерфейс?
- Через десять секунд без новых событий автомат выходит из правки, не сохранив случайные шаги. Залипание превращается из поломки в неловкий эпизод. Таймаут бездействия с возвратом в меню без записи
- Где правильно детектить короткое нажатие?
- В момент касания намерение неизвестно: палец может собирался на длинное. Поэтому короткое — только на отпускании и при погашенном флаге longDone. На отпускании, если удержание не дотянуло до длинного и длинное не сработало
- Что даёт автоповтор с разгоном?
- Первые три повтора по 300 мс дают точное попадание в соседнее значение, дальше период 120 мс — прогон больших значений без девяноста нажатий. Мелкие шаги в начале, быстрый бег при удержании — как в настройке времени на наручных часах