Structuring a Nerves/Phoenix application

Hi, I’m relatively new to the Elixir/OTP world and I’m wondering how to structure a Phoenix/Nerves application

I’m building a project to control IKEA lights on a Raspberry Pi, and I currently have a phoenix application alongside a nerves one in a ‘Poncho’ project.

I’m currently using Phoenix Pub/Sub to communicate between the two, which works well. However, I want to be able to keep track of the state of the lights which seems difficult. It makes sense to me to have the light state in the firmware part of the application where the lights are controlled, but I’m not sure how to access this state on demand from Phoenix.

I can’t depend on the Nerves application from the web one as this would create a circular dependency, and I don’t want to roll my own request/response system over PubSub.

What would be the best way to create a single source of truth for my application that both parts can depend on?

For those interested, the project is here: GitHub - th0mas/Fancy-Lights: A Raspberry Pi firmware for controlling IKEA Dioder lights.
Relevant files are in fancy_lights_firmware/lib/fancy_lights_firmware/light_manager.ex and fancy_lights_ui/lib/fancy_lights_ui_web/channels/light_channel.ex

Many Thanks.

2 Likes

Have you thought of using Phoenix presence?

I’ve run into the same sort of issue when learning how to use LED lights on my Raspberry Pi. I think the thing to remember is that both projects are running in the same BEAM instance, so they can communicate via normal process message passing. In my case, I gave each LED a Genserver in the nerves app, and hardcoded a name for it like so:

  def start_link(output_pin) do
    GenServer.start_link(
      __MODULE__,
      %{pin: output_pin, circuit_ref: nil, active: false},
      name: :red_light
    )
  end

So then from my Phoenix app I could just do the following: GenServer.call(:red_light, :turn_on) and it would work.

Not sure if that’s the “right” way to do it, but it doesn’t require using Phoenix PubSub from the Nerves end.

2 Likes

Funny enough, I was just having this exact same questionin a Nerves/Phoenix application. It makes sense to keep device state-sorts of things in the firmware side, but as soon as you start wanting to manipulate things from the Phoenix application, it gets messy!

There’s a couple approaches I’ve been thinking of taking. The easiest approach would be to just place all the business and UI logic both in the Phoenix app (in which case the “firmware” application would just be for packaging/bootstrapping the Phoenix application). As you say though, this doesn’t necessarily “feel” right.

Another approach would be to define the GenServers and their interfaces in the Phoenix layer, but actually have them supervised and run in the firmware layer. Then you could use your existing PubSub (or just the Registry) to inform the UI layer of their PIDs, but still have all the GenServer client code accessible from the UI layer.

Or, you could extract any code that both the UI and firmware need to be able to call out into a 3rd dependency, more of a “library” dependency than an application. Again, the processes could be started in and supervised by the firmware, but once the firmware told the UI what PIDs to use, the UI could call IkeaLights.change_colour/1.

(Of course in your sample application it looks like IkeaLights is a named GenServer, so I guess you wouldn’t even need to communicate the PID from the firmware to the UI application.)

I’m sure there are other good approaches, these are the first few that come to mind for me though.

1 Like

I was thinking about doing this and calling the GenServer straight from the UI application, but I was under the impression this wouldn’t be the most “idiomatic” approach. The ‘recommended’ way to interact with GenServers seems to be using wrappers to you don’t have to call GenServer.call/2 directly - although I could be wrong and an happy to be corrected on this.

On making a 3d dependency, this does seem like a good option that would make ‘logical’ sense, but it seems like a lot of overhead and boilerplate for what is essentially a toy project.

I’ve just done a bit of googling on this but there doesn’t seem to be an easy way to register a global ‘Presence’ that tracks my application state and can be accessed from outside the Phoenix app - am I missing something here?

Again, I’m new to the elixir way of doing things having come from a mostly imperative/OO background so all input is appreciated :slight_smile:

I’d say @jordan0day’s approach is the most idiomatic-simple. If you want something slightly more sophisticated you can set your nerves module to an “@ tribute” inside the Phoenix module, which lets you mock values in test (use Mox!!).

The presence idea I think is also idiomatic. Obviously more complex, but the chief difference being “it’s push” versus the simple module being “pull”, requiring a polling loop in your Phoenix module if you want real-time updates.

I’m not sure what you mean by global Presences? But I feel like Presence will take care of that for you*! Just remember to configure presence application only on one app (probably the one that has the pubsub) and only use it as a dependency on the other apps.

*Remember that presence tracks processes which by their nature are global. I have a system in prod where I track Presences and then forward the pubsub messages to an aggregator 1000s of miles away using TLS. Works just fine.

1 Like

Thanks for the input. For future reference, I’ve gone for the simple solution of calling a named GenServer from my UI application - I’ll leave figuring out Phoenix Presence for another day :slight_smile:

Thanks for all the help

2 Likes

If you’re interested in some other approaches: recently I’ve been using wrapper modules and “fakes” in combination with config to swap out the real nerves modules (e.g. networking, or communicating with a sensor) with some fake modules for local development & testing.

As an example, I have a module which talks to an AM2320 temperature sensor over I2C. Obviously this is only available on the device, but I still want to be able to develop my UI/application that uses the data, and write tests for it.

I use 2 projects side-by-side, “appleyard” (the main app) and “appleyard firmware” which contains very little except the nerves config and “client” modules which wrap any of the hardware code.

Here is the client for the AM2320:

defmodule Clients.AM2320 do
  alias Circuits.I2C

  @callback open() :: {:ok, I2C.bus()}
  def open(), do: I2C.open("i2c-1")

  @callback read(I2C.bus()) :: {:ok, float(), float()}
  def read(ref) do
    I2C.write(ref, 0x5C, <<0x03, 0x00, 0x04>>, retries: 1)
    {:ok, <<0x03, 0x04, humidity::size(16), temperature::size(16), _x::size(8), _y::size(8)>>} = I2C.read(ref, 0x5C, 8, retries: 1)
    {:ok, temperature, humidity}
  end

  defmodule Fake do
    def open(), do: :fake_ref
    def read(_ref), do: {:ok, :rand.uniform() * 50, :rand.uniform() * 50 + 50}
  end
end

In the main project I have this config:

config :appleyard, :am2320_client, Clients.AM2320.Fake

and in the firmware project’s target config I have

config :appleyard, :am2320_client, Clients.AM2320

In testing I can use Mox to setup specific values or expectations. And in development if I need more realistic data, it’s trivial to extend the Fake.

In this example, I’ve used a Genserver in my application project to maintain the i2c connection state, and it uses whichever client is configured.

2 Likes

Are you able to share your genserver code or github repo?

This exactly what I ended up doing recently!