Nerves hub pt2 (#654)
* Rework Networking * Rework Configurator * Fix NervesHub not connectingpull/974/head
parent
451dfe1343
commit
56f6749595
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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} =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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,
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
use Mix.Config
|
||||
config :farmbot, :captive_portal_address, "192.168.24.1"
|
|
@ -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
|
|
@ -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
|
|
@ -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__]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
defmodule Farmbot.Firmware.AutoDetector do
|
||||
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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
|
|
@ -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, "/")
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Farmbot.Target.DiskUsageWorker do
|
||||
defmodule Farmbot.Target.InfoWorker.DiskUsage do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Farmbot.Target.MemoryUsageWorker do
|
||||
defmodule Farmbot.Target.InfoWorker.MemoryUsage do
|
||||
@moduledoc false
|
||||
|
||||
use GenServer
|
|
@ -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}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +1,4 @@
|
|||
defmodule Farmbot.Target.Network.TzdataTask do
|
||||
defmodule Farmbot.Target.TzdataTask do
|
||||
use GenServer
|
||||
|
||||
@data_path Farmbot.OS.FileSystem.data_path()
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue