Merge pull request #1173 from FarmBot/staging

Publish v9.2.1
pull/1186/head v9.2.1
Rick Carlino 2020-03-24 14:25:49 -05:00 committed by GitHub
commit 1ab623a2b8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 482 additions and 416 deletions

View File

@ -1,5 +1,12 @@
# Changelog # Changelog
# 9.2.1
* Improve firmware debug messages.
* Remove confusing firmware debug messages, such as "Error OK".
* Improved camera support on FarmBot express.
* Bug fix to prevents OTA updates occuring when one is already in progress.
# 9.2.0 # 9.2.0
* Support for criteria-based groups. * Support for criteria-based groups.

10
COVERAGE.md 100644
View File

@ -0,0 +1,10 @@
# Jan - Mar 2020
| Project | Jan 1 20 | Feb 6 20 | Mar 4 20 |STATUS|
|-----------------------|----------|----------|----------|------|
| farmbot_celery_script | 53.7% | 54.0% | 54.0% |OK |
| farmbot_core | 22.2% | 19.8% | 26.3% |OK |
| farmbot_ext | 53.6% | 52.7% | 38.1% |FIX | !!!
| farmbot_firmware | 13.8% | 56.4% | 62.0% |OK |
| farmbot_os | 22.0% | 27.6% | 45.3% |OK |
| farmbot_telemetry | ??.?% | ??.?% | ??.?% |LATER |

View File

@ -1 +1 @@
9.2.0 9.2.1

View File

@ -189,7 +189,6 @@ defmodule FarmbotCore.Asset.Command do
# Catch-all use case: # Catch-all use case:
def update(asset_kind, id, params) do def update(asset_kind, id, params) do
Logger.warn("AssetCommand needs implementation: #{asset_kind}")
mod = as_module!(asset_kind) mod = as_module!(asset_kind)
case Repo.get_by(mod, id: id) do case Repo.get_by(mod, id: id) do

View File

@ -94,7 +94,6 @@ defmodule FarmbotCore.AssetMonitor do
Map.put(sub_state, id, updated_at) Map.put(sub_state, id, updated_at)
is_nil(sub_state[id]) -> is_nil(sub_state[id]) ->
Logger.debug("#{inspect(kind)} #{id} needs to be started")
asset = Repo.preload(asset, AssetWorker.preload(asset)) asset = Repo.preload(asset, AssetWorker.preload(asset))
:ok = AssetSupervisor.start_child(asset) |> assert_result!(asset) :ok = AssetSupervisor.start_child(asset) |> assert_result!(asset)
Map.put(sub_state, id, updated_at) Map.put(sub_state, id, updated_at)

View File

@ -1,6 +1,8 @@
defmodule FarmbotCore.BotState do defmodule FarmbotCore.BotState do
@moduledoc "Central State accumulator." @moduledoc "Central State accumulator."
alias FarmbotCore.BotStateNG alias FarmbotCore.BotStateNG
alias FarmbotCore.BotState.JobProgress.Percent
require FarmbotCore.Logger require FarmbotCore.Logger
use GenServer use GenServer
@ -28,7 +30,7 @@ defmodule FarmbotCore.BotState do
def set_position(bot_state_server \\ __MODULE__, x, y, z) do def set_position(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_position, x, y, z}) GenServer.call(bot_state_server, {:set_position, x, y, z})
end end
@doc "Sets the location_data.load" @doc "Sets the location_data.load"
def set_load(bot_state_server \\ __MODULE__, x, y, z) do def set_load(bot_state_server \\ __MODULE__, x, y, z) do
GenServer.call(bot_state_server, {:set_load, x, y, z}) GenServer.call(bot_state_server, {:set_load, x, y, z})
@ -161,6 +163,10 @@ defmodule FarmbotCore.BotState do
GenServer.call(bot_state_server, :enter_maintenance_mode) GenServer.call(bot_state_server, :enter_maintenance_mode)
end end
def job_in_progress?(job_name, bot_state_server \\ __MODULE__) do
GenServer.call(bot_state_server, {:job_in_progress?, job_name})
end
@doc false @doc false
def start_link(args, opts \\ [name: __MODULE__]) do def start_link(args, opts \\ [name: __MODULE__]) do
GenServer.start_link(__MODULE__, args, opts) GenServer.start_link(__MODULE__, args, opts)
@ -175,6 +181,13 @@ defmodule FarmbotCore.BotState do
FarmbotCore.Logger.error 1, "BotState crashed! #{inspect(reason)}" FarmbotCore.Logger.error 1, "BotState crashed! #{inspect(reason)}"
end end
def handle_call({:job_in_progress?, job_name}, _from, state) do
progress = (state.tree.jobs[job_name] || %Percent{}).percent
in_progress? = (progress > 0.0 && progress < 100.0)
{:reply, in_progress?, state}
end
@doc false @doc false
def handle_call(:subscribe, {pid, _} = _from, state) do def handle_call(:subscribe, {pid, _} = _from, state) do
# TODO Just replace this with Elixir.Registry? # TODO Just replace this with Elixir.Registry?

View File

@ -90,8 +90,8 @@ defmodule FarmbotCore.FirmwareOpenTask do
Config.update_config_value(:bool, "settings", "firmware_needs_open", false) Config.update_config_value(:bool, "settings", "firmware_needs_open", false)
timer = Process.send_after(self(), :open, 5000) timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}} {:noreply, %{state | timer: timer, attempts: 0}}
_ -> other ->
FarmbotCore.Logger.debug 3, "Firmware failed to open" FarmbotCore.Logger.debug 3, "Firmware failed to open: #{inspect(other)}"
timer = Process.send_after(self(), :open, 5000) timer = Process.send_after(self(), :open, 5000)
{:noreply, %{state | timer: timer, attempts: 0}} {:noreply, %{state | timer: timer, attempts: 0}}
end end

View File

@ -193,7 +193,14 @@ defmodule FarmbotCore.FirmwareSideEffects do
def handle_debug_message([message]) do def handle_debug_message([message]) do
fbos_config = Asset.fbos_config() fbos_config = Asset.fbos_config()
should_log? = fbos_config.firmware_debug_log || fbos_config.arduino_debug_messages should_log? = fbos_config.firmware_debug_log || fbos_config.arduino_debug_messages
should_log? && FarmbotCore.Logger.debug(3, "Firmware debug message: " <> message) should_log? && do_send_debug_message(message)
end
# TODO(Rick): 0 means OK, but firmware debug logs say "error 0". Why?
def do_send_debug_message("error 0"), do: do_send_debug_message("OK")
def do_send_debug_message(message) do
FarmbotCore.Logger.debug(3, "Firmware debug message: " <> message)
end end
@impl FarmbotFirmware.SideEffects @impl FarmbotFirmware.SideEffects

View File

@ -13,7 +13,8 @@ defmodule FarmbotCore.Leds.StubHandler do
def white5(status), do: do_debug(:white, status) def white5(status), do: do_debug(:white, status)
defp do_debug(color, status) do defp do_debug(color, status) do
msg = [IO.ANSI.reset(), "LED STATUS: ", unless System.get_env("LOG_SILENCE") do
msg = [IO.ANSI.reset(), "LED STATUS: ",
apply(IO.ANSI, color, []), apply(IO.ANSI, color, []),
status_in(status), status_in(status),
to_string(color), to_string(color),
@ -22,7 +23,8 @@ defmodule FarmbotCore.Leds.StubHandler do
status_out(status), status_out(status),
IO.ANSI.reset() IO.ANSI.reset()
] ]
IO.puts(msg) IO.puts(msg)
end
end end
defp status_in(:slow_blink), do: IO.ANSI.blink_slow() defp status_in(:slow_blink), do: IO.ANSI.blink_slow()

View File

@ -23,7 +23,9 @@ defmodule FarmbotCore.LogExecutor do
do: level, do: level,
else: :info else: :info
Elixir.Logger.bare_log(logger_level, log, logger_meta) unless System.get_env("LOG_SILENCE") do
Elixir.Logger.bare_log(logger_level, log, logger_meta)
end
log log
end end
end end

View File

@ -23,3 +23,4 @@ erl_crash.dump
farmbot_ext-*.tar farmbot_ext-*.tar
*.sqlite3 *.sqlite3
*.coverdata

View File

@ -11,3 +11,4 @@ config :farmbot_celery_script, FarmbotCeleryScript.SysCalls,
import_config "ecto.exs" import_config "ecto.exs"
import_config "farmbot_core.exs" import_config "farmbot_core.exs"
import_config "lagger.exs" import_config "lagger.exs"
import_config "test.exs"

View File

@ -0,0 +1,5 @@
use Mix.Config
if Mix.env() == :test do
config :farmbot_ext, FarmbotExt, children: []
end

View File

@ -0,0 +1,6 @@
{
"coverage_options": {
"treat_no_relevant_lines_as_covered": true,
"minimum_coverage": 6.5
}
}

View File

@ -1,19 +1,18 @@
defmodule FarmbotExt do defmodule FarmbotExt do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false @moduledoc false
use Application use Application
def start(_type, _args) do def start(_type, _args) do
# List all child processes to be supervised
children = [
FarmbotExt.Bootstrap
]
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options
opts = [strategy: :one_for_one, name: __MODULE__] opts = [strategy: :one_for_one, name: __MODULE__]
Supervisor.start_link(children, opts) Supervisor.start_link(children(), opts)
end
# This only exists because I was getting too many crashed
# supervisor reports in the test suite (distraction from
# real test failures).
def children do
config = Application.get_env(:farmbot_ext, __MODULE__) || []
Keyword.get(config, :children, [FarmbotExt.Bootstrap])
end end
end end

View File

@ -0,0 +1,46 @@
defmodule FarmbotExt.AMQP.AutoSyncAssetHandler do
require Logger
alias FarmbotCore.{Asset, BotState, Leds}
alias FarmbotExt.API.{EagerLoader}
# Sync messgages about these assets
# should not be cached. They need to be applied
# in real time.
@no_cache_kinds ~w(
Device
FbosConfig
FirmwareConfig
FarmwareEnv
FarmwareInstallation
)
def handle_asset(asset_kind, id, params) do
if Asset.Query.auto_sync?() do
:ok = BotState.set_sync_status("syncing")
_ = Leds.green(:really_fast_blink)
Asset.Command.update(asset_kind, id, params)
:ok = BotState.set_sync_status("synced")
_ = Leds.green(:solid)
else
cache_sync(asset_kind, id, params)
end
end
def cache_sync(kind, id, params) when kind in @no_cache_kinds do
:ok = Asset.Command.update(kind, id, params)
end
def cache_sync(_, _, nil) do
:ok = BotState.set_sync_status("sync_now")
_ = Leds.green(:slow_blink)
end
def cache_sync(asset_kind, id, params) do
Logger.info("Autocaching sync #{asset_kind} #{id} #{inspect(params)}")
changeset = Asset.Command.new_changeset(asset_kind, id, params)
:ok = EagerLoader.cache(changeset)
:ok = BotState.set_sync_status("sync_now")
_ = Leds.green(:slow_blink)
end
end

View File

@ -8,14 +8,13 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
use GenServer use GenServer
use AMQP use AMQP
alias FarmbotCore.{Asset, BotState, JSON, Leds}
alias FarmbotExt.AMQP.ConnectionWorker
alias FarmbotExt.API.{EagerLoader, Preloader}
require Logger
require FarmbotCore.Logger require FarmbotCore.Logger
require FarmbotTelemetry require FarmbotTelemetry
alias FarmbotCore.{Asset, BotState, JSON, Leds}
alias FarmbotExt.AMQP.{ConnectionWorker, AutoSyncAssetHandler}
alias FarmbotExt.API.{EagerLoader, Preloader}
# The API dispatches messages for other resources, but these # The API dispatches messages for other resources, but these
# are the only ones that Farmbot needs to sync. # are the only ones that Farmbot needs to sync.
@known_kinds ~w( @known_kinds ~w(
@ -35,18 +34,8 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
Tool Tool
) )
# Sync messgaes about these assets
# should not be cached. They need to be applied
# in real time.
@no_cache_kinds ~w(
Device
FbosConfig
FirmwareConfig
FarmwareEnv
FarmwareInstallation
)
defstruct [:conn, :chan, :jwt, :preloaded] defstruct [:conn, :chan, :jwt, :preloaded]
alias __MODULE__, as: State alias __MODULE__, as: State
@doc "Gets status of auto_sync connection for diagnostics / tests." @doc "Gets status of auto_sync connection for diagnostics / tests."
@ -68,12 +57,15 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
def terminate(reason, state) do def terminate(reason, state) do
FarmbotCore.Logger.error(1, "Disconnected from AutoSync channel: #{inspect(reason)}") FarmbotCore.Logger.error(1, "Disconnected from AutoSync channel: #{inspect(reason)}")
# If a channel was still open, close it. # If a channel was still open, close it.
if state.chan, do: ConnectionWorker.close_channel(state.chan) if state.chan do
ConnectionWorker.close_channel(state.chan)
end
try do try do
EagerLoader.Supervisor.drop_all_cache() EagerLoader.Supervisor.drop_all_cache()
catch catch
_, _ -> :ok _, _ ->
:ok
end end
end end
@ -103,6 +95,7 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
end end
def handle_info(:connect, state) do def handle_info(:connect, state) do
# THIS IS WHERE state.chan GETS SET
result = ConnectionWorker.maybe_connect_autosync(state.jwt.bot) result = ConnectionWorker.maybe_connect_autosync(state.jwt.bot)
compute_reply_from_amqp_state(state, result) compute_reply_from_amqp_state(state, result)
end end
@ -134,13 +127,11 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
case String.split(key, ".") do case String.split(key, ".") do
["bot", ^device, "sync", asset_kind, id_str] when asset_kind in @known_kinds -> ["bot", ^device, "sync", asset_kind, id_str] when asset_kind in @known_kinds ->
id = data["id"] || String.to_integer(id_str) id = data["id"] || String.to_integer(id_str)
_ = handle_asset(asset_kind, id, body) _ = AutoSyncAssetHandler.handle_asset(asset_kind, id, body)
["bot", ^device, "sync", asset_kind, _id_str] ->
Logger.warn("Unknown syncable asset: #{asset_kind}")
_ -> _ ->
Logger.info("ignoring route: #{key}") ""
# Logger.info("ignoring route: #{key}")
end end
:ok = ConnectionWorker.rpc_reply(chan, device, label) :ok = ConnectionWorker.rpc_reply(chan, device, label)
@ -158,36 +149,6 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do
{:reply, reply, state} {:reply, reply, state}
end end
def handle_asset(asset_kind, id, params) do
if Asset.Query.auto_sync?() do
:ok = BotState.set_sync_status("syncing")
_ = Leds.green(:really_fast_blink)
# Logger.info "Syncing #{asset_kind} #{id} #{inspect(params)}"
Asset.Command.update(asset_kind, id, params)
:ok = BotState.set_sync_status("synced")
_ = Leds.green(:solid)
else
cache_sync(asset_kind, id, params)
end
end
def cache_sync(kind, id, params) when kind in @no_cache_kinds do
:ok = Asset.Command.update(kind, id, params)
end
def cache_sync(_, _, nil) do
:ok = BotState.set_sync_status("sync_now")
_ = Leds.green(:slow_blink)
end
def cache_sync(asset_kind, id, params) do
Logger.info("Autocaching sync #{asset_kind} #{id} #{inspect(params)}")
changeset = Asset.Command.new_changeset(asset_kind, id, params)
:ok = EagerLoader.cache(changeset)
:ok = BotState.set_sync_status("sync_now")
_ = Leds.green(:slow_blink)
end
defp compute_reply_from_amqp_state(state, %{conn: conn, chan: chan}) do defp compute_reply_from_amqp_state(state, %{conn: conn, chan: chan}) do
{:noreply, %{state | conn: conn, chan: chan}} {:noreply, %{state | conn: conn, chan: chan}}
end end

View File

@ -66,7 +66,6 @@ defmodule FarmbotExt.API.EagerLoader do
* a remote `id` field. * a remote `id` field.
""" """
def cache(%Changeset{data: %module{}} = changeset) do def cache(%Changeset{data: %module{}} = changeset) do
Logger.info("Caching #{inspect(changeset)}")
id = Changeset.get_field(changeset, :id) id = Changeset.get_field(changeset, :id)
updated_at = Changeset.get_field(changeset, :updated_at) updated_at = Changeset.get_field(changeset, :updated_at)
id || change_error(changeset, "Can't cache a changeset with no :id attribute") id || change_error(changeset, "Can't cache a changeset with no :id attribute")

View File

@ -24,7 +24,7 @@ defmodule FarmbotExt.API.Preloader do
with {:ok, sync_changeset} <- API.get_changeset(Sync), with {:ok, sync_changeset} <- API.get_changeset(Sync),
sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_0()) do sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_0()) do
FarmbotCore.Logger.success(3, "Successfully preloaded resources.") FarmbotCore.Logger.success(3, "Successfully preloaded resources.")
maybe_auto_sync(sync_changeset, Query.auto_sync?()) maybe_auto_sync(sync_changeset, Query.auto_sync?() || false)
end end
end end

View File

@ -51,8 +51,6 @@ defmodule FarmbotExt.Bootstrap do
end end
def try_auth(email, server, password, _secret) do def try_auth(email, server, password, _secret) do
Logger.debug("using password to auth")
with {:ok, tkn} <- Authorization.authorize_with_password(email, password, server), with {:ok, tkn} <- Authorization.authorize_with_password(email, password, server),
_ <- update_config_value(:string, "authorization", "token", tkn), _ <- update_config_value(:string, "authorization", "token", tkn),
{:ok, pid} <- Supervisor.start_child(FarmbotExt, Bootstrap.Supervisor) do {:ok, pid} <- Supervisor.start_child(FarmbotExt, Bootstrap.Supervisor) do

View File

