-
-
Notifications
You must be signed in to change notification settings - Fork 113
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
6126bd9
commit c1a9db2
Showing
16 changed files
with
569 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.