Nerves hub pt2 (#654)

* Rework Networking

* Rework Configurator

* Fix NervesHub not connecting
pull/974/head
Connor Rigby 2018-11-28 12:12:25 -08:00 committed by Connor Rigby
parent 451dfe1343
commit 56f6749595
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
64 changed files with 1523 additions and 960 deletions

View File

@ -56,15 +56,6 @@ build_firmware_steps: &build_firmware_steps
- run:
name: Setup ENV
command: |
<<<<<<< HEAD
echo $MIX_TARGET > MIX_TARGET
echo $MIX_ENV > MIX_ENV
cp mix.lock MIX_LOCK
- restore_cache:
key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }}
- restore_cache:
key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }}
=======
echo "/nerves/build/farmbot_os/$MIX_TARGET" > MIX_TARGET
echo "/nerves/build/farmbot_os/$MIX_ENV" > MIX_ENV
cp /nerves/build/farmbot_os/mix.lock.$MIX_TARGET MIX_LOCK
@ -72,7 +63,6 @@ build_firmware_steps: &build_firmware_steps
key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }}
- restore_cache:
key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock.host" }}
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
- <<: *install_elixir
- <<: *install_hex_archives
- run:
@ -88,11 +78,7 @@ build_firmware_steps: &build_firmware_steps
- run:
name: Create artifacts
command: |
<<<<<<< HEAD
cp _build/${MIX_TARGET}_${MIX_ENV}/nerves/images/farmbot.fw /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
=======
cp /nerves/build/farmbot_os/_build/${MIX_TARGET}/${MIX_ENV}/nerves/images/farmbot.fw /nerves/deploy/system/artifacts/farmbot-${MIX_TARGET}-$(cat VERSION).fw
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
- save_cache:
key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }}
paths:
@ -111,15 +97,9 @@ deploy_nerves_hub_firmware_steps: &deploy_nerves_hub_firmware_steps
- run:
name: Setup ENV
command: |
<<<<<<< HEAD
echo $MIX_TARGET > MIX_TARGET
echo $MIX_ENV > MIX_ENV
cp mix.lock MIX_LOCK
=======
echo "/nerves/build/farmbot_os/$MIX_TARGET" > MIX_TARGET
echo "/nerves/build/farmbot_os/$MIX_ENV" > MIX_ENV
cp /nerves/build/farmbot_os/mix.lock.$MIX_TARGET MIX_LOCK
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
- restore_cache:
key: v9-fbos-{{ checksum "MIX_TARGET" }}-{{ checksum "MIX_ENV" }}-dependency-cache-{{ checksum "MIX_LOCK" }}
- restore_cache:
@ -174,12 +154,8 @@ jobs:
environment:
MIX_ENV: test
MIX_TARGET: host
<<<<<<< HEAD
ELIXIR_VERSION: 1.8.1
=======
NERVES_LOG_DISABLE_PROGRESS_BAR: "yes"
ELIXIR_VERSION: 1.7.3
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
steps:
- checkout
- restore_cache:
@ -213,9 +189,6 @@ jobs:
- checkout
- run: git submodule update --init --recursive
- restore_cache:
<<<<<<< HEAD
key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }}
=======
keys:
- v9-fbcore-test-dependency-cache-{{ checksum "farmbot_core/mix.lock" }}
- restore_cache:
@ -296,7 +269,6 @@ jobs:
- run: git submodule update --init --recursive
- restore_cache:
key: v9-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock.host" }}
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
- <<: *install_elixir
- <<: *install_hex_archives
- run:
@ -308,17 +280,10 @@ jobs:
mix format --check-formatted
mix test
- save_cache:
<<<<<<< HEAD
key: v9-fbos-host-test-dependency-cache-{{ checksum "mix.lock" }}
paths:
- _build/host_test
- deps
=======
key: v9-fbos-host-test-dependency-cache-{{ checksum "farmbot_os/mix.lock.host" }}
paths:
- farmbot_os/_build/host
- farmbot_os/deps/host
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
################################################################################
# target=rpi app_env=prod #
@ -433,22 +398,14 @@ jobs:
name: Setup ENV
command: |
echo rpi3 > MIX_TARGET_RPI3
<<<<<<< HEAD
cp mix.lock MIX_LOCK_RPI3
=======
cp /nerves/build/farmbot_os/mix.lock.rpi3 MIX_LOCK_RPI3
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
# echo rpi > MIX_TARGET_RPI
# cp /nerves/build/farmbot_os/mix.lock.rpi MIX_LOCK_RPI
echo $MIX_ENV > MIX_ENV
- restore_cache:
key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI3" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
# - restore_cache:
<<<<<<< HEAD
# key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
=======
# key: nerves/deploy/system-{{ checksum "MIX_TARGET_RPI" }}-{{ .Branch }}-{{ .Revision }}-{{ .Environment.CIRCLE_TAG }}
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
- <<: *install_elixir
- <<: *install_hex_archives
- <<: *install_ghr
@ -497,11 +454,7 @@ jobs:
name: Setup ENV
command: |
echo rpi3 > MIX_TARGET_RPI3
<<<<<<< HEAD
cp mix.lock MIX_LOCK_RPI3
=======
cp /nerves/build/farmbot_os/mix.lock.rpi3 MIX_LOCK_RPI3
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates
# echo rpi > MIX_TARGET_RPI
# cp /nerves/build/farmbot_os/mix.lock.rpi MIX_LOCK_RPI
echo $MIX_ENV > MIX_ENV
@ -695,32 +648,6 @@ workflows:
# - build_rpi_prod
- build_rpi3_prod
<<<<<<< HEAD
# # staging branch to staging.farmbot.io
# nerves_hub_rpi_prod_staging_staging:
# jobs:
# # - build_rpi_prod:
# # context: farmbot-staging
# # filters:
# # branches:
# # only:
# # - staging
# - build_rpi3_prod:
# context: farmbot-staging
# filters:
# branches:
# only:
# - staging
# # - deploy_rpi_prod_staging:
# # context: farmbot-staging
# # requires:
# # - build_rpi_prod
# - deploy_rpi3_prod_staging:
# context: farmbot-staging
# requires:
# - build_rpi3_prod
=======
# staging branch to staging.farmbot.io
nerves_hub_rpi_prod_staging_staging:
jobs:
@ -822,4 +749,3 @@ workflows:
# - next
# requires:
# - build_rpi_prod
>>>>>>> e9b01741... [WIP] Pull in NervesHub updates

View File

@ -2,6 +2,7 @@ defmodule Farmbot.Asset do
alias Farmbot.Asset.{
Repo,
Device,
DeviceCert,
DiagnosticDump,
FarmwareEnv,
FarmwareInstallation,
@ -166,4 +167,13 @@ defmodule Farmbot.Asset do
end
## End DiagnosticDump
## Begin DeviceCert
def new_device_cert(params) do
DeviceCert.changeset(%DeviceCert{}, params)
|> Repo.insert()
end
## End DeviceCert
end

View File

@ -0,0 +1,36 @@
defmodule Farmbot.Asset.DeviceCert do
@moduledoc """
DeviceCerts describe a connection to NervesHub
"""
use Farmbot.Asset.Schema, path: "/api/device_cert"
schema "device_certs" do
field(:id, :id)
has_one(:local_meta, Farmbot.Asset.Private.LocalMeta,
on_delete: :delete_all,
references: :local_id,
foreign_key: :asset_local_id
)
field(:serial_number, :string)
field(:tags, {:array, :string})
timestamps()
end
view device_cert do
%{
id: device_cert.id,
serial_number: device_cert.serial_number,
tags: device_cert.tags
}
end
def changeset(device_cert, params \\ %{}) do
device_cert
|> cast(params, [:id, :serial_number, :tags, :created_at, :updated_at])
|> validate_required([])
end
end

View File

@ -16,7 +16,6 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FbosConfig do
end
def handle_info(:timeout, %FbosConfig{} = fbos_config) do
maybe_reinit_firmware(fbos_config)
bool("arduino_debug_messages", fbos_config.arduino_debug_messages)
bool("auto_sync", fbos_config.auto_sync)
bool("beta_opt_in", fbos_config.beta_opt_in)
@ -50,22 +49,4 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FbosConfig do
update_config_value(:float, "settings", key, val / 1)
:ok = Farmbot.BotState.set_config_value(key, val)
end
defp maybe_reinit_firmware(%FbosConfig{firmware_hardware: nil}) do
:ok
end
defp maybe_reinit_firmware(%FbosConfig{firmware_path: nil}) do
:ok
end
defp maybe_reinit_firmware(%FbosConfig{}) do
alias Farmbot.Firmware
alias Farmbot.Core.FirmwareSupervisor
if is_nil(Process.whereis(Firmware)) do
Logger.warn("Starting Farmbot firmware")
FirmwareSupervisor.reinitialize()
end
end
end

View File

@ -58,11 +58,24 @@ defmodule Farmbot.BotState do
GenServer.call(bot_state_server, {:set_firmware_locked, false})
end
@doc "Sets informational_settings.status"
def set_sync_status(bot_state_server \\ __MODULE__, s)
when s in ["syncing", "synced", "error"] do
GenServer.call(bot_state_server, {:set_sync_status, s})
end
@doc "sets informational_settings.update_available"
def set_update_available(bot_state_server \\ __MODULE__, bool)
when is_boolean(bool) do
GenServer.call(bot_state_server, {:set_update_available, bool})
end
@doc "sets informational_settings.node_name"
def set_node_name(bot_state_server \\ __MODULE__, node_name)
when is_binary(node_name) do
GenServer.call(bot_state_server, {:set_node_name, node_name})
end
@doc "Fetch the current state."
def fetch(bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, :fetch)
@ -209,6 +222,26 @@ defmodule Farmbot.BotState do
{:reply, reply, state}
end
def handle_call({:set_update_available, bool}, _from, state) do
change = %{informational_settings: %{update_available: bool}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:set_node_name, node_name}, _from, state) do
change = %{informational_settings: %{node_name: node_name}}
{reply, state} =
BotStateNG.changeset(state.tree, change)
|> dispatch_and_apply(state)
{:reply, reply, state}
end
def handle_call({:report_disk_usage, percent}, _form, state) do
change = %{informational_settings: %{disk_usage: percent}}
@ -219,7 +252,7 @@ defmodule Farmbot.BotState do
{:reply, reply, state}
end
def handle_call({:memory_usage, megabytes}, _form, state) do
def handle_call({:report_memory_usage, megabytes}, _form, state) do
change = %{informational_settings: %{memory_usage: megabytes}}
{reply, state} =
@ -239,7 +272,7 @@ defmodule Farmbot.BotState do
{:reply, reply, state}
end
def handle_call({:uptime, seconds}, _form, state) do
def handle_call({:report_uptime, seconds}, _form, state) do
change = %{informational_settings: %{uptime: seconds}}
{reply, state} =

View File

@ -26,6 +26,7 @@ defmodule Farmbot.BotStateNG.InformationalSettings do
field(:last_status, :string)
field(:cache_bust, :integer)
field(:busy, :boolean)
field(:update_available, :boolean, default: false)
end
def new do
@ -52,7 +53,8 @@ defmodule Farmbot.BotStateNG.InformationalSettings do
locked: informational_settings.locked,
last_status: informational_settings.last_status,
cache_bust: informational_settings.cache_bust,
busy: informational_settings.busy
busy: informational_settings.busy,
update_available: informational_settings.update_available
}
end
@ -75,7 +77,8 @@ defmodule Farmbot.BotStateNG.InformationalSettings do
:locked,
:last_status,
:cache_bust,
:busy
:busy,
:update_available
])
end
end

View File

