Handling button presses for Arduino. Cross OOP and ICA. Part 1

    A couple of months ago I bought a not-so-new KTM 250EXC motorcycle, unscrewed the throttle stick, pulled Motu into the sky, and sat on my ass and broke something in my back there. As a result, you should not sit on a motorcycle for two months at least. Why am I doing this? Yes. A slightly tired moped turned out to have a faulty dashboard and I got ready to make my own homemade while I was at home.

    image

    Quickly assembled the layout, the numbers are running, the clocks are running, the odometers are remembered in FRAM - beauty, but ... buttons were needed to control this beauty.

    Today I’ll tell you about the buttons, then about the ignition sensor, and only then about the tidy itself, okay?

    Drawing on a 16x2 Chinese screen via i2c is easy, the engine’s speed and RPM sensors took external interrupts, the temperature is read from the analog port, the info is stored in FRAM, and the Chinese watch is stuck too. All this rotates asynchronously, much like SmartDelay , about which I wrote recently here.

    Yes, buttons!

    It was easy to make one button to slow down the blinking of the LED, like other toys. But sticking a huge keyboard to the dashboard of the motorcycle enduro does not work, there is no place. I had to smash my head and confine myself to four buttons:
    1. Mode
    2. Up
    3. Way down
    4. OK / Reset


    To enter both the menu and the control into this, one must recognize tyk, tyyyk and tyyyyk. That is, pressing buttons of different durations. I wrote a big footcloth from switch and if, I realized that I could not read it in a couple of months and took up the pros again.

    The task turned out to be similar to the SmartDelay library :
    • Hide code in the library as much as possible.
    • The button processing code should not interfere with the programming of the "case."
    • It should be possible to use it somewhere else in other subsequent projects.
    • It must be beautiful, or something.


    If you know similar things, please let us know in the comments what you are using.

    First, I drew a finite state machine on paper. With a raid did not work, without paper.

    image

    Then I googled that instead of switch / if I could make it a sign. The last time I turned to the topic of the ICA about 30 years ago, it was necessary to refresh the theory in my memory.

    image

    As a result, I gave birth to an abstract SmartButton class . This creation hides inside itself the MCA, listens to digital ports and pulls empty abstract methods on click, hold and long hold. To use this class, you need to create your own and override the necessary methods.

    #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().
    }
    


    It can be seen that the code is a bit, everything is more or less clear. There are no callbacks right here so explicitly described. In loop () there is only one run () call for each button, somewhere the class and the button itself are defined. It’s possible to create, scary ladders of MCA for processing sticks of buttons with style C do not interfere.

    Let's take a look at the code. The whole project lies on the github .

    Without inventing anything better, I made available the time interval settings from the outside. Here, accordingly, delays for a click, hold, long hold and so long that you should ignore such a click at all. In SmartButton.h, I defined these constants carefully so that they could be redefined before #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
    


    I made states and influences as enum in particular, and because I automatically got their numbers StatesNumber and InputsNumber.

    enum state {Idle = 0, PreClick, Click, Hold, LongHold, ForcedIdle, StatesNumber};
    enum input {Release = 0, WaitDebounce, WaitHold, WaitLongHold, WaitIdle, Press, InputsNumber};


    He broke his head slightly and drew this type. This is a pointer to a method of this class. Do not laugh, the pros somehow passed me by, I am not a master at them.

    typedef void (SmartButton::*FSM)(enum state st, enum input in);
    


    Here I had to tinker. This is a conversion table. Fuss was with links to methods, how to write them so that the compiler does not swear and the links were to methods of a specific instance of the class. Not a static method, not just a left-handed function, but a method, so that it has access to private class variables.

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


    All methods were declared private, and only run () and empty stubs for overriding in the generated classes remained in public.

    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.
    


    I use pinMode (pin, INPUT_PULLUP) mode since the circuit is built for this, but in the near future I am going to add the ability to select a mode.

    The run () method simply translates the time intervals into the input actions of the spacecraft.

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


    The private DoAction method (state, effect) simply calls the function from the table, if there is an address.

    void SmartButton::DoAction(enum state st, enum input in) {
      if (action[st][in] == NULL) return;
      (this->*(action[st][in]))(st, in);
    }
    


    Most actions look simple enough. It simply sets the state and calls an abstract method, which can be overridden in the generated class. This is such an analogue of the callback.

    void SmartButton::ToClick(enum state st, enum input in) {
      btState = Click;
      onClick(); // Вот это аналог колбека в плоском С.
    }
    


    The fattest handler turned out for the Idle state because they came to it from various other states, but I wanted to create abstract methods for such events.

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


    With such a tool, I am ready to generate classes for the buttons for selecting the display mode, navigation up and down, and the overloaded select / reset button that were mentioned at the beginning of the article.

    It is clear that I am facing another spacecraft, much more complex. There are few buttons, but many actions. If it’s interesting, I’ll write next time as an example of a real practical application of the library just described.

    Only registered users can participate in the survey. Please come in.

    Is OOP good to use in such cases?

    • 23.9% Yes, of course. 17
    • 12.6% Yes, most libraries for the Arduino IDE are made with OOP. 9
    • 22.5% doesn't matter. 16
    • 23.9% Bad, better on flat plain C. 17
    • 4.2% You should not reinvent the wheel, but it is better to spend time on googling. 3
    • 0% Everything has already been invented before us. 0
    • 12.6% What is OOP? 9

    Also popular now: