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

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.

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