Skip to content

Commit c1a9db2

Browse files
committed
Add distribution example
1 parent 6126bd9 commit c1a9db2

16 files changed

+569
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ application's root directory.
2525

2626
* [`blinky`](https://github.com/nerves-project/nerves-examples/blob/main/blinky/README.md)
2727
* [`hello_erlang`](https://github.com/nerves-project/nerves-examples/blob/main/hello_erlang/README.md)
28+
* [`hello_distribution`](https://github.com/nerves-project/nerves-examples/blob/main/hello_distribution/README.md)
2829
* [`hello_gpio`](https://github.com/nerves-project/nerves-examples/blob/main/hello_gpio/README.md)
2930
* [`hello_leds`](https://github.com/nerves-project/nerves-examples/blob/main/hello_leds/README.md)
3031
* [`hello_lfe`](https://github.com/nerves-project/nerves-examples/blob/main/hello_lfe/README.md)

hello_distribution/.formatter.exs

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Used by "mix format"
2+
[
3+
inputs: [
4+
"{mix,.formatter}.exs",
5+
"{config,lib,test}/**/*.{ex,exs}",
6+
"rootfs_overlay/etc/iex.exs"
7+
]
8+
]

hello_distribution/.gitignore

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump

hello_distribution/README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Hello Distribution
2+
3+
This example builds a Nerves firmware image for supported Nerves devices that demonstrates using Mdns Lite, Erlang Distribution and Phoenix PubSub
4+
to build a communication mechanism between two or more Nerves devices.
5+
6+
This example has all the same configuration as the [https://github.com/nerves-project/nerves_examples/tree/main/hello_wifi](Hello WiFi Example).
7+
8+
The first step will be to build firmware for your boards:
9+
10+
```bash
11+
cd hello_distribution
12+
13+
# Set the target to rpi0, rpi3, or rpi4 depending on what you have
14+
export MIX_TARGET=rpi0
15+
mix deps.get
16+
mix firmware
17+
18+
# Insert a MicroSD card or whatever media your board takes
19+
mix burn
20+
```
21+
22+
Next configure the board so it connects to you WiFi network.
23+
24+
Finally, open two ssh sessions - one to each board and use `Node.connect/1` to connect them via Erlang Distribution.
25+
For example (you will need to replace `nerves-bea0.local` with your devices hostname.)
26+
27+
```elixir
28+
iex(hello@nerves-080c.local)4> Node.connect(:"[email protected]")
29+
true
30+
```
31+
32+
Once connected, you can use Phoenix PubSub to send messages back and forth on the network:
33+
34+
on one device:
35+
36+
```elixir
37+
iex(hello@nerves-bea0.local)3> Phoenix.PubSub.subscribe(HelloDistribution.PubSub, "test-event")
38+
```
39+
40+
and the other:
41+
42+
```elixir
43+
iex(hello@nerves-080c.local)5> Phoenix.PubSub.broadcast(HelloDistribution.PubSub, "test-event", {:hello, :world})
44+
```
45+
46+
Now back on the first device, you should be able to see the message:
47+
48+
```elixir
49+
iex(hello@nerves-bea0.local)4> receive do
50+
...(hello@nerves-bea0.local)4> event -> IO.inspect(event)
51+
...(hello@nerves-bea0.local)4> end
52+
...(hello@nerves-bea0.local)5> flush()
53+
{:hello, :world}
54+
```

hello_distribution/config/config.exs

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
#
4+
# This configuration file is loaded before any dependency and
5+
# is restricted to this project.
6+
import Config
7+
8+
# Enable the Nerves integration with Mix
9+
Application.start(:nerves_bootstrap)
10+
11+
config :hello_distribution, target: Mix.target()
12+
13+
# Customize non-Elixir parts of the firmware. See
14+
# https://hexdocs.pm/nerves/advanced-configuration.html for details.
15+
16+
config :nerves, :firmware, rootfs_overlay: "rootfs_overlay"
17+
18+
# Set the SOURCE_DATE_EPOCH date for reproducible builds.
19+
# See https://reproducible-builds.org/docs/source-date-epoch/ for more information
20+
21+
config :nerves, source_date_epoch: "1630590634"
22+
23+
# Use Ringlogger as the logger backend and remove :console.
24+
# See https://hexdocs.pm/ring_logger/readme.html for more information on
25+
# configuring ring_logger.
26+
27+
config :logger, backends: [RingLogger]
28+
29+
if Mix.target() == :host or Mix.target() == :"" do
30+
import_config "host.exs"
31+
else
32+
import_config "target.exs"
33+
end

hello_distribution/config/host.exs

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import Config
2+
3+
# Add configuration that is only needed when running on the host here.

hello_distribution/config/target.exs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Config
2+
3+
# Use shoehorn to start the main application. See the shoehorn
4+
# docs for separating out critical OTP applications such as those
5+
# involved with firmware updates.
6+
7+
config :shoehorn,
8+
init: [:nerves_runtime, :nerves_pack],
9+
app: Mix.Project.config()[:app]
10+
11+
# Nerves Runtime can enumerate hardware devices and send notifications via
12+
# SystemRegistry. This slows down startup and not many programs make use of
13+
# this feature.
14+
15+
config :nerves_runtime, :kernel, use_system_registry: false
16+
17+
# Erlinit can be configured without a rootfs_overlay. See
18+
# https://github.com/nerves-project/erlinit/ for more information on
19+
# configuring erlinit.
20+
21+
config :nerves,
22+
erlinit: [
23+
hostname_pattern: "nerves-%s"
24+
]
25+
26+
# Configure the device for SSH IEx prompt access and firmware updates
27+
#
28+
# * See https://hexdocs.pm/nerves_ssh/readme.html for general SSH configuration
29+
# * See https://hexdocs.pm/ssh_subsystem_fwup/readme.html for firmware updates
30+
31+
keys =
32+
[
33+
Path.join([System.user_home!(), ".ssh", "id_rsa.pub"]),
34+
Path.join([System.user_home!(), ".ssh", "id_ecdsa.pub"]),
35+
Path.join([System.user_home!(), ".ssh", "id_ed25519.pub"])
36+
]
37+
|> Enum.filter(&File.exists?/1)
38+
39+
if keys == [],
40+
do:
41+
Mix.raise("""
42+
No SSH public keys found in ~/.ssh. An ssh authorized key is needed to
43+
log into the Nerves device and update firmware on it using ssh.
44+
See your project's config.exs for this error message.
45+
""")
46+
47+
config :nerves_ssh,
48+
authorized_keys: Enum.map(keys, &File.read!/1)
49+
50+
# Configure the network using vintage_net
51+
# See https://github.com/nerves-networking/vintage_net for more information
52+
config :vintage_net,
53+
regulatory_domain: "US",
54+
additional_name_servers: [{127, 0, 0, 53}],
55+
config: [
56+
{"usb0", %{type: VintageNetDirect}},
57+
{"eth0",
58+
%{
59+
type: VintageNetEthernet,
60+
ipv4: %{method: :dhcp}
61+
}},
62+
{"wlan0", %{type: VintageNetWiFi}}
63+
]
64+
65+
# Set the SSID for the network to join and the DNS name to use
66+
# in the browser.
67+
# see https://github.com/nerves-networking/vintage_net_wizard
68+
config :vintage_net_wizard,
69+
dns_name: "hello_wifi.config"
70+
71+
config :mdns_lite,
72+
dns_bridge_enabled: true,
73+
dns_bridge_ip: {127, 0, 0, 53},
74+
dns_bridge_port: 53,
75+
dns_bridge_recursive: true,
76+
# The `host` key specifies what hostnames mdns_lite advertises. `:hostname`
77+
# advertises the device's hostname.local. For the official Nerves systems, this
78+
# is "nerves-<4 digit serial#>.local". mdns_lite also advertises
79+
# "nerves.local" for convenience. If more than one Nerves device is on the
80+
# network, delete "nerves" from the list.
81+
82+
host: [:hostname, "nerves"],
83+
ttl: 120,
84+
85+
# Advertise the following services over mDNS.
86+
services: [
87+
%{
88+
protocol: "ssh",
89+
transport: "tcp",
90+
port: 22
91+
},
92+
%{
93+
protocol: "sftp-ssh",
94+
transport: "tcp",
95+
port: 22
96+
},
97+
%{
98+
protocol: "epmd",
99+
transport: "tcp",
100+
port: 4369
101+
}
102+
]
103+
104+
# Import target specific config. This must remain at the bottom
105+
# of this file so it overrides the configuration defined above.
106+
# Uncomment to use target specific configurations
107+
108+
# import_config "#{Mix.target()}.exs"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
defmodule HelloDistribution do
2+
@moduledoc """
3+
Documentation for HelloDistribution.
4+
"""
5+
6+
@doc """
7+
Hello world.
8+
9+
## Examples
10+
11+
iex> HelloDistribution.hello
12+
:world
13+
14+
"""
15+
def hello do
16+
{:ok, :world}
17+
end
18+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
defmodule HelloDistribution.Application do
2+
# See https://hexdocs.pm/elixir/Application.html
3+
# for more information on OTP Applications
4+
@moduledoc false
5+
6+
use Application
7+
require Logger
8+
9+
@ifname "wlan0"
10+
11+
def start(_type, _args) do
12+
maybe_start_wifi_wizard()
13+
maybe_start_distribution()
14+
opts = [strategy: :one_for_one, name: HelloDistribution.Supervisor]
15+
gpio_pin = Application.get_env(:hello_distribution, :button_pin, 17)
16+
17+
children = [
18+
{HelloDistribution.Button, gpio_pin},
19+
{Phoenix.PubSub, name: HelloDistribution.PubSub}
20+
]
21+
22+
Supervisor.start_link(children, opts)
23+
end
24+
25+
@doc false
26+
def on_wizard_exit() do
27+
# This function is used as a callback when the WiFi Wizard
28+
# exits which is useful if you need to do work after
29+
# configuration is done, like restart web servers that might
30+
# share a port with the wizard, etc etc
31+
Logger.info("[#{inspect(__MODULE__)}] - WiFi Wizard stopped")
32+
end
33+
34+
def maybe_start_distribution() do
35+
_ = :os.cmd('epmd -daemon')
36+
{:ok, hostname} = :inet.gethostname()
37+
38+
case Node.start(:"hello@#{hostname}.local") do
39+
{:ok, _pid} -> Logger.info("Distribution started at hello@#{hostname}.local")
40+
_error -> Logger.error("Failed to start distribution")
41+
end
42+
end
43+
44+
def maybe_start_wifi_wizard() do
45+
with true <- has_wifi?() || :no_wifi,
46+
true <- wifi_configured?() || :not_configured,
47+
true <- has_networks?() || :no_networks do
48+
# By this point we know there is a wlan interface available
49+
# and already configured with networks. This would normally
50+
# mean that you should then skip starting the WiFi wizard
51+
# here so that the device doesn't start the WiFi wizard after
52+
# every reboot.
53+
#
54+
# However, for the example we want to always run the
55+
# WiFi wizard on startup. Comment/remove the function below
56+
# if you want a more typical experience skipping the wizard
57+
# after it has been configured once.
58+
VintageNetWizard.run_wizard(on_exit: {__MODULE__, :on_wizard_exit, []})
59+
else
60+
:no_wifi ->
61+
Logger.error(
62+
"[#{inspect(__MODULE__)}] Device does not support WiFi - Skipping wizard start"
63+
)
64+
65+
status ->
66+
info_message(status)
67+
VintageNetWizard.run_wizard(on_exit: {__MODULE__, :on_wizard_exit, []})
68+
end
69+
end
70+
71+
def has_wifi?() do
72+
@ifname in VintageNet.all_interfaces()
73+
end
74+
75+
def wifi_configured?() do
76+
@ifname in VintageNet.configured_interfaces()
77+
end
78+
79+
def has_networks?() do
80+
VintageNet.get_configuration(@ifname)[:vintage_net_wifi][:networks] != []
81+
end
82+
83+
def info_message(status) do
84+
msg =
85+
case status do
86+
:not_configured -> "WiFi has not been configured"
87+
:no_networks -> "WiFi was configured without any networks"
88+
end
89+
90+
Logger.info("[#{inspect(__MODULE__)}] #{msg} - Starting WiFi Wizard")
91+
end
92+
93+
def target() do
94+
Application.get_env(:hello_distribution, :target)
95+
end
96+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule HelloDistribution.Button do
2+
use GenServer
3+
4+
@moduledoc """
5+
This GenServer starts the wizard if a button is depressed for long enough.
6+
"""
7+
8+
alias Circuits.GPIO
9+
10+
@doc """
11+
Start the button monitor
12+
13+
Pass an index to the GPIO that's connected to the button.
14+
"""
15+
@spec start_link(non_neg_integer()) :: GenServer.on_start()
16+
def start_link(gpio_pin) do
17+
GenServer.start_link(__MODULE__, gpio_pin)
18+
end
19+
20+
@impl true
21+
def init(gpio_pin) do
22+
{:ok, gpio} = GPIO.open(gpio_pin, :input)
23+
:ok = GPIO.set_interrupts(gpio, :both)
24+
{:ok, %{pin: gpio_pin, gpio: gpio}}
25+
end
26+
27+
@impl true
28+
def handle_info({:circuits_gpio, gpio_pin, _timestamp, 1}, %{pin: gpio_pin} = state) do
29+
# Button pressed. Start a timer to launch the wizard when it's long enough
30+
{:noreply, state, 5_000}
31+
end
32+
33+
@impl true
34+
def handle_info({:circuits_gpio, gpio_pin, _timestamp, 0}, %{pin: gpio_pin} = state) do
35+
# Button released. The GenServer timer is implicitly cancelled by receiving this message.
36+
{:noreply, state}
37+
end
38+
39+
@impl true
40+
def handle_info(:timeout, state) do
41+
:ok = VintageNetWizard.run_wizard(on_exit: {HelloDistribution, :on_wizard_exit, []})
42+
{:noreply, state}
43+
end
44+
end

0 commit comments

Comments
 (0)