How to do a long or short button press

I’m posting before attempting to integrate it into the current MGOS code. If there is interest in integration with the library and a PR, happy to chat.

The simple use case this addresses is being able to tell if a button press is short or long. Current mgos int handler only lets you choose release or press edges, so you can’t use it to time the start and end with different callbacks. This code basically starts a timer on the first press and then that timer keeps repeating until the button is released, so effectively provides protection against multiple event fires while the button keeps being pressed!
Note, this code could be, and likely will be tomorrow, improved with mgos events being fired on short or long press detections that an user could then subscribe to with a callback. As the code seems to me to be non blocking (unlike the provision library mgos sleep approach) I can’t really see why this code could be built in as standard functionality or even a config flag/function on the gpio.

// User button variables
#define SF_USERBUTTON_GPIO (14)
long int userButton_time_start = (long int)time(NULL);
mgos_gpio_pull_type userButton_gpio_direction = MGOS_GPIO_PULL_UP;
mgos_timer_id userButton_timer_id;
int userButton_timer_delay = 150;
bool userButton_running = false;
bool userButton_detected = false;
int userButton_count = 0;
int userButton_count_required = 5;

unsigned long IRAM_ATTR millis() {
    return (unsigned long)(esp_timer_get_time() / 1000ULL);
}
void timer_userButton_checkLongPress(void* arg) {
    int level = mgos_gpio_read(SF_USERBUTTON_GPIO);
    if ((userButton_gpio_direction == MGOS_GPIO_PULL_UP && level == 0)
        || (userButton_gpio_direction == MGOS_GPIO_PULL_DOWN && level == 1)){
        // Means it's still being pressed
        userButton_count++;
    } else {
        // Did we see a short press?
        if (!userButton_detected && userButton_count < userButton_count_required){
            LOG(LL_INFO, ("User button press (%d) SHORT press detected! count of %d ", SF_USERBUTTON_GPIO, userButton_count));
        } else {
            LOG(LL_INFO, ("User button press (%d) cleared after a LONG press count of %d ", SF_USERBUTTON_GPIO, userButton_count));
        }
        mgos_clear_timer(userButton_timer_id);
        userButton_count = 0;
        userButton_detected = false;
        userButton_running = false;
    }

    if (!userButton_detected && userButton_count >= userButton_count_required) {
        // We take a threshold approach, once reached we ignore if the button keeps being pressed
        userButton_detected = true; // the timer will keep going until released, but this won't trigger multiple times
        long int diff = millis() - userButton_time_start;
        LOG(LL_INFO, ("User button press (%d) detected LONG press %lu", SF_USERBUTTON_GPIO, diff));
    }

    (void)arg;
}
static void gpio_userbutton_press_cb(int pin, void* arg) {
    long int now = millis();
    long int diff = now - userButton_time_start;

    if (!userButton_running || diff > 1000 * 10) {
        LOG(LL_INFO, ("Starting user button fresh (%d) time diff was %lu", pin, diff));
        userButton_count = 0;
        userButton_time_start = now;
        userButton_detected = false;
        if (!userButton_running) {
            userButton_timer_id = mgos_set_timer(userButton_timer_delay, MGOS_TIMER_REPEAT, timer_userButton_checkLongPress, NULL);
            userButton_running = true;
        }
    } else {
        LOG(LL_INFO, ("Ignored button press (%d) interrupt diff was %lu", pin, diff));
    }


    (void)arg;
}


// init code:
enum mgos_app_init_result mgos_app_init(void) {
    // Press button
    if (!mgos_gpio_set_mode(SF_USERBUTTON_GPIO, MGOS_GPIO_MODE_INPUT)
        || !mgos_gpio_set_pull(SF_USERBUTTON_GPIO, MGOS_GPIO_PULL_UP)
        || !mgos_gpio_set_int_handler(SF_USERBUTTON_GPIO, MGOS_GPIO_INT_EDGE_NEG, gpio_userbutton_press_cb, NULL)
        || !mgos_gpio_enable_int(SF_USERBUTTON_GPIO)) {

        LOG(LL_WARN, ("Failed to setup userbutton PIN (%d) for PRESS", SF_USERBUTTON_GPIO));
    }
    return MGOS_APP_INIT_SUCCESS;
}

Thanks for posting this, I have a need to do the same thing. In fact it seems to be a common question, although not super common. I’ll be looking at this carefully, did you ever do the update you mentioned?.

