Debouncing `:circuits_gpio` messages on button push

I detect the button push by using Circuits.GPIO.set_interrupts(gpio_ref, :rising); however it is too sensitive that I want to debounce it. I am thinking about setting a condition using the second data item in the message tuple {:circuits_gpio, 17, 1233456, 1} for controlling the sensitivity. Is it good? Is there a better way?

My target is Rpi4. My button is a 6mm mini button.

defmodule LedBlinker.Button do
  use GenServer, restart: :temporary

  @debounce_time 200_000_000

  def start_link({gpio_pin, on_push_fn}) when is_number(gpio_pin) and is_function(on_push_fn) do
    GenServer.start_link(__MODULE__, {gpio_pin, on_push_fn})
  end

  def init({gpio_pin, on_push_fn}) do
    # Get a ref to the GPIO pin.
    {:ok, gpio_ref} = Circuits.GPIO.open(gpio_pin, :input)

    # Get messages every time the button is pushed.
    Circuits.GPIO.set_interrupts(gpio_ref, :rising)

    {
      :ok,
      %{
        gpio_pin: gpio_pin,
        gpio_ref: gpio_ref,
        last_pushed_at: 0,
        on_push_fn: on_push_fn
      }
    }
  end

  def handle_info({:circuits_gpio, _, at, 1} = message, state) do
    %{last_pushed_at: last_pushed_at, on_push_fn: on_push_fn} = state

    # Debounce the message.
    if @debounce_time < at - last_pushed_at do
      on_push_fn.(message)
    end

    {:noreply, %{state | last_pushed_at: at}}
  end
end
2 Likes

I’d use send_after & cancel_timer or sleep functions instead of if statement. For example: Process.send_after(self(), {:debounce, on_push_fn, msg}, @debounce_time) + handle_info for :debounce.

1 Like

I think either way should get you what you need and probably more up to person prefference.

I like the if statement appoarch. Speaking from my expereince, having to manage timers and sleeps normally does not work out too well for me. However, that might just be user error on my part :slight_smile:.

1 Like

@ondrej-tucek @mattludwigs Thank you for your input. It was helpful.

After all, I realized that I could use Rpi’s internal pull-down resister, which I had not known is used for at that time. Circuits.GPIO.set_pull_mode(gpio, pull_mode) handles it all without adding any physical resister! I mean sensitivity feels right without any tuning. It makes my genserver simpler.

defmodule LedBlinker.GpioButton do
  use GenServer, restart: :temporary

  def start_link({gpio_pin, on_push_fn}) when is_number(gpio_pin) and is_function(on_push_fn) do
    GenServer.start_link(__MODULE__, {gpio_pin, on_push_fn})
  end

  def init({gpio_pin, on_push_fn}) do
    # Get a ref to the GPIO pin.
    {:ok, gpio_ref} = Circuits.GPIO.open(gpio_pin, :input)

    # Use internal pull-down resister.
    :ok = Circuits.GPIO.set_pull_mode(gpio_ref, :pulldown)

    # Get messages every time the button is pushed.
    Circuits.GPIO.set_interrupts(gpio_ref, :rising)

    {
      :ok,
      %{
        gpio_pin: gpio_pin,
        gpio_ref: gpio_ref,
        on_push_fn: on_push_fn
      }
    }
  end

  def handle_info({:circuits_gpio, _, at, 1}, %{on_push_fn: on_push_fn} = state) do
    on_push_fn.(at)
    {:noreply, state}
  end
end

It was still a great opportunity to learn how I could control the sensitivity of incoming data.

1 Like

Cool, I think you’ve found the best solution. Just in case though I wanted to provider another option. I’ve had a couple of cases where I needed debounce behaviour and implement a module:

if you add this module to your dependencies:

def deps do
   [{:debouncer, "~> 0.1.3"}]
end

you can just do:

  def handle_info({:circuits_gpio, _, at, 1}, %{on_push_fn: on_push_fn} = state) do
    Debouncer.immediate(:circuits_gpio, fn -> on_push_fn.(at) end, 5_000)
    {:noreply, state}
  end 

And it will ensure that the event identified by the first argument (:circuits_gpio) is not triggered more often than every 5_000 millisecond.

There is actually a range of different debounce behaviors I needed in some of my projects so the module implements four different functions. All of them take 3 argument: (event_key, function, timeout)

  • apply() - executes an event after TIMEOUT and so creates a regular interval from frequent events
  • immediate() - is the same as apply() but triggers also immediate on the first event
  • immediate2() - executes the first event as well immediately but mutes all further events until after TIMEOUT
  • delay() - every event delays the trigger by TIMEOUT, will issue an event only once there has been no event for TIMEOUT milliseconds

Or here in a text graph:

  EVENT        X1---X2------X3-------X4----------
  TIMEOUT      ----------|----------|----------|-
  ===============================================
  apply()      ----------X2---------X3---------X4
  immediate()  X1--------X2---------X3---------X4
  immediate2() X1-----------X3-------------------
  delay()      --------------------------------X4

Hope this helps some readers.

4 Likes

Wow, nice! Thanks. I will definitely keep that in my toolbox.

2 Likes