Skip to content

Instantly share code, notes, and snippets.

@lucaong
Last active February 13, 2018 13:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucaong/8dfd088c06cb6b53d1d4f9eb87c778c2 to your computer and use it in GitHub Desktop.
Save lucaong/8dfd088c06cb6b53d1d4f9eb87c778c2 to your computer and use it in GitHub Desktop.
Multiple SSIDs with NervesNetwork

Configuring multiple WiFi SSID on Nerves

This gist quickly demonstrates my setup to allow multiple WiFi SSID/PSK to be configured at the same time on Nerves. It relies on NervesNetwork, which is a dependency. Currently, this is bound to the "wlan0" interface, but it can be made more generic with little effort.

How it works

In the nerves_network configuration, I omit setting "wlan0" (I want to take care of it myself).

Then, I start the MyProject.WiFi GenServer (see code in wifi.ex) passing the WiFi configurations:

wifi_networks = [
  first_wifi: [
    ssid: "FirstWiFi",
    psk: "wifi_password_here",
    key_mgmt: :"WPA-PSK",
    priority: 10,
  ],
  second_wifi: [
    ssid: "SecondWiFi",
    psk: "wifi_password_here",
    key_mgmt: :"WPA-PSK",
    priority: 5,
  ]
]

{:ok, pid} = MyProject.WiFi.start_link(wifi_networks)

The WiFi module works very similarly to NervesNetwork. The trick is that, instead of sending SELECT_NETWORK to WpaSupplicant, it sends ENABLE_NETWORK (see the setup_ssid/3 function). With little changes, new SSIDs can be also added at runtime (by calling setup_ssid/3 again).

Improvements

  • Tidy up and improve the state machine code
  • Make the interface configurable
defmodule MyProject.WiFi do
alias Nerves.WpaSupplicant
alias Nerves.Leds
require Logger
use GenServer
@wpa_supplicant_path "/usr/sbin/wpa_supplicant"
@wpa_control_path "/var/run/wpa_supplicant"
@wpa_config_file "/tmp/nerves_network_wpa.conf"
@interface "wlan0"
@control_pipe "#{@wpa_control_path}/#{@interface}"
def start_link(networks) do
GenServer.start_link(__MODULE__, networks)
end
def interface, do: @interface
# OTP callbacks
def init(networks) do
{:ok, _} = Registry.register(Nerves.NetworkInterface, @interface, [])
{:ok, _} = Registry.register(Nerves.Udhcpc, @interface, [])
Leds.set debug: :slowblink
if Enum.member?(Nerves.NetworkInterface.interfaces, @interface) do
:ok = Nerves.NetworkInterface.ifup(@interface)
{:ok, {:added, networks, %{wpa_pid: nil, dhcp_pid: nil}}}
else
{:ok, {:start, networks, %{wpa_pid: nil, dhcp_pid: nil}}}
end
end
def handle_info({Nerves.NetworkInterface, :ifadded, _}, {:start, networks, pids}) do
:ok = Nerves.NetworkInterface.ifup(@interface)
{:noreply, {:added, networks, pids}}
end
def handle_info({Nerves.NetworkInterface, :ifchanged, %{is_up: true}}, {:added, networks, pids}) do
{:ok, wpa_pid} = start_wpa_supplicant()
{:ok, _} = Registry.register(Nerves.WpaSupplicant, @interface, [])
setup_wifi(wpa_pid, networks)
{:noreply, {:up, networks, %{pids | wpa_pid: wpa_pid}}}
end
def handle_info({Nerves.NetworkInterface, :ifchanged, %{is_up: false}}, {:up, networks, pids}) do
stop_wpa_supplicant(pids)
stop_dhcp(pids)
:ok = Nerves.Network.Resolvconf.clear(Nerves.Network.Resolvconf, @interface)
{:noreply, {:added, networks, %{wpa_pid: nil, dhcp_pid: nil}}}
end
def handle_info({Nerves.NetworkInterface, :ifremoved, _}, {:added, networks, pids}) do
stop_wpa_supplicant(pids)
stop_dhcp(pids)
{:noreply, {:start, networks, %{wpa_pid: nil, dhcp_pid: nil}}}
end
def handle_info({Nerves.WpaSupplicant, :"CTRL-EVENT-CONNECTED", _}, {:up, networks, pids}) do
{:ok, dhcp_pid} = start_dhcp(pids)
Leds.set debug: false
# Here is a good place to notify other processes of network changes
# (for example, I found I need to tell my MQTT client to reconnect)
{:noreply, {:connected, networks, %{pids | dhcp_pid: dhcp_pid}}}
end
def handle_info({Nerves.WpaSupplicant, :"CTRL-EVENT-DISCONNECTED", _}, {:connected, networks, pids}) do
stop_dhcp(pids)
Leds.set debug: :slowblink
{:noreply, {:up, networks, %{pids | dhcp_pid: nil}}}
end
def handle_info({Nerves.Udhcpc, :bound, info}, state) do
:ok = Nerves.NetworkInterface.setup(@interface, info)
:ok = Nerves.Network.Resolvconf.setup(Nerves.Network.Resolvconf, @interface, info)
Logger.debug(fn -> "WiFi - bound: #{@interface}" end)
{:noreply, state}
end
def handle_info(message, state) do
Logger.debug(fn -> "WiFi - unhandled message: #{inspect(message)}" end)
{:noreply, state}
end
# helper functions
defp start_wpa_supplicant() do
if !File.exists?(@control_pipe) do
File.write!(@wpa_config_file, "country=DE")
{_, 0} = System.cmd(@wpa_supplicant_path,
["-i#{@interface}",
"-c#{@wpa_config_file}",
"-C#{@wpa_control_path}",
"-Dnl80211,wext",
"-B"])
Logger.debug(fn -> "WiFi - started wpa_supplicant on #{@interface}" end)
end
:timer.sleep(300) # wait for pipe to be created
WpaSupplicant.start_link(@interface, @control_pipe, name: :"Nerves.WpaSupplicant.#{@interface}")
end
defp stop_wpa_supplicant(pids) do
%{wpa_pid: wpa_pid} = pids
if is_pid(wpa_pid), do: WpaSupplicant.stop(wpa_pid)
end
defp start_dhcp(pids) do
stop_dhcp(pids)
Nerves.Network.Udhcpc.start_link(@interface)
end
defp stop_dhcp(pids) do
%{dhcp_pid: dhcp_pid} = pids
if is_pid(dhcp_pid), do: Nerves.Network.Udhcpc.stop(dhcp_pid)
end
defp setup_wifi(pid, networks) do
WpaSupplicant.request(pid, {:REMOVE_NETWORK, :all})
networks |> Enum.each(fn {name, options} -> setup_ssid(pid, name, options) end)
end
defp setup_ssid(pid, name, options) do
nid = WpaSupplicant.request(pid, :ADD_NETWORK)
Enum.each(options, fn {key, value} ->
WpaSupplicant.request(pid, {:SET_NETWORK, nid, key, value})
end)
WpaSupplicant.request(pid, {:SET_NETWORK, nid, :id_str, Atom.to_string(name)})
WpaSupplicant.request(pid, {:ENABLE_NETWORK, nid})
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment