Skip to content

Commit

Permalink
Add distribution example
Browse files Browse the repository at this point in the history
  • Loading branch information
ConnorRigby committed Sep 13, 2021
1 parent 6126bd9 commit c1a9db2
Show file tree
Hide file tree
Showing 16 changed files with 569 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ application's root directory.

* [`blinky`](https://github.com/nerves-project/nerves-examples/blob/main/blinky/README.md)
* [`hello_erlang`](https://github.com/nerves-project/nerves-examples/blob/main/hello_erlang/README.md)
* [`hello_distribution`](https://github.com/nerves-project/nerves-examples/blob/main/hello_distribution/README.md)
* [`hello_gpio`](https://github.com/nerves-project/nerves-examples/blob/main/hello_gpio/README.md)
* [`hello_leds`](https://github.com/nerves-project/nerves-examples/blob/main/hello_leds/README.md)
* [`hello_lfe`](https://github.com/nerves-project/nerves-examples/blob/main/hello_lfe/README.md)
Expand Down
8 changes: 8 additions & 0 deletions hello_distribution/.formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Used by "mix format"
[
inputs: [
"{mix,.formatter}.exs",
"{config,lib,test}/**/*.{ex,exs}",
"rootfs_overlay/etc/iex.exs"
]
]
17 changes: 17 additions & 0 deletions hello_distribution/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# The directory Mix will write compiled artifacts to.
/_build/

# If you run "mix test --cover", coverage assets end up here.
/cover/

# The directory Mix downloads your dependencies sources to.
/deps/

# Where third-party dependencies like ExDoc output generated docs.
/doc/

# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch

# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
54 changes: 54 additions & 0 deletions hello_distribution/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Hello Distribution

This example builds a Nerves firmware image for supported Nerves devices that demonstrates using Mdns Lite, Erlang Distribution and Phoenix PubSub
to build a communication mechanism between two or more Nerves devices.

This example has all the same configuration as the [https://github.com/nerves-project/nerves_examples/tree/main/hello_wifi](Hello WiFi Example).

The first step will be to build firmware for your boards:

```bash
cd hello_distribution

# Set the target to rpi0, rpi3, or rpi4 depending on what you have
export MIX_TARGET=rpi0
mix deps.get
mix firmware

# Insert a MicroSD card or whatever media your board takes
mix burn
```

Next configure the board so it connects to you WiFi network.

Finally, open two ssh sessions - one to each board and use `Node.connect/1` to connect them via Erlang Distribution.
For example (you will need to replace `nerves-bea0.local` with your devices hostname.)

```elixir
iex(hello@nerves-080c.local)4> Node.connect(:"[email protected]")
true
```

Once connected, you can use Phoenix PubSub to send messages back and forth on the network:

on one device:

```elixir
iex(hello@nerves-bea0.local)3> Phoenix.PubSub.subscribe(HelloDistribution.PubSub, "test-event")
```

and the other:

```elixir
iex(hello@nerves-080c.local)5> Phoenix.PubSub.broadcast(HelloDistribution.PubSub, "test-event", {:hello, :world})
```

Now back on the first device, you should be able to see the message:

```elixir
iex(hello@nerves-bea0.local)4> receive do
...(hello@nerves-bea0.local)4> event -> IO.inspect(event)
...(hello@nerves-bea0.local)4> end
...(hello@nerves-bea0.local)5> flush()
{:hello, :world}
```
33 changes: 33 additions & 0 deletions hello_distribution/config/config.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.
import Config

# Enable the Nerves integration with Mix
Application.start(:nerves_bootstrap)

config :hello_distribution, target: Mix.target()

# Customize non-Elixir parts of the firmware. See
# https://hexdocs.pm/nerves/advanced-configuration.html for details.

config :nerves, :firmware, rootfs_overlay: "rootfs_overlay"

# Set the SOURCE_DATE_EPOCH date for reproducible builds.
# See https://reproducible-builds.org/docs/source-date-epoch/ for more information

config :nerves, source_date_epoch: "1630590634"

# Use Ringlogger as the logger backend and remove :console.
# See https://hexdocs.pm/ring_logger/readme.html for more information on
# configuring ring_logger.

config :logger, backends: [RingLogger]

if Mix.target() == :host or Mix.target() == :"" do
import_config "host.exs"
else
import_config "target.exs"
end
3 changes: 3 additions & 0 deletions hello_distribution/config/host.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import Config

# Add configuration that is only needed when running on the host here.
108 changes: 108 additions & 0 deletions hello_distribution/config/target.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import Config

# Use shoehorn to start the main application. See the shoehorn
# docs for separating out critical OTP applications such as those
# involved with firmware updates.

config :shoehorn,
init: [:nerves_runtime, :nerves_pack],
app: Mix.Project.config()[:app]

# Nerves Runtime can enumerate hardware devices and send notifications via
# SystemRegistry. This slows down startup and not many programs make use of
# this feature.

config :nerves_runtime, :kernel, use_system_registry: false

# Erlinit can be configured without a rootfs_overlay. See
# https://github.com/nerves-project/erlinit/ for more information on
# configuring erlinit.

config :nerves,
erlinit: [
hostname_pattern: "nerves-%s"
]

# Configure the device for SSH IEx prompt access and firmware updates
#
# * See https://hexdocs.pm/nerves_ssh/readme.html for general SSH configuration
# * See https://hexdocs.pm/ssh_subsystem_fwup/readme.html for firmware updates

keys =
[
Path.join([System.user_home!(), ".ssh", "id_rsa.pub"]),
Path.join([System.user_home!(), ".ssh", "id_ecdsa.pub"]),
Path.join([System.user_home!(), ".ssh", "id_ed25519.pub"])
]
|> Enum.filter(&File.exists?/1)

if keys == [],
do:
Mix.raise("""
No SSH public keys found in ~/.ssh. An ssh authorized key is needed to
log into the Nerves device and update firmware on it using ssh.
See your project's config.exs for this error message.
""")

config :nerves_ssh,
authorized_keys: Enum.map(keys, &File.read!/1)

# Configure the network using vintage_net
# See https://github.com/nerves-networking/vintage_net for more information
config :vintage_net,
regulatory_domain: "US",
additional_name_servers: [{127, 0, 0, 53}],
config: [
{"usb0", %{type: VintageNetDirect}},
{"eth0",
%{
type: VintageNetEthernet,
ipv4: %{method: :dhcp}
}},
{"wlan0", %{type: VintageNetWiFi}}
]

# Set the SSID for the network to join and the DNS name to use
# in the browser.
# see https://github.com/nerves-networking/vintage_net_wizard
config :vintage_net_wizard,
dns_name: "hello_wifi.config"

config :mdns_lite,
dns_bridge_enabled: true,
dns_bridge_ip: {127, 0, 0, 53},
dns_bridge_port: 53,
dns_bridge_recursive: true,
# The `host` key specifies what hostnames mdns_lite advertises. `:hostname`
# advertises the device's hostname.local. For the official Nerves systems, this
# is "nerves-<4 digit serial#>.local". mdns_lite also advertises
# "nerves.local" for convenience. If more than one Nerves device is on the
# network, delete "nerves" from the list.

host: [:hostname, "nerves"],
ttl: 120,

# Advertise the following services over mDNS.
services: [
%{
protocol: "ssh",
transport: "tcp",
port: 22
},
%{
protocol: "sftp-ssh",
transport: "tcp",
port: 22
},
%{
protocol: "epmd",
transport: "tcp",
port: 4369
}
]

# Import target specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
# Uncomment to use target specific configurations

# import_config "#{Mix.target()}.exs"
18 changes: 18 additions & 0 deletions hello_distribution/lib/hello_distribution.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule HelloDistribution do
@moduledoc """
Documentation for HelloDistribution.
"""

@doc """
Hello world.
## Examples
iex> HelloDistribution.hello
:world
"""
def hello do
{:ok, :world}
end
end
96 changes: 96 additions & 0 deletions hello_distribution/lib/hello_distribution/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
defmodule HelloDistribution.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false

use Application
require Logger

@ifname "wlan0"

def start(_type, _args) do
maybe_start_wifi_wizard()
maybe_start_distribution()
opts = [strategy: :one_for_one, name: HelloDistribution.Supervisor]
gpio_pin = Application.get_env(:hello_distribution, :button_pin, 17)

children = [
{HelloDistribution.Button, gpio_pin},
{Phoenix.PubSub, name: HelloDistribution.PubSub}
]

Supervisor.start_link(children, opts)
end

@doc false
def on_wizard_exit() do
# This function is used as a callback when the WiFi Wizard
# exits which is useful if you need to do work after
# configuration is done, like restart web servers that might
# share a port with the wizard, etc etc
Logger.info("[#{inspect(__MODULE__)}] - WiFi Wizard stopped")
end

def maybe_start_distribution() do
_ = :os.cmd('epmd -daemon')
{:ok, hostname} = :inet.gethostname()

case Node.start(:"hello@#{hostname}.local") do
{:ok, _pid} -> Logger.info("Distribution started at hello@#{hostname}.local")
_error -> Logger.error("Failed to start distribution")
end
end

def maybe_start_wifi_wizard() do
with true <- has_wifi?() || :no_wifi,
true <- wifi_configured?() || :not_configured,
true <- has_networks?() || :no_networks do
# By this point we know there is a wlan interface available
# and already configured with networks. This would normally
# mean that you should then skip starting the WiFi wizard
# here so that the device doesn't start the WiFi wizard after
# every reboot.
#
# However, for the example we want to always run the
# WiFi wizard on startup. Comment/remove the function below
# if you want a more typical experience skipping the wizard
# after it has been configured once.
VintageNetWizard.run_wizard(on_exit: {__MODULE__, :on_wizard_exit, []})
else
:no_wifi ->
Logger.error(
"[#{inspect(__MODULE__)}] Device does not support WiFi - Skipping wizard start"
)

status ->
info_message(status)
VintageNetWizard.run_wizard(on_exit: {__MODULE__, :on_wizard_exit, []})
end
end

def has_wifi?() do
@ifname in VintageNet.all_interfaces()
end

def wifi_configured?() do
@ifname in VintageNet.configured_interfaces()
end

def has_networks?() do
VintageNet.get_configuration(@ifname)[:vintage_net_wifi][:networks] != []
end

def info_message(status) do
msg =
case status do
:not_configured -> "WiFi has not been configured"
:no_networks -> "WiFi was configured without any networks"
end

Logger.info("[#{inspect(__MODULE__)}] #{msg} - Starting WiFi Wizard")
end

def target() do
Application.get_env(:hello_distribution, :target)
end
end
44 changes: 44 additions & 0 deletions hello_distribution/lib/hello_distribution/button.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
defmodule HelloDistribution.Button do
use GenServer

@moduledoc """
This GenServer starts the wizard if a button is depressed for long enough.
"""

alias Circuits.GPIO

@doc """
Start the button monitor
Pass an index to the GPIO that's connected to the button.
"""
@spec start_link(non_neg_integer()) :: GenServer.on_start()
def start_link(gpio_pin) do
GenServer.start_link(__MODULE__, gpio_pin)
end

@impl true
def init(gpio_pin) do
{:ok, gpio} = GPIO.open(gpio_pin, :input)
:ok = GPIO.set_interrupts(gpio, :both)
{:ok, %{pin: gpio_pin, gpio: gpio}}
end

@impl true
def handle_info({:circuits_gpio, gpio_pin, _timestamp, 1}, %{pin: gpio_pin} = state) do
# Button pressed. Start a timer to launch the wizard when it's long enough
{:noreply, state, 5_000}
end

@impl true
def handle_info({:circuits_gpio, gpio_pin, _timestamp, 0}, %{pin: gpio_pin} = state) do
# Button released. The GenServer timer is implicitly cancelled by receiving this message.
{:noreply, state}
end

@impl true
def handle_info(:timeout, state) do
:ok = VintageNetWizard.run_wizard(on_exit: {HelloDistribution, :on_wizard_exit, []})
{:noreply, state}
end
end
Loading

0 comments on commit c1a9db2

Please sign in to comment.