Try to use mgos_gpio_set_button_handler with MGOS_GPIO_INT_EDGE_ANY and handle the interval between edges in the callback.

1 Like

I did try before writing the code. Simply doesn’t work. Edge any was basically commented out as a detection handler in the library. You can only do POS or NEG, not both, not ANY, or I would have simply had two functions playing tennis with each other with a stopwatch.

My code above could be incorporated into the MOS library, hence my question about interest.

Hrm, i see there was a january commit to add edge any support. Trying now to remember why it didn’t work.

@Mark_Terrill Did you ever get this working?

I’m currently trying to achieve long press detection and have run in to an issue. This is the code I’m using:

static double on_button_down_time = 0;

static void on_off_button_cb(int pin, void *arg)
{
  (void)arg;
  LOG(LL_INFO, ("Button Presses: ON/OFF on pin %d", pin));

  // Button is down - active low
  if (mgos_gpio_read(pin) == false)
  {
    on_button_down_time = mgos_uptime();
    LOG(LL_INFO, ("ON/OFF Button DOWN"));
  }
  else
  {
    LOG(LL_INFO, ("ON/OFF Button UP"));

    if (isgreater((mgos_uptime() - on_button_down_time), 5.0))
      LOG(LL_INFO, ("### LONG PRESS"));
  }
}

enum mgos_app_init_result mgos_app_init(void)
{
  mgos_gpio_set_mode(on_off_button_pin, MGOS_GPIO_MODE_INPUT);
  mgos_gpio_set_button_handler(on_off_button_pin, MGOS_GPIO_PULL_UP, MGOS_GPIO_INT_EDGE_ANY, 10, on_off_button_cb, NULL);

  return MGOS_APP_INIT_SUCCESS;
}

If works great if you press the button firmly and slowly, but if you press the button normally it doesn’t register the DOWN/UP event as expected.

Basically when pressing the button normally I see:

Normal first press:

Button Presses: ON/OFF on pin
Button down

Normal second press:

Button Presses: ON/OFF on pin 
Button up

But if I press the buttons slowly I see
Slow first press:

Button Presses: ON/OFF on pin 
Button down
Button Presses: ON/OFF on pin 
Button up

Slow second press:

Button Presses: ON/OFF on pin
Button down
Button Presses: ON/OFF on pin
Button up

I haven’t looked at the waveforms yet, could be something there?
Or maybe even something around the way I’m I’m checking for the down/up button state by reading the gpio… Couldn’t find any examples of how to know which side of the wave was being detected when using MGOS_GPIO_INT_EDGE_ANY

Working example of long press ripped off from the provision library. Tested on ESP32:

static mgos_timer_id s_hold_timer = MGOS_INVALID_TIMER_ID; // The var for long press detection

static void button_timer_cb(void *arg)
{
  (void)arg;
  int pin = 35;

  LOG(LL_INFO, ("Button Timer Callback"));

  int n = 0; /* Number of times the button is reported down */
  for (int i = 0; i < 10; i++)
  {
    if (mgos_gpio_read(pin) == false)
      n++;

    mgos_msleep(1);
  }

  if (s_hold_timer != MGOS_INVALID_TIMER_ID)
    mgos_clear_timer(s_hold_timer);

  if (n > 7)
  {
    enable_ap();
    LOG(LL_INFO, ("Long Press"));
  }
}

static void on_off_button_cb(int pin, void *arg)
{
  (void)arg;
  int duration = 5000;

  LOG(LL_INFO, ("Button Pressed"));

  // Do any short press stuff

  // Do the long press scheduling stuff
  if (s_hold_timer != MGOS_INVALID_TIMER_ID)
    mgos_clear_timer(s_hold_timer);

  LOG(LL_INFO, ("on_off_button_cb | Setting %d ms timer", duration));
  s_hold_timer = mgos_set_timer(duration, 0, button_timer_cb, arg);
}

enum mgos_app_init_result mgos_app_init(void)
{
  mgos_gpio_set_mode(on_off_button_pin, MGOS_GPIO_MODE_INPUT);
  mgos_gpio_set_button_handler(on_off_button_pin, MGOS_GPIO_PULL_UP, MGOS_GPIO_INT_EDGE_NEG, 50, on_off_button_cb, NULL);

  return MGOS_APP_INIT_SUCCESS;
}

