Пару месяцев назад я купил не сильно новый мотоцикл KTM 250EXC, открутил ручку газа в горку, моту пульнул в небо, а сам сел на задницу и что-то там сломал в спине. В результате, на мотоцикл не сесть два месяца как минимум. К чему я это? Да. У немного подуставшего мопеда оказалась неисправная приборная панель и я собрался, пока лежу дома, сделать самодельную новую свою.

Быстро собрал макет, циферки бегают, часики ходят, одометры запоминаются в FRAM — красота, но… понадобились кнопочки для управления этой красотой.
Сегодня с расскажу про кнопочки, потом про датчик зажигания, а уже потом про саму приборку, Ладно?
Рисовать на китайском экране 16х2 через i2c просто, датчики скорости и оборотов мотора сели на внешние прерывания, температура читается с аналогового порта, инфа хранится в FRAM, ну и часики тоже китайские воткнуты. Всё это крутится асинхронно примерно как SmartDelay, про который писал недавно здесь.
Да, кнопочки!
Сделать одну кнопку для притормаживания мигания светодиода оказалось легко, как и прочие игрушки. Прилепить же огромную клавиатуру к приборной панели мотоцикла эндуро не получится, нет места. Пришлось поломать голову и ограничиться четырьмя кнопками:
Чтобы вписать в это и меню и управление, надо распознавать тык, тыыык и тыыыык. То есть нажатия на кнопки разной длительности. Я написал большую портянку из switch и if, понял, что прочитать это через пару месяцев я не смогу и взялся снова за плюсы.
Задача оказалась похожа на библиотеку SmartDelay:
Если вы знаете разное похожее, пожалуйста, сообщите в комментариях, чем вы пользуетесь.
Сначала я бумаге нарисовал конечный автомат. С налёту не получилось, без бумаги.

Потом я прогуглил, что можно вместо switch/if сделать табличкой. Я последний раз обращался к теме МКА где-то лет 30 назад, понадобилось освежить в памяти теорию.

В результате я родил абстрактный класс SmartButton. Данное творение прячет внутри себя МКА, слушает цифровые порты и дёргает пустые абстрактные методы на клик, удержание и долгое удержание. Для использования этого класса надо создать свой и переопределить нужные методы.
Видно, что кода чуть, всё более-менее понятно. Нет колбеков прямо вот так явно описанных. В loop() есть только один вызов run() для каждой кнопки, где-то определяется класс и сама кнопка. Можно творить, страшные лестницы МКА для обработки тыков кнопок с стиле C не мешают.
Давайте посмотрим в код. Весь проект лежит на гитхабе.
Не придумав ничего лучше, я сделал доступными настройки временных интервалов снаружи. Вот, соответственно, задержки для клика, удержания, долгого удержания и настолько долгого, что стоит проигнорировать такое нажатие вообще. В SmartButton.h определил эти константы осторожно так, чтобы их можно было переопределить до #include.
Состояния и воздействия я сделал как enum в частности и потому, что автоматом получил их количества StatesNumber и InputsNumber.
Слегка поломав голову, нарисовал вот такой тип. Это указатель на метод этого класса. Не смейтесь, плюсы как-то мимо меня прошли, я в них не мастер.
Вот здесь пришлось повозиться. Это таблица переходов. Возня была со ссылками на методы, как их написать так, чтобы и компилятор не ругался и ссылки были на методы конкретного экземпляра класса. Не на статический метод, не просто левую функцию, а именно на метод, чтобы он имел доступ к приватным переменным класса.
Все методы были объявлены как private, а в public остались лишь run() и пустые заглушки для переопределения в порождённых классах.
Я использую режим pinMode(pin,INPUT_PULLUP) так как схема собрана под это, но в ближайшее время собираюсь добавить возможность выбора режима.
Метод run() просто переводит временные интервалы во входные воздействия КА.
Приватный же метод DoAction(состояние, воздействие) просто вызывает функцию из таблицы, если там есть адрес.
Большинство действий выглядят достаточно просто. Там просто устанавливается состояние и вызывается абстрактный метод, который может переопределиться в порождённом классе. Это такой аналог колбека.
Самый жирный обработчик получился для состояния Idle потому, что в него приходят из разных других состояний, а сделать абстрактные методы для таких событий хотелось.
С таким инструментом я уже готов порождать классы для упомянутых в начале статьи кнопок выбора режима дисплея, навигации вверх и вниз, перегруженной кнопки выбора/сброса.
Понятно, что мне грозит ещё один КА, намного более сложный. Кнопок мало, а действий много. Если интересно, напишу в следующий раз в качестве примера реального практического применения вот только что описанной библиотеки.

Быстро собрал макет, циферки бегают, часики ходят, одометры запоминаются в 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;
}
}
С таким инструментом я уже готов порождать классы для упомянутых в начале статьи кнопок выбора режима дисплея, навигации вверх и вниз, перегруженной кнопки выбора/сброса.
Понятно, что мне грозит ещё один КА, намного более сложный. Кнопок мало, а действий много. Если интересно, напишу в следующий раз в качестве примера реального практического применения вот только что описанной библиотеки.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Хорошо ли использовать ООП в таких случаях?
24.66% Да, конечно.18
12.33% Да, большинство библиотек для Arduino IDE сделано с ООП.9
23.29% Не имеет значения.17
23.29% Плохо, лучше на плоском простом C.17
4.11% Не стоит изобретать велосипед, а лучше потратить время на гугление.3
0% Всё уже придумано до нас.0
12.33% Что такое ООП?9
Проголосовали 73 пользователя. Воздержались 47 пользователей.