Аквариум на 120 литров у родителей прожил зиму на моём терморегуляторе, и каждые две недели мне звонили с одной и той же просьбой: подвинь порог на полградуса. Меню к тому моменту умело включать и выключать нагреватель, свет и компрессор, но любое число было зашито в скетч, а компромисс между комфортом рыб и счётом за электричество искался только наощупь. Одноуровневый каркас из урока про меню на двух кнопках честно переключал ON/OFF, а про числа в нём было ровно два слова — и оба не подходили. Настройки надо было пустить внутрь пунктов, и по-прежнему на двух кнопках.
Сегодня достраиваем второй уровень: меню и редактор значения в одном скетче. Разберём, почему это конечный автомат с двумя состояниями, как одна и та же кнопка становится то «листать», то «изменить», и какие границы нужны числам, чтобы пульт однажды не сварил аквариум. Дисциплина та же, что в уроке про конечный автомат на светофоре, только состояния называются «меню» и «редактор». Если после этого урока вы захотите третий уровень — меню в меню, — держите в голове, что всё сказанное ниже честно масштабируется и туда, просто переменная режимов станет числом.
Два состояния: меню и редактор
Ключ к вложенности — одна переменная bool editing. Пока она false, кнопки живут в режиме меню: D2 листает пункты, D3 входит в выбранный. Нажали D3 — editing стал true, и те же кнопки сменили профессию: D2 теперь прибавляет значение на шаг, D3 сохраняет и возвращает в меню. Никакой магии и никаких рекурсий: в каждый момент времени скетч находится ровно в одном состоянии, и каждая кнопка знает, что ей делать в этом состоянии. Это и есть конечный автомат в прикладном виде — два состояния и два перехода, умещающиеся в один if. Формально автомат — это список состояний плюс правила перехода между ними, и наша пара «меню/редактор» тому живой пример.
В редакторе экран должен сам подсказывать пользователю, где тот находится. Мой набор из двух приёмов: курсор выбранного пункта меняется с «>» на «=», а вторая строка показывает допустимый диапазон — например, «(10..40)». Пользователь видит не только текущее число, но и рамку, в которой оно живёт: насколько далеко до края, куда крутить дальше. Границы храню массивами рядом со значениями: mins[3] и maxs[3] — как и сами values[3], это один массив на всю систему настроек, а не россыпь переменных, которую придётся штопать при добавлении четвёртой настройки. Диапазоны подбираю под железо, а не «от балды»: порог от 10 до 40 градусов, потому что ниже аквариум замёрзнет, выше — закипит, а гистерезис от 0 до 5, потому что больше пяти реле начнёт щёлкать раз в час.
int values[3] = {24, 1, 10}; // порог, гистерезис, времяint mins[3] = {10, 0, 1}; // нижние границыint maxs[3] = {40, 5, 60}; // верхние границыint item = 0;bool editing = false;int prevNext = HIGH;int prevOk = HIGH; void loop() { int n = digitalRead(btnNext); int ok = digitalRead(btnOk); if (n == LOW && prevNext == HIGH) { // фронт нажатия if (editing) { values[item]++; if (values[item] > maxs[item]) values[item] = mins[item]; } else { item++; if (item > 2) item = 0; } drawMenu(); delay(20); // срезаем дребезг } if (ok == LOW && prevOk == HIGH) { editing = !editing; // вход и выход drawMenu(); delay(20); } prevNext = n; prevOk = ok; delay(10);}Заметьте: сохранение в этой схеме бесплатное. Значение лежит в values[item] с первого же нажатия, и выход из редактора ничего не пересчитывает. Кто-то предпочитает явное «подтвердить» с временной копией — мне в простых пультах она не пригождалась, зато лишний переход — это лишнее место, где пользователь запутается. Моё мнение с границей: для настроек уровня «порог, гистерезис, время» прямая запись — самое честное поведение; но когда одно значение завязано на другое (гистерезис не может быть больше половины диапазона), без временной копии и проверки при выходе уже не обойтись.
Теперь границы, потому что это не эстетика, а безопасность. Порог без рамки однажды станет двести: зажатая кнопка в редакторе пробегает значения с пугающей скоростью — при шаге 1 и delay(20) до пятидесяти единиц в секунду удержания. Верхняя и нижняя границы, записанные массивом, закрывают вопрос раз и навсегда: значение либо упирается в стенку, либо ходит по кругу. Я выбрал круг — он не требует второй кнопки «в минус» и не запирает пользователя у края. И последнее: drawMenu() всегда начинается с clear(), поэтому старые цифры не живут на экране призраками после выхода из редактора; если увидите на своём пульте «наложение» двух чисел — вы забыли именно этот вызов.
Тренажёр: настройки внутри пунктов
В тренажёре — меню из трёх настроек: порог, гистерезис, время. Кнопка на D2 в меню листает пункты, в редакторе прибавляет значение; кнопка на D3 входит в пункт и выходит с сохранением. Диапазоны прописаны честные: порог ходит от 10 до 40, гистерезис от 0 до 5, время от 1 до 60. Сценарий: войдите в «Porog», накрутите значение на три-четыре шага, посмотрите на строку диапазона внизу, выйдите кнопкой D3 — и убедитесь, что в меню пункт показывает уже новое число. Затем проверьте круг: значение, дошедшее до верхней границы, должно перескочить на нижнюю.
Когда пунктов-настроек становится много, у двух строк 16x2 наступает предел: видно текущий и следующий пункт, а остальное — только по кругу. Это не баг, а ограничение формата, и бороться с ним стоит не третьим уровнем вложенности, а переносом на экран побольше или группировкой настроек в смысловые пары. Второй предел — человеческий: три-пять настроек пользователь ещё помнит, больше — нужны подсказки прямо на экране. И самое частое письмо, которое я получаю про пульты, звучит так: «после выхода из редактора на экране призраки старых цифр». Ответ всегда один: clear() перед перерисовкой, и призраки исчезают.
Куда расти. Подсветка, гаснущая по бездействию, делает пульт законченным — механика разобрана в уроке про подсветку и сон: кнопка будит экран, таймер гасит, бюджет тока вздыхает спокойно. А если нужна и скорость листания, и точность, следующий шаг — энкодер: у нас есть разбор EC11 с прерываниями, там одна ручка заменяет обе кнопки, и меню листается как настройка радиоприёмника. Мой инкубаторный пульт в итоге дожил именно до такой версии: ручка листает и крутит, кнопка одна — вход-выход, и родителям больше не нужно звонить.
| Элемент | В меню (editing = false) | В редакторе (editing = true) |
|---|---|---|
| Кнопка D2 | Листает пункты по кругу | Прибавляет значение; через максимум — на минимум |
| Кнопка D3 | Входит в пункт, включает редактор | Сохраняет и возвращает в меню |
| Экран | Курсор «>», внизу следующий пункт | Курсор «=», внизу диапазон (мин..макс) |
- Добавьте четвёртую настройку со своим диапазоном: строки в values, mins и maxs, ветка в printName — и проверьте круг на новой границе.
- Сделайте шаг порога равным пяти: значения пойдут 10, 15, 20 — и убедитесь, что граница 40 не проскакивается.
- Запретите гистерезис больше половины порога: при выходе из редактора сравните значения и верните нарушителя.
- Заведите пункт Sbros с массивом-копией заводских значений из note-блока и проверьте возврат после самого варварского прогона.
- Вложенное меню — конечный автомат из двух состояний: переменная editing переключает профессии обеих кнопок
- В редакторе экран подписывает себя: курсор «=» вместо «>» и строка диапазона (мин..макс) внизу
- У каждой настройки есть границы в массивах mins и maxs: значение ходит по кругу и не уходит в аварию
- Сохранение бесплатное: значение живёт в массиве с первого нажатия, выход из редактора ничего не пересчитывает
- Для жизни без питания настройки пишут в EEPROM, а пункт «сброс к заводским» стоит одной строки и спасает пульт
- 1. Что в скетче урока переключает профессии кнопок?
- 2. Почему у каждой настройки должны быть минимальное и максимальное значения?
- 3. Что показывает вторая строка экрана в режиме редактора?
- 4. Что происходит со значением при выходе из редактора кнопкой D3?
- 5. Сколько перезаписей выдерживает ячейка EEPROM Arduino?
Частые вопросы по теме урока
- Что в скетче урока переключает профессии кнопок?
- bool editing — единственный переключатель: false означает меню, true — редактор, и кнопки читают это состояние в каждом цикле. Переменная editing
- Почему у каждой настройки должны быть минимальное и максимальное значения?
- При шаге 1 и delay(20) удержание меняет значение до пятидесяти раз в секунду: без рамки порог однажды станет двести, а аквариум — супом. Зажатая кнопка пробегает значения за секунды, и число может уйти в аварию
- Что показывает вторая строка экрана в режиме редактора?
- Строка вида (10..40) подсказывает пользователю рамку значения, пока курсор пункта сменился с «>» на «=»: где вы, видно без документации. Допустимый диапазон значения
- Что происходит со значением при выходе из редактора кнопкой D3?
- Редактор меняет значения напрямую в массиве, поэтому «сохранение» не требует ни копий, ни пересчётов: выход лишь переключает экран обратно. Ничего: оно лежало в values с первого нажатия
- Сколько перезаписей выдерживает ячейка EEPROM Arduino?
- Порядка ста тысяч циклов на ячейку: настройка, которую меняют пару раз в месяц, переживёт и плату, и автора урока. Около 100 000