@ -19,7 +19,9 @@ defmodule Farmbot.Config.NetworkInterface do
field(:password, :string)
# Advanced settings.
# this should be ipv4_address_method
field(:ipv4_method, :string)
field(:ipv4_address, :string)
field(:ipv4_gateway, :string)
field(:ipv4_subnet_mask, :string)
@ -36,6 +38,7 @@ defmodule Farmbot.Config.NetworkInterface do
config
|> cast(params, @required_fields)
|> validate_required(@required_fields)
|> validate_inclusion(:type, ["wireless", "wired"])
|> unique_constraint(:name)
end
end

View File

@ -1,62 +1,20 @@
defmodule Farmbot.System.Init.Ecto do
@moduledoc "Init module for bringup and teardown of ecto."
use Supervisor
@doc "This will run migrations on all Farmbot Repos."
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
defmodule Farmbot.EctoMigrator do
def child_spec(_opts) do
%{
id: __MODULE__,
start: {__MODULE__, :migrate, []},
type: :worker,
restart: :transient,
shutdown: 500
}
end
def init([]) do
migrate()
:ignore
end
@doc "Replacement for Mix.Tasks.Ecto.Create"
def setup do
repos = Application.get_env(:farmbot, :ecto_repos)
for repo <- repos do
Application.put_env(:farmbot, :repo_hack, repo)
setup(repo)
end
end
def setup(repo) do
db_file = Application.get_env(:farmbot, repo)[:database]
unless File.exists?(db_file) do
:ok = repo.__adapter__.storage_up(repo.config)
end
end
@doc "Replacement for Mix.Tasks.Ecto.Drop"
def drop do
repos = Application.get_env(:farmbot, :ecto_repos)
for repo <- repos do
case drop(repo) do
:ok -> :ok
{:error, :already_down} -> :ok
{:error, reason} -> raise reason
end
end
end
def drop(repo) do
repo.__adapter__.storage_down(repo.config)
end
@doc "Replacement for Mix.Tasks.Ecto.Migrate"
def migrate do
repos = Application.get_env(:farmbot, :ecto_repos)
Application.put_env(:farmbot, :repo_hack, nil)
for repo <- repos do
Application.put_env(:farmbot, :repo_hack, repo)
# setup(repo)
migrate(repo)
end
repos = Application.get_env(:farmbot_core, :ecto_repos)
for repo <- repos, do: migrate(repo)
:ignore
end
def migrate(Farmbot.Asset.Repo) do
@ -81,4 +39,21 @@ defmodule Farmbot.System.Init.Ecto do
Mix.Ecto.restart_apps_if_migrated(apps, migrated)
Process.sleep(500)
end
@doc "Replacement for Mix.Tasks.Ecto.Drop"
def drop do
repos = Application.get_env(:farmbot_core, :ecto_repos)
for repo <- repos do
case drop(repo) do
:ok -> :ok
{:error, :already_down} -> :ok
{:error, reason} -> raise reason
end
end
end
def drop(repo) do
repo.__adapter__.storage_down(repo.config)
end
end

View File

@ -9,13 +9,15 @@ defmodule Farmbot.Core do
def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
def init([]) do
children = [
{Farmbot.BotState, []},
{Farmbot.Logger.Supervisor, []},
{Farmbot.Config.Supervisor, []},
{Farmbot.Asset.Supervisor, []},
{Farmbot.Core.FirmwareSupervisor, []},
{Farmbot.Core.CeleryScript.Supervisor, []},
Farmbot.EctoMigrator,
Farmbot.BotState,
Farmbot.Logger.Supervisor,
Farmbot.Config.Supervisor,
Farmbot.Asset.Supervisor,
Farmbot.Core.FirmwareSupervisor,
Farmbot.Core.CeleryScript.Supervisor,
]
Supervisor.init(children, [strategy: :one_for_all])
end

View File

@ -0,0 +1,13 @@
defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateDeviceCertsTable do
use Ecto.Migration
def change do
create table("device_certs", primary_key: false) do
add(:local_id, :binary_id, primary_key: true)
add(:id, :id)
add(:serial_number, :string)
add(:tags, {:array, :string})
timestamps(inserted_at: :created_at, type: :utc_datetime)
end
end
end

View File

@ -41,6 +41,24 @@ defmodule Farmbot.BotStateNGTest do
end
describe "informational_settings" do
test "sets update_available" do
orig = BotStateNG.new()
assert orig.informational_settings.update_available == false
mut1 =
BotStateNG.changeset(orig, %{informational_settings: %{update_available: true}})
|> Ecto.Changeset.apply_changes()
assert mut1.informational_settings.update_available == true
mut2 =
BotStateNG.changeset(orig, %{informational_settings: %{update_available: false}})
|> Ecto.Changeset.apply_changes()
assert mut2.informational_settings.update_available == false
end
test "reports soc_temp" do
orig = BotStateNG.new()

View File

@ -1,5 +1,5 @@
use Mix.Config
config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization
config :logger, handle_otp_reports: true, handle_sasl_reports: true
import_config "ecto.exs"
import_config "farmbot_core.exs"
import_config "lagger.exs"

View File

@ -1,5 +1,8 @@
use Mix.Config
config :farmbot_core,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]
config :farmbot_ext,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]

View File

@ -7,9 +7,11 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
Queue
}
require Farmbot.Logger
alias Farmbot.AMQP.ConnectionWorker
require Logger
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
require Farmbot.Logger
import Farmbot.Config, only: [get_config_value: 3]
alias Farmbot.{
API.EagerLoader,
@ -22,7 +24,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
@exchange "amq.topic"
defstruct [:conn, :chan, :bot]
defstruct [:conn, :chan, :jwt]
alias __MODULE__, as: State
@doc false
@ -30,21 +32,39 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([conn, jwt]) do
def init(args) do
Process.flag(:sensitive, true)
{:ok, chan} = Channel.open(conn)
:ok = Basic.qos(chan, global: true)
{:ok, _} = Queue.declare(chan, jwt.bot <> "_auto_sync", auto_delete: false)
:ok =
Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, routing_key: "bot.#{jwt.bot}.sync.#")
{:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), no_ack: true)
{:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)}
jwt = Keyword.fetch!(args, :jwt)
{:ok, %State{conn: nil, chan: nil, jwt: jwt}, 1000}
end
def terminate(_reason, _state) do
update_config_value(:bool, "settings", "needs_http_sync", true)
def terminate(reason, state) do
Farmbot.Logger.error(1, "Disconnected from AutoSync channel: #{inspect(reason)}")
# If a channel was still open, close it.
if state.chan, do: AMQP.Channel.close(state.chan)
end
def handle_info(:timeout, state) do
jwt = state.jwt
bot = jwt.bot
auto_sync = bot <> "_auto_sync"
route = "bot.#{bot}.sync.#"
with %{} = conn <- ConnectionWorker.connection(),
{:ok, chan} <- Channel.open(conn),
:ok <- Basic.qos(chan, global: true),
{:ok, _} <- Queue.declare(chan, auto_sync, auto_delete: false),
:ok <- Queue.bind(chan, auto_sync, @exchange, routing_key: route),
{:ok, _} <- Basic.consume(chan, auto_sync, self(), no_ack: true) do
{:noreply, %{state | conn: conn, chan: chan}}
else
nil ->
{:noreply, %{state | conn: nil, chan: nil}, 5000}
error ->
Farmbot.Logger.error(1, "Failed to connect to AutoSync channel: #{inspect(error)}")
{:noreply, %{state | conn: nil, chan: nil}, 1000}
end
end
# Confirmation sent by the broker after registering this process as a consumer
@ -64,13 +84,21 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
end
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
device = state.jwt.bot
case String.split(key, ".") do
["bot", ^device, "sync", asset_kind, id_str] ->
asset_kind = Module.concat([Farmbot, Asset, asset_kind])
data = JSON.decode!(payload)
id = data["id"] || String.to_integer(id_str)
params = data["body"]
label = data["args"]["label"]
handle_asset(asset_kind, label, id, params, state)
end
end
def handle_asset(asset_kind, label, id, params, state) do
auto_sync? = get_config_value(:bool, "settings", "auto_sync")
device = state.bot
["bot", ^device, "sync", asset_kind, id_str] = String.split(key, ".")
asset_kind = Module.concat([Farmbot, Asset, asset_kind])
data = JSON.decode!(payload)
id = data["id"] || String.to_integer(id_str)
params = data["body"]
cond do
# TODO(Connor) no way to cache a deletion yet
@ -125,7 +153,8 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
end
end
json = JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"})
device = state.bot
json = JSON.encode!(%{args: %{label: label}, kind: "rpc_ok"})
:ok = Basic.publish(state.chan, @exchange, "bot.#{device}.from_device", json)
{:noreply, state}
end

View File

@ -1,14 +1,17 @@
defmodule Farmbot.AMQP.BotStateTransport do
use GenServer
use AMQP
alias AMQP.Channel
require Farmbot.Logger
alias Farmbot.AMQP.ConnectionWorker
# Pushes a state tree every 5 seconds for good luck.
@default_force_time_ms 5_000
@default_error_retry_ms 100
@exchange "amq.topic"
defstruct [:conn, :chan, :bot, :state_cache]
defstruct [:conn, :chan, :jwt, :state_cache]
alias __MODULE__, as: State
def force do
@ -20,20 +23,40 @@ defmodule Farmbot.AMQP.BotStateTransport do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([conn, jwt]) do
def init(args) do
jwt = Keyword.fetch!(args, :jwt)
Process.flag(:sensitive, true)
initial_bot_state = Farmbot.BotState.subscribe()
{:ok, chan} = AMQP.Channel.open(conn)
:ok = Basic.qos(chan, global: true)
{:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state), 0}
{:ok, %State{conn: nil, chan: nil, jwt: jwt, state_cache: nil}, 0}
end
def terminate(reason, state) do
Farmbot.Logger.error(1, "Disconnected from BotState channel: #{inspect(reason)}")
# If a channel was still open, close it.
if state.chan, do: Channel.close(state.chan)
end
def handle_cast(:force, state) do
{:noreply, state, 0}
end
def handle_info(:timeout, %{state_cache: bot_state} = state) do
case push_bot_state(state.chan, state.bot, bot_state) do
def handle_info(:timeout, %{state_cache: nil} = state) do
with %{} = conn <- ConnectionWorker.connection(),
{:ok, chan} <- Channel.open(conn),
:ok <- Basic.qos(chan, global: true) do
initial_bot_state = Farmbot.BotState.subscribe()
{:noreply, %{state | conn: conn, chan: chan, state_cache: initial_bot_state}, 0}
else
nil ->
{:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 5000}
err ->
Farmbot.Logger.error(1, "Failed to connect to BotState channel: #{inspect(err)}")
{:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 1000}
end
end
def handle_info(:timeout, %{state_cache: %{} = bot_state, chan: %{}} = state) do
case push_bot_state(state.chan, state.jwt.bot, bot_state) do
:ok ->
{:noreply, state, @default_force_time_ms}
@ -54,6 +77,6 @@ defmodule Farmbot.AMQP.BotStateTransport do
|> Farmbot.BotStateNG.view()
|> Farmbot.JSON.encode!()
AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.status", json)
Basic.publish(chan, @exchange, "bot.#{bot}.status", json)
end
end

View File

