Distributed programming made easy with Elixir

Multinode RPC Requests

The goal for the next project is to have a PC node query Pi nodes for diagnostic information. This project is a little different from the earlier project, in that a module is loaded on the Raspberry Pi to send back custom status messages (Figure 5).

Figure 5: Remote diagnostic from multiple nodes.

To get the Raspberry PI's CPU, for example, with a Bash command, use:

# Bash command to get the Pi CPU temperature
$ /opt/vc/bin/vcgencmd measure_temp

This Bash command can be incorporated into a small Elixir script (Listing 2) that is loaded and compiled on each of the Pi nodes. The PI_stats module in the PI_stats.ex script contains the function cpu_temp, which returns a string containing the Pi node name and the output from the shell command to get the CPU temperature.

Listing 2


01 #------------
02 # PI_stats.ex - Get Some Stats
03 #------------
04 defmodule PI_stats do
05   def cpu_temp() do
06    "#{Node.self()}  #{:os.cmd(:"/opt/vc/bin/vcgencmd measure_temp")}"
07   end
08   # Add more diagnostics like: available RAM, idle time ...
09 end

To compile Elixir scripts, use the elexirc command; then, their modules are available to iex shells called from that directory. The code to compile and then test the PI_stats module from a Raspberry Pi node is:

## compile an Elixir script
$ elixirc PI_stats.ex
## test the PI_stats.cpu_temp function locally
$ iex --name pi3@ --cookie pitest
iex> PI_stats.cpu_temp()
{"pi3@ temp=47.8\'C\n'}

An Erlang :rpc.multicall function can be used on the PC node to retrieve the Pi CPU temperatures. This function is passed the node list, module name, function call, and any additional arguments:

iex> :rpc.multicall( [:"pi3@", :"pi4@"], PI_stats, :cpu_temp, [])
 {["pi3@  temp=47.2'C\n", "pi4@ temp=43.8'C\n"], []}

The get_temps.exs script in Listing 3 is run on the PC to get the Raspberry Pi CPU temperatures and present the data in a Zenity dialog.

Listing 3


01 #----------------------------------------
02 # get_temps.exs - get PI CPU temperatures
03 #  - show results on Zenity Dialog
04 #----------------------------------------
05 pinodes = [ :"pi3@", :"pi4@"]
06 Enum.map(pinodes, fn x-> Node.connect x end)
08 # Get results from remote PI nodes
09 {result,_badnodes}  = :rpc.multicall( pinodes, PI_stats, :cpu_temp, [])
11 # Format the output for a Zenity info dialog
12 output = Enum.map(result, fn x -> x end) |> Enum.join
13 :os.cmd(:"zenity --info --text=\"#{output}\" --title='Pi Diagnostics'")

To make the code more flexible, all the Pi nodes are stored in a list (pinodes). The Eum.map function iterates over the Pi node list and connects to each node.

The results from the RPC multicall are a little messy, so the Enum.map and Enum.join functions format the results into one long string that is passed to a Zenity info dialog box.

As in the earlier project, the Elixir script is run with the common project cookie with a unique username (Figure 6).

Figure 6: Remote Pi CPU temperatures.

Note that once the PI_stats.ex script is compiled on the Pi nodes, no other action is required; as in the first project, the RPC request is processed by the underlying Erlang VM.

Data Sharing Between Nodes

Elixir offers a number of data storage options. For simple multinode data sharing, I found that the Erlang :mnesia package for the Mnesia database management system to be a good fit. In this last project, I set up a shared schema between the three nodes (Figure 7); the Pi nodes populate tables with their GPIO pin status every two seconds.

Figure 7: Distributed data sharing with Mnesia.

On the PC, I use the first project to write to the GPIO pins, and I create a new script to monitor the status of the pins within a Mnesia shared table. The :mnesia.create_schema function creates a shared schema for all the listed nodes. To create a shared or distributed schema, Mnesia needs to be stopped on all nodes; then, after the schema is created, Mnesia is restarted. The :rpc.multicall function is extremely useful when identical actions need to occur on distributed nodes:

iex> # Create a distributed schema
iex> allnodes = [ :"pete@" , :"pi3@", :"pi4@"]
iex> :rpc.multicall( allnodes, :mnesia, :stop, [])
iex> :mnesia.create_schema(allnodes)
iex> :rpc.multicall( allnodes, :mnesia, :start, [])

If a schema already exists, you need to delete it with the :mnesia.delete_schema([node()]) call before a new one can be created.

After creating a shared schema, the next step is to add a table (Pi3) of GPIO pin values for the Raspberry Pi 3:

iex> :mnesia.create_table(Pi3,  [attributes: [ :gpio, :value] ])

For nodes that are writing to a specific table, the table should be defined as both a RAM and disk copy. To do this, log in to that node and enter:

iex> :mnesia.change_table_copy_type(Pi3, node(), :disc_copies)

For large projects in which multiple nodes are reading and writing into tables, you should use transaction statements. For small projects that involve just one node writing into a table, you can use "dirty" reads and writes (i.e., uncommitted data in a database). To write the value 1 for pin 4 into the Pi3 table and read the record back, use:

iex> :mnesia.dirty_write({Pi3, 4,1})
iex> pin4val = :mnesia.dirty_read({Pi3, 4})
[{Pi3, 4, 1}]

Now that you can make simple writes and reads, the next step is to create a script that continually populates the Pi3 table with GPIO pin values.

Populating a Mnesia Table

The Elixir programming language has some interesting syntax features that allow you to write efficient code. Two features that will help streamline a table input function are anonymous and enumeration functions.

The ampersand (&) character creates a short hard (anonymous function) that can be created on the fly. The following code shows simple and complex examples that read a GPIO pin value and remove the trailing newline character:

iex> # A basic example
iex> sum = &(&1 + &2)
iex> sum.(2, 3)
iex> getpin=&(:os.cmd(:"gpio read #{(&1)} | tr -d \"\n\" ") )
iex> getpin.(7)

The Enum.map function can implement complex for-each loops. These two Elixir features together can read 27 Raspberry Pi GPIO pins and write data to a Mnesia table. The Gpio3write.exs script in Listing 4 writes GPIO values into a Mnesia table every two seconds.

Listing 4


01 #---------------
02 # Gpio3write.exs - Write Pi 3 GPIO values into Mnesia every 2 seconds
03 #---------------
04 defmodule Gpio3write do
05   def do_write do
06     getpin=&(:os.cmd(:"gpio read #{(&1)} | tr -d \"\n\" ") )
07     Enum.map(0..26, fn x-> :mnesia.dirty_write({Pi3, x, getpin.(x) }) end)
08     :timer.sleep(2000)
09     do_write()
10   end
11 end
12 # Start Mnesia
13 :mnesia.start()
14 # Cycle every 2 seconds and write values
15 Gpio3write.do_write()

The command

$ elixir --name pi3@ --cookie pitest Gpio3write.exs

starts the script on the Pi node.

Buy this article as PDF

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Linux Magazine

Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content

  • Elixir 1.0

    Developers will appreciate Elixir's ability to build distributed, fault-tolerant, and scalable applications.

  • RaspPi-Controlled Toy Sailboat

    With Node-RED, you can create a web dashboard that instructs a Raspberry Pi to set the rudder position on a toy sailboat.

  • WiFi Thermo-Hygrometer

    A WiFi sensor monitors indoor humidity and temperature and a Node-RED dashboard reports the results, helping you to maintain a pleasant environment.

  • Go on the Rasp Pi

    We show you how to create a Go web app that controls Raspberry Pi I/O.

  • Node-RED on Android

    We show you how to control devices connected to Rasp Pi GPIO pins with text messages from an Android phone.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More