How to monitor a GPIO in GenServer (Raspberry Pi)

I am trying to monitor a GPIO on the Raspberry Pi. The use case is to build a shutdown button which will turn off the device if the button is pressed for some time (e.g. half a second).

I am pretty sure this can be done with the Circuits.GPIO library. But can I use any pin? Specifically, I wonder if I can use GPIO 3 (Pin #5) on the RPi 4. The pin is also called SCL1 and is normally used for I2C. I don’t use I2C on my system, so does that mean it’s safe to use as a GPIO?

I have already experimented a little on my Nerves system. I can SSH into the box and open the GPIO, and then I get events in the process mailbox like this:

iex(5)> {:ok, gpio} = Circuits.GPIO.open(3, :input)
{:ok, #Reference<0.1860275132.899809292.10759>}
iex(6)> :ok = Circuits.GPIO.set_interrupts(gpio, :both)
:ok
iex(7)> flush
{:circuits_gpio, 3, 190486901208, 1}
:ok
iex(8)> flush
{:circuits_gpio, 3, 194963466058, 0}
{:circuits_gpio, 3, 195712390258, 1}
:ok
iex(9)> flush
{:circuits_gpio, 3, 196928666676, 0}
{:circuits_gpio, 3, 197254295893, 1}

So that’s great. But I wrote a GenServer to monitor the button presses, and it seems I am not getting the messages. Sometimes I get the first message which has the initial state, but sometimes not. I think it’s probably a really basic mistake, maybe someone can spot it?

defmodule PoweroffDaemon do
  # When the button goes low, start a timer. 
  # If it does not go high before timer ticks, shut down.
  # If it goes high before the timer ticks, cancel the timer.
  use GenServer, restart: :temporary

  require Logger

  defstruct [:timer, :click_ref]

  def start_link(opts \\ []) do
    GenServer.start_link(__MODULE__, opts)
  end

  @impl GenServer
  def init(_) do
    IO.puts("starting server")
    {:ok, gpio} = Circuits.GPIO.open(3, :input)
    :ok = Circuits.GPIO.set_interrupts(gpio, :both)
    {:ok, %__MODULE__{}}
  end

  @impl GenServer
  def handle_info({:circuits_gpio, 3, _ts, 0}, state) do
    IO.puts("button went low")    
    click_ref = make_ref()
    timer = Process.send_after(self(), {:shutdown, click_ref}, 500)
    {:noreply, %{state | timer: timer, click_ref: click_ref}}
  end

  def handle_info({:circuits_gpio, 3, _ts, 1}, %{timer: nil} = state) do
    IO.puts("ignoring initial gpio high event")    
    {:noreply, state}
  end

  def handle_info({:circuits_gpio, 3, _ts, 1}, state) do
    IO.puts("button went high")    
    Process.cancel_timer(state.timer)
    {:noreply, %{state | timer: nil, click_ref: nil}}
  end

  def handle_info({:shutdown, click_ref}, %{click_ref: click_ref} = state) do
    IO.puts("SHUTDOWN!!!")    
    Nerves.Runtime.poweroff()
    {:noreply, state}
  end

  def handle_info({:shutdown, _click_ref}, state) do
    IO.puts("ignoring cancelled shutdown timer")    
    {:noreply, state}
  end

  def handle_info(msg, state) do
    IO.puts("unhandled msg msg=#{inspect(msg)}, state=#{inspect(state)}")    
    {:noreply, state}
  end
end
1 Like

Try saving the gpio reference in your GenServer’s state. The GPIO reference is getting garbage collected because there aren’t any references. Once it’s collected, it will stop sending messages.

3 Likes

Thanks, this worked! :tada: I updated the init callback so it looks like this:

defmodule PoweroffDaemon do
  # When the button goes low, start a timer. 
  # If it does not go high before timer ticks, shut down.
  # If it goes high before the timer ticks, cancel the timer.
  use GenServer, restart: :temporary
  defstruct [:gpio, :timer, :click_ref]
  ... 

  @impl GenServer
  def init(_) do
    IO.puts("starting server")
    {:ok, gpio} = Circuits.GPIO.open(3, :input)
    :ok = Circuits.GPIO.set_interrupts(gpio, :both)
    {:ok, %__MODULE__{gpio: gpio}}
  end
  ...
end
5 Likes