@ -1,13 +1,22 @@
defmodule Farmbot.AMQP.CeleryScriptTransport do
use GenServer
use AMQP
alias AMQP.{
Channel,
Queue
}
require Farmbot.Logger
require Logger
alias Farmbot.AMQP.ConnectionWorker
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
@exchange "amq.topic"
defstruct [:conn, :chan, :bot]
defstruct [:conn, :chan, :jwt]
alias __MODULE__, as: State
@doc false
@ -15,20 +24,39 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([conn, jwt]) do
def init(args) do
jwt = Keyword.fetch!(args, :jwt)
Process.flag(:sensitive, true)
{:ok, chan} = AMQP.Channel.open(conn)
:ok = Basic.qos(chan, global: true)
{:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_from_clients", auto_delete: true)
{:ok, _} = AMQP.Queue.purge(chan, jwt.bot <> "_from_clients")
{:ok, %State{conn: nil, chan: nil, jwt: jwt}, 0}
end
:ok =
AMQP.Queue.bind(chan, jwt.bot <> "_from_clients", @exchange,
routing_key: "bot.#{jwt.bot}.from_clients"
)
def terminate(reason, state) do
Farmbot.Logger.error(1, "Disconnected from CeleryScript channel: #{inspect(reason)}")
# If a channel was still open, close it.
if state.chan, do: AMQP.Channel.close(state.chan)
end
{:ok, _tag} = Basic.consume(chan, jwt.bot <> "_from_clients", self(), no_ack: true)
{:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)}
def handle_info(:timeout, state) do
bot = state.jwt.bot
from_clients = bot <> "_from_clients"
route = "bot.#{bot}.from_clients"
with %{} = conn <- ConnectionWorker.connection(),
{:ok, chan} <- Channel.open(conn),
:ok <- Basic.qos(chan, global: true),
{:ok, _} <- Queue.declare(chan, from_clients, auto_delete: true),
{:ok, _} <- Queue.purge(chan, from_clients),
:ok <- Queue.bind(chan, from_clients, @exchange, routing_key: route),
{:ok, _tag} <- Basic.consume(chan, from_clients, self(), no_ack: true) do
{:noreply, %{state | conn: conn, chan: chan}}
else
nil ->
{:noreply, %{state | conn: nil, chan: nil}, 5000}
err ->
Farmbot.Logger.error(1, "Failed to connect to CeleryScript channel: #{inspect(err)}")
{:noreply, %{state | conn: nil, chan: nil}, 1000}
end
end
# Confirmation sent by the broker after registering this process as a consumer
@ -53,7 +81,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
end
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
device = state.bot
device = state.jwt.bot
["bot", ^device, "from_clients"] = String.split(key, ".")
spawn_link(fn ->
@ -76,7 +104,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
Logger.error(message)
end
AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.bot}.from_device", reply)
AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.jwt.bot}.from_device", reply)
results_ast
end)
end

View File

@ -4,7 +4,7 @@ defmodule Farmbot.AMQP.ChannelSupervisor do
alias Farmbot.JWT
alias Farmbot.AMQP.{
ConnectionWorker,
NervesHubTransport,
LogTransport,
BotStateTransport,
AutoSyncTransport,
@ -17,14 +17,14 @@ defmodule Farmbot.AMQP.ChannelSupervisor do
def init([token]) do
Process.flag(:sensitive, true)
conn = ConnectionWorker.connection()
jwt = JWT.decode!(token)
children = [
{LogTransport, [conn, jwt]},
{BotStateTransport, [conn, jwt]},
{AutoSyncTransport, [conn, jwt]},
{CeleryScriptTransport, [conn, jwt]}
{NervesHubTransport, [jwt: jwt]},
{LogTransport, [jwt: jwt]},
{BotStateTransport, [jwt: jwt]},
{AutoSyncTransport, [jwt: jwt]},
{CeleryScriptTransport, [jwt: jwt]}
]
Supervisor.init(children, strategy: :one_for_one)

View File

@ -3,7 +3,8 @@ defmodule Farmbot.AMQP.ConnectionWorker do
alias Farmbot.JWT
require Farmbot.Logger
require Logger
import Farmbot.Config, only: [update_config_value: 4]
defstruct [:opts, :conn]
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
@ -14,20 +15,16 @@ defmodule Farmbot.AMQP.ConnectionWorker do
end
def init(opts) do
token = Keyword.fetch!(opts, :token)
email = Keyword.fetch!(opts, :email)
Process.flag(:sensitive, true)
Process.flag(:trap_exit, true)
jwt = JWT.decode!(token)
IO.puts("OPEN")
{:ok, conn} = open_connection(token, email, jwt.bot, jwt.mqtt, jwt.vhost)
IO.puts("OPENED")
Process.link(conn.pid)
Process.monitor(conn.pid)
{:ok, conn}
{:ok, %__MODULE__{conn: nil, opts: opts}, 0}
end
def terminate(reason, conn) do
def terminate(reason, %{conn: nil}) do
Logger.info("AMQP connection not open: #{inspect(reason)}")
end
def terminate(reason, %{conn: conn}) do
if Process.alive?(conn.pid) do
try do
Logger.info("Closing AMQP connection: #{inspect(reason)}")
@ -40,19 +37,29 @@ defmodule Farmbot.AMQP.ConnectionWorker do
end
end
def handle_info({:DOWN, _, :process, _pid, reason}, conn) do
ok_reasons = [:normal, :shutdown, :token_refresh]
update_config_value(:bool, "settings", "ignore_fbos_config", false)
def handle_info(:timeout, state) do
token = Keyword.fetch!(state.opts, :token)
email = Keyword.fetch!(state.opts, :email)
jwt = JWT.decode!(token)
if reason not in ok_reasons do
Farmbot.Logger.error(1, "AMQP Connection closed: #{inspect(reason)}")
update_config_value(:bool, "settings", "log_amqp_connected", true)
case open_connection(token, email, jwt.bot, jwt.mqtt, jwt.vhost) do
{:ok, conn} ->
Process.link(conn.pid)
Process.monitor(conn.pid)
{:noreply, %{state | conn: conn}}
err ->
Logger.error("Error connecting to AMPQ: #{inspect(err)}")
{:noreply, %{state | conn: nil}, 5000}
end
end
def handle_info({:DOWN, _, :process, _pid, reason}, conn) do
Logger.error("Connection crash: #{inspect(reason)}")
{:stop, reason, conn}
end
def handle_call(:connection, _, conn), do: {:reply, conn, conn}
def handle_call(:connection, _, %{conn: conn} = state), do: {:reply, conn, state}
defp open_connection(token, email, bot, mqtt_server, vhost) do
Logger.info("Opening new AMQP connection.")
@ -74,14 +81,6 @@ defmodule Farmbot.AMQP.ConnectionWorker do
virtual_host: vhost
]
case AMQP.Connection.open(opts) do
{:ok, conn} ->
{:ok, conn}
{:error, reason} ->
Logger.error("Error connecting to AMPQ: #{inspect(reason)}")
Process.sleep(5000)
open_connection(token, email, bot, mqtt_server, vhost)
end
AMQP.Connection.open(opts)
end
end

View File

@ -1,14 +1,16 @@
defmodule Farmbot.AMQP.LogTransport do
use GenServer
use AMQP
alias AMQP.Channel
alias Farmbot.AMQP.ConnectionWorker
require Farmbot.Logger
require Logger
import Farmbot.Config, only: [update_config_value: 4]
@exchange "amq.topic"
@checkup_ms 100
defstruct [:conn, :chan, :bot, :state_cache]
defstruct [:conn, :chan, :jwt, :state_cache]
alias __MODULE__, as: State
@doc false
@ -16,26 +18,36 @@ defmodule Farmbot.AMQP.LogTransport do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([conn, jwt]) do
def init(args) do
jwt = Keyword.fetch!(args, :jwt)
Process.flag(:sensitive, true)
initial_bot_state = Farmbot.BotState.subscribe()
{:ok, chan} = AMQP.Channel.open(conn)
:ok = Basic.qos(chan, global: true)
state = struct(State, conn: conn, chan: chan, bot: jwt.bot, state_cache: initial_bot_state)
{:ok, state, 0}
{:ok, %State{conn: nil, chan: nil, jwt: jwt, state_cache: nil}, 0}
end
def terminate(reason, state) do
ok_reasons = [:normal, :shutdown, :token_refresh]
update_config_value(:bool, "settings", "ignore_fbos_config", false)
if reason not in ok_reasons do
Farmbot.Logger.error(1, "Logger amqp client Died: #{inspect(reason)}")
update_config_value(:bool, "settings", "log_amqp_connected", true)
end
Farmbot.Logger.error(1, "Disconnected from Log channel: #{inspect(reason)}")
# If a channel was still open, close it.
if state.chan, do: AMQP.Channel.close(state.chan)
if state.chan, do: Channel.close(state.chan)
end
def handle_info(:timeout, %{state_cache: nil} = state) do
with %{} = conn <- ConnectionWorker.connection(),
{:ok, chan} <- Channel.open(conn),
:ok <- Basic.qos(chan, global: true) do
initial_bot_state = Farmbot.BotState.subscribe()
{:noreply, %{state | conn: conn, chan: chan, state_cache: initial_bot_state}, 0}
else
nil ->
{:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 5000}
err ->
Farmbot.Logger.error(1, "Failed to connect to Log channel: #{inspect(err)}")
{:noreply, %{state | conn: nil, chan: nil, state_cache: nil}, 1000}
end
end
def handle_info(:timeout, state) do
{:noreply, state, {:continue, Farmbot.Logger.handle_all_logs()}}
end
def handle_info({Farmbot.BotState, change}, state) do
@ -43,10 +55,6 @@ defmodule Farmbot.AMQP.LogTransport do
{:noreply, %{state | state_cache: new_state_cache}, @checkup_ms}
end
def handle_info(:timeout, state) do
{:noreply, state, {:continue, Farmbot.Logger.handle_all_logs()}}
end
def handle_continue([log | rest], state) do
case do_handle_log(log, state) do
:ok ->
@ -86,13 +94,13 @@ defmodule Farmbot.AMQP.LogTransport do
}
json_log = add_position_to_log(log_without_pos, location_data)
push_bot_log(state.chan, state.bot, json_log)
push_bot_log(state.chan, state.jwt.bot, json_log)
end
end
defp push_bot_log(chan, bot, log) do
json = Farmbot.JSON.encode!(log)
:ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.logs", json)
:ok = Basic.publish(chan, @exchange, "bot.#{bot}.logs", json)
end
defp add_position_to_log(%{} = log, %{position: %{x: x, y: y, z: z}}) do

View File

@ -0,0 +1,102 @@
defmodule Farmbot.AMQP.NervesHubTransport do
use GenServer
use AMQP
alias AMQP.{
Channel,
Queue
}
require Farmbot.Logger
require Logger
alias Farmbot.JSON
alias Farmbot.AMQP.ConnectionWorker
@exchange "amq.topic"
defstruct [:conn, :chan, :jwt]
alias __MODULE__, as: State
@doc false
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init(args) do
jwt = Keyword.fetch!(args, :jwt)
Process.flag(:sensitive, true)
{:ok, %State{conn: nil, chan: nil, jwt: jwt}, 0}
end
def terminate(reason, state) do
Farmbot.Logger.error(1, "Disconnected from NervesHub AMQP channel: #{inspect(reason)}")
# If a channel was still open, close it.
if state.chan, do: AMQP.Channel.close(state.chan)
end
def handle_info(:timeout, state) do
bot = state.jwt.bot
nerves_hub = bot <> "_nerves_hub"
route = "bot.#{bot}.nerves_hub"
with %{} = conn <- ConnectionWorker.connection(),
{:ok, chan} <- Channel.open(conn),
:ok <- Basic.qos(chan, global: true),
{:ok, _} <- Queue.declare(chan, nerves_hub, auto_delete: false, durable: true),
:ok <- Queue.bind(chan, nerves_hub, @exchange, routing_key: route),
{:ok, _tag} <- Basic.consume(chan, nerves_hub, self(), []) do
{:noreply, %{state | conn: conn, chan: chan}}
else
nil ->
{:noreply, %{state | conn: nil, chan: nil}, 5000}
err ->
Farmbot.Logger.error(1, "Failed to connect to NervesHub AMQP channel: #{inspect(err)}")
{:noreply, %{state | conn: nil, chan: nil}, 1000}
end
end
# Confirmation sent by the broker after registering this process as a consumer
def handle_info({:basic_consume_ok, _}, state) do
{:noreply, state}
end
# 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
# Confirmation sent by the broker to the consumer process after a Basic.cancel
def handle_info({:basic_cancel_ok, _}, state) do
{:noreply, state}
end
def handle_info({:basic_deliver, payload, %{routing_key: key} = opts}, state) do
device = state.jwt.bot
["bot", ^device, "nerves_hub"] = String.split(key, ".")
handle_nerves_hub(payload, opts, state)
end
def handle_nerves_hub(payload, options, state) do
alias Farmbot.System.NervesHub
with {:ok, %{"cert" => base64_cert, "key" => base64_key}} <- JSON.decode(payload),
{:ok, cert} <- Base.decode64(base64_cert),
{:ok, key} <- Base.decode64(base64_key),
:ok <- NervesHub.configure_certs(cert, key),
:ok <- NervesHub.connect() do
:ok = Basic.ack(state.chan, options[:delivery_tag])
{:noreply, state}
else
{:error, reason} ->
Logger.error(1, "NervesHub failed to configure. #{inspect(reason)}")
{:noreply, state}
:error ->
Logger.error(1, "NervesHub payload invalid. (base64)")
{:noreply, state}
end
end
end

