377 lines
9.9 KiB
Elixir
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
|