IEx Remote Shell into your Elixir driven GRiSP Board

A quick tutorial on opening a remote connection to your GRiSP board. This facilitates quicker development iterations, better shell functionality and remote debugging.


Motivation

The GRiSP framework, and the GRiSP board combines custom made software and hardware, into a system that allows us to run a version of the Erlang VM(Beam) on a lightweight real-time operating system. This gives us access to the high level features provided by Erlang(or Elixir), on less powerful hardware than one would typically need. In the long run this could lead to easier fault tolerance, easier use of concurrency, higher developer productivity and easier role-out of massively distributed systems.

In the meantime, development is still a bit tricky. Boot times for the board is slow enough to make iterations frustrating, and the serial terminal connection does not have all the features we are used to on our personal computers. Both of these issues can be solved by connecting a terminal on your personal computer to the target board via a WiFi connection. In this terminal we get faster response and more powerful features, and we can even use the remote connection to upload code into the devices memory.

There are already very good instructions available on how to do this for pure Erlang development. This post is for those of you who, like me, want to program in Elixir instead of Erlang, but do not have the necessary Erlang background for the existing instructions to be sufficient.


The basics of working with GRiSP

In a previous post, I described how to get started with running Elixir code on the GRiSP platform. The post included the preparation of the local environment, deployment of a basic application, serial connection with the device and some blinking light examples. I would like to refer you to this post if you are at all unclear on these details. For now, I would like to focus mostly on the topic at hand(apart from generating an example app).


Generating a our example App

We generate a normal Elixir app with a supervisor using the mix:

mix new grisp_wifi_connect --sup

In our mix.exs file, we add the same dependencies as in our blinking light example.

defp deps do
[
{:grisp, "~> 1.1"},
{:mix_grisp, "~> 0.1.0", only: :dev}
]
end

Follow this by mix deps.get to fetch the added dependencies.

Next, add the needed grisp configuration, this is also in mix.exs. The following snippet goes below start_permanent: Mix.env() == :prod,.

 deps: deps(),
grisp: [
otp: [verson: "21.0"],
deploy: [
destination: "/media/theuns/GRISP"
]
]

Remember to replace /media/theuns/GRISP with the path to your own SD card.

Finally we change extra_applications: [:logger] in the list of started apps to the following: extra_applications: [:grisp, :iex, :logger]


Preparing for release

As before, we will be building a release with Distillery.

Initiate the release config by running mix release.init

Replace the contents of /rel/config.exs with the following:

~w(rel plugins *.exs)
|> Path.join()
|> Path.wildcard()
|> Enum.map(&Code.eval_file(&1))
use Mix.Releases.Config,
default_release: :default,
default_environment: :grisp
environment :grisp do
set include_erts: true
set include_src: false
set cookie: :"GRiSP"
end
release :grisp_wifi_connect do
set version: current_version(:grisp_wifi_connect)
end

Runtime Confguration

As with our blinking light example, we need to add a configuration template that will tell our device how to boot.

To this end, we add grisp/grisp_base/files/grisp.ini.mustache. In this we place the following contents:

boot]
image_path = /media/mmcsd-0-0/{{release_name}}/erts-{{erts_vsn}}/bin/beam.bin
[erlang]
args = erl.rtems -- -mode embedded -home . -pa . -root {{release_name}} -boot {{release_name}}/releases/{{release_version}}/{{release_name}} -s elixir start_cli -noshell -user Elixir.IEx.CLI -extra --no-halt
[network]
ip_self = dhcp
wlan = enable

Networking configuration

With the previous steps completed, we now have an app that we could deploy. Let us now modify this app to give us the WiFi access that we are after.

In order to achieve this, we need to make the following changes:

  1. Add WiFi configuration to our template.
  2. Add epmd(Erlang Port Mapping Daemon) as a dependency to in our project to facilitate address lookup for our Erlang nodes.
  3. Add a configuration file to our repository, to set the network configuration for Erlang.
  4. Adapt our Erlang boot configuration to start epmd as a part of the boot process.