View File

@ -22,7 +22,7 @@ defmodule Farmbot.API do
uri = Map.fetch!(tkn, :iss) |> URI.parse()
url = (uri.scheme || "https") <> "://" <> uri.host <> ":" <> to_string(uri.port)
Tesla.build_client([
Tesla.client([
{Tesla.Middleware.BaseUrl, url},
{Tesla.Middleware.Headers,
[
@ -34,7 +34,7 @@ defmodule Farmbot.API do
end
def storage_client(%StorageAuth{url: url}) do
Tesla.build_client(
Tesla.client(
[
{Tesla.Middleware.BaseUrl, "https:" <> url},
{Tesla.Middleware.Headers,

View File

@ -26,7 +26,7 @@ defmodule Farmbot.API.DirtyWorker do
@impl GenServer
def init(args) do
Logger.disable(self())
# Logger.disable(self())
module = Keyword.fetch!(args, :module)
timeout = Keyword.get(args, :timeout, @timeout)
{:ok, %{module: module, timeout: timeout}, timeout}

View File

@ -4,6 +4,7 @@ defmodule Farmbot.API.DirtyWorker.Supervisor do
alias Farmbot.Asset.{
Device,
DeviceCert,
DiagnosticDump,
FarmEvent,
FarmwareEnv,
@ -27,6 +28,7 @@ defmodule Farmbot.API.DirtyWorker.Supervisor do
def init(_args) do
children = [
{DirtyWorker, Device},
{DirtyWorker, DeviceCert},
{DirtyWorker, DiagnosticDump},
{DirtyWorker, FarmEvent},
{DirtyWorker, FarmwareEnv},

View File

@ -0,0 +1,58 @@
defmodule Farmbot.Bootstrap do
use GenServer
require Logger
alias Farmbot.Bootstrap.Authorization
import Farmbot.Config, only: [update_config_value: 4, get_config_value: 3]
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
update_config_value(:bool, "settings", "log_amqp_connected", true)
{:ok, nil, 0}
end
def handle_info(:timeout, nil) do
email = get_config_value(:string, "authorization", "email")
server = get_config_value(:string, "authorization", "server")
password = get_config_value(:string, "authorization", "password")
secret = get_config_value(:string, "authorization", "secret")
try_auth(email, server, password, secret)
end
def try_auth(nil, _server, _password, _secret) do
{:noreply, nil, 5000}
end
def try_auth(_email, nil, _password, _secret) do
{:noreply, nil, 5000}
end
def try_auth(email, server, nil, secret) when is_binary(secret) do
Logger.debug("using secret to auth")
with {:ok, tkn} <- Authorization.authorize_with_secret(email, secret, server),
_ <- update_config_value(:string, "authorization", "token", tkn),
{:ok, pid} <- Supervisor.start_child(Farmbot.Ext, Farmbot.Bootstrap.Supervisor) do
{:noreply, pid}
else
_ -> {:noreply, nil, 5000}
end
end
# TODO(Connor) - drop password and save secret here somehow.
def try_auth(email, server, password, _secret) do
Logger.debug("using password to auth")
# require IEx; IEx.pry
with {:ok, tkn} <- Authorization.authorize_with_password(email, password, server),
_ <- update_config_value(:string, "authorization", "token", tkn),
{:ok, pid} <- Supervisor.start_child(Farmbot.Ext, Farmbot.Bootstrap.Supervisor) do
{:noreply, pid}
else
_ -> {:noreply, nil, 5000}
end
end
end

View File

@ -50,12 +50,12 @@ defmodule Farmbot.Bootstrap.Authorization do
|> build_payload()
end
defp build_payload(secret) do
def build_payload(secret) do
%{user: %{credentials: secret |> Base.encode64()}}
|> Farmbot.JSON.encode()
end
defp build_secret(email, password, rsa_key) do
def build_secret(email, password, rsa_key) do
%{email: email, password: password, id: UUID.uuid1(), version: 1}
|> Farmbot.JSON.encode!()
|> RSA.encrypt({:public, rsa_key})
@ -67,7 +67,7 @@ defmodule Farmbot.Bootstrap.Authorization do
]
@spec fetch_rsa_key(server) :: {:ok, term} | {:error, String.t() | atom}
def fetch_rsa_key(server) do
def fetch_rsa_key(server) when is_binary(server) do
url = "#{server}/api/public_key"
with {:ok, body} <- do_request({:get, url, "", @headers}) do

View File

@ -1,138 +1,21 @@
defmodule Farmbot.Bootstrap.Supervisor do
@moduledoc """
Bootstraps the application.
It is expected that there is authorization credentials in the application's
environment by this point. This can be configured via a `Farmbot.Init` module.
For example:
# config.exs
use Mix.Config
config :farmbot_ext, :init, [
Farmbot.Configurator
]
config :farmbot_ext, :behaviour,
authorization: Farmbot.Configurator
# farmbot_configurator.ex
defmodule Farmbot.Configurator do
@moduledoc false
@behaviour Farmbot.Bootstrap.Authorization
# Callback for Farmbot.System.Init.
# This can return {:ok, pid} if it should be a supervisor.
def start_link(_args, _opts) do
creds = [
email: "some_user@some_server.org",
password: "some_secret_password_dont_actually_store_in_plain_text",
server: "https://my.farmbot.io"
]
Application.put_env(:farmbot_ext, :behaviour, creds)
:ignore
end
# Callback for Farmbot.Bootstrap.Authorization.
# Should return `{:ok, token}` where `token` is a binary jwt, or
# {:error, reason} reason can be anything, but a binary is easiest to
# Parse.
def authorize(email, password, server) do
# some intense http stuff or whatever.
{:ok, token}
end
end
This will cause the `creds` to be stored in the application's environment.
This moduld then will try to use the configured module to `authorize`.
If either of these things error, the bot try to factory reset
"""
use Supervisor
# alias Farmbot.Bootstrap.Authorization, as: Auth
alias Farmbot.Config
import Config, only: [update_config_value: 4, get_config_value: 3]
require Farmbot.Logger
error_msg = """
Please configure an authorization module!
for example:
config: :farmbot_ext, :behaviour, [
authorization: Farmbot.Bootstrap.Authorization
]
"""
@auth_task Application.get_env(:farmbot_ext, :behaviour)[:authorization]
@auth_task || Mix.raise(error_msg)
@doc "Start Bootstrap services."
@doc "Start Bootstraped services."
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
# Make sure we log when amqp is connected.
update_config_value(:bool, "settings", "log_amqp_connected", true)
email = get_config_value(:string, "authorization", "email")
server = get_config_value(:string, "authorization", "server")
password = get_config_value(:string, "authorization", "password")
secret = get_config_value(:string, "authorization", "secret")
children = [
Farmbot.API.EagerLoader.Supervisor,
Farmbot.API.DirtyWorker.Supervisor,
Farmbot.Bootstrap.APITask,
Farmbot.AMQP.Supervisor,
Farmbot.API.ImageUploader
]
cond do
is_nil(email) -> exit("No email")
is_nil(server) -> exit("No server")
is_nil(secret) && is_nil(password) -> exit("No password or secret.")
secret -> actual_init(&auth_with_secret/3, email, secret, server)
password -> actual_init(&auth_with_password/3, email, password, server)
end
end
defp actual_init(fun, email, password_or_secret, server) when is_function(fun) do
busy_msg = "Beginning Bootstrap authorization: #{email} - #{server}"
Farmbot.Logger.busy(2, busy_msg)
# get a token
case apply_auth_fun(fun, [email, password_or_secret, server]) do
{:ok, token} ->
success_msg = "Successful Bootstrap authorization: #{email} - #{server}"
Farmbot.Logger.success(2, success_msg)
update_config_value(:bool, "settings", "first_boot", false)
update_config_value(:bool, "settings", "needs_http_sync", true)
update_config_value(:string, "authorization", "token", token)
children = [
Farmbot.API.EagerLoader.Supervisor,
Farmbot.API.DirtyWorker.Supervisor,
Farmbot.Bootstrap.APITask,
Farmbot.AMQP.Supervisor,
Farmbot.API.ImageUploader
]
opts = [strategy: :one_for_one]
Supervisor.init(children, opts)
{:error, reason} ->
exit(reason)
end
end
defp apply_auth_fun(fun, args) do
try do
apply(fun, args)
rescue
e -> {:error, Exception.message(e)}
end
end
defp auth_with_secret(e, sec, s) do
Farmbot.Logger.debug(3, "Using secret to authorize.")
@auth_task.authorize_with_secret(e, sec, s)
end
defp auth_with_password(e, pass, s) do
Farmbot.Logger.debug(3, "Using password to authorize.")
@auth_task.authorize_with_password(e, pass, s)
opts = [strategy: :one_for_one]
Supervisor.init(children, opts)
end
end

View File

@ -8,7 +8,7 @@ defmodule Farmbot.Ext do
def start(_type, _args) do
# List all child processes to be supervised
children = [
{Farmbot.Bootstrap.Supervisor, []}
Farmbot.Bootstrap
]
# See https://hexdocs.pm/elixir/Supervisor.html

View File

@ -35,7 +35,7 @@ config :farmbot_core, :behaviour,
config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization
config :ecto, json_library: Farmbot.JSON
config :farmbot,
config :farmbot_core,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]
config :farmbot_core, Farmbot.Config.Repo,
@ -67,7 +67,13 @@ config :farmbot, Farmbot.Platform.Supervisor,
import_config("lagger.exs")
if Mix.Project.config()[:target] == "host" do
import_config("host/#{Mix.env()}.exs")
if File.exists?("config/host/#{Mix.env()}.exs") do
import_config("host/#{Mix.env()}.exs")
end
else
import_config("target/#{Mix.env()}.exs")
if File.exists?("config/target/#{Mix.Project.config()[:target]}.exs") do
import_config("target/#{Mix.Project.config()[:target]}.exs")
end
end

View File

@ -40,6 +40,3 @@ config :farmbot_core, :behaviour,
pin_binding_handler: Farmbot.PinBinding.StubHandler,
celery_script_io_layer: Farmbot.OS.IOLayer,
firmware_handler: Farmbot.Firmware.UartHandler
config :farmbot_core, :uart_handler, tty: "/dev/ttyACM0"
import_config("auth_secret.exs")

View File

@ -1,6 +1,7 @@
use Mix.Config
data_path = Path.join(["/", "tmp", "farmbot"])
File.mkdir_p(data_path)
config :farmbot_ext,
data_path: data_path

View File

@ -3,8 +3,22 @@ local_file = Path.join(System.user_home!(), ".ssh/id_rsa.pub")
local_key = if File.exists?(local_file), do: [File.read!(local_file)], else: []
config :nerves_firmware_ssh,
authorized_keys: local_key,
ssh_console_port: 22
authorized_keys: local_key
config :nerves_network, regulatory_domain: "US"
config :nerves_init_gadget,
ifname: "usb0",
address_method: :dhcpd,
mdns_domain: "farmbot.local",
node_name: "farmbot",
node_host: :mdns_domain
config :shoehorn,
init: [:nerves_runtime, :nerves_init_gadget, :nerves_firmware_ssh, :farmbot_core, :farmbot_ext],
app: :farmbot
config :tzdata, :autoupdate, :disabled
config :farmbot_core, :behaviour,
firmware_handler: Farmbot.Firmware.StubHandler,
@ -15,8 +29,7 @@ config :farmbot_core, :behaviour,
data_path = Path.join("/", "root")
config :farmbot_ext,
data_path: data_path
config :farmbot, Farmbot.OS.FileSystem, data_path: data_path
config :logger_backend_ecto, LoggerBackendEcto.Repo,
adapter: Sqlite.Ecto2,
@ -38,31 +51,24 @@ config :farmbot_core, Farmbot.Asset.Repo,
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3")
config :farmbot,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]
config :farmbot, Farmbot.System.Init.Supervisor,
init_children: [
{Farmbot.Target.Leds.AleHandler, []},
{Farmbot.Firmware.UartHandler.AutoDetector, []}
],
platform_children: [
{Farmbot.Target.Bootstrap.Configurator, []},
{Farmbot.Target.Network, []},
{Farmbot.Target.SSHConsole, []},
{Farmbot.Target.Network.WaitForTime, []},
{Farmbot.Target.Network.DnsTask, []},
{Farmbot.Target.Network.TzdataTask, []},
# Reports Disk usage every 60 seconds.
{Farmbot.Target.DiskUsageWorker, []},
# Reports Memory usage every 60 seconds.
{Farmbot.Target.MemoryUsageWorker, []},
# Reports SOC temperature every 60 seconds.
{Farmbot.Target.SocTempWorker, []},
# Reports Uptime every 60 seconds.
{Farmbot.Target.UptimeWorker, []},
{Farmbot.Target.Network.InfoSupervisor, []},
{Farmbot.Target.Uevent.Supervisor, []}
Farmbot.Target.Leds.AleHandler
]
config :farmbot, :behaviour, system_tasks: Farmbot.Target.SystemTasks
config :farmbot, Farmbot.Platform.Supervisor,
platform_children: [
Farmbot.System.NervesHub,
Farmbot.Target.Network.Supervisor,
Farmbot.Target.Configurator.Supervisor,
Farmbot.Target.SSHConsole,
Farmbot.Target.Uevent.Supervisor,
Farmbot.Target.InfoWorker.Supervisor
]
config :farmbot, Farmbot.System, system_tasks: Farmbot.Target.SystemTasks
config :farmbot, Farmbot.System.NervesHub,
farmbot_nerves_hub_handler: Farmbot.System.NervesHubClient

View File

@ -1,5 +1,23 @@
use Mix.Config
config :nerves_firmware_ssh,
authorized_keys: []
config :nerves_network, regulatory_domain: "US"
config :nerves_init_gadget,
ifname: "usb0",
address_method: :dhcpd,
mdns_domain: "farmbot.local",
node_name: "farmbot",
node_host: :mdns_domain
config :shoehorn,
init: [:nerves_runtime, :nerves_init_gadget, :nerves_firmware_ssh, :farmbot_core, :farmbot_ext],
app: :farmbot
config :tzdata, :autoupdate, :disabled
config :farmbot_core, :behaviour,
firmware_handler: Farmbot.Firmware.StubHandler,
leds_handler: Farmbot.Target.Leds.AleHandler,
@ -9,8 +27,7 @@ config :farmbot_core, :behaviour,
data_path = Path.join("/", "root")
config :farmbot_ext,
data_path: data_path
config :farmbot, Farmbot.OS.FileSystem, data_path: data_path
config :logger_backend_ecto, LoggerBackendEcto.Repo,
adapter: Sqlite.Ecto2,
@ -32,31 +49,24 @@ config :farmbot_core, Farmbot.Asset.Repo,
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3")
config :farmbot,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo]
config :farmbot, Farmbot.System.Init.Supervisor,
init_children: [
{Farmbot.Target.Leds.AleHandler, []},
{Farmbot.Firmware.UartHandler.AutoDetector, []}
],
platform_children: [
{Farmbot.Target.Bootstrap.Configurator, []},
{Farmbot.Target.Network, []},
{Farmbot.Target.SSHConsole, []},
{Farmbot.Target.Network.WaitForTime, []},
{Farmbot.Target.Network.DnsTask, []},
{Farmbot.Target.Network.TzdataTask, []},
# Reports Disk usage every 60 seconds.
{Farmbot.Target.DiskUsageWorker, []},
# Reports Memory usage every 60 seconds.
{Farmbot.Target.MemoryUsageWorker, []},
# Reports SOC temperature every 60 seconds.
{Farmbot.Target.SocTempWorker, []},
# Reports Uptime every 60 seconds.
{Farmbot.Target.UptimeWorker, []},
{Farmbot.Target.Network.InfoSupervisor, []},
{Farmbot.Target.Uevent.Supervisor, []}
Farmbot.Target.Leds.AleHandler
]
config :farmbot, :behaviour, system_tasks: Farmbot.Target.SystemTasks
config :farmbot, Farmbot.Platform.Supervisor,
platform_children: [
Farmbot.System.NervesHub,
Farmbot.Target.Network.Supervisor,
Farmbot.Target.Configurator.Supervisor,
Farmbot.Target.SSHConsole,
Farmbot.Target.Uevent.Supervisor,
Farmbot.Target.InfoWorker.Supervisor
]
config :farmbot, Farmbot.System, system_tasks: Farmbot.Target.SystemTasks
config :farmbot, Farmbot.System.NervesHub,
farmbot_nerves_hub_handler: Farmbot.System.NervesHubClient

View File

@ -0,0 +1,2 @@
use Mix.Config
config :farmbot, :captive_portal_address, "192.168.24.1"

View File

@ -1,31 +0,0 @@
defmodule Farmbot.System.CoreStart do
@moduledoc false
use Supervisor
require Logger
@doc false
def start_link(args) do
Supervisor.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
:ok = start_core_app(Farmbot.BootState.read())
Supervisor.init([], [strategy: :one_for_one])
end
defp start_core_app(state) do
case Application.ensure_all_started(:farmbot_core) do
{:ok, _} ->
Farmbot.BootState.write(:UPANDRUNNING)
:ok
{:error, {:farmbot_core, {{:shutdown, {:failed_to_start_child, child, reason}}, _}}} ->
msg = "Failed to start farmbot_core while in state: #{inspect state} child: #{child} => #{inspect reason}"
maybe_reset(msg)
:ok
end
end
defp maybe_reset(msg) do
Farmbot.System.factory_reset(msg)
end
end

View File

@ -1,34 +0,0 @@
defmodule Farmbot.System.ExtStart do
@moduledoc false
use Supervisor
require Logger
@doc false
def start_link(args) do
Supervisor.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
:ok = start_ext_app(Farmbot.BootState.read())
Supervisor.init([], [strategy: :one_for_one])
end
defp start_ext_app(state) do
case Application.ensure_all_started(:farmbot_ext) do
{:ok, _} ->
Farmbot.BootState.write(:UPANDRUNNING)
:ok
{:error, {:farmbot_ext, {{:shutdown, {:failed_to_start_child, child, reason}}, _}}} ->
msg = "Failed to start farmbot_ext while in state: #{inspect state} child: #{child} => #{inspect reason}"
maybe_reset(msg)
:ok
end
end
defp maybe_reset(msg) do
case Farmbot.Config.get_config_value(:bool, "settings", "first_boot") do
true -> Farmbot.System.factory_reset(msg)
false -> Logger.error(msg)
end
end
end

View File

@ -8,9 +8,7 @@ defmodule Farmbot.OS do
def start(_type, _args) do
children = [
{Farmbot.System.Init.Supervisor, []},
{Farmbot.System.CoreStart, []},
{Farmbot.Platform.Supervisor, []},
{Farmbot.System.ExtStart, []},
{Farmbot.EasterEggs, []},
]
opts = [strategy: :one_for_one, name: __MODULE__]

View File

@ -0,0 +1,3 @@
defmodule Farmbot.Firmware.AutoDetector do
end

View File

@ -6,9 +6,9 @@ defmodule Farmbot.System.Init.Supervisor do
end
def init([]) do
children = Application.get_env(:farmbot, :init_children, []) ++ [
config = Application.get_env(:farmbot, __MODULE__)
children = (config[:init_children] || []) ++ [
{Farmbot.System.Init.FSCheckup, []},
{Farmbot.System.Init.Ecto, []},
]
Supervisor.init(children, [strategy: :one_for_all])
end

View File

@ -105,6 +105,9 @@ defmodule Farmbot.System.NervesHub do
def get_config do
@handler.config()
|> Enum.map(fn(val) ->
if val == "", do: nil, else: val
end)
end
def connect do
@ -126,18 +129,21 @@ defmodule Farmbot.System.NervesHub do
# Creates a device in NervesHub
# or updates it if one exists.
def configure(tags) when is_list(tags) do
Farmbot.Logger.debug 1, "Configuring OTA Service: #{inspect tags}"
payload = %{
alias Farmbot.Asset.{Repo, DeviceCert}
Farmbot.Logger.debug 1, "Configuring OTA Service DeviceCert: #{inspect tags}"
serial = serial()
old = Repo.get_by(DeviceCert, serial_number: serial) || %DeviceCert{}
params = %{
serial_number: serial(),
tags: tags
} |> Farmbot.JSON.encode!()
case Farmbot.HTTP.post("/api/device_cert", payload) do
}
change = DeviceCert.changeset(old, params)
case Repo.insert_or_update(change) do
{:ok, _} ->
:ok
{:error, reason} ->
Logger.error 1, "Failed to configure nerveshub due to http error: #{inspect(reason)}"
Logger.error 1, "Failed to configure nerveshub due to database error: #{inspect(reason)}"
end
:ok
end
# Message comes over AMQP.

View File

@ -1,31 +0,0 @@
defmodule Farmbot.OS.ShoehornHandler do
use Shoehorn.Handler
require Logger
def init do
{:ok, %{}}
end
def application_exited(:farmbot_core, reason, state) do
Logger.error "FarmbotCore exited: #{inspect reason}"
Application.stop(:farmbot)
Application.ensure_all_started(:farmbot)
{:continue, state}
end
def application_exited(:farmbot, reason, state) do
Logger.error "FarmbotOS exited: #{inspect reason}"
Application.ensure_all_started(:farmbot)
{:continue, state}
end
def application_exited(:farmbot_ext, reason, state) do
Logger.error "FarmbotExt exited: #{inspect reason}"
Application.ensure_all_started(:farmbot_ext)
{:continue, state}
end
def application_exited(_app, _reason, state) do
{:continue, state}
end
end

View File

@ -41,8 +41,7 @@ defmodule Farmbot.OS.MixProject do
def application do
[
mod: {Farmbot.OS, []},
extra_applications: [:logger, :runtime_tools, :eex],
included_applications: [:farmbot_core, :farmbot_ext]
extra_applications: [:logger, :runtime_tools, :eex]
]
end
@ -69,9 +68,6 @@ defmodule Farmbot.OS.MixProject do
defp deps(target) do
[
# Configurator
{:cowboy, "~> 2.5"},
{:plug, "~> 1.6"},
# override: true because AMQP
{:ranch, "~> 1.5", override: true},
{:cors_plug, "~> 2.0"},
@ -85,7 +81,8 @@ defmodule Farmbot.OS.MixProject do
{:mdns, "~> 1.0"},
{:nerves_firmware_ssh, "~> 0.3"},
{:nerves_init_gadget, "~> 0.5", only: :dev},
{:elixir_ale, "~> 1.2"}
{:elixir_ale, "~> 1.2"},
{:toolshed, "~> 0.2"}
] ++ system(target)
end

View File

@ -92,7 +92,8 @@
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
"system_registry": {:hex, :system_registry, "0.8.1", "7df1f66f0e4fcd0940ecd0473d2787d69d2abd6267d21e8f8ecbab58a14415ce", [:mix], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"toolshed": {:hex, :toolshed, "0.2.2", "30fc70ef7d5ce9cdad17f99c28cfbd4ba4ea6199fb9245c542b674fb9f0248a3", [:mix], [], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"uboot_env": {:hex, :uboot_env, "0.1.0", "176d277c8461d849614d8b82595060bf03ace6ca59d49c5b53707d90cb9e2f5a", [:mix], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},

View File

@ -81,6 +81,7 @@
"system_registry": {:hex, :system_registry, "0.8.1", "7df1f66f0e4fcd0940ecd0473d2787d69d2abd6267d21e8f8ecbab58a14415ce", [:mix], [], "hexpm"},
"tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"toolshed": {:hex, :toolshed, "0.2.2", "30fc70ef7d5ce9cdad17f99c28cfbd4ba4ea6199fb9245c542b674fb9f0248a3", [:mix], [], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"uboot_env": {:hex, :uboot_env, "0.1.0", "176d277c8461d849614d8b82595060bf03ace6ca59d49c5b53707d90cb9e2f5a", [:mix], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"},

View File

@ -1,115 +0,0 @@
defmodule Farmbot.Target.Bootstrap.Configurator do
@moduledoc """
This init module is used to bring up initial configuration.
If it can't find a configuration it will bring up a captive portal for a device to connect to.
"""
use Supervisor
require Farmbot.Logger
alias Farmbot.Config
import Config, only: [get_config_value: 3, update_config_value: 4]
alias Farmbot.Target.Bootstrap.Configurator
@doc """
This particular init module should block until all settings have been validated.
It handles things such as:
* Initial flashing of the firmware.
* Initial configuration of network settings.
* Initial configuration of farmbot web app settings.
When finished will return `:ignore` if all went well, or
`{:error, reason}` if there were errors. This will cause a factory
reset and the user will need to configureate again.
"""
def start_link(_) do
Farmbot.Logger.busy(3, "Configuring Farmbot.")
supervisor = Supervisor.start_link(__MODULE__, [self()], name: __MODULE__)
case supervisor do
{:ok, pid} ->
wait(pid)
:ignore ->
:ignore
end
end
defp wait(pid) do
if Process.alive?(pid) do
Process.sleep(1000)
wait(pid)
else
:ignore
end
end
def init(_) do
first_boot? = get_config_value(:bool, "settings", "first_boot")
autoconfigure? =
Nerves.Runtime.KV.get("farmbot_auto_configure")
|> case do
"" -> false
other when is_binary(other) -> true
_ -> false
end
if first_boot? do
maybe_configurate(autoconfigure?)
else
:ignore
end
end
def stop(supervisor, status) do
Supervisor.stop(supervisor, :normal)
status
end
defp maybe_configurate(false) do
Farmbot.Logger.info(3, "Building new configuration.")
update_config_value(:bool, "settings", "ignore_fbos_config", true)
update_config_value(:bool, "settings", "ignore_fw_config", true)
import Supervisor.Spec
:ets.new(:session, [:named_table, :public, read_concurrency: true])
Config.destroy_all_network_configs()
children = [
worker(Configurator.CaptivePortal, [], restart: :transient),
{Plug.Adapters.Cowboy,
scheme: :http, plug: Configurator.Router, options: [port: 80, acceptors: 1]}
]
opts = [strategy: :one_for_one]
Supervisor.init(children, opts)
end
defp maybe_configurate(_) do
ifname = Nerves.Runtime.KV.get("farmbot_network_iface")
ssid = Nerves.Runtime.KV.get("farmbot_network_ssid")
psk = Nerves.Runtime.KV.get("farmbot_network_psk")
email = Nerves.Runtime.KV.get("farmbot_email")
server = Nerves.Runtime.KV.get("farmbot_server")
password = Nerves.Runtime.KV.get("farmbot_password")
Config.input_network_config!(%{
name: ifname,
ssid: ssid,
security: "WPA-PSK",
psk: psk,
type: if(ssid, do: "wireless", else: "wired"),
domain: nil,
name_servers: nil,
ipv4_method: "dhcp",
ipv4_address: nil,
ipv4_gateway: nil,
ipv4_subnet_mask: nil
})
update_config_value(:string, "authorization", "email", email)
update_config_value(:string, "authorization", "password", password)
update_config_value(:string, "authorization", "server", server)
update_config_value(:string, "authorization", "token", nil)
:ignore
end
end

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.Bootstrap.Configurator.Router do
defmodule Farmbot.Target.Configurator.Router do
@moduledoc "Routes web connections."
use Plug.Router
@ -12,6 +12,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
require Farmbot.Logger
import Phoenix.HTML
alias Farmbot.Config
alias Farmbot.Target.Network
import Config,
only: [
@ -24,7 +25,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
end
@version Farmbot.Project.version()
@data_path Application.get_env(:farmbot, :data_path)
@data_path Farmbot.OS.FileSystem.data_path()
get "/generate_204" do
send_resp(conn, 204, "")
@ -82,7 +83,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
# NETWORKCONFIG
get "/network" do
interfaces = Farmbot.Target.Network.get_interfaces()
interfaces = Network.list_interfaces()
render_page(conn, "network", interfaces: interfaces, post_action: "select_interface")
end
@ -119,7 +120,7 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
opts = [
ifname: ifname,
ssids: Farmbot.Target.Network.scan(ifname),
ssids: Network.Utils.scan(ifname),
post_action: "config_wireless_step_1"
]
@ -230,24 +231,41 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
update_config_value(:string, "settings", "authorized_ssh_key", ssh_key)
end
Config.input_network_config!(%{
name: ifname,
ssid: ssid,
security: security,
psk: psk,
type: if(ssid, do: "wireless", else: "wired"),
domain: domain,
identity: identity,
password: password,
name_servers: name_servers,
ipv4_method: ipv4_method,
ipv4_address: ipv4_address,
ipv4_gateway: ipv4_gateway,
ipv4_subnet_mask: ipv4_subnet_mask,
regulatory_domain: reg_domain
})
type = if(ssid, do: "wireless", else: "wired")
redir(conn, "/firmware")
old =
Config.Repo.get_by(Config.NetworkInterface, name: ifname) || %Config.NetworkInterface{}
changeset =
Config.NetworkInterface.changeset(old, %{
name: ifname,
ssid: ssid,
security: security,
psk: psk,
type: type,
domain: domain,
identity: identity,
password: password,
name_servers: name_servers,
ipv4_method: ipv4_method,
ipv4_address: ipv4_address,
ipv4_gateway: ipv4_gateway,
ipv4_subnet_mask: ipv4_subnet_mask,
regulatory_domain: reg_domain
})
case Config.Repo.insert_or_update(changeset) do
{:ok, _} ->
redir(conn, "/firmware")
{:error, %{errors: r}} when type == "wireless" ->
Farmbot.Logger.error(1, "Network error: #{inspect(r)}")
redir(conn, "/config_wireless?ifname=#{ifname}")
{:error, %{errors: r}} when type == "wired" ->
Farmbot.Logger.error(1, "Network error: #{inspect(r)}")
redir(conn, "/config_wired?ifname=#{ifname}")
end
rescue
e in MissingField ->
Farmbot.Logger.error(1, Exception.message(e))
@ -257,21 +275,6 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
# /NETWORKCONFIG
get "/credentials" do
email = get_config_value(:string, "authorization", "email") || ""
pass = get_config_value(:string, "authorization", "password") || ""
server = get_config_value(:string, "authorization", "server") || ""
first_boot = get_config_value(:bool, "settings", "first_boot")
update_config_value(:string, "authorization", "token", nil)
render_page(conn, "credentials",
server: server,
email: email,
password: pass,
first_boot: first_boot
)
end
get "/firmware" do
render_page(conn, "firmware")
end
@ -285,9 +288,9 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
if Application.get_env(:farmbot, :behaviour)[:firmware_handler] ==
Farmbot.Firmware.UartHandler do
Farmbot.Logger.warn(1, "Updating #{hw} firmware.")
Farmbot.Logger.warn(1, "Updating #{hw} firmware is broke!!!!!!")
# /shrug?
Farmbot.Firmware.UartHandler.Update.force_update_firmware(hw)
# Farmbot.Firmware.UartHandler.Update.force_update_firmware(hw)
end
redir(conn, "/credentials")
@ -304,6 +307,21 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
end
end
get "/credentials" do
email = get_config_value(:string, "authorization", "email") || ""
pass = get_config_value(:string, "authorization", "password") || ""
server = get_config_value(:string, "authorization", "server") || ""
first_boot = get_config_value(:bool, "settings", "first_boot")
update_config_value(:string, "authorization", "token", nil)
render_page(conn, "credentials",
server: server,
email: email,
password: pass,
first_boot: first_boot
)
end
post "/configure_credentials" do
{:ok, _, conn} = read_body(conn)
@ -333,29 +351,8 @@ defmodule Farmbot.Target.Bootstrap.Configurator.Router do
network = !Enum.empty?(Config.get_all_network_configs())
if email && pass && server && network do
conn = render_page(conn, "finish")
spawn(fn ->
try do
alias Farmbot.Target.Bootstrap.Configurator
Farmbot.Logger.success(2, "Configuration finished.")
# Allow the page to render and send.
Process.sleep(2500)
:ok = GenServer.stop(Configurator.CaptivePortal, :normal)
# :ok = Supervisor.terminate_child(Configurator, Configurator.CaptivePortal)
:ok = Supervisor.stop(Configurator)
# Good luck.
Process.sleep(2500)
rescue
e ->
Farmbot.Logger.warn(
1,
"Falied to close captive portal. Good luck. " <> Exception.message(e)
)
end
end)
conn
Network.reload()
render_page(conn, "finish")
else
Farmbot.Logger.warn(3, "Not configured yet. Restarting configuration.")
redir(conn, "/")

View File

@ -0,0 +1,23 @@
defmodule Farmbot.Target.Configurator.Supervisor do
use Supervisor
alias Farmbot.Target.Configurator.{Router, Validator}
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init(_args) do
transport_opts = [
num_acceptors: 1
]
opts = [port: 80, transport_options: transport_opts]
children = [
Validator,
{Plug.Adapters.Cowboy, scheme: :http, plug: Router, options: opts}
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -0,0 +1,108 @@
defmodule Farmbot.Target.Configurator.Validator do
use GenServer
require Logger
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
alias Farmbot.Target.Network
import Farmbot.Target.Network.Utils
alias Farmbot.Bootstrap.Authorization
@steps [
:ntp,
:dns,
:auth
]
@checkup_time_ms 7000
# 30 minutes
@success_time_ms 1_800_000
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init(_args) do
{:ok, 0, 0}
end
def handle_info(:timeout, -1) do
{:noreply, 0, 0}
end
def handle_info(:timeout, step_index) do
case Enum.at(@steps, step_index) do
:ntp -> check_ntp(step_index)
:dns -> check_dns(step_index)
:auth -> check_auth(step_index)
end
end
def check_ntp(indx) do
Logger.info("Checking for valid NTP")
if Nerves.Time.synchronized?() do
{:noreply, indx + 1, 0}
else
Nerves.Time.restart_ntpd()
{:noreply, 0, @checkup_time_ms}
end
end
def check_dns(indx) do
Logger.info("Checking for valid DNS")
case test_dns() do
{:ok, _} -> {:noreply, indx + 1, 0}
{:error, _} -> {:noreply, 0, @checkup_time_ms}
end
end
def check_auth(_indx) do
email = get_config_value(:string, "authorization", "email")
password = get_config_value(:string, "authorization", "password")
secret = get_config_value(:string, "authorization", "secret")
server = get_config_value(:string, "authorization", "server")
cond do
email && password && server ->
authorize_with_password(email, password, server)
email && secret && server ->
authorize_with_secret(email, secret, server)
end
end
defp authorize_with_password(email, password, server) do
with {:ok, {:RSAPublicKey, _, _} = rsa_key} <- Authorization.fetch_rsa_key(server),
secret <- Authorization.build_secret(email, password, rsa_key),
{:ok, payload} <- Authorization.build_payload(secret),
{:ok, resp} <- Authorization.request_token(server, payload),
{:ok, %{"token" => %{"encoded" => tkn}}} <- Farmbot.JSON.decode(resp) do
update_config_value(:string, "authorization", "token", tkn)
update_config_value(:string, "authorization", "secret", secret)
update_config_value(:string, "authorization", "password", nil)
Farmbot.Target.Network.hostap_down()
Network.validate()
{:noreply, -1, @success_time_ms}
else
error ->
Logger.error("Authorization with password failure: #{inspect(error)}")
{:noreply, 0, @checkup_time_ms}
end
end
defp authorize_with_secret(_email, secret, server) do
with {:ok, payload} <- Authorization.build_payload(secret),
{:ok, resp} <- Authorization.request_token(server, payload),
{:ok, %{"token" => %{"encoded" => tkn}}} <- Farmbot.JSON.decode(resp) do
update_config_value(:string, "authorization", "token", tkn)
update_config_value(:string, "authorization", "password", nil)
Farmbot.Target.Network.hostap_down()
Network.validate()
{:noreply, -1, @success_time_ms}
else
error ->
Logger.error("Authorization with secret failure: #{inspect(error)}")
{:noreply, 0, @checkup_time_ms}
end
end
end

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.DiskUsageWorker do
defmodule Farmbot.Target.InfoWorker.DiskUsage do
@moduledoc false
use GenServer

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.MemoryUsageWorker do
defmodule Farmbot.Target.InfoWorker.MemoryUsage do
@moduledoc false
use GenServer

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.SocTempWorker do
defmodule Farmbot.Target.InfoWorker.SocTemp do
@moduledoc false
use GenServer
@ -13,7 +13,7 @@ defmodule Farmbot.Target.SocTempWorker do
{:ok, nil, 0}
end
def handle_info(:report_temp, state) do
def handle_info(:timeout, state) do
{temp_str, 0} = Nerves.Runtime.cmd("vcgencmd", ["measure_temp"], :return)
temp =
@ -21,11 +21,11 @@ defmodule Farmbot.Target.SocTempWorker do
|> String.trim()
|> String.split("=")
|> List.last()
|> Float.parse()
|> Integer.parse()
|> elem(0)
if GenServer.whereis(Farmbot.BotState) do
Farmbot.BotState.report_soc_temp(temp)
:ok = Farmbot.BotState.report_soc_temp(temp)
{:noreply, state, @default_timeout_ms}
else
{:noreply, state, @error_timeout_ms}

View File

@ -0,0 +1,28 @@
defmodule Farmbot.Target.InfoWorker.Supervisor do
@moduledoc false
use Supervisor
alias Farmbot.Target.InfoWorker.{
DiskUsage,
MemoryUsage,
SocTemp,
Uptime,
WifiLevel
}
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
children = [
DiskUsage,
MemoryUsage,
SocTemp,
Uptime,
{WifiLevel, ifname: "wlan0"}
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.UptimeWorker do
defmodule Farmbot.Target.InfoWorker.Uptime do
@moduledoc false
use GenServer
@ -13,7 +13,7 @@ defmodule Farmbot.Target.UptimeWorker do
{:ok, nil, 0}
end
def handle_info(:report_uptime, state) do
def handle_info(:timeout, state) do
usage = collect_report()
if GenServer.whereis(Farmbot.BotState) do

View File

@ -0,0 +1,42 @@
defmodule Farmbot.Target.InfoWorker.WifiLevel do
use GenServer
alias Farmbot.Config
import Farmbot.Target.Network.Utils
@checkup_time_ms 15_000
@error_time_ms 60_000
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
def init(args) do
ifname = Keyword.fetch!(args, :ifname)
{:ok, ifname, 0}
end
def handle_info(:timeout, ifname) do
case Config.Repo.get_by(Config.NetworkInterface, name: ifname) do
nil ->
{:noreply, ifname, @error_time_ms}
%{ssid: ssid} ->
do_check_level(ssid, ifname)
end
end
def do_check_level(ssid, ifname) do
scan(ifname)
|> Enum.find(fn %{ssid: scanned} ->
scanned == ssid
end)
|> case do
nil ->
{:noreply, ifname, @error_time_ms}
%{level: level} ->
Farmbot.BotState.report_wifi_level(level)
{:noreply, ifname, @checkup_time_ms}
end
end
end

View File

@ -0,0 +1,238 @@
defmodule Farmbot.Target.Network do
@moduledoc "Manages Network Connections"
use GenServer
require Logger
alias Nerves.NetworkInterface
import Farmbot.Target.Network.Utils
alias Farmbot.Config
@validation_ms 30_000
defmodule State do
@moduledoc false
defstruct ifnames: [],
config: %{},
hostap: :down,
hostap_dhcp_server_pid: nil,
hostap_wpa_supplicant_pid: nil
end
def reload do
for %{name: ifname} = settings <- Config.Repo.all(Config.NetworkInterface) do
settings = validate_settings(settings)
Logger.warn("Trying to configure #{ifname}: #{inspect(settings)}")
setup(ifname, settings)
end
end
def validate_settings(%{type: "wired"} = settings) do
validate_advanced(settings)
end
def validate_settings(settings) do
ssid = Map.fetch!(settings, :ssid)
psk = Map.fetch!(settings, :psk)
key_mgmt =
case Map.fetch!(settings, :security) do
"WPA-PSK" -> :"WPA-PSK"
"WPA2-PSK" -> :"WPA-PSK"
"NONE" -> :NONE
end
[
ssid: ssid,
psk: psk,
key_mgmt: key_mgmt
] ++ validate_advanced(settings)
end
def validate_advanced(%{ipv4_address: "static"} = settings) 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)
]
end
def validate_advanced(%{ipv4_method: _} = _settings) do
[]
end
def list_interfaces do
ifnames()
|> List.delete("lo")
|> Enum.map(fn ifname ->
IO.puts("Looking up #{ifname}")
{:ok, settings} = Nerves.NetworkInterface.settings(ifname)
{ifname, settings}
end)
end
@doc "List all ifnames the Network Manager knows about."
def ifnames do
GenServer.call(__MODULE__, :ifnames)
end
@doc "Bring down hostap, bring up networking. Will reset if not `validate/0`d in time."
def setup(ifname, settings) do
GenServer.cast(__MODULE__, {:setup, ifname, settings})
end
@doc "Validate the config given to the Network manager"
def validate do
GenServer.cast(__MODULE__, :validate)
end
@doc "Bring down networking, bring up hostap"
def hostap_up do
GenServer.cast(__MODULE__, :hostap_up)
end
@doc "Bring down hostap, Bring up networking"
def hostap_down do
GenServer.cast(__MODULE__, :hostap_down)
end
@doc "Start the Network manager."
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init(args) do
config = Keyword.get(args, :config, %{})
{:ok, %State{config: config}, 0}
end
def terminate(_, state) do
state = try_stop_dhcp(state)
for ifname <- state.ifnames do
Nerves.Network.teardown(ifname)
Nerves.NetworkInterface.ifdown(ifname)
end
end
def handle_call(:ifnames, _from, state) do
{:reply, state.ifnames, state}
end
def handle_cast({:setup, ifname, opts}, state) do
{:noreply, %{state | config: Map.put(state.config, ifname, opts)}, 0}
end
def handle_cast(:hostap_up, state) do
setup_hostap(state)
end
def handle_cast(:hostap_down, state) do
{:noreply, stop_hostap(state), 0}
end
def handle_cast(:validate, state) do
Logger.info("Config validated")
{:noreply, %{state | hostap: :validated}}
end
def handle_info(:timeout, %{ifnames: []} = state) do
Logger.info("Detecting ifnames")
ifnames = NetworkInterface.interfaces()
{:noreply, %{state | ifnames: ifnames}, 0}
end
def handle_info(:timeout, %{config: c} = state) when map_size(c) == 0 do
setup_hostap(state)
end
def handle_info(:timeout, %{hostap: :pending_validation} = state) do
Logger.warn("Config not validated in time.")
setup_hostap(%{state | config: %{}})
end
def handle_info(:timeout, state) do
Logger.info("Waiting #{@validation_ms} ms for config to be validated.")
state = stop_hostap(state)
for {ifname, conf} <- state.config do
Logger.debug("Nerves.Network.setup(#{inspect(ifname)}, #{inspect(conf)}")
Nerves.Network.setup(ifname, conf)
end
{:noreply, %{state | hostap: :pending_validation}, @validation_ms}
end
def stop_hostap(%{hostap: :up} = state) do
state = try_stop_dhcp(state)
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}
end
# hostap down or not available.
def stop_hostap(state), do: state
def setup_hostap(%{hostap: :up} = state), do: {:noreply, state}
def setup_hostap(state) do
hostap_opts = [
ssid: build_hostap_ssid(),
key_mgmt: :NONE,
mode: 2
]
ip_opts = [
ipv4_address_method: :static,
ipv4_address: "192.168.24.1",
ipv4_gateway: "192.168.24.1",
ipv4_subnet_mask: "255.255.0.0",
nameservers: ["192.168.24.1"]
]
dhcp_opts = [
gateway: "192.168.24.1",
netmask: "255.255.255.0",
range: {"192.168.24.2", "192.168.24.10"},
domain_servers: ["192.168.24.1"]
]
Nerves.Network.teardown("wlan0")
Nerves.Network.setup("wlan0", hostap_opts)
Nerves.NetworkInterface.setup("wlan0", ip_opts)
{:ok, hostap_dhcp_server_pid} = DHCPServer.start_link("wlan0", dhcp_opts)
{:ok, hostap_wpa_supplicant_pid} = wait_for_wpa("wlan0")
{:noreply,
%{
state
| hostap: :up,
hostap_dhcp_server_pid: hostap_dhcp_server_pid,
hostap_wpa_supplicant_pid: hostap_wpa_supplicant_pid
}}
end
defp wait_for_wpa(ifname) do
# Logger.debug("waiting for #{ifname} wpa_supplicant")
name = :"Nerves.WpaSupplicant.#{ifname}"
case GenServer.whereis(name) do
nil -> wait_for_wpa(ifname)
pid -> {:ok, pid}
end
end
defp try_stop_dhcp(state) do
if state.hostap_dhcp_server_pid && Process.alive?(state.hostap_dhcp_server_pid) do
Logger.debug("Stopping DHCP Server")
GenServer.stop(state.hostap_dhcp_server_pid, :normal)
end
%{state | hostap_dhcp_server_pid: nil}
end
end

View File

@ -0,0 +1,175 @@
defmodule Farmbot.Target.Network.Distribution do
@moduledoc false
use GenServer
require Logger
defmodule State do
@moduledoc false
defstruct ip: nil, is_up: nil, opts: nil
end
def start_link(opts) do
GenServer.start_link(__MODULE__, opts)
end
def init(opts) do
# Register for updates from system registry
SystemRegistry.register()
state =
%State{opts: opts}
|> init_mdns(opts)
|> init_net_kernel(opts)
{:ok, state}
end
def handle_info({:system_registry, :global, registry}, state) do
{changed, new_state} = update_state(state, registry)
case changed do
:up -> handle_if_up(new_state)
:down -> handle_if_down(new_state)
_ -> :ok
end
{:noreply, new_state}
end
defp update_state(state, registry) do
new_ip = get_in(registry, [:state, :network_interface, state.opts.ifname, :ipv4_address])
case {new_ip == state.ip, new_ip} do
{true, _} -> {:unchanged, state}
{false, nil} -> {:down, %{state | ip: nil, is_up: false}}
{false, _ip} -> {:up, %{state | ip: new_ip, is_up: true}}
end
end
defp handle_if_up(state) do
Logger.debug("#{state.opts.ifname} is up. IP is now #{state.ip}")
update_mdns(state.ip, state.opts.mdns_domain)
update_net_kernel(state.ip, state.opts)
end
defp handle_if_down(_state), do: :ok
defp init_mdns(state, %{mdns_domain: nil}), do: state
defp init_mdns(state, opts) do
Mdns.Server.add_service(%Mdns.Server.Service{
domain: resolve_mdns_name(opts.mdns_domain),
data: :ip,
ttl: 120,
type: :a
})
state
end
defp resolve_mdns_name(nil), do: nil
defp resolve_mdns_name(:hostname) do
{:ok, hostname} = :inet.gethostname()
to_dot_local_name(hostname)
end
defp resolve_mdns_name(mdns_name), do: mdns_name
defp to_dot_local_name(name) do
# Use the first part of the domain name and concatenate '.local'
name
|> to_string()
|> String.split(".")
|> hd()
|> Kernel.<>(".local")
end
defp update_mdns(_ip, nil), do: :ok
defp update_mdns(ip, _mdns_domain) do
ip_tuple = string_to_ip(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 init_net_kernel(state, opts) do
if erlang_distribution_enabled?(opts) do
:os.cmd('epmd -daemon')
end
state
end
defp update_net_kernel(ip, opts) do
new_name = make_node_name(opts, ip)
if new_name do
:net_kernel.stop()
case :net_kernel.start([new_name]) do
{:ok, _} ->
:ok = Farmbot.BotState.set_node_name(to_string(new_name))
Logger.debug("Restarted Erlang distribution as node #{inspect(new_name)}")
{:error, reason} ->
Logger.error("Erlang distribution failed to start: #{inspect(reason)}")
end
end
end
defp string_to_ip(s) do
{:ok, ip} = :inet.parse_address(to_charlist(s))
ip
end
defp erlang_distribution_enabled?(opts) do
make_node_name(opts, "fake.ip") != nil
end
defp resolve_dhcp_name(fallback) do
with {:ok, hostname} <- :inet.gethostname(),
{:ok, {:hostent, dhcp_name, _, _, _, _}} <- :inet.gethostbyname(hostname) do
dhcp_name
else
_ -> fallback
end
end
defp make_node_name(%{node_name: name, node_host: :ip}, ip) do
to_node_name(name, ip)
end
defp make_node_name(%{node_name: name, node_host: :dhcp}, ip) do
to_node_name(name, resolve_dhcp_name(ip))
end
defp make_node_name(%{node_name: name, node_host: :mdns_domain, mdns_domain: host}, _ip)
when host != nil do
to_node_name(name, resolve_mdns_name(host))
end
defp make_node_name(%{node_name: name, node_host: :mdns_domain, mdns_domain: host}, ip)
when host == nil do
# revert to IP address if no mdns domain
to_node_name(name, ip)
end
defp make_node_name(%{node_name: name, node_host: host}, _ip) do
to_node_name(name, host)
end
defp to_node_name(nil, _host), do: nil
defp to_node_name(_name, nil), do: nil
defp to_node_name(name, host), do: :"#{name}@#{host}"
end

View File

@ -1,19 +0,0 @@
defmodule Farmbot.Target.Network.InfoSupervisor do
use GenServer
alias Farmbot.Config
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
configs = Config.get_all_network_configs()
children =
Enum.map(configs, fn config ->
{Farmbot.Target.Network.InfoWorker, [config]}
end)
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -1,31 +0,0 @@
defmodule Farmbot.Target.Network.InfoWorker do
@moduledoc false
use GenServer
@default_timeout_ms 60_000
def start_link(args) do
GenServer.start_link(__MODULE__, args)
end
def init([config]) do
{:ok, config, 0}
end
def handle_info(:timeout, %{type: "wireless"} = config) do
if level = Farmbot.Target.Network.get_level(config.name, config.ssid) do
report_wifi_level(level)
end
{:noreply, config, @default_timeout_ms}
end
def handle_info(:timeout, config) do
{:noreply, config, :hibernate}
end
def report_wifi_level(level) do
if GenServer.whereis(Farmbot.BotState) do
Farmbot.BotState.report_wifi_level(level)
end
end
end

View File

@ -1,100 +0,0 @@
defmodule Farmbot.Target.Network.NotFoundTimer do
use GenServer
import Farmbot.Config, only: [get_config_value: 3]
require Farmbot.Logger
def query do
GenServer.call(__MODULE__, :query)
end
def start do
GenServer.call(__MODULE__, :start)
end
def stop do
GenServer.call(__MODULE__, :stop)
end
def start_link(args) do
GenServer.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
{:ok, %{timer: nil}}
end
def handle_call(:query, _, state) do
if state.timer do
r = Process.read_timer(state.timer)
{:reply, r, state}
else
{:reply, nil, state}
end
end
def handle_call(:start, _from, %{timer: nil} = state) do
minutes = get_config_value(:float, "settings", "network_not_found_timer") || 1
ms = (minutes * 60_000) |> round()
timer = Process.send_after(self(), :timer, ms)
Farmbot.Logger.debug(1, "Starting network not found timer: #{minutes} minute(s)")
{:reply, :ok, %{state | timer: timer}}
end
# Timer already started
def handle_call(:start, _from, state) do
{:reply, :ok, state}
end
def handle_call(:stop, _from, state) do
if state.timer do
Process.cancel_timer(state.timer)
end
{:reply, :ok, %{state | timer: nil}}
end
def handle_info(:timer, state) do
delay_minutes = get_config_value(:float, "settings", "network_not_found_timer") || 1
disable_factory_reset? = get_config_value(:bool, "settings", "disable_factory_reset")
first_boot? = get_config_value(:bool, "settings", "first_boot")
cond do
disable_factory_reset? ->
Farmbot.Logger.warn(1, "Factory reset is disabled. Not resetting.")
{:noreply, %{state | timer: nil}}
first_boot? ->
msg = """
Network not found after #{delay_minutes} minute(s).
possible causes of this include:
1) A typo if you manually inputted the SSID.
2) The access point is out of range
3) There is too much radio interference around Farmbot.
5) There is a hardware issue.
"""
Farmbot.Logger.error(1, msg)
Farmbot.System.factory_reset(msg)
{:stop, :normal, %{state | timer: nil}}
true ->
Farmbot.Logger.error(1, "Network not found after timer. Farmbot is disconnected.")
msg = """
Network not found after #{delay_minutes} minute(s).
This can happen if your wireless access point is no longer available,
out of range, or there is too much radio interference around Farmbot.
If you see this message intermittently you should disable \"automatic
factory reset\" or tune the \"network not found
timer\" value in the Farmbot Web Application.
"""
Farmbot.System.factory_reset(msg)
{:stop, :normal, %{state | timer: nil}}
end
end
end

View File

@ -0,0 +1,35 @@
defmodule Farmbot.Target.Network.Supervisor do
use Supervisor
def start_link(args) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end
def init([]) do
{:ok, hostname} = :inet.gethostname()
confs = Farmbot.Config.Repo.all(Farmbot.Config.NetworkInterface)
config =
Map.new(confs, fn %{name: ifname} = settings ->
{ifname, Farmbot.Target.Network.validate_settings(settings)}
end)
distribution_children =
Enum.map(confs, fn %{name: ifname} ->
opts = %{
ifname: ifname,
mdns_domain: "#{hostname}.local",
node_name: "farmbot",
node_host: :mdns_domain
}
{Farmbot.Target.Network.Distribution, opts}
end)
children = [
{Farmbot.Target.Network, config: config} | distribution_children
]
Supervisor.init(children, strategy: :one_for_one)
end
end

View File

@ -0,0 +1,141 @@
defmodule Farmbot.Target.Network.Utils do
import Farmbot.Config, only: [get_config_value: 3]
require Farmbot.Logger
alias Farmbot.Target.Network.ScanResult
def build_hostap_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
@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 = 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
@fb_data_dir Farmbot.OS.FileSystem.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,38 +0,0 @@
defmodule Farmbot.Target.Network.WaitForTime do
@moduledoc "Blocks until time is synced."
require Farmbot.Logger
nerves_time =
case Nerves.Time.FileTime.time() do
{:error, _} -> NaiveDateTime.utc_now()
ndt -> ndt
end
@nerves_time nerves_time
def start_link(_args) do
:ok = wait_for_time()
Farmbot.Logger.success(3, "Time seems to be set. Moving on.")
IO.puts("Check: #{inspect(@nerves_time)}")
IO.puts("Current: #{inspect(NaiveDateTime.utc_now())}")
:ignore
end
# • -1 -- the first date comes before the second one
# • 0 -- both arguments represent the same date when coalesced to the same
# timezone.
# • 1 -- the first date comes after the second one
defp wait_for_time do
case Timex.compare(NaiveDateTime.utc_now(), get_file_time()) do
1 ->
:ok
_ ->
Process.sleep(1000)
wait_for_time()
end
end
def get_file_time, do: @nerves_time
end

View File

@ -1,4 +1,4 @@
defmodule Farmbot.Target.Network.TzdataTask do
defmodule Farmbot.Target.TzdataTask do
use GenServer
@data_path Farmbot.OS.FileSystem.data_path()

View File

@ -1,10 +1,43 @@
## Add custom options here
## Distributed Erlang Options
## The cookie needs to be configured prior to vm boot for
## for read only filesystem.
# -name farmbot@0.0.0.0
-setcookie democookie
## Use Ctrl-C to interrupt the current shell rather than invoking the emulator's
## break handler and possibly exiting the VM.
+Bc
# Allow time warps so that the Erlang system time can more closely match the
# OS system time.
+C multi_time_warp
## Load code at system startup
## See http://erlang.org/doc/system_principles/system_principles.html#code-loading-strategy
# -mode embedded
## Save the shell history between reboots
## See http://erlang.org/doc/man/kernel_app.html for additional options
-kernel shell_history enabled
-mode embedded
-sname farmbot
## Enable heartbeat monitoring of the Erlang runtime system
-heart -env HEART_BEAT_TIMEOUT 30
## Start the Elixir shell
-noshell
-user Elixir.IEx.CLI
-heart
+C multi_time_warp
## Enable colors in the shell
-elixir ansi_enabled true
## Options added after -extra are interpreted as plain arguments and can be
## retrieved using :init.get_plain_arguments(). Options before the "--" are
## interpreted by Elixir and anything afterwards is left around for other IEx
## and user applications.
-extra --no-halt
--
--dot-iex /etc/iex.exs

View File

@ -1,22 +1 @@
# Pull in Nerves-specific helpers to the IEx session
use Nerves.Runtime.Helpers
# Be careful when adding to this file. Nearly any error can crash the VM and
# cause a reboot.
alias Farmbot.System.{
ConfigStorage
}
alias Farmbot.Asset
alias Farmbot.Asset.{
Device,
FarmEvent,
GenericPointer,
Peripheral,
Point,
Regimen,
Sensor,
Sequence,
ToolSlot,
Tool
}
use Toolshed