@ -1,6 +1,6 @@
defmodule FarmbotExt.Bootstrap.Supervisor do defmodule FarmbotExt.Bootstrap.Supervisor do
@moduledoc """ @moduledoc """
Supervisor responsible for starting all Supervisor responsible for starting all
the tasks and processes that require authentication. the tasks and processes that require authentication.
""" """
use Supervisor use Supervisor
@ -20,7 +20,6 @@ defmodule FarmbotExt.Bootstrap.Supervisor do
FarmbotExt.Bootstrap.DropPasswordTask FarmbotExt.Bootstrap.DropPasswordTask
] ]
opts = [strategy: :one_for_one] Supervisor.init(children, strategy: :one_for_one)
Supervisor.init(children, opts)
end end
end end

View File

@ -0,0 +1,25 @@
defmodule AutoSyncAssetHandlerTest do
use ExUnit.Case, async: true
use Mimic
setup :verify_on_exit!
setup :set_mimic_global
alias FarmbotExt.AMQP.AutoSyncAssetHandler
alias FarmbotCore.{Asset, BotState, Leds}
def auto_sync_off, do: expect(Asset.Query, :auto_sync?, fn -> false end)
def expect_sync_status_to_be(status),
do: expect(BotState, :set_sync_status, fn ^status -> :ok end)
def expect_green_leds(status),
do: expect(Leds, :green, 1, fn ^status -> :ok end)
test "handling of deleted assets when auto_sync is disabled" do
auto_sync_off()
expect_sync_status_to_be("sync_now")
expect_green_leds(:slow_blink)
AutoSyncAssetHandler.handle_asset("Point", 23, nil)
end
end

View File

@ -1,18 +1,15 @@
defmodule AutoSyncChannelTest do defmodule AutoSyncChannelTest do
require Helpers
use ExUnit.Case, async: true
use Mimic
alias FarmbotExt.AMQP.AutoSyncChannel alias FarmbotExt.AMQP.AutoSyncChannel
use ExUnit.Case alias FarmbotExt.{
use Mimic AMQP.ConnectionWorker,
API.Preloader,
alias FarmbotCore.JSON JWT
alias FarmbotCore.Asset.{
Query,
Command,
Sync
} }
alias FarmbotExt.{JWT, API, AMQP.ConnectionWorker}
setup :verify_on_exit! setup :verify_on_exit!
setup :set_mimic_global setup :set_mimic_global
@ -35,203 +32,127 @@ defmodule AutoSyncChannelTest do
"eXTEVkqw7rved84ogw6EKBSFCVqwRA-NKWLpPMV_q7fRwiEG" <> "eXTEVkqw7rved84ogw6EKBSFCVqwRA-NKWLpPMV_q7fRwiEG" <>
"Wj7R-KZqRweALXuvCLF765E6-ENxA" "Wj7R-KZqRweALXuvCLF765E6-ENxA"
def pretend_network_returned(fake_value) do def generate_pid do
apply_default_mocks()
jwt = JWT.decode!(@fake_jwt) jwt = JWT.decode!(@fake_jwt)
test_pid = self()
expect(Query, :auto_sync?, 2, fn -> false end)
expect(API, :get_changeset, fn _module ->
send(test_pid, :preload_all_called)
changeset = Sync.changeset(%Sync{}, %{})
{:ok, changeset}
end)
expect(ConnectionWorker, :maybe_connect_autosync, fn jwt ->
send(test_pid, {:maybe_connect_called, jwt})
fake_value
end)
stub(ConnectionWorker, :close_channel, fn _ ->
send(test_pid, :close_channel_called)
:ok
end)
stub(ConnectionWorker, :rpc_reply, fn chan, jwt_dot_bot, label ->
send(test_pid, {:rpc_reply_called, chan, jwt_dot_bot, label})
:ok
end)
{:ok, pid} = AutoSyncChannel.start_link([jwt: jwt], []) {:ok, pid} = AutoSyncChannel.start_link([jwt: jwt], [])
pid
Map.merge(%{pid: pid}, AutoSyncChannel.network_status(pid))
end end
def under_normal_conditions() do def apply_default_mocks do
fake_con = %{fake: :conn} ok1 = fn _ -> :whatever end
fake_chan = %{fake: :chan} stub(FarmbotExt.API.EagerLoader.Supervisor, :drop_all_cache, fn -> :ok end)
pretend_network_returned(%{conn: fake_con, chan: fake_chan}) stub(ConnectionWorker, :close_channel, ok1)
stub(ConnectionWorker, :maybe_connect_autosync, fn _ ->
%{conn: %{fake_conn: true}, chan: %{fake_chan: true}}
end)
end end
test "network returns `nil`" do def ensure_response_to(msg) do
results = pretend_network_returned(nil) # Not much to check here other than matching clauses.
%{conn: has_conn, chan: has_chan, preloaded: is_preloaded} = results # AMQP lib handles most all of this.
expect(Preloader, :preload_all, 1, fn -> :ok end)
assert has_chan == nil pid = generate_pid()
assert has_conn == nil send(pid, msg)
assert is_preloaded Process.sleep(5)
end end
test "network returns unexpected object (probably an error)" do test "basic_cancel", do: ensure_response_to({:basic_cancel, :anything})
results = pretend_network_returned({:something, :else}) test "basic_cancel_ok", do: ensure_response_to({:basic_cancel_ok, :anything})
%{conn: has_conn, chan: has_chan, preloaded: is_preloaded} = results test "basic_consume_ok", do: ensure_response_to({:basic_consume_ok, :anything})
assert has_chan == nil test "init / terminate - auto_sync enabled" do
assert has_conn == nil expect(Preloader, :preload_all, 1, fn -> :ok end)
assert is_preloaded expect(FarmbotCore.Asset.Query, :auto_sync?, 1, fn -> true end)
expect(FarmbotCore.BotState, :set_sync_status, 1, fn "synced" -> :ok end)
expect(FarmbotCore.Leds, :green, 2, fn
:solid ->
:ok
:really_fast_blink ->
:ok
end)
# Helpers.expect_log("Failed to connect to AutoSync channel: :whatever")
# Helpers.expect_log("Disconnected from AutoSync channel: :normal")
pid = generate_pid()
assert %{chan: nil, conn: nil, preloaded: true} == AutoSyncChannel.network_status(pid)
GenServer.stop(pid, :normal)
end end
test "catch-all clause for inbound AMQP messages" do test "init / terminate - auto_sync disabled" do
fake_con = %{fake: :conn} expect(Preloader, :preload_all, 1, fn -> :ok end)
fake_chan = %{fake: :chan} expect(FarmbotCore.Asset.Query, :auto_sync?, 1, fn -> false end)
fake_response = %{conn: fake_con, chan: fake_chan} expect(FarmbotCore.BotState, :set_sync_status, 1, fn "sync_now" -> :ok end)
%{pid: pid} = pretend_network_returned(fake_response) expect(FarmbotCore.Leds, :green, 2, fn
:slow_blink ->
:ok
payload = :really_fast_blink ->
JSON.encode!(%{ :ok
args: %{label: "xyz"} end)
Helpers.expect_log("Disconnected from AutoSync channel: :normal")
pid = generate_pid()
assert %{chan: nil, conn: nil, preloaded: true} == AutoSyncChannel.network_status(pid)
GenServer.stop(pid, :normal)
end
test "init / terminate - auto_sync error" do
Helpers.expect_log("Error preloading. #{inspect("a test example")}")
Helpers.expect_log("Disconnected from AutoSync channel: :normal")
expect(FarmbotCore.BotState, :set_sync_status, 1, fn "sync_error" -> :ok end)
expect(Preloader, :preload_all, 1, fn -> {:error, "a test example"} end)
expect(FarmbotCore.Leds, :green, 2, fn
:slow_blink ->
:ok
:really_fast_blink ->
:ok
end)
pid = generate_pid()
assert %{chan: nil, conn: nil, preloaded: false} == AutoSyncChannel.network_status(pid)
GenServer.stop(pid, :normal)
end
test "delivery of auto sync messages" do
expect(Preloader, :preload_all, 1, fn -> :ok end)
expect(ConnectionWorker, :rpc_reply, 1, fn chan, device, label ->
assert chan == %{fake_chan: true}
assert device == "device_15"
assert label == "thisismylabelinatestsuite"
:ok
end)
key = "bot.device_15.sync.Device.46"
{:ok, payload} =
FarmbotCore.JSON.encode(%{
"id" => 46,
"args" => %{
"label" => "thisismylabelinatestsuite"
},
"body" => %{name: "This is my bot"}
}) })
send(pid, {:basic_deliver, payload, %{routing_key: "WRONG!"}}) pid = generate_pid()
assert_receive {:rpc_reply_called, %{fake: :chan}, "device_15", "xyz"} # We need the process to be preloaded for these tests to work:
end %{preloaded: true} = AutoSyncChannel.network_status(pid)
test "wont autosync unknown assets" do
fake_con = %{fake: :conn}
fake_chan = %{fake: :chan}
fake_response = %{conn: fake_con, chan: fake_chan}
%{pid: pid} = pretend_network_returned(fake_response)
payload =
JSON.encode!(%{
args: %{label: "xyz"}
})
send(pid, {:basic_deliver, payload, %{routing_key: "bot.device_15.sync.SavedGarden.999"}})
assert_receive {:rpc_reply_called, %{fake: :chan}, "device_15", "xyz"}
end
test "ignores asset deletion when auto_sync is off" do
%{pid: pid} = under_normal_conditions()
test_pid = self()
payload = '{"args":{"label":"foo"}}'
key = "bot.device_15.sync.Device.999"
stub(Query, :auto_sync?, fn ->
send(test_pid, :called_auto_sync?)
false
end)
send(pid, {:basic_deliver, payload, %{routing_key: key}})
assert_receive :called_auto_sync?
end
test "handles Device assets" do
%{pid: pid} = under_normal_conditions()
test_pid = self()
payload = '{"args":{"label":"foo"},"body":{}}'
key = "bot.device_15.sync.Device.999"
stub(Query, :auto_sync?, fn -> true end)
stub(Command, :update, fn x, y, z ->
send(test_pid, {:update_called, x, y, z})
:ok
end)
send(pid, {:basic_deliver, payload, %{routing_key: key}})
assert_receive {:update_called, "Device", 999, %{}}
end
def simple_asset_test_singleton(module_name) do
%{pid: pid} = under_normal_conditions()
test_pid = self()
payload = '{"args":{"label":"foo"},"body":{"foo": "bar"}}'
key = "bot.device_15.sync.#{module_name}.999"
stub(Query, :auto_sync?, fn -> true end)
stub(Command, :update, fn x, y, z ->
send(test_pid, {:update_called, x, y, z})
:ok
end)
stub(Command, :update, fn x, y, z ->
send(test_pid, {:update_called, x, y, z})
:ok
end)
send(pid, {:basic_deliver, payload, %{routing_key: key}}) send(pid, {:basic_deliver, payload, %{routing_key: key}})
assert_receive {:update_called, ^module_name, 999, %{"foo" => "bar"}} expect(FarmbotExt.AMQP.AutoSyncAssetHandler, :handle_asset, fn kind, id, body ->
end assert kind == "Device"
assert id == 46
test "handles auto_sync of 'no_cache' when auto_sync is false" do assert body == %{"name" => "This is my bot"}
test_pid = self()
%{pid: pid} = under_normal_conditions()
key = "bot.device_15.sync.FbosConfig.999"
payload = '{"args":{"label":"foo"},"body":{"foo": "bar"}}'
stub(Query, :auto_sync?, fn ->
send(test_pid, :called_auto_sync?)
false
end)
stub(Command, :update, fn kind, id, params ->
send(test_pid, {:update_called, kind, id, params})
:ok :ok
end) end)
send(pid, {:basic_deliver, payload, %{routing_key: key}}) Process.sleep(1000)
assert_receive :called_auto_sync?
assert_receive {:update_called, "FbosConfig", 999, %{"foo" => "bar"}}
end
test "auto_sync disabled, resource not in @cache_kinds" do
under_normal_conditions()
stub(Query, :auto_sync?, fn ->
false
end)
stub(Command, :new_changeset, fn _kind, _id, _params ->
:ok
end)
end
test "handles FbosConfig", do: simple_asset_test_singleton("FbosConfig")
test "handles FirmwareConfig", do: simple_asset_test_singleton("FirmwareConfig")
test "handles FarmwareEnv", do: simple_asset_test_plural("FarmwareEnv")
test "handles FarmwareInstallation", do: simple_asset_test_plural("FarmwareInstallation")
defp simple_asset_test_plural(module_name) do
%{pid: pid} = under_normal_conditions()
test_pid = self()
payload = '{"args":{"label":"foo"},"body":{"foo": "bar"}}'
key = "bot.device_15.sync.#{module_name}.999"
stub(Query, :auto_sync?, fn -> true end)
stub(Command, :update, fn x, y, z ->
send(test_pid, {:update_called, x, y, z})
:ok
end)
send(pid, {:basic_deliver, payload, %{routing_key: key}})
assert_receive {:update_called, ^module_name, 999, %{"foo" => "bar"}}
end end
end end

View File

@ -0,0 +1,25 @@
defmodule FarmbotExt.AMQP.BotStateChannelTest do
use ExUnit.Case
use Mimic
# alias FarmbotExt.AMQP.BotStateChannel
# alias FarmbotCore.BotState
setup :verify_on_exit!
setup :set_mimic_global
defmodule FakeState do
defstruct conn: %{fake: :conn}, chan: "fake_chan_", jwt: "fake_jwt_", cache: %{fake: :cache}
end
test "terminate" do
expected = "Disconnected from BotState channel: \"foo\""
expect(AMQP.Channel, :close, 1, fn "fake_chan_" -> :ok end)
expect(FarmbotCore.LogExecutor, :execute, 1, fn log ->
assert log.message == expected
end)
FarmbotExt.AMQP.BotStateChannel.terminate("foo", %FakeState{})
end
end

View File

@ -0,0 +1,12 @@
defmodule FarmbotExt.API.ViewTest do
use ExUnit.Case
def render(%{ok: :ok}) do
:yep
end
test "render/2" do
result = FarmbotExt.API.View.render(__MODULE__, %{ok: :ok})
assert :yep == result
end
end

View File

@ -1,14 +1,33 @@
Application.ensure_all_started(:farmbot) Application.ensure_all_started(:farmbot)
timeout = System.get_env("EXUNIT_TIMEOUT")
Mimic.copy(AMQP.Channel)
Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs) Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs)
Mimic.copy(FarmbotCore.Asset.Command) Mimic.copy(FarmbotCore.Asset.Command)
Mimic.copy(FarmbotCore.Asset.Query) Mimic.copy(FarmbotCore.Asset.Query)
Mimic.copy(FarmbotCore.BotState)
Mimic.copy(FarmbotCore.Leds)
Mimic.copy(FarmbotCore.LogExecutor)
Mimic.copy(FarmbotExt.AMQP.ConnectionWorker) Mimic.copy(FarmbotExt.AMQP.ConnectionWorker)
Mimic.copy(FarmbotExt.API.EagerLoader.Supervisor)
Mimic.copy(FarmbotExt.API.Preloader)
Mimic.copy(FarmbotExt.API) Mimic.copy(FarmbotExt.API)
Mimic.copy(FarmbotExt.AMQP.AutoSyncAssetHandler)
timeout = System.get_env("EXUNIT_TIMEOUT")
System.put_env("LOG_SILENCE", "true")
if timeout do if timeout do
ExUnit.start(assert_receive_timeout: String.to_integer(timeout)) ExUnit.start(assert_receive_timeout: String.to_integer(timeout))
else else
ExUnit.start() ExUnit.start()
end end
defmodule Helpers do
defmacro expect_log(message) do
quote do
expect(FarmbotCore.LogExecutor, :execute, fn log ->
assert log.message == unquote(message)
end)
end
end
end

View File

@ -300,20 +300,10 @@ defmodule FarmbotFirmware do
name: state.reset name: state.reset
) do ) do
{:ok, pid} -> {:ok, pid} ->
Logger.debug(
"Firmware reset #{state.reset} started. #{
inspect(state.transport_args)
}"
)
{:noreply, %{state | reset_pid: pid}} {:noreply, %{state | reset_pid: pid}}
# TODO(Rick): I have no idea what's going on here. # TODO(Rick): I have no idea what's going on here.
{:error, {:already_started, pid}} -> {:error, {:already_started, pid}} ->
Logger.debug(
"Firmware reset complete. #{inspect(state.transport_args)}"
)
{:noreply, %{state | reset_pid: pid}} {:noreply, %{state | reset_pid: pid}}
error -> error ->

View File

@ -4,7 +4,7 @@ defmodule FarmbotOS.SysCalls.Farmware do
require FarmbotCore.Logger require FarmbotCore.Logger
alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime} alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime}
alias FarmbotExt.API.ImageUploader alias FarmbotExt.API.ImageUploader
@farmware_timeout 30_000 @farmware_timeout 60_000
def update_farmware(farmware_name) do def update_farmware(farmware_name) do
with {:ok, installation} <- lookup_installation(farmware_name) do with {:ok, installation} <- lookup_installation(farmware_name) do

View File

@ -41,8 +41,8 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do
{:error, reason} when is_binary(reason) -> {:error, reason} when is_binary(reason) ->
{:error, reason} {:error, reason}
{_, exit_code} when is_number(exit_code) -> error ->
{:error, "avrdude error: #{exit_code}"} {:error, "flash_firmware misc error: #{inspect(error)}"}
end end
end end

View File

@ -95,6 +95,9 @@ defmodule FarmbotOS.SysCalls.Movement do
end end
end end
# TODO(Rick): Figure out source of Error: {:ok, "ok"} logs.
def handle_movement_error({:ok, _}), do: :ok
def handle_movement_error(reason) do def handle_movement_error(reason) do
msg = "Movement failed. #{inspect(reason)}" msg = "Movement failed. #{inspect(reason)}"
FarmbotCore.Logger.error(1, msg) FarmbotCore.Logger.error(1, msg)

View File

@ -126,7 +126,7 @@ defmodule FarmbotOS.MixProject do
{:busybox, "~> 0.1.4", targets: @all_targets}, {:busybox, "~> 0.1.4", targets: @all_targets},
{:farmbot_system_rpi3, "1.10.0-farmbot.1", {:farmbot_system_rpi3, "1.10.0-farmbot.1",
runtime: false, targets: :rpi3}, runtime: false, targets: :rpi3},
{:farmbot_system_rpi, "1.10.0-farmbot.1", runtime: false, targets: :rpi} {:farmbot_system_rpi, "1.10.0-farmbot.2", runtime: false, targets: :rpi}
] ]
end end

View File

@ -1,98 +1,98 @@
%{ %{
"amqp": {:hex, :amqp, "1.4.0", "4172595d467b9360850a8eca254c5946af9970684d335d555a9f3410a0e43995", [:mix], [{:amqp_client, "~> 3.8.0", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm"}, "amqp": {:hex, :amqp, "1.4.0", "4172595d467b9360850a8eca254c5946af9970684d335d555a9f3410a0e43995", [:mix], [{:amqp_client, "~> 3.8.0", [hex: :amqp_client, repo: "hexpm", optional: false]}], "hexpm", "333ace582c4eacc65ebb8378358e0b8bd46474556f2855c417d4257bbf425dbf"},
"amqp_client": {:hex, :amqp_client, "3.8.2", "b50ac381c3c016a697d6ab8f08367043a08358cfeb8ee97832ccc7d101e59cef", [:make, :rebar3], [{:rabbit_common, "3.8.2", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"}, "amqp_client": {:hex, :amqp_client, "3.8.2", "b50ac381c3c016a697d6ab8f08367043a08358cfeb8ee97832ccc7d101e59cef", [:make, :rebar3], [{:rabbit_common, "3.8.2", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm", "f453386e02a7ae77e0aa88bf4cf071aa31cce54e1b8511ca805961b0d83f164e"},
"busybox": {:hex, :busybox, "0.1.4", "9b07860c0663e7d0ace3093100ede44860bd73c22b17c2941a4b17e25893cc36", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "busybox": {:hex, :busybox, "0.1.4", "9b07860c0663e7d0ace3093100ede44860bd73c22b17c2941a4b17e25893cc36", [:make, :mix], [{:elixir_make, "~> 0.5", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "49f4a47821b31e1d5936aa7887109e4b6829a5c4d9233d5f3b2b62d6766e634d"},
"certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"},
"circuits_gpio": {:hex, :circuits_gpio, "0.4.3", "1a53dff1eaeefb9f67f4ebc2c1852b603683eedaa6053bed51c038dd64b978bb", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "circuits_gpio": {:hex, :circuits_gpio, "0.4.3", "1a53dff1eaeefb9f67f4ebc2c1852b603683eedaa6053bed51c038dd64b978bb", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3b9e0879af04d3cb0db219954355e73b2b4ed1cd427014d613bea5c11654787b"},
"circuits_i2c": {:hex, :circuits_i2c, "0.3.5", "43e043d7efc3aead364061f8a7ed627f81ff7cef52bfa47cb629d8a68ca56a9f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "circuits_i2c": {:hex, :circuits_i2c, "0.3.5", "43e043d7efc3aead364061f8a7ed627f81ff7cef52bfa47cb629d8a68ca56a9f", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "37a301618b19fe82d311c9e2858e25a3c4f131fec44d9e0d0b18849e9434120e"},
"circuits_uart": {:hex, :circuits_uart, "1.4.0", "799abad2d5f355bd571c46de089e62c6341e6b08f9fdf51f4d53d50f5d5bbda9", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "circuits_uart": {:hex, :circuits_uart, "1.4.0", "799abad2d5f355bd571c46de089e62c6341e6b08f9fdf51f4d53d50f5d5bbda9", [:mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f4543b823bd7ba2c7d4eb3ec634205003dfaaf141fbc4f668fe20eb193b18760"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
"cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "cors_plug": {:hex, :cors_plug, "2.0.0", "238ddb479f92b38f6dc1ae44b8d81f0387f9519101a6da442d543ab70ee0e482", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "118162367ef41448c9742ced8c8bc33ae2857d958d6b997e1db26402dd8c6f37"},
"cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e5580029080f3f1ad17436fb97b0d5ed2ed4e4815a96bac36b5a992e20f58db6"},
"cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"},
"credentials_obfuscation": {:hex, :credentials_obfuscation, "1.1.0", "513793cc20c18afc9e03e584b436192a751a8344890e03a8741c65c8d6866fab", [:rebar3], [], "hexpm"}, "credentials_obfuscation": {:hex, :credentials_obfuscation, "1.1.0", "513793cc20c18afc9e03e584b436192a751a8344890e03a8741c65c8d6866fab", [:rebar3], [], "hexpm", "2d1bc574d129ff76309a03874c245193c6375bc766734e008888e636b250d5df"},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm", "5f0a16a58312a610d5eb0b07506280c65f5137868ad479045f2a2dc4ced80550"},
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"},
"dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"}, "dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm", "6818589d8e59c03a2c73001e5cd7a957f99c30a796021aa32445ea14d0f3356b"},
"earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "hexpm", "8cf8a291ebf1c7b9539e3cddb19e9cef066c2441b1640f13c34c1d3cfc825fec"},
"ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm", "f1e20ddf41713b4db247443a3bea9045c4103b27c0e64b0c21ec50edde51fba8"},
"elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"},
"erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm"}, "erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm", "423a8f6ac70b77f0001c18adbff2b10413afed6901c2975aa33151a9c1263307"},
"esqlite": {:hex, :esqlite, "0.2.5", "cab6d87aeb5f33d848b9bb8a21129e9512ea608f930d4c63576942d8f7d72218", [:rebar3], [], "hexpm"}, "esqlite": {:hex, :esqlite, "0.2.5", "cab6d87aeb5f33d848b9bb8a21129e9512ea608f930d4c63576942d8f7d72218", [:rebar3], [], "hexpm", "3dd1163c8807b24a05ec4d88fd0f1bb286a2640ed340898fd792e3a67bb70f10"},
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "f1155337ae17ff7a1255217b4c1ceefcd1860b7ceb1a1874031e7a861b052e39"},
"excoveralls": {:hex, :excoveralls, "0.10.6", "e2b9718c9d8e3ef90bc22278c3f76c850a9f9116faf4ebe9678063310742edc2", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.10.6", "e2b9718c9d8e3ef90bc22278c3f76c850a9f9116faf4ebe9678063310742edc2", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b06c73492aa9940c4c29cfc1356bcf5540ae318f17b423749a0615a66ee3e049"},
"farmbot_system_rpi": {:hex, :farmbot_system_rpi, "1.10.0-farmbot.1", "baf3e08e53adfaa4f6af76f544713b7943b00295cf41be5f117f4f33b5d68cc6", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm"}, "farmbot_system_rpi": {:hex, :farmbot_system_rpi, "1.10.0-farmbot.2", "d26af0578dc3c7327399dfaf60c2ff113b2bc6d7a9b38bd478e18eeeb601f368", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm", "1c6c8b8b450e3b202eb5e6c2d771ef5bb7d9faa340bde6e1767e053ef05f1af8"},
"farmbot_system_rpi0": {:hex, :farmbot_system_rpi0, "1.8.0-farmbot.0", "017d5c50a462a30acee8de882c62c584131048b6efd7ea3367cbf56be50deebe", [:mix], [{:nerves, "~> 1.4", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.8.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm"}, "farmbot_system_rpi0": {:hex, :farmbot_system_rpi0, "1.8.0-farmbot.0", "017d5c50a462a30acee8de882c62c584131048b6efd7ea3367cbf56be50deebe", [:mix], [{:nerves, "~> 1.4", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.8.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", [hex: :nerves_toolchain_armv6_rpi_linux_gnueabi, repo: "hexpm", optional: false]}], "hexpm"},
"farmbot_system_rpi3": {:hex, :farmbot_system_rpi3, "1.10.0-farmbot.1", "8032728161829e80f526dd37d075864f5edf20738e3f0e24962d62664ac96937", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm"}, "farmbot_system_rpi3": {:hex, :farmbot_system_rpi3, "1.10.0-farmbot.1", "8032728161829e80f526dd37d075864f5edf20738e3f0e24962d62664ac96937", [:mix], [{:nerves, "~> 1.5.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.10.0", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm", "40b35222d282619b4dbcd5ea64a6d1601ae6b0b06da31a1df5d0514bee947653"},
"fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm"}, "fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm", "d12990ebda7d485d0eb7502df7aa9a56e66f67b5eda158c352db1de48e3f0518"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm", "5cacd405e72b2609a7e1f891bddb80c53d0b3b7b0036d1648e7382ca108c41c8"},
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm", "e0b8598e802676c81e66b061a2148c37c03886b24a3ca86a1f98ed40693b94b3"},
"goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"}, "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm", "99cb4128cffcb3227581e5d4d803d5413fa643f4eb96523f77d9e6937d994ceb"},
"hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"},
"idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "4bdd305eb64e18b0273864920695cb18d7a2021f31a11b9c5fbcd9a253f936e2"},
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "jason": {:hex, :jason, "1.2.0", "10043418c42d2493d0ee212d3fddd25d7ffe484380afad769a0a38795938e448", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "116747dbe057794c3a3e4e143b7c8390b29f634e16c78a7f59ba75bfa6852e7f"},
"jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"}, "jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm", "8ee1db1cabafdd578a2776a6aaae87c2a8ce54b47b59e9ec7dab5d7eb71cd8dc"},
"lager": {:hex, :lager, "3.8.0", "3402b9a7e473680ca179fc2f1d827cab88dd37dd1e6113090c6f45ef05228a1c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"}, "lager": {:hex, :lager, "3.8.0", "3402b9a7e473680ca179fc2f1d827cab88dd37dd1e6113090c6f45ef05228a1c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm", "f6cb541b688eab60730d8d286eb77256a5a9ad06eac10d43beaf55d07e68bbb6"},
"luer": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []}, "luer": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []},
"luerl": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []}, "luerl": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []},
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "a10c6eb62cca416019663129699769f0c2ccf39428b3bb3c0cb38c718a0c186d"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "d4b316c7222a85bbaa2fd7c6e90e37e953257ad196dc229505137c5e505e9eff"},
"mdns_lite": {:hex, :mdns_lite, "0.6.1", "6be652b99612d6790594b7e24ad04efd064028be4b9f199a8924fd4a89dd8881", [:mix], [{:dns, "~> 2.1", [hex: :dns, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.6", [hex: :vintage_net, repo: "hexpm", optional: true]}], "hexpm"}, "mdns_lite": {:hex, :mdns_lite, "0.6.1", "6be652b99612d6790594b7e24ad04efd064028be4b9f199a8924fd4a89dd8881", [:mix], [{:dns, "~> 2.1", [hex: :dns, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.6", [hex: :vintage_net, repo: "hexpm", optional: true]}], "hexpm", "e664f7ddacf7b811f9b7cec0c06c4e39788971bbdce24a19dc0f83c137aef8a0"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"}, "mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [:mix], [], "hexpm"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"}, "mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
"muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "muontrap": {:hex, :muontrap, "0.5.1", "98fe96d0e616ee518860803a37a29eb23ffc2ca900047cb1bb7fd37521010093", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "3c11b7f151b202148912c73cbdd633b76fa68fabc26cc441c9d6d140e22290dc"},
"nerves": {:hex, :nerves, "1.5.3", "14abb71fa1ce0cd281ffb6ba743c6c896b664efc3c2dd542f8682a55602176d8", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "nerves": {:hex, :nerves, "1.5.4", "d5a2a29a642e92277d5680f40d0fadff17796e75faa82de87ba0bc920ffcf818", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "283ce855f369ff209f3358d25e58f1ac941d58aef3ce7e8cc0be9919d25bf4f5"},
"nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm", "98c40104d0d2c6e6e8cce22f8c8fd8ad5b4b97f8694e42a9101ca44befac38f0"},
"nerves_hub": {:hex, :nerves_hub, "0.7.4", "0e104cad468c3d601ed423e116ea3422fbd31b7eedb263bcb2a5c489dca8b53b", [:mix], [{:fwup, "~> 0.3.0", [hex: :fwup, repo: "hexpm", optional: false]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nerves_hub_cli, "~> 0.9", [hex: :nerves_hub_cli, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:phoenix_client, "~> 0.7", [hex: :phoenix_client, repo: "hexpm", optional: false]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: false]}, {:x509, "~> 0.5", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_hub": {:hex, :nerves_hub, "0.7.4", "0e104cad468c3d601ed423e116ea3422fbd31b7eedb263bcb2a5c489dca8b53b", [:mix], [{:fwup, "~> 0.3.0", [hex: :fwup, repo: "hexpm", optional: false]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nerves_hub_cli, "~> 0.9", [hex: :nerves_hub_cli, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:phoenix_client, "~> 0.7", [hex: :phoenix_client, repo: "hexpm", optional: false]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: false]}, {:x509, "~> 0.5", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "af1daf7e879f1175c9db1957340b1773f11a00e1c63eb591427d1bf7f3d40b47"},
"nerves_hub_cli": {:hex, :nerves_hub_cli, "0.9.0", "ee02d6a4ce7706b7860df925a5a578c0856757123d7df56dfb38f85818f80aba", [:mix], [{:nerves_hub_user_api, "~> 0.6", [hex: :nerves_hub_user_api, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:table_rex, "~> 2.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_hub_cli": {:hex, :nerves_hub_cli, "0.9.0", "ee02d6a4ce7706b7860df925a5a578c0856757123d7df56dfb38f85818f80aba", [:mix], [{:nerves_hub_user_api, "~> 0.6", [hex: :nerves_hub_user_api, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:table_rex, "~> 2.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "0d0c2cf36c8e784534d6ba916587cbc282b00d317b577e8b2972eccd5ffe6314"},
"nerves_hub_user_api": {:hex, :nerves_hub_user_api, "0.6.0", "14f7bd249275c647981e6601ebef909fd4036391aef010ff74d01d4799b90bdf", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_hub_user_api": {:hex, :nerves_hub_user_api, "0.6.0", "14f7bd249275c647981e6601ebef909fd4036391aef010ff74d01d4799b90bdf", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "de682a5f5302d1f602a92f82fe2380abd658640ca25f620a8e9854e883020ea0"},
"nerves_runtime": {:hex, :nerves_runtime, "0.10.3", "8671c805262a6b8819a92b16afb100060af55a807a30f62395136c133e72b4ab", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.8.0", [hex: :system_registry, repo: "hexpm", optional: false]}, {:uboot_env, "~> 0.1.1", [hex: :uboot_env, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_runtime": {:hex, :nerves_runtime, "0.10.3", "8671c805262a6b8819a92b16afb100060af55a807a30f62395136c133e72b4ab", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.8.0", [hex: :system_registry, repo: "hexpm", optional: false]}, {:uboot_env, "~> 0.1.1", [hex: :uboot_env, repo: "hexpm", optional: false]}], "hexpm", "1ed7d228490f2138a028bf293f9f492b716da10d33f43615e4318731961f3866"},
"nerves_system_br": {:hex, :nerves_system_br, "1.10.0", "29169ebad0415b916bf3b9890f821a91b70be9af93a7fd824aa661f17b193548", [:mix], [], "hexpm"}, "nerves_system_br": {:hex, :nerves_system_br, "1.10.0", "29169ebad0415b916bf3b9890f821a91b70be9af93a7fd824aa661f17b193548", [:mix], [], "hexpm", "3f13276c2e28141e6033d7c0c7b041f690727c3f72b8d0f5d9001acf6a18fe74"},
"nerves_system_linter": {:hex, :nerves_system_linter, "0.3.0", "84e0f63c8ac196b16b77608bbe7df66dcf352845c4e4fb394bffd2b572025413", [:mix], [], "hexpm"}, "nerves_system_linter": {:hex, :nerves_system_linter, "0.3.0", "84e0f63c8ac196b16b77608bbe7df66dcf352845c4e4fb394bffd2b572025413", [:mix], [], "hexpm", "bffbdfb116bc72cde6e408c34c0670b199846e9a8f0953cc1c9f1eea693821a1"},
"nerves_time": {:hex, :nerves_time, "0.3.2", "cbd1048701a756695cda6ec5835419e47505a7fe437f97088c9475dc6f8ab625", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_time": {:hex, :nerves_time, "0.3.2", "cbd1048701a756695cda6ec5835419e47505a7fe437f97088c9475dc6f8ab625", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm", "b2da78b6d98775c29a7bd296d4b293a89a849a0fed2a685774892b50b2b63444"},
"nerves_toolchain_arm_unknown_linux_gnueabihf": {:hex, :nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", "ba48ce7c846ee12dfca8148dc7240988d96a3f2eb9c234bf08bffe4f0f7a3c62", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_toolchain_arm_unknown_linux_gnueabihf": {:hex, :nerves_toolchain_arm_unknown_linux_gnueabihf, "1.2.0", "ba48ce7c846ee12dfca8148dc7240988d96a3f2eb9c234bf08bffe4f0f7a3c62", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "18df425fee48a9088bf941d3615c677b818b537310123c4b4c90b710e4a34180"},
"nerves_toolchain_armv6_rpi_linux_gnueabi": {:hex, :nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", "007668c7ad1f73bad8fd54ad1a27a3b0fb91bca51b4af6bb3bbdac968ccae0ba", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_toolchain_armv6_rpi_linux_gnueabi": {:hex, :nerves_toolchain_armv6_rpi_linux_gnueabi, "1.2.0", "007668c7ad1f73bad8fd54ad1a27a3b0fb91bca51b4af6bb3bbdac968ccae0ba", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.6.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm", "4e843f405d4b8e6137419f94e5b8f491bff5a87b02ac2223e126182e8cec4256"},
"nerves_toolchain_ctng": {:hex, :nerves_toolchain_ctng, "1.6.0", "452f8589c1a58ac787477caab20a8cfc6671e345837ccc19beefe49ae35ba983", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm"}, "nerves_toolchain_ctng": {:hex, :nerves_toolchain_ctng, "1.6.0", "452f8589c1a58ac787477caab20a8cfc6671e345837ccc19beefe49ae35ba983", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm", "7ee5744dc606c6debf3e459ef122e77c13d6a1be9e093f7e29af3759896f9dbb"},
"nimble_csv": {:hex, :nimble_csv, "0.6.0", "a3673f26d41f986774fe6060e309615343d3cb83a6d435754d8b1fdbd5764879", [:mix], [], "hexpm"}, "nimble_csv": {:hex, :nimble_csv, "0.6.0", "a3673f26d41f986774fe6060e309615343d3cb83a6d435754d8b1fdbd5764879", [:mix], [], "hexpm", "b78e15f1c82fd963745ecc4073c47e1f0cdaf3622143342b8c30e9b5eaa546c6"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm", "589b5af56f4afca65217a1f3eb3fee7e79b09c40c742fddc1c312b3ac0b3399f"},
"one_dhcpd": {:hex, :one_dhcpd, "0.2.4", "2664f2e1ac72cbae0474a88d1a3d55019ccc3ee582ded382589511627a8ed516", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, "one_dhcpd": {:hex, :one_dhcpd, "0.2.4", "2664f2e1ac72cbae0474a88d1a3d55019ccc3ee582ded382589511627a8ed516", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "38f08c228d066153dbe352b150910345e395eacd2db3c19085d54c0baeeebacb"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
"pbcs": {:hex, :pbcs, "0.1.1", "199c7fd4af3351758378355909145a2d187c565555ed16bde30b5055114652ed", [:mix], [], "hexpm"}, "pbcs": {:hex, :pbcs, "0.1.1", "199c7fd4af3351758378355909145a2d187c565555ed16bde30b5055114652ed", [:mix], [], "hexpm", "7bf2553c32bc7948959e599de0b39745ef988b0914fdb2cf89ea2bed569c1357"},
"phoenix_client": {:hex, :phoenix_client, "0.10.0", "a77ace5495c400001808e96980673dd3b97b1048f296fd032991c52e8f5fe93d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: true]}], "hexpm"}, "phoenix_client": {:hex, :phoenix_client, "0.10.0", "a77ace5495c400001808e96980673dd3b97b1048f296fd032991c52e8f5fe93d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: true]}], "hexpm", "9374a29a9f835125cec73a2b45086eedce8df6b4d7c5353fced11bb48a3d6800"},
"phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8b01b3d6d39731ab18aa548d928b5796166d2500755f553725cfe967bafba7d9"},
"plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "plug": {:hex, :plug, "1.8.3", "12d5f9796dc72e8ac9614e94bda5e51c4c028d0d428e9297650d09e15a684478", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "164baaeb382d19beee0ec484492aa82a9c8685770aee33b24ec727a0971b34d0"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.1.0", "b75768153c3a8a9e8039d4b25bb9b14efbc58e9c4a6e6a270abff1cd30cbe320", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "6cd8ddd1bd1fbfa54d3fc61d4719c2057dae67615395d58d40437a919a46f132"},
"plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"rabbit_common": {:hex, :rabbit_common, "3.8.2", "6f5653e7ba8bbf76447b126d1ac224e1be5ed853808542bd67cbcff87fbd2493", [:make, :rebar3], [{:credentials_obfuscation, "1.1.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:jsx, "2.9.0", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.8.0", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}, {:recon, "2.5.0", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, "rabbit_common": {:hex, :rabbit_common, "3.8.2", "6f5653e7ba8bbf76447b126d1ac224e1be5ed853808542bd67cbcff87fbd2493", [:make, :rebar3], [{:credentials_obfuscation, "1.1.0", [hex: :credentials_obfuscation, repo: "hexpm", optional: false]}, {:jsx, "2.9.0", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.8.0", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}, {:recon, "2.5.0", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "c36e9613093910ed3a86f43a21fddcd48c3015a81ba74586824bdc4b2ae5519b"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"}, "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"},
"ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm"}, "ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm", "9b2f482e4346c13c11ef555f898202d0ddbfda6e2354e5c6e0559d2b4e0cf781"},
"sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm", "ba952bfa35b374e1e5d84bc5f5efe8360c6f99dc93b3118f714a9a2dff6c9e19"},
"shoehorn": {:hex, :shoehorn, "0.6.0", "f9a1b7d6212cf18ba91c4f71c26076059df33cea4db2eb3c098bfa6673349412", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm"}, "shoehorn": {:hex, :shoehorn, "0.6.0", "f9a1b7d6212cf18ba91c4f71c26076059df33cea4db2eb3c098bfa6673349412", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm", "e54a1f58a121caf8f0f3a355686b2661258b1bc0d4fffef8923bd7b11c2f9d79"},
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm"}, "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
"sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm", "a588e8e4ab9570c32a03605fabff86f0fee1040530d33edc4fc4392db4d81700"},
"sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, "sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm", "0e1974b48684ba85255d2fe16c6106d52f5e759b260c95f676b23aa13b708a96"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm", "13104d7897e38ed7f044c4de953a6c28597d1c952075eb2e328bc6d6f2bfc496"},
"system_registry": {:hex, :system_registry, "0.8.2", "df791dc276652fcfb53be4dab823e05f8269b96ac57c26f86a67838dbc0eefe7", [:mix], [], "hexpm"}, "system_registry": {:hex, :system_registry, "0.8.2", "df791dc276652fcfb53be4dab823e05f8269b96ac57c26f86a67838dbc0eefe7", [:mix], [], "hexpm", "f7acdede22c73ab0b3735eead7f2095efb2a7a6198366564205274db2ca2a8f8"},
"table_rex": {:hex, :table_rex, "2.0.0", "712783cbc2decb4d644d2ab8ad9315294f960c41b2cf0539308164922e352084", [:mix], [], "hexpm"}, "table_rex": {:hex, :table_rex, "2.0.0", "712783cbc2decb4d644d2ab8ad9315294f960c41b2cf0539308164922e352084", [:mix], [], "hexpm", "b183ff68abe9ace8764af4c2828767865d2c854d1d6f8cfd3660218166ed89a1"},
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
"tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, 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]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, 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]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "93a7cacc5ca47997759cfa1d3ab25501d291e490908006d5be56f37f89d96693"},
"timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [: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.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [: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", "b8fd8c9fcfaef1fa9c415e0792e2e82783c7ec8a282dfceef7d48158d4cfb3e1"},
"toolshed": {:hex, :toolshed, "0.2.11", "0cd5312bd6a48f5b654b6ffa9239a63af9f3d200da414790fe25f066e14842a9", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm"}, "toolshed": {:hex, :toolshed, "0.2.11", "0cd5312bd6a48f5b654b6ffa9239a63af9f3d200da414790fe25f066e14842a9", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm", "f22ae95d77136f9f7db93cddd40d42bc8252d825f15a772a17d4c7947b6faad5"},
"tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "69c9913feb9b9f84c7e70e448e1f425d450d95119800ba420c5121a59bdd12c6"},
"uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm"}, "uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm", "f7b82da0cb40c8db9c9fb1fc977780ab0c28d961ec1f3c7ab265c4352e4141ae"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
"vintage_net": {:hex, :vintage_net, "0.7.5", "4e843a3f029a32cd5644aa3397bbb1463e123b06848e5a3c99cefc58af395a2a", [:make, :mix], [{:busybox, "~> 0.1.4", [hex: :busybox, repo: "hexpm", optional: true]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.1", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm"}, "vintage_net": {:hex, :vintage_net, "0.7.5", "4e843a3f029a32cd5644aa3397bbb1463e123b06848e5a3c99cefc58af395a2a", [:make, :mix], [{:busybox, "~> 0.1.4", [hex: :busybox, repo: "hexpm", optional: true]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:gen_state_machine, "~> 2.0.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.5.1", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm", "cc2e3d3d1e5e04a15df32ce624d8c3cccc88432dcada48eccc6de9bec891acc2"},
"vintage_net_direct": {:hex, :vintage_net_direct, "0.7.0", "6a8d95432fc2fb9a9e0225bf1a6dbb7c1ba097bb509a989e947ad3f44d2b0f28", [:mix], [{:one_dhcpd, "~> 0.2.3", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"}, "vintage_net_direct": {:hex, :vintage_net_direct, "0.7.0", "6a8d95432fc2fb9a9e0225bf1a6dbb7c1ba097bb509a989e947ad3f44d2b0f28", [:mix], [{:one_dhcpd, "~> 0.2.3", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "9c091687dccbc379ba3b8de01458a3e911321aa391e2c2ae3be7edadd0c51f76"},
"vintage_net_ethernet": {:hex, :vintage_net_ethernet, "0.7.0", "b66a4da0846b4a55a471d15d8ab2bedbbef9c75c18f4e511c43777f7cfeffa09", [:mix], [{:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"}, "vintage_net_ethernet": {:hex, :vintage_net_ethernet, "0.7.0", "b66a4da0846b4a55a471d15d8ab2bedbbef9c75c18f4e511c43777f7cfeffa09", [:mix], [{:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "bff0fa24b153b80dab959a0697777c4de7c3272c387ffa618f73805d66757c3f"},
"vintage_net_wifi": {:hex, :vintage_net_wifi, "0.7.0", "be3d8275fa40ba357159033523606c74c8f612dbd60801840df60835453a6906", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm"}, "vintage_net_wifi": {:hex, :vintage_net_wifi, "0.7.0", "be3d8275fa40ba357159033523606c74c8f612dbd60801840df60835453a6906", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:vintage_net, "~> 0.7.0", [hex: :vintage_net, repo: "hexpm", optional: false]}], "hexpm", "a9dfac64705f0b7cd7e0fc2338cf891eef091a49385805d0a42c22fb0aaa7c84"},
"websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"}, "websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm", "b864fa076f059b615da4ab99240e515b26132ce4d2d0f9df5d7f22f01fa04b65"},
"x509": {:hex, :x509, "0.8.0", "b286b9dbb32801f76f48bdea476304d280c64ce172ea330c23d8df7ea9e75ce6", [:mix], [], "hexpm"}, "x509": {:hex, :x509, "0.8.0", "b286b9dbb32801f76f48bdea476304d280c64ce172ea330c23d8df7ea9e75ce6", [:mix], [], "hexpm", "8aafaafdcafb1ea9f06bfc32c3b03ccc66f087e0faf36ef94c0195bb7a04157e"},
} }

View File

@ -2,7 +2,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
@moduledoc """ @moduledoc """
NervesHub.Client implementation. NervesHub.Client implementation.
This should be one of the very first processes to be started. This should be one of the very first processes to be started.
Because it is started so early, it has to check for things that Because it is started so early, it has to check for things that
might not be available in the environment. Environment is checked might not be available in the environment. Environment is checked
in this order: in this order:
@ -397,8 +397,8 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
%{is_applying_update: false, probably_connected: true} = state %{is_applying_update: false, probably_connected: true} = state
) do ) do
if should_auto_apply_update?() && update_available?() do if should_auto_apply_update?() && update_available?() do
FarmbotCore.Logger.busy(1, "Applying OTA update") FarmbotCore.Logger.busy(1, "Applying OTA update (1)")
spawn_link(fn -> NervesHub.update() end) run_update_but_only_once()
{:noreply, %{state | is_applying_update: true}} {:noreply, %{state | is_applying_update: true}}
else else
Process.send_after(self(), :checkup, @checkup_timeout_ms) Process.send_after(self(), :checkup, @checkup_timeout_ms)
@ -423,7 +423,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
{:handle_nerves_hub_error, error}, {:handle_nerves_hub_error, error},
%{is_applying_update: true} = state %{is_applying_update: true} = state
) do ) do
FarmbotCore.Logger.error(1, "Error applying OTA: #{inspect(error)}") FarmbotCore.Logger.error(1, "Error applying OTA (1): #{inspect(error)}")
{:noreply, state} {:noreply, state}
end end
@ -449,7 +449,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
def handle_cast({:handle_nerves_hub_fwup_message, {:error, _, reason}}, state) do def handle_cast({:handle_nerves_hub_fwup_message, {:error, _, reason}}, state) do
_ = set_ota_progress(100) _ = set_ota_progress(100)
FarmbotCore.Logger.error(1, "Error applying OTA: #{inspect(reason)}") FarmbotCore.Logger.error(1, "Error applying OTA (2): #{inspect(reason)}")
{:noreply, state} {:noreply, state}
end end
@ -465,7 +465,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
) do ) do
case should_auto_apply_update?() do case should_auto_apply_update?() do
true -> true ->
FarmbotCore.Logger.busy(1, "Applying OTA update") FarmbotCore.Logger.busy(1, "Applying OTA update (2)")
_ = set_update_available_in_bot_state() _ = set_update_available_in_bot_state()
_ = update_device_last_ota_checkup() _ = update_device_last_ota_checkup()
_ = set_firmware_needs_flash() _ = set_firmware_needs_flash()
@ -474,7 +474,6 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
_ -> _ ->
_ = set_update_available_in_bot_state() _ = set_update_available_in_bot_state()
_ = update_device_last_ota_checkup() _ = update_device_last_ota_checkup()
FarmbotCore.Logger.info(1, "New Farmbot OS is available!")
Process.send_after(self(), :checkup, @checkup_timeout_ms) Process.send_after(self(), :checkup, @checkup_timeout_ms)
{:reply, :ignore, %{state | firmware_url: url}} {:reply, :ignore, %{state | firmware_url: url}}
end end
@ -484,7 +483,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
_ = set_update_available_in_bot_state() _ = set_update_available_in_bot_state()
_ = update_device_last_ota_checkup() _ = update_device_last_ota_checkup()
_ = set_firmware_needs_flash() _ = set_firmware_needs_flash()
FarmbotCore.Logger.busy(1, "Applying OTA update") FarmbotCore.Logger.busy(1, "Applying OTA update (3)")
{:reply, :apply, %{state | is_applying_update: true}} {:reply, :apply, %{state | is_applying_update: true}}
end end
@ -498,9 +497,10 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
_ = set_update_available_in_bot_state() _ = set_update_available_in_bot_state()
_ = update_device_last_ota_checkup() _ = update_device_last_ota_checkup()
_ = set_firmware_needs_flash() _ = set_firmware_needs_flash()
FarmbotCore.Logger.busy(1, "Applying OTA update") FarmbotCore.Logger.busy(1, "Attempting OTA update...")
# This is where the NervesHub update gets called.
spawn_link(fn -> NervesHub.update() end) # Maybe we can check if the BotState has job progress for "FBOS_OTA"
run_update_but_only_once()
{:reply, data, %{state | is_applying_update: true}} {:reply, data, %{state | is_applying_update: true}}
end end
end end
@ -511,38 +511,41 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
ota_hour = Asset.device(:ota_hour) ota_hour = Asset.device(:ota_hour)
timezone = Asset.device(:timezone) timezone = Asset.device(:timezone)
# if ota_hour is nil, auto apply the update # if ota_hour is nil, auto apply the update
if ota_hour && timezone do result =
# check that now.hour == device.ota_hour if ota_hour && timezone do
case Timex.Timezone.convert(now, timezone) do # check that now.hour == device.ota_hour
%{hour: ^ota_hour} -> case Timex.Timezone.convert(now, timezone) do
FarmbotCore.Logger.debug( %{hour: ^ota_hour} ->
3, FarmbotCore.Logger.debug(
"current hour: #{ota_hour} (utc=#{now.hour}) == ota_hour #{ota_hour}. auto_update=#{ 3,
auto_update "current hour: #{ota_hour} (utc=#{now.hour}) == ota_hour #{
}" ota_hour
) }. auto_update=#{auto_update}"
)
auto_update auto_update
%{hour: now_hour} -> %{hour: now_hour} ->
FarmbotCore.Logger.debug( FarmbotCore.Logger.debug(
3, 3,
"current hour: #{now_hour} (utc=#{now.hour}) != ota_hour: #{ "current hour: #{now_hour} (utc=#{now.hour}) != ota_hour: #{
ota_hour ota_hour
}. auto_update=#{auto_update}" }. auto_update=#{auto_update}"
) )
false false
end
else
# ota_hour or timezone are nil
FarmbotCore.Logger.debug(
3,
"ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}"
)
true
end end
else
# ota_hour or timezone are nil
FarmbotCore.Logger.debug(
3,
"ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}"
)
true result && !currently_downloading?()
end
end end
def update_available?() do def update_available?() do
@ -615,7 +618,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
Process.monitor(conn.pid) Process.monitor(conn.pid)
{:ok, conn} {:ok, conn}
# Squash this log since it will be displayed for the # Squash this log since it will be displayed for the
# main AMQP connection # main AMQP connection
{:error, :unknown_host} = err -> {:error, :unknown_host} = err ->
err err
@ -678,4 +681,18 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do
end end
end end
end end
def currently_downloading?, do: BotState.job_in_progress?("FBOS_OTA")
def run_update_but_only_once do
if currently_downloading?() do
FarmbotCore.Logger.error(
1,
"Can't perform OTA. OTA alread in progress. Restart device if problem persists."
)
else
FarmbotCore.Logger.success(1, "OTA started.")
spawn_link(fn -> NervesHub.update() end)
end
end
end end

View File

@ -188,7 +188,7 @@ defmodule FarmbotOS.Platform.Target.Network do
_meta}, _meta},
state state
) do ) do
FarmbotCore.Logger.warn( FarmbotCore.Logger.success(
1, 1,
"Interface #{ifname} connected to local area network" "Interface #{ifname} connected to local area network"
) )
@ -202,7 +202,7 @@ defmodule FarmbotOS.Platform.Target.Network do
_meta}, _meta},
state state
) do ) do
FarmbotCore.Logger.warn(1, "Interface #{ifname} connected to internet") FarmbotCore.Logger.success(1, "Interface #{ifname} connected to internet")
state = cancel_network_not_found_timer(state) state = cancel_network_not_found_timer(state)
FarmbotTelemetry.event(:network, :wan_connect, nil, interface: ifname) FarmbotTelemetry.event(:network, :wan_connect, nil, interface: ifname)
{:noreply, %{state | first_connect?: false}} {:noreply, %{state | first_connect?: false}}

View File

@ -9,7 +9,7 @@ defmodule FarmbotOS.SysCalls.FarmwareTest do
expect(FarmbotCore.LogExecutor, :execute, fn log -> expect(FarmbotCore.LogExecutor, :execute, fn log ->
expected = expected =
"Farmware did not exit after 30.0 seconds. Terminating :FAKE_PID" "Farmware did not exit after 60.0 seconds. Terminating :FAKE_PID"
assert log.message == expected assert log.message == expected
:ok :ok