Each of these steps will now be discussed in a bit more detail:

WiFi configuration

For the wifi configuration we add two lines to the [network] section of grisp/grisp_base/files/grisp.ini.mustache. The first line, hostname = my_grisp_board, literally sets the name of the host instance that will be running on the board. The second line, wpa = wpa_supplicant.conf, sets the location for the WiFi connection info to: grisp/grisp_base/files/wpa_supplicant.conf.

The final contents of the [network] section looks as follows:

[network]
ip_self = dhcp
wlan = enable
hostname = my_grisp_board
wpa = wpa_supplicant.conf

Next we add the wifi config file that was mentioned above(grisp/grisp_base/files/wpa_supplicant.conf), and add to it the contents shown below.

network={
ssid="my_ssid"
key_mgmt=WPA-PSK
psk="my_password"
}

Note that my_password should be replaced with your network password, and my_ssid should be replaced with your own wireless access point id.

Additional dependencies

To add epmd to our project, adjust the project deps to look as follows:

defp deps do
[
{:grisp, "~> 1.1"},
{:mix_grisp, "~> 0.1.0", only: :dev},
{:epmd, git: "https://github.com/erlang/epmd.git", ref: "4d1a59", app: false}
]
end

As per usual, this is followed by mix deps.get. The empd package will now be available in our built release. This daemon allows our different Erlang nodes to find each others` addresses, using the name of the node as an identifier. For the current example the we will have two Erlang nodes. The first node is the application running on the GRiSP board, and the second is the IEx terminal running on our development PC.

Note what we set the app option for epmd to false. This stops our supervision tree from automatically starting up the epmd application. We will be starting up the epmd as a part of the boot process, which would cause our app to crash if our supervision tree also attempted to start it.

Add Erlang network configuration

We add a file: grisp/grisp_base/files/erl_inetrc and add to it the following contents:

%--- Erlang Inet Configuration --------------------------
% Add hosts
{host, {X,X,X,X}, ["my_host"]}.
% Do not monitor the hosts file
{hosts_file, ""}.
% Disable caching
{cache_size, 0}.
% Specify lookup method
{lookup, [file, native]}.

Here we need to replace X,X,X,X with the real IP address of our development PC, and “my_host” is the name of the host name on the same PC.

Erlang boot configuration

We change the grisp/grisp_base/files/grisp.ini.mustache, this time changing the arguments that we pass in when we start up our Erlang runtime.

We add the following arguments:

  1. -internal_epmd epmd_sup — this sets the module that manages the address mapping.
  2. -kernel inetrc "./erl_inetrc” — this sets the location of the user-defined IP configuration.
  3. -sname {{release_name}} — this sets the name of the node equal to our app name.
  4. -setcookie GRiSP — sets the shared secret that will allow our nodes to securely communicate with each other(note that in the future, for security purposes, you will need to set the value to something other than GRiSP)

In the end, the [erlang] section in the configuration file should look as follows:

[boot]
image_path = /media/mmcsd-0-0/{{release_name}}/erts-{{erts_vsn}}/bin/beam.bin
[erlang]
args = erl.rtems -- -home . -pa . -root {{release_name}} -boot {{release_name}}/releases/{{release_version}}/{{release_name}} -internal_epmd epmd_sup -kernel inetrc "./erl_inetrc" -sname {{release_name}} -setcookie GRiSP -s elixir start_cli -noshell -user Elixir.IEx.CLI -extra --no-halt
[network]
ip_self = dhcp
wlan = enable
hostname = my_grisp_board
wpa = wpa_supplicant.conf

Boot up and connect

With most of the config in place, we can now deploy our app. To do so, we mount our memory card on the development machine, and run mix grisp.deploy. After doing so, eject the card and place it in the card slot on the GRiSP board. Now, plug in the usb cable and wait for the device to boot up(this may take a while). If you would like to keep track of what is happening, connect to your device over the serial connection as discussed in my previous post.

Also keep an eye on your router and grab the IP address of your GRiSP board once it is connected. I used a wireless hot-spot on my Android smartphone to have this information readily available.

Add the IP address to the rest of the hosts in /etc/hosts on your development PC. Remember that the host name needs to match the configuration on the GRiSP board. So the entry should have the following form:

X.X.X.X   grisp_wifi_connect

Now we are ready to connect nodes running on the different machines.

First test the connection by pinging the board from your development PC. To do so, run the following in a terminal on your development PC(with the working directory set to the projects root):

$ iex --sname my_local_shell --cookie GRiSP -S mix run --no-start

Note that this opens an IEx terminal without starting our app. By giving the node a name, epmd will automatically be started. This allows the remote connections to and from this node(provided the host configuration has been set correctly).

Within the terminal that is now open, run Node.ping(:grisp_wifi_connect@my_grisp_board) the GRiSP should respond with :pong if our local node could reach it.

We could now actually spawn tasks on the board already using Node.spawn/4(try it out when you have a chance).

But let us open the remote terminal like we had set out to do.

Start by closing the current terminal with CTRL + C now open a new terminal using the --remsh option for IEx. The required command is shown below:

$ iex --sname my_remote_shell --cookie GRiSP --remsh "grisp_wifi_connect@my_grisp_board"

You should now have a remote terminal open.

Playing around with our terminal

With the terminal now open, let us quickly try out a few commands.

For this, we will reuse some of the things we learnt in our blinking light tutorial.

Try the following two commands, one after the other:

:grisp_led.color(1, :red)

and

:grisp_led.off(1)

The red light should now toggle on and off.

Next, let us see how we can load code into the RAM of the board through our remote terminal.

Copy the following code and paste it into the remote shell(note that this is the same module that we wrote in the blinking light example):

defmodule GrispBlink.Blinker do
use Agent
def start_link(_) do
Agent.start_link(fn -> false end, name: __MODULE__)
end
def toggle_led() do
Agent.get_and_update(__MODULE__, fn(state) ->
toggle_led(state)
{state, not state}
end)
end
def toggle_led(true) do
:grisp_led.color(1, :red)
end
def toggle_led(false) do
:grisp_led.off(1)
end
end

Hit enter and wait. You will see that the code is live-compiled. The output of the compilation will look as follows:

{:module, GrispBlink.Blinker,
<<70, 79, 82, 49, 0, 0, 8, 128, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 42,
0, 0, 0, 28, 25, 69, 108, 105, 120, 105, 114, 46, 71, 114, 105, 115, 112, 66,
108, 105, 110, 107, 46, 66, 108, 105, 110, ...>>, {:toggle_led, 1}}

You have just pasted the code for a module into a terminal on your PC, which is now available for execution on your GRiSP board. How cool is that?

Lets make sure it works.

Start the up the Agent module using GrispBlink.Blinker.start_link([]), and toggle the LED using GrispBlink.Blinker.toggle_led(). You will see the LED blink just like when we manually deployed the code to the board.

You will also be able to automate the blinking like before. To do this, execute the following command in the terminal {:ok, pid} = :timer.apply_interval(1000, GrispBlink.Blinker, :toggle_led, []). This should blink the light every second. As before, kill it using :timer.cancel(pid).

This is as far as we will go for now. But feel free to test the limits of your newly created remote terminal.

Conclusion

We have illustrated how one can get a useful level of control over the GRiSP board, using a WiFi connection. In the process we managed to execute some basic commands from the grisp package, as well as paste custom modules directly into the terminal. This, however, only scratches the surface of what is possible with this type of distributed computing. I invite you to play around, contribute and push the boundaries. And when you do so, please also share what you have learnt. I myself and many other people would love to hear about it.