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).
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).
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.
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.
« Previous 1 2 3 Next »
Buy this article as PDF
(incl. VAT)