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
temp=42.3'C

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

PI_stats.ex

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@192.168.0.105 --cookie pitest
iex> PI_stats.cpu_temp()
{"pi3@192.168.0.105 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@192.168.0.105", :"pi4@192.168.0.101"], PI_stats, :cpu_temp, [])
 {["pi3@192.168.0.105  temp=47.2'C\n", "pi4@192.168.0.101 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

get_temps.exs

01 #----------------------------------------
02 # get_temps.exs - get PI CPU temperatures
03 #  - show results on Zenity Dialog
04 #----------------------------------------
05 pinodes = [ :"pi3@192.168.0.105", :"pi4@192.168.0.101"]
06 Enum.map(pinodes, fn x-> Node.connect x end)
07
08 # Get results from remote PI nodes
09 {result,_badnodes}  = :rpc.multicall( pinodes, PI_stats, :cpu_temp, [])
10
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@192.168.0.120" , :"pi3@192.168.0.105", :"pi4@192.168.0.101"]
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})
:ok
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)
5
iex> getpin=&(:os.cmd(:"gpio read #{(&1)} | tr -d \"\n\" ") )
iex> getpin.(7)
'1'

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

Gpio3write.exs

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@192.168.0.105 --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

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
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.

  • Xonsh

    Create lightweight Raspberry Pi scripts with Xonsh, a Python shell that lets you write scripts in Python with Bash commands mixed in.

  • 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.

  • Julia on the Pi

    Create GUIs and a web app that connects to sensors.

comments powered by Disqus