Обработка нажатий кнопок для 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 

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;
  }
}

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

Понятно, что мне грозит ещё один КА, намного более сложный. Кнопок мало, а действий много. Если интересно, напишу в следующий раз в качестве примера реального практического применения вот только что описанной библиотеки.

Комментарии (3)

  • 10 января 2017 в 21:36

    0

    Считаю, что с точки зрения UserInterface «тыыык и тыыыык» неудобны. Более удобны были-бы длинные или сдвоенные нажатия, меняющие отклик всех или остальных кнопок.
    • 10 января 2017 в 23:12

      0

      Это мотоцикл. Руки в перчатках, время есть тк все тыки идут на стоящем моте.
      В проекте пока что так:
      Два экрана: нормальный и спортивный. Переключаются сликом по кнопке режим (1).
      В спортивном кнопка ввода (4) даёт старт/стоп круга по клику. Удержание сбрасявыет всю статистику гонки.
      В нормальном режиме клики вверх/вниз (2/3) преключают одометры. Долгое удержание ввода (4) сбрасывает текущий одометр. Удержание ввода (4) переводит экран в меню и можно ходить по полям: одометр, моточасы, часы. Ходить кнопками вверх/вниз. Клик на ввод приводит к редактированию поля, если можно. Можно только часы портить. Клик ввода завершает редактирование и выходит из меню. Долгое удержание выводит из меню. Очень долгое обнуляет поле.

      Думаю добавить в протокол класса флаг для слишком долгого удержания на замену его многокликом. Достаточно внутри просто вызвать переход к toIdle. Это полезно для настройки часиков.

      • 10 января 2017 в 23:49

        0

        Vapor, вроде, всего 3 мя кнопками обходится.
        Для спортивного режима кнопку старт/стопа удобнее на руль тогда выводить. Если круги короткие и без остановки — погрешность с запуском с приборки будет приличная.
        Но вообще идея интересная. По размерам большая получилась? Ну и корпус стоит делать не убиваемый. С таким катанием при приземлении на руль приборку можно в кашу превратить.
        Офтоп: Чтобы так не летать рекомендую держать палец на сцеплении. Иногда реально спасает.

© Habrahabr.ru