Пару месяцев назад я купил не сильно новый мотоцикл KTM 250EXC, открутил ручку газа в горку, моту пульнул в небо, а сам сел на задницу и что-то там сломал в спине. В результате, на мотоцикл не сесть два месяца как минимум. К чему я это? Да. У немного подуставшего мопеда оказалась неисправная приборная панель и я собрался, пока лежу дома, сделать самодельную новую свою.
Быстро собрал макет, циферки бегают, часики ходят, одометры запоминаются в FRAM — красота, но… понадобились кнопочки для управления этой красотой.
Сегодня с расскажу про кнопочки, потом про датчик зажигания, а уже потом про саму приборку, Ладно?
Рисовать на китайском экране 16х2 через i2c просто, датчики скорости и оборотов мотора сели на внешние прерывания, температура читается с аналогового порта, инфа хранится в FRAM, ну и часики тоже китайские воткнуты. Всё это крутится асинхронно примерно как SmartDelay, про который писал недавно здесь.
Да, кнопочки!
Сделать одну кнопку для притормаживания мигания светодиода оказалось легко, как и прочие игрушки. Прилепить же огромную клавиатуру к приборной панели мотоцикла эндуро не получится, нет места. Пришлось поломать голову и ограничиться четырьмя кнопками:
- Режим
- Вверх
- Вниз
- ОК/Сброс
Чтобы вписать в это и меню и управление, надо распознавать тык, тыыык и тыыыык. То есть нажатия на кнопки разной длительности. Я написал большую портянку из switch и if, понял, что прочитать это через пару месяцев я не смогу и взялся снова за плюсы.
Задача оказалась похожа на библиотеку SmartDelay:
- Максимально спрятать код в библиотеку.
- Код обработки кнопок не должен мешать программировать «по делу».
- Должно быть возможно использовать ещё где-то и в других последующих проектах.
- Должно быть красиво, что ли.
Если вы знаете разное похожее, пожалуйста, сообщите в комментариях, чем вы пользуетесь.
Сначала я бумаге нарисовал конечный автомат. С налёту не получилось, без бумаги.
Потом я прогуглил, что можно вместо switch/if сделать табличкой. Я последний раз обращался к теме МКА где-то лет 30 назад, понадобилось освежить в памяти теорию.
В результате я родил абстрактный класс SmartButton. Данное творение прячет внутри себя МКА, слушает цифровые порты и дёргает пустые абстрактные методы на клик, удержание и долгое удержание. Для использования этого класса надо создать свой и переопределить нужные методы.
#include <SmartButton.h>
byte menuMode = 0;
// Новый класс из SmartButton
class modeSmartButton: public SmartButton {
public:
modeSmartButton(int p) : SmartButton(p) {}
virtual void onClick(); // Методы для использования
virtual void offClick(); // В данном случае, лишь два.
};
// Действие на клик: переключаем некий режим меню.
void modeSmartButton::onClick() {
Serial.println("Key pressed.");
if (menuMode) {
Serial.println("Menu mode off.");
} else {
Serial.println("Menu mode on.");
}
menuMode^=1;
}
// Действие на отпускание кнопки после клика. Ничего не делаем.
void modeSmartButton::offClick() {
Serial.println("Key depressed.");
}
// Собственно объект, кнопка на 6 ножке ардуины.
modeSmartButton btMode(6);
void setup() {
Serial.begin(9600);
Serial.println("Ready");
}
void loop() {
btMode.run(); // это должно быть в loop().
}
Видно, что кода чуть, всё более-менее понятно. Нет колбеков прямо вот так явно описанных. В loop() есть только один вызов run() для каждой кнопки, где-то определяется класс и сама кнопка. Можно творить, страшные лестницы МКА для обработки тыков кнопок с стиле C не мешают.
Давайте посмотрим в код. Весь проект лежит на гитхабе.
Не придумав ничего лучше, я сделал доступными настройки временных интервалов снаружи. Вот, соответственно, задержки для клика, удержания, долгого удержания и настолько долгого, что стоит проигнорировать такое нажатие вообще. В SmartButton.h определил эти константы осторожно так, чтобы их можно было переопределить до #include.
#ifndef SmartButton_debounce
#define SmartButton_debounce 10
#endif
#ifndef SmartButton_hold
#define SmartButton_hold 1000
#endif
#ifndef SmartButton_long
#define SmartButton_long 5000
#endif
#ifndef SmartButton_idle
#define SmartButton_idle 10000
#endif
Состояния и воздействия я сделал как enum в частности и потому, что автоматом получил их количества StatesNumber и InputsNumber.
enum state {Idle = 0, PreClick, Click, Hold, LongHold, ForcedIdle, StatesNumber};
enum input {Release = 0, WaitDebounce, WaitHold, WaitLongHold, WaitIdle, Press, InputsNumber};
Слегка поломав голову, нарисовал вот такой тип. Это указатель на метод этого класса. Не смейтесь, плюсы как-то мимо меня прошли, я в них не мастер.
typedef void (SmartButton::*FSM)(enum state st, enum input in);
Вот здесь пришлось повозиться. Это таблица переходов. Возня была со ссылками на методы, как их написать так, чтобы и компилятор не ругался и ссылки были на методы конкретного экземпляра класса. Не на статический метод, не просто левую функцию, а именно на метод, чтобы он имел доступ к приватным переменным класса.
FSM action[StatesNumber][InputsNumber] = {
{NULL, NULL, NULL, NULL, NULL, &SmartButton::ToPreClick},
{&SmartButton::ToIdle, &SmartButton::ToClick, NULL, NULL, NULL, NULL},
{&SmartButton::ToIdle, NULL, &SmartButton::ToHold, NULL, NULL, NULL},
{&SmartButton::ToIdle, NULL, NULL, &SmartButton::ToLongHold, NULL, NULL},
{&SmartButton::ToIdle, NULL, NULL, NULL, &SmartButton::ToForcedIdle, NULL},
{&SmartButton::ToIdle, NULL, NULL, NULL, NULL, NULL}
};
Все методы были объявлены как private, а в public остались лишь run() и пустые заглушки для переопределения в порождённых классах.
inline virtual void onClick() {}; // On click.
inline virtual void onHold() {}; // On hold.
inline virtual void onLongHold() {}; // On long hold.
inline virtual void onIdle() {}; // On timeout with too long key pressing.
inline virtual void offClick() {}; // On depress after click.
inline virtual void offHold() {}; // On depress after hold.
inline virtual void offLongHold() {}; // On depress after long hold.
inline virtual void offIdle() {}; // On depress after too long key pressing.
Я использую режим pinMode(pin,INPUT_PULLUP) так как схема собрана под это, но в ближайшее время собираюсь добавить возможность выбора режима.
Метод run() просто переводит временные интервалы во входные воздействия КА.
void SmartButton::run() {
unsigned long mls = millis();
if (!digitalRead(btPin)) {
if (btState == Idle) {
DoAction(btState, Press);
return;
}
if (mls - pressTimeStamp > SmartButton_debounce) {
DoAction(btState, WaitDebounce);
}
if (mls - pressTimeStamp > SmartButton_hold) {
DoAction(btState, WaitHold);
}
if (mls - pressTimeStamp > SmartButton_long) {
DoAction(btState, WaitLongHold);
}
if (mls - pressTimeStamp > SmartButton_idle) {
DoAction(btState, WaitIdle);
}
return;
} else {
if (btState != Idle) {
DoAction(btState, Release);
return;
}
}
}
Приватный же метод DoAction(состояние, воздействие) просто вызывает функцию из таблицы, если там есть адрес.
void SmartButton::DoAction(enum state st, enum input in) {
if (action[st][in] == NULL) return;
(this->*(action[st][in]))(st, in);
}
Большинство действий выглядят достаточно просто. Там просто устанавливается состояние и вызывается абстрактный метод, который может переопределиться в порождённом классе. Это такой аналог колбека.
void SmartButton::ToClick(enum state st, enum input in) {
btState = Click;
onClick(); // Вот это аналог колбека в плоском С.
}
Самый жирный обработчик получился для состояния Idle потому, что в него приходят из разных других состояний, а сделать абстрактные методы для таких событий хотелось.
void SmartButton::ToIdle(enum state st, enum input in) {
btState = Idle;
switch (st) {
case Click: offClick(); break;
case Hold: offHold(); break;
case LongHold: offLongHold(); break;
case WaitIdle: onIdle(); break;
}
}
С таким инструментом я уже готов порождать классы для упомянутых в начале статьи кнопок выбора режима дисплея, навигации вверх и вниз, перегруженной кнопки выбора/сброса.
Понятно, что мне грозит ещё один КА, намного более сложный. Кнопок мало, а действий много. Если интересно, напишу в следующий раз в качестве примера реального практического применения вот только что описанной библиотеки.
Автор: nwwind