2019-03-05 12:35:09 -07:00
|
|
|
defmodule FarmbotOS.Platform.Target.NervesHubClient do
|
2018-11-21 17:20:11 -07:00
|
|
|
@moduledoc """
|
2019-04-24 17:31:54 -06:00
|
|
|
NervesHub.Client implementation.
|
|
|
|
|
2020-03-23 13:06:31 -06:00
|
|
|
This should be one of the very first processes to be started.
|
2019-04-24 17:31:54 -06:00
|
|
|
Because it is started so early, it has to check for things that
|
|
|
|
might not be available in the environment. Environment is checked
|
|
|
|
in this order:
|
|
|
|
* token
|
|
|
|
* email
|
|
|
|
* amqp access (relies on network + NTP)
|
|
|
|
* `DeviceCert`
|
|
|
|
* nerves_hub `cert` + `key`
|
|
|
|
|
|
|
|
## State flow
|
|
|
|
|
|
|
|
The basic flow of state changes follows that path as well.
|
|
|
|
1) check for nerves_hub `cert` and `key`
|
|
|
|
1a) if `cert` and `key` are available goto 4.
|
|
|
|
1b) if not, goto 2.
|
|
|
|
2) check for Farmbot `DeviceCert`.
|
|
|
|
2a) if available update tags. goto 3.
|
|
|
|
2b) if not avialable, create. goto 3.
|
|
|
|
3) Wait for Farmbot API to dispatch nerves_hub `cert` and `key`.
|
|
|
|
4) When `cert` and `key` is available, try to connect to `nerves_hub`.
|
|
|
|
|
|
|
|
## After connection
|
|
|
|
|
|
|
|
While connected to NervesHub this process implements the
|
|
|
|
`NervesHub.Client` behaviour. When an update becomes available from a
|
|
|
|
NervesHub deployment, `update_available` will be called. This should
|
|
|
|
check if the Farmbot settings allow for auto updating. If so, apply the
|
|
|
|
update, if not wait for a CeleryScript request to update via `check_update`
|
2018-11-21 17:20:11 -07:00
|
|
|
"""
|
|
|
|
|
|
|
|
use GenServer
|
2019-04-24 17:31:54 -06:00
|
|
|
use AMQP
|
|
|
|
|
|
|
|
alias AMQP.{
|
|
|
|
Channel,
|
|
|
|
Queue
|
|
|
|
}
|
|
|
|
|
|
|
|
alias FarmbotCore.Project
|
2019-07-05 14:51:47 -06:00
|
|
|
alias FarmbotCore.{BotState, BotState.JobProgress.Percent, Config}
|
2019-09-18 11:43:31 -06:00
|
|
|
alias FarmbotCore.{Asset, Asset.Private, Config, JSON}
|
2019-04-24 17:31:54 -06:00
|
|
|
alias FarmbotExt.JWT
|
2019-03-05 12:35:09 -07:00
|
|
|
require FarmbotCore.Logger
|
2019-04-24 17:31:54 -06:00
|
|
|
require Logger
|
2018-12-05 11:30:36 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
alias FarmbotExt.AMQP.ConnectionWorker
|
2018-11-21 17:20:11 -07:00
|
|
|
@behaviour NervesHub.Client
|
2018-12-14 11:05:29 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@exchange "amq.topic"
|
2019-11-08 10:00:31 -07:00
|
|
|
# one hour
|
2019-11-13 16:03:25 -07:00
|
|
|
@checkup_timeout_ms 600_000
|
2019-04-24 17:31:54 -06:00
|
|
|
|
2019-07-18 09:34:59 -06:00
|
|
|
defstruct [
|
|
|
|
:conn,
|
|
|
|
:chan,
|
|
|
|
:jwt,
|
|
|
|
:key,
|
|
|
|
:cert,
|
|
|
|
:is_applying_update,
|
|
|
|
:firmware_url,
|
|
|
|
:probably_connected
|
|
|
|
]
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
alias __MODULE__, as: State
|
|
|
|
|
|
|
|
@doc false
|
|
|
|
def start_link(args) do
|
|
|
|
GenServer.start_link(__MODULE__, args, name: __MODULE__)
|
|
|
|
end
|
2018-12-14 11:05:29 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc "Returns the serial number of this device"
|
2019-03-07 14:30:19 -07:00
|
|
|
def serial_number(:rpi0), do: serial_number("rpi")
|
|
|
|
def serial_number(:rpi3), do: serial_number("rpi")
|
2018-11-21 17:20:11 -07:00
|
|
|
|
|
|
|
def serial_number(plat) do
|
2018-11-23 11:25:14 -07:00
|
|
|
:os.cmd(
|
|
|
|
'/usr/bin/boardid -b uboot_env -u nerves_serial_number -b uboot_env -u serial_number -b #{
|
|
|
|
plat
|
|
|
|
}'
|
|
|
|
)
|
2018-11-21 17:20:11 -07:00
|
|
|
|> to_string()
|
|
|
|
|> String.trim()
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc "Returns the serial number of this device"
|
2019-03-05 12:35:09 -07:00
|
|
|
def serial_number, do: serial_number(Project.target())
|
2018-11-21 17:20:11 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc "Returns the uuid of the running firmware"
|
|
|
|
def uuid, do: Nerves.Runtime.KV.get_active("nerves_fw_uuid")
|
2018-12-17 10:53:39 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc "Loads the cert from storage"
|
2020-01-17 08:58:53 -07:00
|
|
|
def load_cert,
|
|
|
|
do: Nerves.Runtime.KV.get_active("nerves_hub_cert") |> filter_parens()
|
2018-12-17 10:53:39 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc "Loads the key from storage"
|
2020-01-17 08:58:53 -07:00
|
|
|
def load_key,
|
|
|
|
do: Nerves.Runtime.KV.get_active("nerves_hub_key") |> filter_parens()
|
2019-04-18 13:55:49 -06:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc false
|
|
|
|
def write_serial(serial_number) do
|
|
|
|
Nerves.Runtime.KV.UBootEnv.put("nerves_serial_number", serial_number)
|
|
|
|
Nerves.Runtime.KV.UBootEnv.put("nerves_fw_serial_number", serial_number)
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc false
|
|
|
|
def write_cert(cert) do
|
|
|
|
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_cert", cert)
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc false
|
|
|
|
def write_key(key) do
|
2019-04-18 14:52:13 -06:00
|
|
|
Nerves.Runtime.KV.UBootEnv.put("nerves_hub_key", key)
|
2019-04-24 17:31:54 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
@impl NervesHub.Client
|
|
|
|
def handle_error(error) do
|
|
|
|
GenServer.cast(__MODULE__, {:handle_nerves_hub_error, error})
|
2018-11-21 17:20:11 -07:00
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl NervesHub.Client
|
|
|
|
def handle_fwup_message(msg) do
|
|
|
|
GenServer.cast(__MODULE__, {:handle_nerves_hub_fwup_message, msg})
|
2018-11-21 17:20:11 -07:00
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl NervesHub.Client
|
|
|
|
def update_available(data) do
|
|
|
|
GenServer.call(__MODULE__, {:handle_nerves_hub_update_available, data})
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@doc """
|
|
|
|
Checks if an update is available, and applies it.
|
|
|
|
"""
|
2018-11-21 17:20:11 -07:00
|
|
|
def check_update do
|
2019-04-24 17:31:54 -06:00
|
|
|
GenServer.call(__MODULE__, :check_update)
|
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-06-25 16:28:08 -06:00
|
|
|
def do_restart_nerves_hub do
|
|
|
|
try do
|
|
|
|
# NervesHub replaces it's own env on startup. Reset it.
|
|
|
|
# Stop Nerves Hub if it is running.
|
2020-01-17 08:58:53 -07:00
|
|
|
_ =
|
|
|
|
Supervisor.terminate_child(
|
|
|
|
FarmbotOS.Init.Supervisor,
|
|
|
|
NervesHub.Supervisor
|
|
|
|
)
|
|
|
|
|
|
|
|
_ =
|
|
|
|
Supervisor.delete_child(FarmbotOS.Init.Supervisor, NervesHub.Supervisor)
|
2019-06-25 16:28:08 -06:00
|
|
|
|
2019-07-15 11:59:44 -06:00
|
|
|
cert = load_cert()
|
|
|
|
key = load_key()
|
|
|
|
|
|
|
|
if cert && key do
|
2019-09-18 11:43:31 -06:00
|
|
|
# Cause NervesRuntime.KV to restart.
|
|
|
|
# _ = GenServer.stop(Nerves.Runtime.KV, :restart)
|
|
|
|
_ = Application.stop(:nerves_runtime)
|
|
|
|
Process.sleep(1000)
|
|
|
|
_ = Application.ensure_all_started(:nerves_runtime)
|
|
|
|
_ = Application.ensure_all_started(:nerves_hub)
|
|
|
|
|
|
|
|
# Wait for a few seconds for good luck.
|
|
|
|
Process.sleep(1000)
|
2019-07-15 11:59:44 -06:00
|
|
|
end
|
2019-06-25 16:28:08 -06:00
|
|
|
catch
|
|
|
|
kind, err ->
|
2020-01-17 08:58:53 -07:00
|
|
|
IO.warn(
|
|
|
|
"NervesHub error: #{inspect(kind)} #{inspect(err)}",
|
|
|
|
__STACKTRACE__
|
|
|
|
)
|
|
|
|
|
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"OTA service error: #{kind} #{inspect(err)}"
|
|
|
|
)
|
2019-06-25 16:28:08 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
# Start the connection again.
|
|
|
|
Supervisor.start_child(FarmbotOS, NervesHub.Supervisor)
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl GenServer
|
|
|
|
def init(_args) do
|
2019-06-25 16:28:08 -06:00
|
|
|
Application.ensure_all_started(:nerves_runtime)
|
|
|
|
Application.ensure_all_started(:nerves_hub)
|
2019-04-24 17:31:54 -06:00
|
|
|
write_serial(serial_number())
|
|
|
|
cert = load_cert()
|
|
|
|
key = load_key()
|
2018-12-10 10:35:47 -07:00
|
|
|
|
2019-10-10 09:14:00 -06:00
|
|
|
_ = set_controller_uuid()
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
if cert && key do
|
|
|
|
send(self(), :connect_nerves_hub)
|
|
|
|
else
|
|
|
|
send(self(), :connect_amqp)
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
2019-04-24 17:31:54 -06:00
|
|
|
|
2019-07-18 09:34:59 -06:00
|
|
|
{:ok,
|
|
|
|
%State{
|
|
|
|
conn: nil,
|
|
|
|
chan: nil,
|
|
|
|
jwt: nil,
|
|
|
|
cert: cert,
|
|
|
|
key: key,
|
|
|
|
is_applying_update: false,
|
|
|
|
probably_connected: false
|
|
|
|
}}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl GenServer
|
|
|
|
def terminate(reason, state) do
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"Disconnected from NervesHub AMQP channel: #{inspect(reason)}"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
# If a channel was still open, close it.
|
|
|
|
if state.chan, do: AMQP.Channel.close(state.chan)
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl GenServer
|
|
|
|
def handle_info(:connect_amqp, %{conn: nil, chan: nil} = state) do
|
|
|
|
FarmbotCore.Logger.debug(1, "Attempting to get OTA certs from AMQP.")
|
|
|
|
|
|
|
|
with {token, %{} = jwt} when is_binary(token) <- get_jwt(),
|
|
|
|
email when is_binary(email) <- get_email(),
|
|
|
|
{:ok, %{} = conn} <- open_connection(email, token, jwt),
|
|
|
|
{:ok, chan} <- Channel.open(conn),
|
|
|
|
:ok <- Basic.qos(chan, global: true),
|
|
|
|
{:ok, _} <-
|
2020-01-17 08:58:53 -07:00
|
|
|
Queue.declare(chan, "#{jwt.bot}_nerves_hub",
|
|
|
|
auto_delete: false,
|
|
|
|
durable: true
|
|
|
|
),
|
2019-04-24 17:31:54 -06:00
|
|
|
:ok <-
|
|
|
|
Queue.bind(chan, "#{jwt.bot}_nerves_hub", @exchange,
|
|
|
|
routing_key: "bot.#{jwt.bot}.nerves_hub"
|
|
|
|
),
|
|
|
|
{:ok, _tag} <- Basic.consume(chan, "#{jwt.bot}_nerves_hub", self(), []) do
|
|
|
|
send(self(), :after_connect_amqp)
|
|
|
|
{:noreply, %{state | conn: conn, chan: chan, jwt: jwt}}
|
|
|
|
else
|
|
|
|
# happens when no token is configured.
|
|
|
|
{nil, nil} ->
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"No credentials yet. Can't connect to OTA Server."
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
Process.send_after(self(), :connect_amqp, 15_000)
|
|
|
|
{:noreply, %{state | conn: nil, chan: nil, jwt: nil}}
|
|
|
|
|
|
|
|
err ->
|
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
3,
|
|
|
|
"Failed to connect to NervesHub AMQP channel: #{inspect(err)}"
|
|
|
|
)
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
Process.send_after(self(), :connect_amqp, 5000)
|
|
|
|
{:noreply, %{state | conn: nil, chan: nil, jwt: nil}}
|
2018-12-05 11:30:36 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_info(:after_connect_amqp, %{key: nil, cert: nil} = state) do
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"Connected to NervesHub AMQP channel. Fetching certs."
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
old_device_cert = Asset.get_device_cert(serial_number: serial_number())
|
|
|
|
|
2019-07-02 16:46:15 -06:00
|
|
|
tags = detect_deployment_tags()
|
2019-04-24 17:31:54 -06:00
|
|
|
|
|
|
|
params = %{
|
|
|
|
serial_number: serial_number(),
|
|
|
|
tags: tags
|
|
|
|
}
|
|
|
|
|
|
|
|
new_device_cert =
|
|
|
|
case old_device_cert do
|
|
|
|
nil -> Asset.new_device_cert(params)
|
|
|
|
%{} -> Asset.update_device_cert(old_device_cert, params)
|
|
|
|
end
|
|
|
|
|
|
|
|
case new_device_cert do
|
2019-09-09 09:48:30 -06:00
|
|
|
{:ok, _data} ->
|
2019-07-02 16:46:15 -06:00
|
|
|
# DO NOT DO THIS. The api will do it behind the scenes
|
|
|
|
# Asset.update_device!(%{update_channel: detect_update_channel()})
|
2019-09-09 09:48:30 -06:00
|
|
|
FarmbotCore.Logger.debug(3, "DeviceCert created")
|
2020-01-17 08:58:53 -07:00
|
|
|
|
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"Waiting for cert and key data from AMQP from farmbot api..."
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
{:noreply, state}
|
|
|
|
|
|
|
|
{:error, reason} ->
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"Failed to create device cert: #{inspect(reason)}"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
Process.send_after(self(), :after_connect_amqp, 5000)
|
|
|
|
{:noreply, state}
|
2018-12-05 11:30:36 -07:00
|
|
|
end
|
2019-04-24 17:31:54 -06:00
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_info(:after_connect_amqp, %{key: _key, cert: _cert} = state) do
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"Connected to NervesHub AMQP channel. Certs already loaded"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
send(self(), :connect_nerves_hub)
|
|
|
|
{:noreply, state}
|
2018-12-05 11:30:36 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_info(:connect_nerves_hub, %{key: nil, cert: nil} = state) do
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"Can't connect to OTA Service. Certs not loaded"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
send(self(), :connect_amqp)
|
|
|
|
{:noreply, state}
|
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-07-18 09:34:59 -06:00
|
|
|
def handle_info(
|
|
|
|
:connect_nerves_hub,
|
|
|
|
%{key: _key, cert: _cert, probably_connected: false} = state
|
|
|
|
) do
|
2019-04-24 17:31:54 -06:00
|
|
|
FarmbotCore.Logger.debug(3, "Starting OTA Service")
|
2019-06-25 16:28:08 -06:00
|
|
|
do_restart_nerves_hub()
|
2019-04-24 17:31:54 -06:00
|
|
|
FarmbotCore.Logger.debug(3, "OTA Service started")
|
2019-07-18 09:34:59 -06:00
|
|
|
{:noreply, %{state | probably_connected: true}}
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_info(
|
|
|
|
:connect_nerves_hub,
|
|
|
|
%{key: _key, cert: _cert, probably_connected: true} = state
|
|
|
|
) do
|
2019-04-24 17:31:54 -06:00
|
|
|
{:noreply, state}
|
2018-12-05 11:30:36 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
# Confirmation sent by the broker after registering this process as a consumer
|
|
|
|
def handle_info({:basic_consume_ok, _}, state) do
|
|
|
|
{:noreply, state}
|
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
# Sent by the broker when the consumer is
|
|
|
|
# unexpectedly cancelled (such as after a queue deletion)
|
|
|
|
def handle_info({:basic_cancel, _}, state) do
|
|
|
|
{:stop, :normal, state}
|
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
# Confirmation sent by the broker to the consumer process after a Basic.cancel
|
|
|
|
def handle_info({:basic_cancel_ok, _}, state) do
|
|
|
|
{:noreply, state}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
|
|
|
|
device = state.jwt.bot
|
|
|
|
["bot", ^device, "nerves_hub"] = String.split(key, ".")
|
|
|
|
|
2020-01-17 08:58:53 -07:00
|
|
|
with {:ok, %{"cert" => base64_cert, "key" => base64_key}} <-
|
|
|
|
JSON.decode(payload),
|
2019-04-24 17:31:54 -06:00
|
|
|
{:ok, cert} <- Base.decode64(base64_cert),
|
|
|
|
{:ok, key} <- Base.decode64(base64_key),
|
|
|
|
:ok <- write_cert(cert),
|
|
|
|
:ok <- write_key(key) do
|
|
|
|
send(self(), :connect_nerves_hub)
|
|
|
|
{:noreply, %{state | cert: cert, key: key}}
|
|
|
|
else
|
|
|
|
{:error, reason} ->
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"OTA Service failed to configure. #{inspect(reason)}"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
{:stop, reason, state}
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
:error ->
|
|
|
|
FarmbotCore.Logger.error(1, "OTA Service payload invalid. (base64)")
|
|
|
|
{:stop, :invalid_payload, state}
|
2018-12-05 11:30:36 -07:00
|
|
|
end
|
2019-04-24 17:31:54 -06:00
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2020-01-17 08:58:53 -07:00
|
|
|
def handle_info(
|
|
|
|
:checkup,
|
|
|
|
%{is_applying_update: false, probably_connected: true} = state
|
|
|
|
) do
|
2019-11-08 10:00:31 -07:00
|
|
|
if should_auto_apply_update?() && update_available?() do
|
2020-03-23 15:24:04 -06:00
|
|
|
FarmbotCore.Logger.busy(1, "Applying OTA update (1)")
|
2020-03-23 13:28:07 -06:00
|
|
|
run_update_but_only_once()
|
2019-11-08 10:00:31 -07:00
|
|
|
{:noreply, %{state | is_applying_update: true}}
|
|
|
|
else
|
|
|
|
Process.send_after(self(), :checkup, @checkup_timeout_ms)
|
|
|
|
{:noreply, state}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def handle_info(:checkup, state) do
|
|
|
|
FarmbotCore.Logger.debug(3, """
|
|
|
|
unknown state for checkup
|
|
|
|
currently applying update?: #{state.is_applying_update}
|
|
|
|
currently connected?: #{state.probably_connected}
|
|
|
|
update available?: #{is_binary(state.firmware_url)}
|
|
|
|
""")
|
|
|
|
|
|
|
|
Process.send_after(self(), :checkup, @checkup_timeout_ms)
|
|
|
|
{:noreply, state}
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
@impl GenServer
|
2020-01-17 08:58:53 -07:00
|
|
|
def handle_cast(
|
|
|
|
{:handle_nerves_hub_error, error},
|
|
|
|
%{is_applying_update: true} = state
|
|
|
|
) do
|
2020-03-23 16:27:16 -06:00
|
|
|
FarmbotCore.Logger.error(1, "Error applying OTA (1): #{inspect(error)}")
|
2019-04-24 17:31:54 -06:00
|
|
|
{:noreply, state}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_cast({:handle_nerves_hub_error, error}, state) do
|
|
|
|
FarmbotCore.Logger.debug(3, "Unexpected NervesHub error: #{inspect(error)}")
|
|
|
|
{:noreply, state}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2020-01-17 08:58:53 -07:00
|
|
|
def handle_cast(
|
|
|
|
{:handle_nerves_hub_fwup_message, {:progress, percent}},
|
|
|
|
state
|
|
|
|
) do
|
2019-04-24 17:31:54 -06:00
|
|
|
_ = set_ota_progress(percent)
|
|
|
|
{:noreply, state}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_cast({:handle_nerves_hub_fwup_message, {:ok, _, _info}}, state) do
|
2019-07-17 11:55:40 -06:00
|
|
|
_ = set_firmware_needs_flash()
|
2019-04-24 17:31:54 -06:00
|
|
|
_ = set_ota_progress(100)
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = update_device()
|
2019-04-24 17:31:54 -06:00
|
|
|
{:noreply, state}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_cast({:handle_nerves_hub_fwup_message, {:error, _, reason}}, state) do
|
|
|
|
_ = set_ota_progress(100)
|
2020-03-23 16:27:16 -06:00
|
|
|
FarmbotCore.Logger.error(1, "Error applying OTA (2): #{inspect(reason)}")
|
2019-04-24 17:31:54 -06:00
|
|
|
{:noreply, state}
|
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_cast({:handle_nerves_hub_fwup_message, _}, state) do
|
|
|
|
{:noreply, state}
|
|
|
|
end
|
|
|
|
|
|
|
|
@impl GenServer
|
2020-01-17 08:58:53 -07:00
|
|
|
def handle_call(
|
|
|
|
{:handle_nerves_hub_update_available, %{"firmware_url" => url}},
|
|
|
|
_from,
|
|
|
|
state
|
|
|
|
) do
|
2019-11-08 10:00:31 -07:00
|
|
|
case should_auto_apply_update?() do
|
2018-12-05 11:30:36 -07:00
|
|
|
true ->
|
2020-03-23 15:24:04 -06:00
|
|
|
FarmbotCore.Logger.busy(1, "Applying OTA update (2)")
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = set_update_available_in_bot_state()
|
|
|
|
_ = update_device_last_ota_checkup()
|
2019-07-17 11:55:40 -06:00
|
|
|
_ = set_firmware_needs_flash()
|
2019-04-24 17:31:54 -06:00
|
|
|
{:reply, :apply, %{state | is_applying_update: true, firmware_url: url}}
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
_ ->
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = set_update_available_in_bot_state()
|
|
|
|
_ = update_device_last_ota_checkup()
|
2019-11-08 10:00:31 -07:00
|
|
|
Process.send_after(self(), :checkup, @checkup_timeout_ms)
|
2019-04-24 17:31:54 -06:00
|
|
|
{:reply, :ignore, %{state | firmware_url: url}}
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
def handle_call({:handle_nerves_hub_update_available, _data}, _from, state) do
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = set_update_available_in_bot_state()
|
|
|
|
_ = update_device_last_ota_checkup()
|
|
|
|
_ = set_firmware_needs_flash()
|
2020-03-23 15:24:04 -06:00
|
|
|
FarmbotCore.Logger.busy(1, "Applying OTA update (3)")
|
2019-04-24 17:31:54 -06:00
|
|
|
{:reply, :apply, %{state | is_applying_update: true}}
|
|
|
|
end
|
|
|
|
|
2018-11-21 17:20:11 -07:00
|
|
|
def handle_call(:check_update, _from, state) do
|
2019-04-24 17:31:54 -06:00
|
|
|
case NervesHub.HTTPClient.update() do
|
|
|
|
{:ok, %{"data" => %{"update_available" => false}}} ->
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = update_device_last_ota_checkup()
|
2019-04-24 17:31:54 -06:00
|
|
|
{:reply, nil, state}
|
|
|
|
|
|
|
|
data ->
|
2019-09-18 11:43:31 -06:00
|
|
|
_ = set_update_available_in_bot_state()
|
|
|
|
_ = update_device_last_ota_checkup()
|
2019-07-17 11:55:40 -06:00
|
|
|
_ = set_firmware_needs_flash()
|
2020-03-23 13:06:31 -06:00
|
|
|
FarmbotCore.Logger.busy(1, "Attempting OTA update...")
|
|
|
|
# This is where the NervesHub update gets called.
|
|
|
|
# Maybe we can check if the BotState has job progress for "FBOS_OTA"
|
2020-03-23 13:28:07 -06:00
|
|
|
run_update_but_only_once()
|
2019-04-24 17:31:54 -06:00
|
|
|
{:reply, data, %{state | is_applying_update: true}}
|
|
|
|
end
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|
2018-12-14 11:05:29 -07:00
|
|
|
|
2019-11-08 10:00:31 -07:00
|
|
|
def should_auto_apply_update?(now \\ nil) do
|
|
|
|
now = now || DateTime.utc_now()
|
|
|
|
auto_update = Asset.fbos_config(:os_auto_update)
|
|
|
|
ota_hour = Asset.device(:ota_hour)
|
|
|
|
timezone = Asset.device(:timezone)
|
|
|
|
# if ota_hour is nil, auto apply the update
|
2020-03-23 16:02:32 -06:00
|
|
|
result =
|
|
|
|
if ota_hour && timezone do
|
|
|
|
# check that now.hour == device.ota_hour
|
|
|
|
case Timex.Timezone.convert(now, timezone) do
|
|
|
|
%{hour: ^ota_hour} ->
|
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"current hour: #{ota_hour} (utc=#{now.hour}) == ota_hour #{
|
|
|
|
ota_hour
|
|
|
|
}. auto_update=#{auto_update}"
|
|
|
|
)
|
|
|
|
|
|
|
|
auto_update
|
|
|
|
|
|
|
|
%{hour: now_hour} ->
|
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"current hour: #{now_hour} (utc=#{now.hour}) != ota_hour: #{
|
|
|
|
ota_hour
|
|
|
|
}. auto_update=#{auto_update}"
|
|
|
|
)
|
|
|
|
|
|
|
|
false
|
|
|
|
end
|
|
|
|
else
|
|
|
|
# ota_hour or timezone are nil
|
|
|
|
FarmbotCore.Logger.debug(
|
|
|
|
3,
|
|
|
|
"ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}"
|
|
|
|
)
|
|
|
|
|
|
|
|
true
|
2019-11-08 10:00:31 -07:00
|
|
|
end
|
|
|
|
|
2020-03-23 16:08:24 -06:00
|
|
|
result && !currently_downloading?()
|
2019-11-08 10:00:31 -07:00
|
|
|
end
|
|
|
|
|
|
|
|
def update_available?() do
|
|
|
|
_ = update_device_last_ota_checkup()
|
|
|
|
|
|
|
|
case NervesHub.HTTPClient.update() do
|
|
|
|
{:ok, %{"data" => %{"update_available" => false}}} ->
|
|
|
|
false
|
|
|
|
|
|
|
|
_data ->
|
|
|
|
true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-09-18 11:43:31 -06:00
|
|
|
defp update_device do
|
|
|
|
now = DateTime.utc_now()
|
|
|
|
|
|
|
|
_ =
|
|
|
|
%{last_ota: now, last_ota_checkup: now}
|
|
|
|
|> Asset.update_device!()
|
|
|
|
|> Private.mark_dirty!(%{})
|
|
|
|
end
|
|
|
|
|
|
|
|
defp update_device_last_ota_checkup do
|
|
|
|
now = DateTime.utc_now()
|
|
|
|
|
|
|
|
_ =
|
|
|
|
%{last_ota_checkup: now}
|
|
|
|
|> Asset.update_device!()
|
|
|
|
|> Private.mark_dirty!(%{})
|
|
|
|
end
|
|
|
|
|
2019-09-06 14:08:17 -06:00
|
|
|
defp set_update_available_in_bot_state() do
|
|
|
|
if Process.whereis(BotState) do
|
|
|
|
BotState.set_update_available(true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-10-10 09:14:00 -06:00
|
|
|
defp set_controller_uuid() do
|
|
|
|
if Process.whereis(BotState) do
|
|
|
|
BotState.set_controller_uuid(uuid())
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
defp get_jwt do
|
|
|
|
token = Config.get_config_value(:string, "authorization", "token")
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
if token do
|
|
|
|
{:ok, jwt} = JWT.decode(token)
|
|
|
|
{token, jwt}
|
|
|
|
else
|
|
|
|
{nil, nil}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp get_email do
|
|
|
|
Config.get_config_value(:string, "authorization", "email")
|
|
|
|
end
|
|
|
|
|
|
|
|
defp open_connection(email, token, jwt) do
|
2020-01-17 08:58:53 -07:00
|
|
|
case ConnectionWorker.open_connection(
|
|
|
|
token,
|
|
|
|
email,
|
|
|
|
jwt.bot,
|
|
|
|
jwt.mqtt,
|
|
|
|
jwt.vhost
|
|
|
|
) do
|
2019-04-24 17:31:54 -06:00
|
|
|
{:ok, conn} ->
|
|
|
|
Process.link(conn.pid)
|
|
|
|
Process.monitor(conn.pid)
|
|
|
|
{:ok, conn}
|
2019-03-05 12:35:09 -07:00
|
|
|
|
2020-03-23 13:06:31 -06:00
|
|
|
# Squash this log since it will be displayed for the
|
2019-09-16 14:20:24 -06:00
|
|
|
# main AMQP connection
|
|
|
|
{:error, :unknown_host} = err ->
|
|
|
|
err
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
err ->
|
2020-01-17 08:58:53 -07:00
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"Error opening AMQP connection for OTA certs #{inspect(err)}"
|
|
|
|
)
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
err
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
defp filter_parens(""), do: nil
|
|
|
|
defp filter_parens(data), do: data
|
2018-12-14 11:05:29 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
defp set_ota_progress(100) do
|
|
|
|
FarmbotCore.Logger.success(1, "OTA Complete Going down for reboot")
|
|
|
|
prog = %Percent{percent: 100, status: "complete"}
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
if Process.whereis(BotState) do
|
|
|
|
BotState.set_job_progress("FBOS_OTA", prog)
|
2018-12-14 11:05:29 -07:00
|
|
|
end
|
2018-11-23 11:25:14 -07:00
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
:ok
|
2018-12-14 11:05:29 -07:00
|
|
|
end
|
|
|
|
|
2019-04-24 17:31:54 -06:00
|
|
|
defp set_ota_progress(percent) do
|
|
|
|
prog = %Percent{percent: percent}
|
|
|
|
|
|
|
|
if Process.whereis(BotState) do
|
|
|
|
BotState.set_job_progress("FBOS_OTA", prog)
|
|
|
|
end
|
|
|
|
|
|
|
|
:ok
|
2018-12-14 11:05:29 -07:00
|
|
|
end
|
|
|
|
|
2019-07-17 11:55:40 -06:00
|
|
|
def set_firmware_needs_flash() do
|
2019-09-09 12:46:44 -06:00
|
|
|
# Config.update_config_value(:bool, "settings", "firmware_needs_flash", true)
|
|
|
|
# Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
|
2019-07-17 11:55:40 -06:00
|
|
|
:ok
|
|
|
|
end
|
|
|
|
|
2019-07-02 16:46:15 -06:00
|
|
|
def detect_deployment_tags() do
|
|
|
|
update_channel = detect_update_channel()
|
2019-04-24 17:31:54 -06:00
|
|
|
["application:#{Project.env()}", "channel:#{update_channel}"]
|
|
|
|
end
|
2018-12-14 11:05:29 -07:00
|
|
|
|
2019-07-02 16:46:15 -06:00
|
|
|
def detect_update_channel() do
|
2019-07-03 08:16:24 -06:00
|
|
|
if Regex.match?(
|
|
|
|
~r/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)-rc(0|[1-9]\d*)+?/,
|
2019-07-02 16:46:15 -06:00
|
|
|
Project.version()
|
|
|
|
) do
|
|
|
|
"beta"
|
|
|
|
else
|
|
|
|
case Project.branch() do
|
|
|
|
"master" -> "stable"
|
|
|
|
branch -> branch
|
|
|
|
end
|
2019-04-24 17:31:54 -06:00
|
|
|
end
|
|
|
|
end
|
2020-03-23 13:06:31 -06:00
|
|
|
|
2020-03-23 16:02:32 -06:00
|
|
|
def currently_downloading?, do: BotState.job_in_progress?("FBOS_OTA")
|
|
|
|
|
2020-03-23 13:06:31 -06:00
|
|
|
def run_update_but_only_once do
|
2020-03-23 16:08:49 -06:00
|
|
|
if currently_downloading?() do
|
2020-03-23 13:06:31 -06:00
|
|
|
FarmbotCore.Logger.error(
|
|
|
|
1,
|
|
|
|
"Can't perform OTA. OTA alread in progress. Restart device if problem persists."
|
|
|
|
)
|
|
|
|
else
|
2020-03-23 16:53:21 -06:00
|
|
|
FarmbotCore.Logger.success(1, "OTA started.")
|
2020-03-23 13:06:31 -06:00
|
|
|
spawn_link(fn -> NervesHub.update() end)
|
|
|
|
end
|
|
|
|
end
|
2018-11-21 17:20:11 -07:00
|
|
|
end
|