Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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_lfe`](https://github.com/nerves-project/nerves-examples/blob/main/hello_lfe/README.md)
* [`hello_phoenix`](https://github.com/nerves-project/nerves-examples/blob/main/hello_phoenix/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
52 changes: 52 additions & 0 deletions hello_distribution/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# 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([email protected])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([email protected])3> Phoenix.PubSub.subscribe(HelloDistribution.PubSub, "test-event")
```

and the other:

```elixir
iex([email protected])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([email protected])> flush
...([email protected])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.
#
# 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 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.
97 changes: 97 additions & 0 deletions hello_distribution/config/target.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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]

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

config :nerves, :erlinit, update_clock: true

# 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