Skip to content
Snippets Groups Projects
Debouncer.h 10.2 KiB
Newer Older
David Gauchard's avatar
David Gauchard committed

#pragma once

#include <stdint.h>

#ifndef DEBOUNCER_DEFAULT_DEBOUNCE_PUSH_MS
#define DEBOUNCER_DEFAULT_DEBOUNCE_PUSH_MS    50    // debounce delay for push button
#endif

#ifndef DEBOUNCER_DEFAULT_DEBOUNCE_ROTARY_MS
#define DEBOUNCER_DEFAULT_DEBOUNCE_ROTARY_MS  1     // debounce delay for rotary button
#endif

#ifndef DEBOUNCER_DEFAULT_SHORT_MS
#define DEBOUNCER_DEFAULT_SHORT_MS            200   // max interval between multiple short clicks
#endif

#ifndef DEBOUNCER_DEFAULT_LONG_MS
#define DEBOUNCER_DEFAULT_LONG_MS             400   // min interval for long click
#endif

#if defined(ESP8266) && !DEBOUNCER_WITH_MILLIS
#pragma message "Debouncer: using esp_get_cycle_count"
#define DEBOUNCER_DEFAULT_CYCLE_FUNC esp_get_cycle_count
#define DEBOUNCER_DEFAULT_CYCLE_PER_MS (F_CPU / 1000)
#else
#if !defined(__AVR__)
#pragma message "Debouncer: using millis"
#endif
#define DEBOUNCER_DEFAULT_CYCLE_FUNC millis
#define DEBOUNCER_DEFAULT_CYCLE_PER_MS (1)
#endif

#if 0
#define DEBUGBUTTON(x...)        do { Serial.print(cycle_f()); Serial.print(":"); x; } while (0)
#else
#define DEBUGBUTTON(x...)        do { } while (0)
#endif

#ifndef IRAM_ATTR
#define IRAM_ATTR // nothing
#endif

///////////////////////////////////////////////////////////////

/*
    ButtonFast::
        !enable_multi && !enable_long:
            short click: response on click
            no long click
    
    ButtonLong::
        !enable_multi && enable_long:
            short click: response on release + debounce time
            long click: response on long click delay
    
    Debouncer<long_cycle=0>::
        enable_multi && !enable_long:
            short clicks number: response on final release + debounce time
            no long click

    Debouncer<true,≠0>:: (default)
        enable_multi && enable_long:
            short clicks number: reponse on final release + debounce time
            long click: response on long click delay
*/

#if __GNUC__ <= 7

#define __cycle_template_decl
#define __cycle_template_args
#define __cycle_template_impl
#define cycle_f DEBOUNCER_DEFAULT_CYCLE_FUNC
#define cyclePerMs DEBOUNCER_DEFAULT_CYCLE_PER_MS

#else

#define __cycle_template_decl \
          auto cycle_f = DEBOUNCER_DEFAULT_CYCLE_FUNC, \
          auto cyclePerMs = DEBOUNCER_DEFAULT_CYCLE_PER_MS, \

#define __cycle_template_impl \
        auto cycle_f, \
        auto cyclePerMs, \

#define __cycle_template_args \
        cycle_f, \
        cyclePerMs, \

#endif

template <const bool enable_multi = true,
          __cycle_template_decl
          const decltype(cycle_f()) long_cycle = DEBOUNCER_DEFAULT_LONG_MS * cyclePerMs, /* 0 to disable long press */
          const decltype(cycle_f()) debounce_cycle = DEBOUNCER_DEFAULT_DEBOUNCE_PUSH_MS * cyclePerMs,
          const decltype(cycle_f()) short_cycle = DEBOUNCER_DEFAULT_SHORT_MS * cyclePerMs>
class Debouncer
{
protected:

    using T = decltype(cycle_f());
    
    enum state_e
    {
        off,
        on,
        on_long,
        on_debounce,
        off_debounce,
    };

    state_e        state;
    uint_fast8_t   short_count;
    bool           long_state;
    T              state_cycle;

public:

    Debouncer (): state(off), short_count(0), long_state(false), state_cycle(0)
    {
        DEBUGBUTTON(Serial.println(enable_multi));
        DEBUGBUTTON(Serial.println(long_cycle));
        DEBUGBUTTON(Serial.println(debounce_cycle));
        DEBUGBUTTON(Serial.println(short_cycle));
    }

    // - must be called in the main loop or from interrupt
    // - return true if a change has occured
    bool update (bool pressed);

    // simulate a long press
    void simLongClick ()
    {
        long_state = true;
    }
    
    // simulate n short presses
    void simShortClick (uint_fast8_t n)
    {
        short_count += n;
    }
    
    // returns long state, does not clear shorts
    bool longState  ()
    {
        bool state = long_state;
        long_state = false;
        return state;
    }

    // returns short count, does not clear long
    uint_fast8_t shortCount ()
    {
        auto count = short_count;
        short_count = 0;
        return count;
    }

    // returns long state and clears/ignore shorts
    bool longClick  ()
    {
        return longState();
    }

    // returns short count and clears/ignore long
    uint_fast8_t shortClick ()
    {
        return shortCount();
    }

    bool isOff () const
    {
        return state == off;
    }

    bool isLong ()
    {
        long_state = false;
        return state == on_long;
    }
};

///////////////////////////////////////////////////////////////

template <__cycle_template_decl
          const decltype(cycle_f()) debounce_cycle = DEBOUNCER_DEFAULT_DEBOUNCE_PUSH_MS * cyclePerMs,
          const decltype(cycle_f()) short_cycle = DEBOUNCER_DEFAULT_SHORT_MS * cyclePerMs>
