Update farmbot_os app to support eap + cleanup

pull/974/head
connor rigby 2019-01-23 08:39:45 -08:00 committed by Connor Rigby
parent 87c3ecf83b
commit a2339546e1
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
9 changed files with 41 additions and 796 deletions

View File

@ -36,7 +36,18 @@ defmodule Farmbot.Config.NetworkInterface do
def changeset(config, params \\ %{}) do
config
|> cast(params, @required_fields ++ [:ssid, :psk, :security, :ipv4_method, :ipv4_address, :ipv4_gateway, :ipv4_subnet_mask, :domain, :name_servers])
|> cast(params, @required_fields ++ [:ssid,
:psk,
:security,
:identity,
:password
:ipv4_method,
:ipv4_address,
:ipv4_gateway,
:ipv4_subnet_mask,
:domain,
:name_servers
])
|> validate_required(@required_fields)
|> validate_inclusion(:type, ["wireless", "wired"])
|> unique_constraint(:name)

View File

@ -2,9 +2,9 @@ defmodule Farmbot.System.ConfigStorage.Migrations.AddEapSettings do
use Ecto.Migration
def change do
alter table("network_interfaces") do
add :identity, :string
add :password, :string
alter table("network_interfaces") do
add(:identity, :string)
add(:password, :string)
end
end
end

View File

@ -20,7 +20,8 @@ defmodule Farmbot.OS.IOLayer do
# Reporting commands
def read_status(_args, _body) do
Farmbot.BotState.fetch()
Farmbot.AMQP.BotStateNGTransport.force()
Farmbot.AMQP.BotStateTransport.force()
:ok
end

View File

