farmbot_os/farmbot_os/platform/target/network.ex

377 lines
9.9 KiB
Elixir

defmodule FarmbotOS.Platform.Target.Network do
@moduledoc "Manages Network Connections"
use GenServer, shutdown: 10_000
require Logger
require FarmbotCore.Logger
require FarmbotTelemetry
import FarmbotOS.Platform.Target.Network.Utils,
only: [
maybe_hack_tzdata: 0,
init_net_kernel: 0,
build_hostap_ssid: 0
]
alias FarmbotOS.Platform.Target.Network.PreSetup
alias FarmbotOS.Platform.Target.Configurator.{Validator, CaptivePortal}
alias FarmbotCore.{Asset, Config, Leds}
@default_network_not_found_timer_minutes 20
def host do
%{
type: CaptivePortal,
vintage_net_wifi: %{
networks: [
%{
ssid: build_hostap_ssid(),
mode: :ap,
key_mgmt: :none
}
]
},
ipv4: %{
method: :static,
address: "192.168.24.1",
netmask: "255.255.255.0"
},
dnsmasq: %{
domain: "farmbot",
server: "192.168.24.1",
address: "192.168.24.1",
start: "192.168.24.2",
end: "192.168.24.10"
}
}
end
def null do
%{type: VintageNet.Technology.Null}
end
def presetup do
%{type: PreSetup}
end
def is_first_connect?() do
# email = Config.get_config_value(:string, "authorization", "email")
# password = Config.get_config_value(:string, "authorization", "password")
# server = Config.get_config_value(:string, "authorization", "server")
token = Config.get_config_value(:string, "authorization", "token")
is_nil(token)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
@impl GenServer
def init(_args) do
_ = maybe_hack_tzdata()
_ = init_net_kernel()
send(self(), :setup)
# If a secret exists, assume that
# farmbot at one point has been connected to the internet
first_connect? = is_first_connect?()
if first_connect? do
_ = Leds.blue(:slow_blink)
:ok = VintageNet.configure("wlan0", null())
Process.sleep(1500)
:ok = VintageNet.configure("wlan0", host())
end
{:ok, %{network_not_found_timer: nil, first_connect?: first_connect?}}
end
@impl GenServer
def terminate(_, _) do
:ok = VintageNet.configure("wlan0", null())
:ok = VintageNet.configure("eth0", null())
end
@impl GenServer
def handle_info(:setup, state) do
configs = Config.get_all_network_configs()
case configs do
[] ->
Process.send_after(self(), :setup, 5_000)
{:noreply, state}
_ ->
_ = VintageNet.subscribe(["interface", "wlan0"])
_ = VintageNet.subscribe(["interface", "eth0"])
:ok = VintageNet.configure("wlan0", presetup())
:ok = VintageNet.configure("eth0", presetup())
Process.sleep(1500)
{:noreply, state}
end
end
def handle_info(
{VintageNet, ["interface", ifname, "type"], _old, type, _meta},
state
)
when type in [PreSetup, VintageNet.Technology.Null] do
FarmbotCore.Logger.debug(
1,
"Network interface needs configuration: #{ifname}"
)
case Config.get_network_config(ifname) do
%Config.NetworkInterface{} = config ->
FarmbotCore.Logger.busy(3, "Setting up network interface: #{ifname}")
case reset_ntp() do
:ok ->
:ok
error ->
FarmbotCore.Logger.error(1, """
Failed to configure NTP: #{inspect(error)}
""")
end
vintage_net_config = to_vintage_net(config)
FarmbotTelemetry.event(:network, :interface_configure, nil,
interface: ifname
)
configure_result = VintageNet.configure(config.name, vintage_net_config)
FarmbotCore.Logger.success(
3,
"#{config.name} setup: #{inspect(configure_result)}"
)
state = start_network_not_found_timer(state)
{:noreply, state}
nil ->
{:noreply, state}
end
end
def handle_info(
{VintageNet, ["interface", ifname, "lower_up"], _old, false, _meta},
state
) do
FarmbotCore.Logger.error(
1,
"Interface #{ifname} disconnected from access point"
)
FarmbotTelemetry.event(:network, :interface_disconnect, nil,
interface: ifname
)
state = start_network_not_found_timer(state)
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", ifname, "lower_up"], _old, true, _meta},
state
) do
FarmbotCore.Logger.success(1, "Interface #{ifname} connected access point")
FarmbotTelemetry.event(:network, :interface_connect, nil, interface: ifname)
state = cancel_network_not_found_timer(state)
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", ifname, "connection"], :disconnected, :lan,
_meta},
state
) do
FarmbotCore.Logger.warn(
1,
"Interface #{ifname} connected to local area network"
)
FarmbotTelemetry.event(:network, :lan_connect, nil, interface: ifname)
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", ifname, "connection"], :lan, :internet,
_meta},
state
) do
FarmbotCore.Logger.warn(1, "Interface #{ifname} connected to internet")
state = cancel_network_not_found_timer(state)
FarmbotTelemetry.event(:network, :wan_connect, nil, interface: ifname)
{:noreply, %{state | first_connect?: false}}
end
def handle_info(
{VintageNet, ["interface", ifname, "connection"], :internet, ifstate,
_meta},
state
) do
FarmbotCore.Logger.warn(
1,
"Interface #{ifname} disconnected from the internet: #{ifstate}"
)
FarmbotExt.AMQP.ConnectionWorker.close()
FarmbotTelemetry.event(:network, :wan_disconnect, nil, interface: ifname)
if state.network_not_found_timer do
{:noreply, state}
else
state = start_network_not_found_timer(state)
{:noreply, state}
end
end
def handle_info(
{VintageNet, ["interface", _, "wifi", "access_points"], _old, _new,
_meta},
state
) do
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", _ifname, "eap_status"], _old,
%{status: :success} = eap_status, _meta},
state
) do
FarmbotCore.Logger.debug(3, """
Farmbot successfully completed EAP Authentication.
#{inspect(eap_status, limit: :infinity)}
""")
{:noreply, state}
end
def handle_info(
{VintageNet, ["interface", _ifname, "eap_status"], _old,
%{status: :failure}, _meta},
state
) do
FarmbotCore.Logger.error(1, """
Farmbot was unable to assosiate with the EAP network.
Please check the identity, password and method of connection
""")
FarmbotOS.System.factory_reset("""
Farmbot was unable to assosiate with the EAP network.
Please check the identity, password and method of connection
""")
{:noreply, state}
end
def handle_info({VintageNet, property, old, new, _meta}, state) do
Logger.debug("""
Unknown property change: #{inspect(property)}
old:
#{inspect(old, limit: :infinity)}
new:
#{inspect(new, limit: :infinity)}
""")
{:noreply, state}
end
def handle_info({:network_not_found_timer, minutes}, state) do
FarmbotCore.Logger.warn(1, """
Farmbot has been disconnected from the network for
#{minutes} minutes. Going down for factory reset.
""")
FarmbotOS.System.factory_reset("""
Farmbot has been disconnected from the network for
#{minutes} minutes.
""")
{:noreply, state}
end
def to_vintage_net(%Config.NetworkInterface{} = config) do
%{
type: Validator,
network_type: config.type,
ssid: config.ssid,
security: config.security,
psk: config.psk,
identity: config.identity,
password: config.password,
domain: config.domain,
name_servers: config.name_servers,
ipv4_method: config.ipv4_method,
ipv4_address: config.ipv4_address,
ipv4_gateway: config.ipv4_gateway,
ipv4_subnet_mask: config.ipv4_subnet_mask,
regulatory_domain: config.regulatory_domain
}
end
defp cancel_network_not_found_timer(state) do
FarmbotCore.Logger.success(
1,
"Farmbot has been reconnected. Canceling scheduled factory reset"
)
old_timer = state.network_not_found_timer
old_timer && Process.cancel_timer(old_timer)
%{state | network_not_found_timer: nil}
end
defp start_network_not_found_timer(state) do
state = cancel_network_not_found_timer(state)
# Stored in minutes
minutes = network_not_found_timer_minutes(state)
millis = minutes * 60000
new_timer =
Process.send_after(self(), {:network_not_found_timer, minutes}, millis)
FarmbotCore.Logger.warn(
1,
"FarmBot will factory reset in #{minutes} minutes if the network does not reassociate. If you see this message directly after configuration, this message can be safely ignored."
)
%{state | network_not_found_timer: new_timer}
end
# if the network has never connected before, make a low
# thresh so that user won't have to wait 20 minutes to reconfigurate
# due to bad wifi credentials.
defp network_not_found_timer_minutes(%{first_connect?: true}), do: 1
defp network_not_found_timer_minutes(_state) do
Asset.fbos_config(:network_not_found_timer) ||
@default_network_not_found_timer_minutes
end
def reset_ntp do
FarmbotTelemetry.event(:ntp, :reset)
ntp_server_1 =
Config.get_config_value(:string, "settings", "default_ntp_server_1")
ntp_server_2 =
Config.get_config_value(:string, "settings", "default_ntp_server_2")
if ntp_server_1 || ntp_server_2 do
Logger.info("Setting NTP servers: [#{ntp_server_1}, #{ntp_server_2}]")
[ntp_server_1, ntp_server_2]
|> Enum.reject(&is_nil/1)
|> NervesTime.set_ntp_servers()
else
Logger.info("Using default NTP servers")
:ok
end
end
end