Обробка натискань кнопок для Arduino. Схрестити ООП і МКА. Частина 1

Пару місяців тому я купив не сильно новий мотоцикл KTM 250EXC, відкрутив ручку газу в гірку, моту стрільнув в небо, а сам сів на дупу і щось там зламав в спині. В результаті, на мотоцикл не сісти два місяці як мінімум. До чого я це? Так. У трохи втомленого мопеда виявилася несправна приладова панель і я зібрався, поки лежу вдома, зробити саморобну нову.

image

Швидко зібрав макет, циферки бігають, годинник ходять, одометри запам'ятовуються в FRAM — краса, але… знадобилися кнопочки для управління цією красою.

Сьогодні розповім про кнопочки, потім про датчик запалювання, а вже потім про саму панель приладів, Гаразд?

Малювати на китайському екрані 16х2 через i2c просто, датчики швидкості і оборотів мотора сіли на зовнішні переривання, температура читається з аналогового порту, інфа зберігається в FRAM, ну і годинник теж китайські встромлені. Все це крутиться асинхронно приблизно як SmartDelay, про який писав нещодавно тут.

Так, кнопочки!

Зробити одну кнопку для пригальмовування мигання світлодіода виявилося легко, як і інші іграшки. Приліпити велику клавіатуру до приладової панелі мотоцикли ендуро не вийде, немає місця. Довелося поламати голову і обмежитися чотирма кнопками:
  1. Режим
  2. Вгору
  3. Вниз
  4. ОК/Скидання


Щоб вписати в це меню і управління, треба розпізнавати тик, тыыык і тыыыык. Тобто натискання на кнопки різної тривалості. Я написав велику онучу з switch і if, зрозумів, що прочитати це через пару місяців я не зможу і взявся знову за плюси.

Задача виявилася схожа на бібліотеки SmartDelay:
  • Максимально сховати код в бібліотеку.
  • Код обробки кнопок не повинен заважати програмувати «по справі».
  • Повинно бути можливо використовувати ще десь і в інших подальших проектах.
  • Має бути красиво, чи що.


Якщо ви знаєте різне схоже, будь ласка, повідомте в коментарях, чим ви користуєтеся.

Спочатку я папері намалював кінцевий автомат. З нальоту не вийшло, без паперу.

image

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

image

В результаті я породив абстрактний клас 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() {}; // hold On long.
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;
}
}


З таким інструментом я вже готовий породжувати класи для згаданих на початку статті кнопок вибору режиму дисплея, навігації вгору і вниз, перевантаженій кнопки вибору/скидання.

Зрозуміло, що мені загрожує ще один КА, набагато складніший. Кнопок мало, а багато дій. Якщо цікаво, напишу в наступний раз в якості прикладу реального практичного застосування ось тільки що описаної бібліотеки.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.