Hey, just spotted this. Yeah it was working. I think I made a few more changes.

It neatly detects short and long presses. I haven’t really tested it for the slightest touch, but I believe it’s working ok. If anything I wouldn’t want a very casual press to trigger it!

Any chance you’d be willing to share the source code you’re using?

I wasn’t able to get the expected behaviour working with MGOS_GPIO_INT_EDGE_ANY

Here is a simple example using mgos_gpio_set_button_handler

#include "mgos.h"

struct button_ctx {
  int short_ms;
  int long_ms;
  int64_t prev_high_low;
  bool prev_state;
};

static void button_press_handler(int pin, void *arg) {
  if (arg == NULL) {
    return;
  }
  struct button_ctx *ctx = (struct button_ctx *) arg;
  // read current state
  bool curr_state = mgos_gpio_read(pin);
  int64_t now_us = mgos_uptime_micros();

  // high->low transition
  if ((curr_state == false) && (ctx->prev_state == true)) {
    ctx->prev_state = curr_state;
    // update prev_high_low
    ctx->prev_high_low = now_us;
    return;
  }

  // low->high transition
  if ((curr_state == true) && (ctx->prev_state == false)) {
    ctx->prev_state = curr_state;
    int64_t press_ms = (now_us - ctx->prev_high_low) / 1000;
    LOG(LL_INFO, ("press_ms: %lld", press_ms));
    if (press_ms >= ctx->long_ms) {
      // call a callback or fire an event
      LOG(LL_INFO, ("Button long press"));
      return;
    }
    if (press_ms >= ctx->short_ms) {
      LOG(LL_INFO, ("Button short press"));
      return;
    } else {
      LOG(LL_INFO, ("Button press"));
    }
  }
}

static void timer_cb(void *arg) {
  static bool s_tick_tock = false;
  LOG(LL_INFO, ("%s uptime: %.2lf, free_heap: %lu, min_free_heap: %lu",
                (s_tick_tock ? "Tick" : "Tock"), mgos_uptime(),
                (unsigned long) mgos_get_free_heap_size(),
                (unsigned long) mgos_get_min_free_heap_size()));
  s_tick_tock = !s_tick_tock;
  (void) arg;
}

enum mgos_app_init_result mgos_app_init(void) {
  int gpio = mgos_sys_config_get_button_gpio();
  if (gpio >= 0) {
    struct button_ctx *ctx = (struct button_ctx *) calloc(1, sizeof(*ctx));
    ctx->short_ms = mgos_sys_config_get_button_short_ms();  // default 1000ms
    ctx->long_ms = mgos_sys_config_get_button_long_ms();    // default 3000ms
    // initial state is high
    ctx->prev_state = true;
    if (mgos_gpio_set_button_handler(gpio, MGOS_GPIO_PULL_UP,
                                     MGOS_GPIO_INT_EDGE_ANY, 50,
                                     button_press_handler, ctx)) {
      LOG(LL_INFO, ("Button handler installed"));
    }
  }
  mgos_set_timer(10000 /* ms */, MGOS_TIMER_REPEAT, timer_cb, NULL);
  return MGOS_APP_INIT_SUCCESS;
}

Output:

[Nov 25 15:07:07.062] main.c:48               press_ms: 346
[Nov 25 15:07:07.066] main.c:58               Button press
[Nov 25 15:07:12.199] main.c:48               press_ms: 990
[Nov 25 15:07:12.202] main.c:58               Button press
[Nov 25 15:07:15.804] main.c:68               Tick uptime: 22.08, free_heap: 228460, min_free_heap: 222772
[Nov 25 15:07:16.692] main.c:48               press_ms: 1420
[Nov 25 15:07:16.696] main.c:55               Button short press
[Nov 25 15:07:25.318] main.c:48               press_ms: 2837
[Nov 25 15:07:25.322] main.c:55               Button short press
[Nov 25 15:07:25.804] main.c:68               Tock uptime: 32.08, free_heap: 228460, min_free_heap: 222772
[Nov 25 15:07:32.941] main.c:48               press_ms: 3405
[Nov 25 15:07:32.944] main.c:51               Button long press
1 Like

Circling back to this - looks like the reason for the poor performance of my function was the location of the LOG function.

Moving the LOG to the end of the function and ensuring the mgos_gpio_read(pin) happens quickly was the difference