@ -32,6 +32,7 @@
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"muontrap": {:hex, :muontrap, "0.4.0", "f3c48f5e2cbb89b6406d28e488fbd0da1ce0ca00af332860913999befca9688a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"},
"nerves": {:hex, :nerves, "1.3.4", "9523cc1936f173c99cf15a132c2b24f9c6f1a5cfe3327bbcd518ff7e441327d3", [:mix], [{:distillery, "2.0.10", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"nerves_hub_cli": {:hex, :nerves_hub_cli, "0.5.1", "9e00c23678c4f34c05b7c2fda4ad04c79a129336ea8644f8e8ce827d35bedfb5", [:mix], [{:nerves_hub_core, "~> 0.2", [hex: :nerves_hub_core, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},
"nerves_hub_core": {:hex, :nerves_hub_core, "0.2.0", "ee627e0c5fd8c2511cf6e975d914c5993fabf4a1de1febca54d83910a2f476c3", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"},

View File

@ -1,214 +0,0 @@
defmodule Farmbot.Target.Bootstrap.Configurator.CaptivePortal do
use GenServer
require Farmbot.Logger
@interface Application.get_env(:farmbot, :captive_portal_interface, "wlan0")
@address Application.get_env(:farmbot, :captive_portal_address, "192.168.25.1")
@mdns_domain "farmbot-setup.local"
@dnsmasq_conf_file "dnsmasq.conf"
@dnsmasq_pid_file "dnsmasq.pid"
def start_link() do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
Farmbot.Logger.busy(3, "Starting captive portal.")
ensure_interface(@interface)
host_ap_opts = [
ssid: build_ssid(),
key_mgmt: :NONE,
mode: 2
]
ip_opts = [
ipv4_address_method: :static,
ipv4_address: @address,
ipv4_subnet_mask: "255.255.0.0"
]
settings = [networks: [host_ap_opts ++ ip_opts]]
Nerves.Network.setup(@interface, settings)
dhcp_opts = [
gateway: @address,
netmask: "255.255.255.0",
range: {dhcp_range_begin(@address), dhcp_range_end(@address)},
domain_servers: [@address]
]
{:ok, dhcp_server} = DHCPServer.start_link(@interface, dhcp_opts)
dnsmasq =
case setup_dnsmasq(@address, @interface) do
{:ok, dnsmasq} ->
dnsmasq
{:error, _} ->
Farmbot.Logger.error(1, "Failed to start DnsMasq")
nil
end
Farmbot.Leds.blue(:slow_blink)
init_mdns(@mdns_domain)
update_mdns(@address)
{:ok, %{dhcp_server: dhcp_server, dnsmasq: dnsmasq}}
end
def terminate(_, state) do
Farmbot.Logger.busy(3, "Stopping captive portal GenServer.")
Farmbot.Logger.busy(3, "Stopping mDNS.")
Mdns.Server.stop()
Farmbot.Logger.busy(3, "Stopping DHCP GenServer.")
GenServer.stop(state.dhcp_server, :normal)
stop_dnsmasq(state)
Nerves.Network.teardown(@interface)
end
def handle_info({_port, {:data, _data}}, state) do
{:noreply, state}
end
defp dhcp_range_begin(address) do
[a, b, c, _] = String.split(address, ".")
Enum.join([a, b, c, "2"], ".")
end
defp dhcp_range_end(address) do
[a, b, c, _] = String.split(address, ".")
Enum.join([a, b, c, "10"], ".")
end
defp ensure_interface(interface) do
unless interface in Nerves.NetworkInterface.interfaces() do
Farmbot.Logger.debug(
2,
"Waiting for #{interface}: #{inspect(Nerves.NetworkInterface.interfaces())}"
)
Process.sleep(100)
ensure_interface(interface)
end
end
defp build_ssid do
{:ok, hostname} = :inet.gethostname()
if String.starts_with?(to_string(hostname), "farmbot-") do
to_string('farmbot-' ++ Enum.take(hostname, -4))
else
to_string(hostname)
end
end
defp setup_dnsmasq(ip_addr, interface) do
dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface)
File.mkdir_p!("/tmp/dnsmasq")
:ok = File.write("/tmp/dnsmasq/#{@dnsmasq_conf_file}", dnsmasq_conf)
dnsmasq_cmd =
"dnsmasq -k --dhcp-lease " <>
"/tmp/dnsmasq/#{@dnsmasq_pid_file} " <> "--conf-dir=/tmp/dnsmasq"
dnsmasq_port = Port.open({:spawn, dnsmasq_cmd}, [:binary])
get_dnsmasq_info(dnsmasq_port, ip_addr, interface)
end
defp get_dnsmasq_info(nil, ip_addr, interface) do
Farmbot.Logger.warn(1, "dnsmasq failed to start.")
Process.sleep(1000)
setup_dnsmasq(ip_addr, interface)
end
defp get_dnsmasq_info(dnsmasq_port, ip_addr, interface) when is_port(dnsmasq_port) do
case Port.info(dnsmasq_port, :os_pid) do
{:os_pid, dnsmasq_os_pid} ->
{dnsmasq_port, dnsmasq_os_pid}
nil ->
Farmbot.Logger.warn(1, "dnsmasq not ready yet.")
Process.sleep(1000)
setup_dnsmasq(ip_addr, interface)
end
end
defp build_dnsmasq_conf(ip_addr, interface) do
"""
interface=#{interface}
address=/#/#{ip_addr}
server=/farmbot/#{ip_addr}
local=/farmbot/
domain=farmbot
"""
end
defp stop_dnsmasq(state) do
case state.dnsmasq do
{dnsmasq_port, dnsmasq_os_pid} ->
Farmbot.Logger.busy(3, "Stopping dnsmasq")
Farmbot.Logger.busy(3, "Killing dnsmasq PID.")
:ok = kill(dnsmasq_os_pid)
Port.close(dnsmasq_port)
Farmbot.Logger.success(3, "Stopped dnsmasq.")
:ok
_ ->
Farmbot.Logger.debug(3, "Dnsmasq not running.")
:ok
end
rescue
e ->
Farmbot.Logger.error(3, "Error stopping dnsmasq: #{Exception.message(e)}")
:ok
end
defp kill(os_pid), do: :ok = cmd("kill -9 #{os_pid}")
defp cmd(cmd_str) do
[command | args] = String.split(cmd_str, " ")
System.cmd(command, args, into: IO.stream(:stdio, :line))
|> print_cmd()
end
defp print_cmd({_, 0}), do: :ok
defp print_cmd({_, num}) do
Farmbot.Logger.error(2, "Encountered an error (#{num})")
:error
end
defp init_mdns(mdns_domain) do
Mdns.Server.add_service(%Mdns.Server.Service{
domain: mdns_domain,
data: :ip,
ttl: 120,
type: :a
})
end
defp update_mdns(ip) do
ip_tuple = to_ip_tuple(ip)
Mdns.Server.stop()
# Give the interface time to settle to fix an issue where mDNS's multicast
# membership is not registered. This occurs on wireless interfaces and
# needs to be revisited.
:timer.sleep(100)
Mdns.Server.start(interface: ip_tuple)
Mdns.Server.set_ip(ip_tuple)
end
defp to_ip_tuple(str) do
str
|> String.split(".")
|> Enum.map(&String.to_integer/1)
|> List.to_tuple()
end
end

View File

@ -173,7 +173,7 @@ defmodule Farmbot.Target.Configurator.Router do
security == "WPA-PSK" ->
render_page(conn, "/config_wireless_step_2_PSK", opts)
security == "WPA-PSK" ->
security == "WPA-PSK" ->
render_page(conn, "/config_wireless_step_2_PSK", opts)
security == "NONE" ->

View File

@ -38,14 +38,31 @@ defmodule Farmbot.Target.Network do
case Map.fetch!(settings, :security) do
"WPA-PSK" -> :"WPA-PSK"
"WPA2-PSK" -> :"WPA-PSK"
"WPA-EAP" -> :"WPA-EAP"
"NONE" -> :NONE
end
[
ssid: ssid,
psk: psk,
key_mgmt: key_mgmt
] ++ validate_advanced(settings)
key_mgmt: key_mgmt,
scan_ssid: 1
] ++ validate_eap(settings) ++ validate_advanced(settings)
end
def validate_eap(%{security: "WPA-EAP"} = settings) do
identity = Map.fetch!(settings, :identity)
password = Map.fetch!(settings, :password)
[
pairwise: :"CCMP TKIP",
group: :"CCMP TKIP",
eap: :PEAP,
identity: identity,
password: password,
phase1: "peapver=auto",
phase2: "MSCHAPV2"
]
end
def validate_advanced(%{ipv4_address: "static"} = settings) do
@ -53,7 +70,8 @@ defmodule Farmbot.Target.Network do
ipv4_address_method: :static,
ipv4_address: Map.fetch!(settings, :ipv4_address),
ipv4_gateway: Map.fetch!(settings, :ipv4_gateway),
ipv4_subnet_mask: Map.fetch!(settings, :ipv4_subnet_mask)
ipv4_subnet_mask: Map.fetch!(settings, :ipv4_subnet_mask),
nameservers: String.split(settings.name_servers, " ")
]
end
@ -65,7 +83,6 @@ defmodule Farmbot.Target.Network do
ifnames()
|> List.delete("lo")
|> Enum.map(fn ifname ->
IO.puts("Looking up #{ifname}")
{:ok, settings} = Nerves.NetworkInterface.settings(ifname)
{ifname, settings}
end)
@ -111,7 +128,6 @@ defmodule Farmbot.Target.Network do
for ifname <- state.ifnames do
Nerves.Network.teardown(ifname)
Nerves.NetworkInterface.ifdown(ifname)
end
end
@ -169,7 +185,6 @@ defmodule Farmbot.Target.Network do
for {ifname, _conf} <- state.config do
Nerves.Network.teardown(ifname)
Nerves.Network.setup(ifname, [])
Nerves.NetworkInterface.setup(ifname, [])
end
%{state | hostap: :down, hostap_wpa_supplicant_pid: nil}
@ -203,8 +218,7 @@ defmodule Farmbot.Target.Network do
]
Nerves.Network.teardown("wlan0")
Nerves.Network.setup("wlan0", hostap_opts)
Nerves.NetworkInterface.setup("wlan0", ip_opts)
Nerves.Network.setup("wlan0", hostap_opts ++ ip_opts)
{:ok, hostap_dhcp_server_pid} = DHCPServer.start_link("wlan0", dhcp_opts)
{:ok, hostap_wpa_supplicant_pid} = wait_for_wpa("wlan0")

View File

@ -1,304 +0,0 @@
### WARNING(Connor) 2018-08-16
### Do not touch anything in this file unless you understand _exactly_
### what you are doing. If you look at it wrong, you will cause the
### Raspberry pi to kernel panic for some reason. I have
### no idea why. Just move along.
### If you are in this file, please at least be kind enough as to not touch any
### of the timing sensitive things. It _will_ break.
defmodule Farmbot.Target.Network.Manager do
use GenServer
require Farmbot.Logger
import Farmbot.Config, only: [get_config_value: 3]
alias Farmbot.Target.Network.NotFoundTimer
import Farmbot.Target.Network, only: [test_dns: 0]
def debug_logs? do
Application.get_env(:farmbot, :network_debug_logs, false)
end
def debug_logs(bool) do
Application.put_env(:farmbot, :network_debug_logs, bool)
end
def get_ip_addr(interface) do
GenServer.call(:"#{__MODULE__}-#{interface}", :ip)
end
def get_state(interface) do
:sys.get_state(:"#{__MODULE__}-#{interface}")
end
def start_link(interface, opts) do
GenServer.start_link(__MODULE__, {interface, opts}, name: :"#{__MODULE__}-#{interface}")
end
def init({interface, opts} = args) do
Farmbot.Logger.busy(3, "Waiting for interface #{interface} up.")
unless interface in Nerves.NetworkInterface.interfaces() do
Process.sleep(1000)
init(args)
end
Farmbot.Logger.success(3, "Interface #{interface} is up.")
Process.flag(:sensitive, true)
s1 = get_config_value(:string, "settings", "default_ntp_server_1")
s2 = get_config_value(:string, "settings", "default_ntp_server_2")
Nerves.Time.set_ntp_servers([s1, s2])
maybe_hack_tzdata()
Nerves.Network.teardown(interface)
Nerves.Network.setup(interface, opts)
{:ok, _} = Elixir.Registry.register(Nerves.NetworkInterface, interface, [])
{:ok, _} = Elixir.Registry.register(Nerves.Udhcpc, interface, [])
{:ok, _} = Elixir.Registry.register(Nerves.WpaSupplicant, interface, [])
domain = node() |> to_string() |> String.split("@") |> List.last() |> Kernel.<>(".local")
init_mdns(domain)
state = %{
# These won't change
mdns_domain: domain,
interface: interface,
opts: opts,
# These change based on
# Events from timers and other processes.
ip_address: nil,
connected: false,
ap_connected: false,
# Tries to reconnect after "network not found" event.
reconnect_timer: nil,
# Tests internet connectivity.
dns_timer: nil
}
{:ok, state}
end
def terminate(_, state) do
Nerves.Network.teardown(state.interface)
end
def handle_call(:ip, _, state) do
{:reply, state.ip_address, state}
end
# When assigned an IP address.
def handle_info({Nerves.Udhcpc, :bound, %{ipv4_address: ip}}, state) do
Farmbot.Logger.debug(3, "Ip address: #{ip}")
NotFoundTimer.stop()
connected = match?({:ok, {:hostent, _, _, :inet, 4, _}}, test_dns())
if connected do
init_mdns(state.mdns_domain)
dns_timer = restart_dns_timer(state.dns_timer, 45_000)
update_mdns(ip, state.mdns_domain)
{:noreply, %{state | dns_timer: dns_timer, ip_address: ip, connected: true}}
else
{:noreply, %{state | connected: false, ip_address: ip}}
end
end
def handle_info(
{Nerves.WpaSupplicant,
{:INFO, "WPA: 4-Way Handshake failed - pre-shared key may be incorrect"}, _},
state
) do
Farmbot.Logger.error(1, "Incorrect PSK.")
Farmbot.System.factory_reset("WIFI Authentication failed. (incorrect psk)")
{:stop, :normal, state}
end
def handle_info({Nerves.WpaSupplicant, {:"CTRL-EVENT-CONNECTED", _bssid, _status, _}, _}, state) do
# Don't update `connected`. This is not a real test of connectivity.
Logger.success 1, "Connected to access point."
NotFoundTimer.stop()
{:noreply, %{state | ap_connected: true}}
end
def handle_info({Nerves.WpaSupplicant, {:"CTRL-EVENT-DISCONNECTED", _}, _}, state) do
# stored in minutes
reconnect_timer = if state.connected, do: restart_connection_timer(state)
maybe_refresh_token()
NotFoundTimer.start()
new_state = %{
state
| ap_connected: false,
connected: false,
ip_address: nil,
reconnect_timer: reconnect_timer
}
{:noreply, new_state}
end
def handle_info({Nerves.WpaSupplicant, :"CTRL-EVENT-CONNECTED", _}, state) do
# Don't update `connected`. This is not a real test of connectivity.
Farmbot.Logger.success(1, "Connected to access point.")
NotFoundTimer.stop()
{:noreply, %{state | ap_connected: true}}
end
def handle_info({Nerves.WpaSupplicant, :"CTRL-EVENT-DISCONNECTED", _}, state) do
# stored in minutes
reconnect_timer = if state.connected, do: restart_connection_timer(state)
maybe_refresh_token()
NotFoundTimer.start()
new_state = %{
state
| ap_connected: false,
connected: false,
ip_address: nil,
reconnect_timer: reconnect_timer
}
{:noreply, new_state}
end
def handle_info({Nerves.WpaSupplicant, {:"CTRL-EVENT-SSID-TEMP-DISABLED", %{duration: 20}}, _}, state) do
reconnect_timer = if state.connected, do: restart_connection_timer(state)
maybe_refresh_token()
NotFoundTimer.start()
new_state = %{state |
ap_connected: false,
connected: false,
ip_address: nil,
reconnect_timer: reconnect_timer
}
{:noreply, new_state}
end
def handle_info(:reconnect_timer, %{ap_connected: false} = state) do
Farmbot.Logger.warn(1, "Wireless network not found still. Trying again.")
{:stop, :reconnect_timer, state}
end
def handle_info(:reconnect_timer, %{ap_connected: true} = state) do
Farmbot.Logger.success(1, "Wireless network reconnected.")
{:noreply, state}
end
def handle_info(:dns_timer, %{connected: true} = state) do
case test_dns() do
{:ok, {:hostent, _host_name, _aliases, :inet, 4, _}} ->
# Farmbot is still connected. NBD
{:noreply, %{state | dns_timer: restart_dns_timer(nil, 45_000)}}
{:error, err} ->
maybe_refresh_token()
Farmbot.Logger.warn(3, "Farmbot was disconnected from the internet: #{inspect(err)}")
{:noreply, %{state | connected: false, dns_timer: restart_dns_timer(nil, 20_000)}}
end
end
def handle_info(:dns_timer, %{ip_address: nil} = state) do
Farmbot.Logger.warn(3, "Farmbot still disconnected from the internet")
{:noreply, %{state | connected: false, dns_timer: restart_dns_timer(nil, 20_000)}}
end
def handle_info(:dns_timer, state) do
case test_dns() do
{:ok, {:hostent, _host_name, aliases, :inet, 4, _}} ->
# If we weren't previously connected, send a log.
Farmbot.Logger.success(3, "Farmbot was reconnected to the internet: #{inspect(aliases)}")
maybe_refresh_token()
new_state = %{state | connected: true, dns_timer: restart_dns_timer(nil, 45_000)}
{:noreply, new_state}
{:error, err} ->
Farmbot.Logger.warn(3, "Farmbot was disconnected from the internet: #{inspect(err)}")
maybe_refresh_token()
{:noreply, %{state | connected: false, dns_timer: restart_dns_timer(nil, 20_000)}}
end
end
def handle_info(_event, state) do
# Farmbot.Logger.warn 3, "unhandled network event: #{inspect event}"
{:noreply, state}
end
defp cancel_timer(timer) do
# If there was a timer, cancel it.
if timer do
# Farmbot.Logger.warn 3, "Cancelling Network timer"
Process.cancel_timer(timer)
end
nil
end
defp restart_dns_timer(timer, time) when is_integer(time) do
cancel_timer(timer)
Process.send_after(self(), :dns_timer, time)
end
defp restart_connection_timer(state) do
cancel_timer(state.reconnect_timer)
Nerves.Network.teardown(state.interface)
Process.sleep(5000)
Nerves.Network.setup(state.interface, state.opts)
Process.send_after(self(), :reconnect_timer, 30_000)
end
defp maybe_refresh_token do
if Process.whereis(Farmbot.Bootstrap.AuthTask) do
Farmbot.Bootstrap.AuthTask.force_refresh()
else
Farmbot.Logger.warn(1, "AuthTask not running yet")
end
end
defp init_mdns(mdns_domain) do
Mdns.Server.add_service(%Mdns.Server.Service{
domain: mdns_domain,
data: :ip,
ttl: 120,
type: :a
})
end
defp update_mdns(ip, _mdns_domain) do
ip_tuple = to_ip_tuple(ip)
Mdns.Server.stop()
# Give the interface time to settle to fix an issue where mDNS's multicast
# membership is not registered. This occurs on wireless interfaces and
# needs to be revisited.
:timer.sleep(100)
Mdns.Server.start(interface: ip_tuple)
Mdns.Server.set_ip(ip_tuple)
end
defp to_ip_tuple(str) do
str
|> String.split(".")
|> Enum.map(&String.to_integer/1)
|> List.to_tuple()
end
@fb_data_dir Application.get_env(:farmbot_ext, :data_path)
@tzdata_dir Application.app_dir(:tzdata, "priv")
def maybe_hack_tzdata do
case Tzdata.Util.data_dir() do
@fb_data_dir ->
:ok
_ ->
Farmbot.Logger.debug(3, "Hacking tzdata.")
objs_to_cp = Path.wildcard(Path.join(@tzdata_dir, "*"))
for obj <- objs_to_cp do
File.cp_r(obj, @fb_data_dir)
end
Application.put_env(:tzdata, :data_dir, @fb_data_dir)
:ok
end
end
end

View File

@ -1,264 +0,0 @@
defmodule Farmbot.Target.Network do
@moduledoc "Bring up network."
import Farmbot.Config, only: [get_config_value: 3, get_all_network_configs: 0]
alias Farmbot.Config.NetworkInterface
alias Farmbot.Target.Network.Manager, as: NetworkManager
alias Farmbot.Target.Network.NotFoundTimer
alias Farmbot.Target.Network.ScanResult
use Supervisor
require Farmbot.Logger
@doc "List available interfaces. Removes unusable entries."
def get_interfaces(tries \\ 5)
def get_interfaces(0), do: []
def get_interfaces(tries) do
case Nerves.NetworkInterface.interfaces() do
["lo"] ->
Process.sleep(100)
get_interfaces(tries - 1)
interfaces when is_list(interfaces) ->
interfaces
# Delete unusable entries if they exist.
|> List.delete("usb0")
|> List.delete("lo")
|> List.delete("sit0")
|> Map.new(fn interface ->
{:ok, settings} = Nerves.NetworkInterface.status(interface)
{interface, settings}
end)
end
end
@doc "Scan on an interface."
def scan(iface) do
do_scan(iface)
|> ScanResult.decode()
|> ScanResult.sort_results()
|> ScanResult.decode_security()
|> Enum.filter(&Map.get(&1, :ssid))
|> Enum.map(&Map.update(&1, :ssid, nil, fn ssid -> to_string(ssid) end))
|> Enum.reject(&String.contains?(&1.ssid, "\\x00"))
|> Enum.uniq_by(fn %{ssid: ssid} -> ssid end)
end
defp wait_for_results(pid) do
Nerves.WpaSupplicant.request(pid, :SCAN_RESULTS)
|> String.trim()
|> String.split("\n")
|> tl()
|> Enum.map(&String.split(&1, "\t"))
|> reduce_decode()
|> case do
[] ->
Process.sleep(500)
wait_for_results(pid)
res ->
res
end
end
defp reduce_decode(results, acc \\ [])
defp reduce_decode([], acc), do: Enum.reverse(acc)
defp reduce_decode([[bssid, freq, signal, flags, ssid] | rest], acc) do
decoded = %{
bssid: bssid,
frequency: String.to_integer(freq),
flags: flags,
level: String.to_integer(signal),
ssid: ssid
}
reduce_decode(rest, [decoded | acc])
end
defp reduce_decode([[bssid, freq, signal, flags] | rest], acc) do
decoded = %{
bssid: bssid,
frequency: String.to_integer(freq),
flags: flags,
level: String.to_integer(signal),
ssid: nil
}
reduce_decode(rest, [decoded | acc])
end
defp reduce_decode([_ | rest], acc) do
reduce_decode(rest, acc)
end
def do_scan(iface) do
pid = :"Nerves.WpaSupplicant.#{iface}"
if Process.whereis(pid) do
Nerves.WpaSupplicant.request(pid, :SCAN)
wait_for_results(pid)
else
[]
end
end
def get_level(ifname, ssid) do
r = Farmbot.Target.Network.scan(ifname)
if res = Enum.find(r, &(Map.get(&1, :ssid) == ssid)) do
res.level
end
end
@doc "Tests if we can make dns queries."
def test_dns(hostname \\ nil)
def test_dns(nil) do
case get_config_value(:string, "authorization", "server") do
nil ->
test_dns(get_config_value(:string, "settings", "default_dns_name"))
url when is_binary(url) ->
%URI{host: hostname} = URI.parse(url)
test_dns(hostname)
end
end
def test_dns(hostname) when is_binary(hostname) do
test_dns(to_charlist(hostname))
end
def test_dns(hostname) do
:ok = :inet_db.clear_cache()
# IO.puts "testing dns: #{hostname}"
case :inet.parse_ipv4_address(hostname) do
{:ok, addr} -> {:ok, {:hostent, hostname, [], :inet, 4, [addr]}}
_ -> :inet_res.gethostbyname(hostname)
end
end
# TODO Expand this to allow for more settings.
def to_network_config(config)
def to_network_config(%NetworkInterface{type: "wireless"} = config) do
Farmbot.Logger.debug(3, "wireless network config: ssid: #{config.ssid}")
Nerves.Network.set_regulatory_domain(config.regulatory_domain)
case config.security do
"WPA-EAP" ->
opts = [
ssid: config.ssid,
scan_ssid: 1,
key_mgmt: :"WPA-EAP",
pairwise: :"CCMP TKIP",
group: :"CCMP TKIP",
eap: :PEAP,
identity: config.identity,
password: config.password,
phase1: "peapver=auto",
phase2: "MSCHAPV2"
]
ip_settings = ip_settings(config)
{config.name, [networks: [opts ++ ip_settings]]}
"WPA-PSK" ->
opts = [ssid: config.ssid, psk: config.psk, key_mgmt: :"WPA-PSK", scan_ssid: 1]
ip_settings = ip_settings(config)
{config.name, [networks: [opts ++ ip_settings]]}
"NONE" ->
opts = [ssid: config.ssid, psk: config.psk, scan_ssid: 1]
ip_settings = ip_settings(config)
{config.name, [networks: [opts ++ ip_settings]]}
other -> raise "Unsupported wireless security type: #{other}"
end
end
def to_network_config(%NetworkInterface{type: "wired"} = config) do
{config.name, ip_settings(config)}
end
defp ip_settings(config) do
case config.ipv4_method do
"static" ->
[
ipv4_address_method: :static,
ipv4_address: config.ipv4_address,
ipv4_gateway: config.ipv4_gateway,
ipv4_subnet_mask: config.ipv4_subnet_mask
]
{name, Keyword.merge(opts, settings)}
"dhcp" ->
{name, opts}
end
|> maybe_use_name_servers(config)
|> maybe_use_domain(config)
end
# This is a typo. It should have been `nameservers` not `name_servers`
# It is however stored in the database as
# `name_servers`, so it can not be changed without a migration.
defp maybe_use_name_servers(opts, config) do
if config.name_servers do
Keyword.put(opts, :nameservers, String.split(config.name_servers, " "))
else
opts
end
end
defp maybe_use_domain(opts, config) do
if config.domain do
Keyword.put(opts, :domain, config.domain)
else
opts
end
end
def to_child_spec({interface, opts}) do
worker(NetworkManager, [interface, opts], restart: :transient)
end
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
config = get_all_network_configs()
Farmbot.Logger.info(3, "Starting Networking")
s1 = Farmbot.Config.get_config_value(:string, "settings", "default_ntp_server_1")
s2 = Farmbot.Config.get_config_value(:string, "settings", "default_ntp_server_2")
Nerves.Time.set_ntp_servers([s1, s2])
maybe_hack_tzdata()
children =
config
|> Enum.map(&to_network_config/1)
|> Enum.map(&to_child_spec/1)
# Don't know why/if we need this?
|> Enum.uniq()
children = [{NotFoundTimer, []}] ++ children
Supervisor.init(children, strategy: :one_for_one, max_restarts: 20, max_seconds: 1)
end
@fb_data_dir Application.get_env(:farmbot_ext, :data_path)
@tzdata_dir Application.app_dir(:tzdata, "priv")
def maybe_hack_tzdata do
case Tzdata.Util.data_dir() do
@fb_data_dir ->
:ok
_ ->
Farmbot.Logger.debug(3, "Hacking tzdata.")
objs_to_cp = Path.wildcard(Path.join(@tzdata_dir, "*"))
for obj <- objs_to_cp do
File.cp_r(obj, @fb_data_dir)
end
Application.put_env(:tzdata, :data_dir, @fb_data_dir)
:ok
end
end
end