class ButtonFast: public Debouncer</*no multi*/false, __cycle_template_args /*no long*/0, debounce_cycle, short_cycle>
{
public:
    // multiclick disabled, longclick disabled
    ButtonFast (): Debouncer<false, __cycle_template_args 0, debounce_cycle, short_cycle>() { }

    bool pressed ()
    {
        using d = Debouncer<false, __cycle_template_args 0, debounce_cycle, short_cycle>;
        return d::state == d::on_debounce || d::state == d::on;
    }

    bool longState () = delete;
    uint_fast8_t shortCount () = delete;
    bool longClick () = delete;
    uint_fast8_t shortClick () = delete;
};

///////////////////////////////////////////////////////////////

template <__cycle_template_decl
          const decltype(cycle_f()) long_cycle = DEBOUNCER_DEFAULT_LONG_MS * cyclePerMs>
class ButtonLong: public Debouncer</* no multi */false, __cycle_template_args long_cycle>
{
public:
    ButtonLong (): Debouncer<false, __cycle_template_args long_cycle>() { }

    bool pressed () { return Debouncer<false, __cycle_template_args long_cycle>::isLong(); }

    bool longState () = delete;
    uint_fast8_t shortCount () = delete;
    bool longClick () = delete;
    uint_fast8_t shortClick () = delete;
};

///////////////////////////////////////////////////////////////

template <__cycle_template_decl
          const decltype(cycle_f()) debounce_cycle = DEBOUNCER_DEFAULT_DEBOUNCE_ROTARY_MS * cyclePerMs>
class Rotary
{
protected:
    ButtonFast<__cycle_template_args debounce_cycle> b1, b2;
    int_fast8_t rot;

public:
    Rotary (): b1(), b2(), rot(0) { }

    bool update (bool in1, bool in2); // steady state with pullup: true, true
    bool right  () { bool ret; if ((ret = (rot > 0))) --rot; return ret; }
    bool left   () { bool ret; if ((ret = (rot < 0))) ++rot; return ret; }
};

///////////////////////////////////////////////////////////////

template <const bool enable_multi,
          __cycle_template_impl
          const decltype(cycle_f()) long_cycle,
          const decltype(cycle_f()) debounce_cycle,
          const decltype(cycle_f()) short_cycle>
IRAM_ATTR // can be called from ISR
bool Debouncer<enable_multi, __cycle_template_args long_cycle, debounce_cycle, short_cycle>::update (bool pressed)
{
    // get current time
    auto cycle = cycle_f();

    switch (state)
    {
    case off:
        if (pressed)
        {
            // button is off, and now pressed
            DEBUGBUTTON(Serial.println("off->on_debounce"));
            state_cycle = cycle;
            if (!enable_multi && /*long press disabled*/(long_cycle == 0))
            {
                bool ret = state != on_debounce;
                state = on_debounce;
                return ret; // true on first change
            }
            state = on_debounce;
        }
        break; // off, no change, return unreaded state
    
    case on_long:
        if (pressed)
            return false; // no change
        DEBUGBUTTON(Serial.println("on_long->off_debounce"));
        state = off_debounce;
        state_cycle = cycle;
        return !enable_multi;

    case on_debounce:
        if ((cycle - state_cycle) < debounce_cycle)
        {
            // don't care before debounce delay
            DEBUGBUTTON(Serial.println("debounce-on"));
            return false;
        }
        DEBUGBUTTON(Serial.println("on_debounce->on"));
        state = on;
        // fall through
    case on:
        if (/*long press enabled*/(long_cycle > 0) && (cycle - state_cycle) > long_cycle)
        {
            // long press in progress
            DEBUGBUTTON(Serial.println("on->on_long"));
            state = on_long;
            long_state = true;
            return true;
        }
        if (!pressed)
        {
            // off after debounce delay
            DEBUGBUTTON(Serial.println("on->off_debounce"));
            state = off_debounce;
            state_cycle = cycle;
            if (!enable_multi)
                // single press is released
                return true;
            // it was a short click
            short_count++;
            return !enable_multi && /*long press enabled*/(long_cycle > 0);
        }
        return false; // no change
    
    case off_debounce:
        if ((cycle - state_cycle) < debounce_cycle)
        {
            // don't care before debounce delay
            DEBUGBUTTON(Serial.println("debounce-off"));
            return false;
        }
        if (pressed)
        {
            // multiclick in progress
            DEBUGBUTTON(Serial.println("off_debounce->on_debounce"));
            state = on_debounce;
            state_cycle = cycle;
            return false;
        }
        if (enable_multi && (cycle - state_cycle) < short_cycle)
        {
            // wait for multiclick
            DEBUGBUTTON(Serial.println("(multiclick?)"));
            return false;
        }
        // released for good
        DEBUGBUTTON(Serial.println("off_debounce->off"));
        state = off;
        break;
    }

    return short_count || long_state;
}

///////////////////////////////////////////////////////////////

template <__cycle_template_impl
          const decltype(cycle_f()) debounce_cycle>
bool Rotary<__cycle_template_args debounce_cycle>::update (bool pressed1, bool pressed2)
{
    bool b1u = b1.update(pressed1);
    bool b2u = b2.update(pressed2);
    if (b1u && b2.isOff() && b1.pressed())
    {
        rot++;
        return true;
    }
    if (b2u && b1.isOff() && b2.pressed())
    {
        rot--;
        return true;
    }
    return false;
}

///////////////////////////////////////////////////////////////