DEV Community

Masatoshi Nishiguchi
Masatoshi Nishiguchi

Posted on

Elixir/Nerves: blinking LED

Introduction

When we first learn Nerves, one of the things we come up with is blinking LEDs on a breadboard.

Today I'd like to try one implementation using GenServer.

This is actually my outcome at this event I participated in last weekend "Shop for parts in Akihabara and get started on Nerves right away!". It was so much fun hanging out in Akihabara, Tokyo with Nerves enthusiasts.

Thank you very much to the organizers myasu and nako_sleep_9h!

myasu is very knowledgeable about electric and electronic stuff. I learned tons of pro tips from him, which I might write up as my future posts. He is the author of this book about Nerves.

nako_sleep_9h is awesome at organizing and mc'ing fun-driven engineering events across Japan. She wrote a report on Day 1 for this event.

めでたい、光った光った〜#piyopiyoex pic.twitter.com/5WBqnFftQU

— nako@9時間睡眠 (@nako_sleep_9h) June 22, 2024

Nerves Livebook

There are more than a few ways to get started with Nerves IoT projects but one of the easiest is to use prebuild Nerves Livebook firmware. Nerves Livebook allows us to control our target devices such as Raspberry Pi, doing Elixir coding in a web browser.

https://github.com/nerves-livebook/nerves_livebook/blob/main/README.md

Blink using Circuits GPIO

Nerves Livebook has a nice tutorial notebook about how to use the circuits_gpio package to control a GPIO as an output, and blink an LED. It shows us what electric parts we need minimally and how we wire them before getting down to business.

https://github.com/nerves-livebook/nerves_livebook/blob/main/priv/samples/basics/blink.livemd

Wrapping our LED blinking logic in GenServer

When we want to repeat the blinking forever but want to stop at some stage without restarting the device, it is nice to prepare a GenServer that manages the LED blinking. There can be many ways to achieve similar effect but here is one implementation using GenServer.

First, let's write a simple module that wraps GPIO-related logic within so that we can forget about how exactly we need to communicate with our LEDs.

defmodule LedBlink do
  def open(led_pin) do
    Circuits.GPIO.open(led_pin, :output)
  end

  def close(led_pin) do
    Circuits.GPIO.close(led_pin)
  end

  def toggle(gpio, 1), do: off(gpio)
  def toggle(gpio, 0), do: on(gpio)
  def toggle(gpio, _), do: on(gpio)

  def on(gpio) do
    :ok = Circuits.GPIO.write(gpio, 1)

    {:ok, 1}
  end

  def off(gpio) do
    :ok = Circuits.GPIO.write(gpio, 0)

    {:ok, 0}
  end
end
Enter fullscreen mode Exit fullscreen mode

Then next, we make a GenServer that lets an LED blink forever.

defmodule BlinkServer do
  use GenServer, restart: :temporary

  require Logger

  @run_interval_ms 1000

  ## Client

  @doc """
  Start a BlinkServer for the provided GPIO pin. It lets the LED blink forever.

  ### Examples

      iex>  BlinkServer.start_link(led_pin: "GPIO17")

  """
  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  def stop() do
    GenServer.stop(__MODULE__)
  end

  ## Callbacks

  @impl true
  def init(opts) do
    led_pin = Access.fetch!(opts, :led_pin)

    initial_state = %{
      gpio_ref: nil,
      led_state: 0,
      led_pin: led_pin
    }

    {:ok, initial_state, {:continue, :init_gpio}}
  end

  @impl true
  def handle_continue(:init_gpio, state) do
    case LedBlink.open(state.led_pin) do
      {:ok, gpio_ref} ->
        new_state = %{state | gpio_ref: gpio_ref}

        send(self(), :toggle_led_state)

        {:noreply, new_state}

      {:error, error} ->
        {:stop, error}
    end
  end

  @impl true
  def handle_info(:toggle_led_state, state) do
    {:ok, new_led_state} = LedBlink.toggle(state.gpio_ref, state.led_state)

    new_state = %{state | led_state: new_led_state}
    Logger.debug("toggled LED: #{new_state.led_state}")

    Process.send_after(self(), :toggle_led_state, @run_interval_ms)

    {:noreply, new_state}
  end

  @impl true
  def terminate(reason, state) do
    LedBlink.close(state.gpio_ref)
    Logger.debug("terminated: #{reason}")

    :ok
  end
end
Enter fullscreen mode Exit fullscreen mode

Here is how to use our BlinkServer.

When starting BlinkServer, we specify the GPIO pin we use for our LED.

When we are done experimenting, we can stop our running BlinkServer worker.

Logger.configure(level: :debug)

BlinkServer.start_link(led_pin: "GPIO17")

Process.sleep(10_000)

BlinkServer.stop()
Enter fullscreen mode Exit fullscreen mode

This implementation of BlinkServer is a singleton GenServer, meaning we are not allowed to start more than one BlinkServer workers. So if we change our mind on the GPIO pin we use, for example, we would need to stop the worker and start a new worker with different options.

circuits_gpio package

circuits_gpio is the one that lets us control GPIO in our Elixir code.

Wrapping up

We wrote a simple GenServer that lets our LED blink.

Once you get a feel of it, I would like to see your LED blinking logic. Please share!


今日は初Nerves!
8月末のSWESTに向けた知識を付けていこう~ https://t.co/SrGEUJjzkS

— FRICK (@TewiEwi_no96) June 21, 2024

Nerves勉強会2日目!
開始〜 https://t.co/zgMKDEGrH8 pic.twitter.com/rT7QiHslAa

— FRICK (@TewiEwi_no96) June 23, 2024

#piyopiyoex
【オフライン】秋葉原でパーツお買い物&そのままNerves入門!
はじまたー pic.twitter.com/BPiPBxZMTq

— ysaito (@ysaito8015) June 23, 2024

神田明神きたよ#piyopiyoex pic.twitter.com/xuxuiWcl2F

— nako@9時間睡眠 (@nako_sleep_9h) June 22, 2024

今日はこちらへ参加! https://t.co/L28N3O6nIF

— myasu (@etcinitd) June 22, 2024

今日はDay2!!https://t.co/hgJwwqGO9W

— myasu (@etcinitd) June 23, 2024

Top comments (0)