Пару месяцев назад я купил не сильно новый мотоцикл 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() {};    // 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;
  }
}


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

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

Проголосовал 61 человек. Воздержалось 34 человека.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Поделиться с друзьями
-->

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


  1. VT100
    10.01.2017 21:36

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


    1. nwwind
      10.01.2017 23:12

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

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


      1. DrZlodberg
        10.01.2017 23:49

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


        1. nwwind
          11.01.2017 12:26

          На руль обязательно. Но это будет параллельно — на руле и на приборке.
          На КТМ, более того, это штатная фича и разъёмы уже есть.


  1. osipov_dv
    11.01.2017 10:02
    +1

    Вот из-за излишнего ООП в либах я так и не смог сделать погодную станцию на уно. Тупо оперативки не хватило, слишком жирно откушано переменными в либах RTC и SD. Остается свободными 300 байт и их не хватает. Либо надо оптимизировать чужие либы, выкусывая ненужный мне код и переменные, либо разносить на 2 микроконтроллера.


    1. nwwind
      11.01.2017 12:30
      +1

      SD толстая либа весьма.
      ООП позволяет как раз одной строкой создать и настроить объект. Если их много, это удобно, конечно же. Ну и спрятать логику глубоко можно. Я проверил, вот этот код занимает совсем мало памяти что на ООП, что так.
      Я бы на твоём месте подумал о втором контроллере с SD и связал бы их по I2C. Например, Pro Mini размером сам с карточку SD.


      1. osipov_dv
        11.01.2017 12:53

        Я хотел сделать все на голом 328, с уходом в спячку и питанием от батареек АА (4 шт). Но с таким раскладом придется делать станцию из двух частей — часть висящая за окном и следящая за погодой за окном, с отправкой по 433 RF, и часть в квартире (не требовательную к питанию), с логгером и RTC. Два рядом вешать смысла не вижу.


  1. ilmarin77
    11.01.2017 12:30

    Есть такая штуковина: State Map Compiler — на специальном языке описываются состояния машины и переходы, и компилятор генерирует код — на C, C++, Java и т.д.
    Может так проще будет?


    1. nwwind
      11.01.2017 12:30

      Спасибо, почитаю сейчас. Для следующего шага мне как раз что-то подобное надо.


  1. Shadow_ru
    11.01.2017 12:31

    Не увидел обработку дребезга кнопки, это где-то глубже?


    1. nwwind
      11.01.2017 12:50

      Нет, на самом виду. Там от нажатия кнопки (PreClick) до события Click 10мс, это как раз дребезг.


      1. osipov_dv
        11.01.2017 12:54

        а почему не отработать ее RC фильтром, обойдясь без кода?


  1. iig
    11.01.2017 12:50

    Контрастность у дешевого дисплея lcd 1602, как по мне, отвратительная. Для outdoor я бы не использовал.
    Ну, и кодировать таблицу состояний можно разными способами, ваш оригинальный.


    1. nwwind
      11.01.2017 12:51

      Спасибо.
      У меня несколько разных дисплеев, я неспешно подбираю поконтрастнее.


  1. Gryphon88
    11.01.2017 12:53

    автомат очень удобно делается с помощью xmacros.
    Подскажи кто-нибудь пример, как грамотно реализовать работу с «шифтами» как на геймпаде к PS3: нажатие кнопки — одно действие, кнопка+шифт — другое, кнопка+оба шифта — третье. Если проверять состояние шифтов перед обработкой, код уж очень некрасиво обрастает if/switch'аим


    1. nwwind
      11.01.2017 12:59

      Там много кнопок и есть специфика своя, я бы сделал для шифт класс управляющий кнопок, а их состояние менял в классе onClick/offClick. ставя / убирая бит соответствующий в некой глобальной переменной shiftState.


    1. nwwind
      11.01.2017 13:05

      На тему xmacros. Там надо много писать.
      Чем плоха запись, как у меня>


      1. Gryphon88
        11.01.2017 13:30

        Вроде столько же или чуть меньше, особенно если кнопки начнут плодиться или зависеть друг от друга. Опять же, ФП, метапрограмирование, пафос :)


        1. nwwind
          11.01.2017 13:44

          Да, я тут видел, что впихали ноду в МК. Жесть.


          1. Gryphon88
            11.01.2017 14:18

            Ну, если упихать и попрыгать сверху… Но вообще я имел в виду вот эту милую наркоманию: 1 2 3 4


            1. nwwind
              11.01.2017 16:16

              Жуть какая…


              1. Gryphon88
                11.01.2017 16:38

                Жуть — это вот это: 1 2 3
                Если Вы думаете, что нельзя написать ещё более мерзкий, непонятный и неподдерживаемый код с использование сишных макросов, то Вы ошибаетесь. На крайний случай всегда есть М4


                1. nwwind
                  11.01.2017 19:03

                  После senmail.cf меня уже не удивить.


  1. nwwind
    18.01.2017 11:19

    В печали пребываю.

    С++ говно.

    Размер объекта 157 байтов. На кнопку! Это перебор уж. Указатель на метод класса — 4 байта, на просто функцию — 2. Переписал в итоге внутри на Цэ плоском старом добром саму МКА. Интерфейс оставил старый, так что, всё прошло незаметно. В итоге 13 байтов на объект, уже ничего.
    Эх, таблицей, конечно же, красиво, но в ембедеде… Жрёт память таблица, да и вообще оопность негуманно её расходует.

    Закоммитил.