diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdc33a7..42bbd9aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # 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 * Support for criteria-based groups. diff --git a/COVERAGE.md b/COVERAGE.md new file mode 100644 index 00000000..f755f4a0 --- /dev/null +++ b/COVERAGE.md @@ -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 | diff --git a/VERSION b/VERSION index deeb3d66..45acc9e6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -9.2.0 +9.2.1 diff --git a/farmbot_core/lib/farmbot_core/asset/command.ex b/farmbot_core/lib/farmbot_core/asset/command.ex index 994a9030..81068600 100644 --- a/farmbot_core/lib/farmbot_core/asset/command.ex +++ b/farmbot_core/lib/farmbot_core/asset/command.ex @@ -189,7 +189,6 @@ defmodule FarmbotCore.Asset.Command do # Catch-all use case: def update(asset_kind, id, params) do - Logger.warn("AssetCommand needs implementation: #{asset_kind}") mod = as_module!(asset_kind) case Repo.get_by(mod, id: id) do diff --git a/farmbot_core/lib/farmbot_core/asset_monitor.ex b/farmbot_core/lib/farmbot_core/asset_monitor.ex index 54b301fa..193a8881 100644 --- a/farmbot_core/lib/farmbot_core/asset_monitor.ex +++ b/farmbot_core/lib/farmbot_core/asset_monitor.ex @@ -94,7 +94,6 @@ defmodule FarmbotCore.AssetMonitor do Map.put(sub_state, id, updated_at) is_nil(sub_state[id]) -> - Logger.debug("#{inspect(kind)} #{id} needs to be started") asset = Repo.preload(asset, AssetWorker.preload(asset)) :ok = AssetSupervisor.start_child(asset) |> assert_result!(asset) Map.put(sub_state, id, updated_at) diff --git a/farmbot_core/lib/farmbot_core/bot_state.ex b/farmbot_core/lib/farmbot_core/bot_state.ex index 3be0803e..ba905543 100644 --- a/farmbot_core/lib/farmbot_core/bot_state.ex +++ b/farmbot_core/lib/farmbot_core/bot_state.ex @@ -1,6 +1,8 @@ defmodule FarmbotCore.BotState do @moduledoc "Central State accumulator." alias FarmbotCore.BotStateNG + alias FarmbotCore.BotState.JobProgress.Percent + require FarmbotCore.Logger use GenServer @@ -28,7 +30,7 @@ defmodule FarmbotCore.BotState do def set_position(bot_state_server \\ __MODULE__, x, y, z) do GenServer.call(bot_state_server, {:set_position, x, y, z}) end - + @doc "Sets the location_data.load" def set_load(bot_state_server \\ __MODULE__, x, y, z) do 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) 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 def start_link(args, opts \\ [name: __MODULE__]) do GenServer.start_link(__MODULE__, args, opts) @@ -175,6 +181,13 @@ defmodule FarmbotCore.BotState do FarmbotCore.Logger.error 1, "BotState crashed! #{inspect(reason)}" 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 def handle_call(:subscribe, {pid, _} = _from, state) do # TODO Just replace this with Elixir.Registry? diff --git a/farmbot_core/lib/farmbot_core/firmware_open_task.ex b/farmbot_core/lib/farmbot_core/firmware_open_task.ex index b94eef1a..844c8c2a 100644 --- a/farmbot_core/lib/farmbot_core/firmware_open_task.ex +++ b/farmbot_core/lib/farmbot_core/firmware_open_task.ex @@ -90,8 +90,8 @@ defmodule FarmbotCore.FirmwareOpenTask do Config.update_config_value(:bool, "settings", "firmware_needs_open", false) timer = Process.send_after(self(), :open, 5000) {:noreply, %{state | timer: timer, attempts: 0}} - _ -> - FarmbotCore.Logger.debug 3, "Firmware failed to open" + other -> + FarmbotCore.Logger.debug 3, "Firmware failed to open: #{inspect(other)}" timer = Process.send_after(self(), :open, 5000) {:noreply, %{state | timer: timer, attempts: 0}} end diff --git a/farmbot_core/lib/farmbot_core/firmware_side_effects.ex b/farmbot_core/lib/farmbot_core/firmware_side_effects.ex index 228d784f..73a9f6db 100644 --- a/farmbot_core/lib/farmbot_core/firmware_side_effects.ex +++ b/farmbot_core/lib/farmbot_core/firmware_side_effects.ex @@ -193,7 +193,14 @@ defmodule FarmbotCore.FirmwareSideEffects do def handle_debug_message([message]) do fbos_config = Asset.fbos_config() 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 @impl FarmbotFirmware.SideEffects diff --git a/farmbot_core/lib/farmbot_core/leds/stub_handler.ex b/farmbot_core/lib/farmbot_core/leds/stub_handler.ex index d56aa4e9..464f1dbe 100644 --- a/farmbot_core/lib/farmbot_core/leds/stub_handler.ex +++ b/farmbot_core/lib/farmbot_core/leds/stub_handler.ex @@ -13,7 +13,8 @@ defmodule FarmbotCore.Leds.StubHandler do def white5(status), do: do_debug(:white, status) 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, []), status_in(status), to_string(color), @@ -22,7 +23,8 @@ defmodule FarmbotCore.Leds.StubHandler do status_out(status), IO.ANSI.reset() ] - IO.puts(msg) + IO.puts(msg) + end end defp status_in(:slow_blink), do: IO.ANSI.blink_slow() diff --git a/farmbot_core/lib/farmbot_core/log_storage/log_executor.ex b/farmbot_core/lib/farmbot_core/log_storage/log_executor.ex index ba1bbee7..75cede54 100644 --- a/farmbot_core/lib/farmbot_core/log_storage/log_executor.ex +++ b/farmbot_core/lib/farmbot_core/log_storage/log_executor.ex @@ -23,7 +23,9 @@ defmodule FarmbotCore.LogExecutor do do: level, 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 end end diff --git a/farmbot_ext/.gitignore b/farmbot_ext/.gitignore index 9575d916..93ae1d43 100644 --- a/farmbot_ext/.gitignore +++ b/farmbot_ext/.gitignore @@ -23,3 +23,4 @@ erl_crash.dump farmbot_ext-*.tar *.sqlite3 +*.coverdata diff --git a/farmbot_ext/config/config.exs b/farmbot_ext/config/config.exs index d19826fd..230cacd8 100644 --- a/farmbot_ext/config/config.exs +++ b/farmbot_ext/config/config.exs @@ -11,3 +11,4 @@ config :farmbot_celery_script, FarmbotCeleryScript.SysCalls, import_config "ecto.exs" import_config "farmbot_core.exs" import_config "lagger.exs" +import_config "test.exs" diff --git a/farmbot_ext/config/test.exs b/farmbot_ext/config/test.exs new file mode 100644 index 00000000..84f43b1f --- /dev/null +++ b/farmbot_ext/config/test.exs @@ -0,0 +1,5 @@ +use Mix.Config + +if Mix.env() == :test do + config :farmbot_ext, FarmbotExt, children: [] +end diff --git a/farmbot_ext/coveralls.json b/farmbot_ext/coveralls.json new file mode 100644 index 00000000..75702d10 --- /dev/null +++ b/farmbot_ext/coveralls.json @@ -0,0 +1,6 @@ +{ + "coverage_options": { + "treat_no_relevant_lines_as_covered": true, + "minimum_coverage": 6.5 + } +} \ No newline at end of file diff --git a/farmbot_ext/lib/farmbot_ext.ex b/farmbot_ext/lib/farmbot_ext.ex index 21e933e9..51c37cf4 100644 --- a/farmbot_ext/lib/farmbot_ext.ex +++ b/farmbot_ext/lib/farmbot_ext.ex @@ -1,19 +1,18 @@ defmodule FarmbotExt do - # See https://hexdocs.pm/elixir/Application.html - # for more information on OTP Applications @moduledoc false use Application 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__] - 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 diff --git a/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_asset_handler.ex b/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_asset_handler.ex new file mode 100644 index 00000000..2f070051 --- /dev/null +++ b/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_asset_handler.ex @@ -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 diff --git a/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_channel.ex b/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_channel.ex index 23b617cc..ab24dde1 100644 --- a/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_channel.ex +++ b/farmbot_ext/lib/farmbot_ext/amqp/auto_sync_channel.ex @@ -8,14 +8,13 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do use GenServer use AMQP - alias FarmbotCore.{Asset, BotState, JSON, Leds} - alias FarmbotExt.AMQP.ConnectionWorker - alias FarmbotExt.API.{EagerLoader, Preloader} - - require Logger require FarmbotCore.Logger 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 # are the only ones that Farmbot needs to sync. @known_kinds ~w( @@ -35,18 +34,8 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do 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] + alias __MODULE__, as: State @doc "Gets status of auto_sync connection for diagnostics / tests." @@ -68,12 +57,15 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do def terminate(reason, state) do FarmbotCore.Logger.error(1, "Disconnected from AutoSync channel: #{inspect(reason)}") # 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 EagerLoader.Supervisor.drop_all_cache() catch - _, _ -> :ok + _, _ -> + :ok end end @@ -103,6 +95,7 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do end def handle_info(:connect, state) do + # THIS IS WHERE state.chan GETS SET result = ConnectionWorker.maybe_connect_autosync(state.jwt.bot) compute_reply_from_amqp_state(state, result) end @@ -134,13 +127,11 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do case String.split(key, ".") do ["bot", ^device, "sync", asset_kind, id_str] when asset_kind in @known_kinds -> id = data["id"] || String.to_integer(id_str) - _ = handle_asset(asset_kind, id, body) - - ["bot", ^device, "sync", asset_kind, _id_str] -> - Logger.warn("Unknown syncable asset: #{asset_kind}") + _ = AutoSyncAssetHandler.handle_asset(asset_kind, id, body) _ -> - Logger.info("ignoring route: #{key}") + "" + # Logger.info("ignoring route: #{key}") end :ok = ConnectionWorker.rpc_reply(chan, device, label) @@ -158,36 +149,6 @@ defmodule FarmbotExt.AMQP.AutoSyncChannel do {:reply, reply, state} 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 {:noreply, %{state | conn: conn, chan: chan}} end diff --git a/farmbot_ext/lib/farmbot_ext/api/eager_loader.ex b/farmbot_ext/lib/farmbot_ext/api/eager_loader.ex index 6b6fd320..5d62e338 100644 --- a/farmbot_ext/lib/farmbot_ext/api/eager_loader.ex +++ b/farmbot_ext/lib/farmbot_ext/api/eager_loader.ex @@ -66,7 +66,6 @@ defmodule FarmbotExt.API.EagerLoader do * a remote `id` field. """ def cache(%Changeset{data: %module{}} = changeset) do - Logger.info("Caching #{inspect(changeset)}") id = Changeset.get_field(changeset, :id) updated_at = Changeset.get_field(changeset, :updated_at) id || change_error(changeset, "Can't cache a changeset with no :id attribute") diff --git a/farmbot_ext/lib/farmbot_ext/api/preloader.ex b/farmbot_ext/lib/farmbot_ext/api/preloader.ex index d79b681f..9c72ff60 100644 --- a/farmbot_ext/lib/farmbot_ext/api/preloader.ex +++ b/farmbot_ext/lib/farmbot_ext/api/preloader.ex @@ -24,7 +24,7 @@ defmodule FarmbotExt.API.Preloader do with {:ok, sync_changeset} <- API.get_changeset(Sync), sync_changeset <- Reconciler.sync_group(sync_changeset, SyncGroup.group_0()) do 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 diff --git a/farmbot_ext/lib/farmbot_ext/bootstrap.ex b/farmbot_ext/lib/farmbot_ext/bootstrap.ex index 8879c75d..ada46199 100644 --- a/farmbot_ext/lib/farmbot_ext/bootstrap.ex +++ b/farmbot_ext/lib/farmbot_ext/bootstrap.ex @@ -51,8 +51,6 @@ defmodule FarmbotExt.Bootstrap do end def try_auth(email, server, password, _secret) do - Logger.debug("using password to auth") - with {:ok, tkn} <- Authorization.authorize_with_password(email, password, server), _ <- update_config_value(:string, "authorization", "token", tkn), {:ok, pid} <- Supervisor.start_child(FarmbotExt, Bootstrap.Supervisor) do diff --git a/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex b/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex index b875c436..5abcdfb1 100644 --- a/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex +++ b/farmbot_ext/lib/farmbot_ext/bootstrap/supervisor.ex @@ -1,6 +1,6 @@ defmodule FarmbotExt.Bootstrap.Supervisor do @moduledoc """ - Supervisor responsible for starting all + Supervisor responsible for starting all the tasks and processes that require authentication. """ use Supervisor @@ -20,7 +20,6 @@ defmodule FarmbotExt.Bootstrap.Supervisor do FarmbotExt.Bootstrap.DropPasswordTask ] - opts = [strategy: :one_for_one] - Supervisor.init(children, opts) + Supervisor.init(children, strategy: :one_for_one) end end diff --git a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs new file mode 100644 index 00000000..950f1aad --- /dev/null +++ b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_asset_handler_test.exs @@ -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 diff --git a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs index 4ea92dbb..c40b3c7b 100644 --- a/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs +++ b/farmbot_ext/test/farmbot_ext/amqp/auto_sync_channel_test.exs @@ -1,18 +1,15 @@ defmodule AutoSyncChannelTest do + require Helpers + use ExUnit.Case, async: true + use Mimic alias FarmbotExt.AMQP.AutoSyncChannel - use ExUnit.Case - use Mimic - - alias FarmbotCore.JSON - - alias FarmbotCore.Asset.{ - Query, - Command, - Sync + alias FarmbotExt.{ + AMQP.ConnectionWorker, + API.Preloader, + JWT } - alias FarmbotExt.{JWT, API, AMQP.ConnectionWorker} setup :verify_on_exit! setup :set_mimic_global @@ -35,203 +32,127 @@ defmodule AutoSyncChannelTest do "eXTEVkqw7rved84ogw6EKBSFCVqwRA-NKWLpPMV_q7fRwiEG" <> "Wj7R-KZqRweALXuvCLF765E6-ENxA" - def pretend_network_returned(fake_value) do + def generate_pid do + apply_default_mocks() 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], []) - - Map.merge(%{pid: pid}, AutoSyncChannel.network_status(pid)) + pid end - def under_normal_conditions() do - fake_con = %{fake: :conn} - fake_chan = %{fake: :chan} - pretend_network_returned(%{conn: fake_con, chan: fake_chan}) + def apply_default_mocks do + ok1 = fn _ -> :whatever end + stub(FarmbotExt.API.EagerLoader.Supervisor, :drop_all_cache, fn -> :ok end) + stub(ConnectionWorker, :close_channel, ok1) + + stub(ConnectionWorker, :maybe_connect_autosync, fn _ -> + %{conn: %{fake_conn: true}, chan: %{fake_chan: true}} + end) end - test "network returns `nil`" do - results = pretend_network_returned(nil) - %{conn: has_conn, chan: has_chan, preloaded: is_preloaded} = results - - assert has_chan == nil - assert has_conn == nil - assert is_preloaded + def ensure_response_to(msg) do + # Not much to check here other than matching clauses. + # AMQP lib handles most all of this. + expect(Preloader, :preload_all, 1, fn -> :ok end) + pid = generate_pid() + send(pid, msg) + Process.sleep(5) end - test "network returns unexpected object (probably an error)" do - results = pretend_network_returned({:something, :else}) - %{conn: has_conn, chan: has_chan, preloaded: is_preloaded} = results + test "basic_cancel", do: ensure_response_to({:basic_cancel, :anything}) + test "basic_cancel_ok", do: ensure_response_to({:basic_cancel_ok, :anything}) + test "basic_consume_ok", do: ensure_response_to({:basic_consume_ok, :anything}) - assert has_chan == nil - assert has_conn == nil - assert is_preloaded + test "init / terminate - auto_sync enabled" do + expect(Preloader, :preload_all, 1, fn -> :ok end) + 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 - test "catch-all clause for inbound AMQP messages" do - fake_con = %{fake: :conn} - fake_chan = %{fake: :chan} - fake_response = %{conn: fake_con, chan: fake_chan} + test "init / terminate - auto_sync disabled" do + expect(Preloader, :preload_all, 1, fn -> :ok end) + expect(FarmbotCore.Asset.Query, :auto_sync?, 1, fn -> false end) + 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 = - JSON.encode!(%{ - args: %{label: "xyz"} + :really_fast_blink -> + :ok + 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!"}}) - assert_receive {:rpc_reply_called, %{fake: :chan}, "device_15", "xyz"} - end - - 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) - + pid = generate_pid() + # We need the process to be preloaded for these tests to work: + %{preloaded: true} = AutoSyncChannel.network_status(pid) send(pid, {:basic_deliver, payload, %{routing_key: key}}) - assert_receive {:update_called, ^module_name, 999, %{"foo" => "bar"}} - end - - test "handles auto_sync of 'no_cache' when auto_sync is false" do - 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}) + expect(FarmbotExt.AMQP.AutoSyncAssetHandler, :handle_asset, fn kind, id, body -> + assert kind == "Device" + assert id == 46 + assert body == %{"name" => "This is my bot"} :ok end) - send(pid, {:basic_deliver, payload, %{routing_key: key}}) - 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"}} + Process.sleep(1000) end end diff --git a/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs b/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs new file mode 100644 index 00000000..d03cf239 --- /dev/null +++ b/farmbot_ext/test/farmbot_ext/amqp/bot_state_channel_test.exs @@ -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 diff --git a/farmbot_ext/test/farmbot_ext/api/view_test.exs b/farmbot_ext/test/farmbot_ext/api/view_test.exs new file mode 100644 index 00000000..b3c4b179 --- /dev/null +++ b/farmbot_ext/test/farmbot_ext/api/view_test.exs @@ -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 diff --git a/farmbot_ext/test/test_helper.exs b/farmbot_ext/test/test_helper.exs index 6b78363b..9fbbf006 100644 --- a/farmbot_ext/test/test_helper.exs +++ b/farmbot_ext/test/test_helper.exs @@ -1,14 +1,33 @@ Application.ensure_all_started(:farmbot) -timeout = System.get_env("EXUNIT_TIMEOUT") +Mimic.copy(AMQP.Channel) Mimic.copy(FarmbotCeleryScript.SysCalls.Stubs) Mimic.copy(FarmbotCore.Asset.Command) 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.API.EagerLoader.Supervisor) +Mimic.copy(FarmbotExt.API.Preloader) Mimic.copy(FarmbotExt.API) +Mimic.copy(FarmbotExt.AMQP.AutoSyncAssetHandler) + +timeout = System.get_env("EXUNIT_TIMEOUT") +System.put_env("LOG_SILENCE", "true") if timeout do ExUnit.start(assert_receive_timeout: String.to_integer(timeout)) else ExUnit.start() 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 diff --git a/farmbot_firmware/lib/farmbot_firmware.ex b/farmbot_firmware/lib/farmbot_firmware.ex index 64b5bd71..7af0d7b8 100644 --- a/farmbot_firmware/lib/farmbot_firmware.ex +++ b/farmbot_firmware/lib/farmbot_firmware.ex @@ -300,20 +300,10 @@ defmodule FarmbotFirmware do name: state.reset ) do {:ok, pid} -> - Logger.debug( - "Firmware reset #{state.reset} started. #{ - inspect(state.transport_args) - }" - ) - {:noreply, %{state | reset_pid: pid}} # TODO(Rick): I have no idea what's going on here. {:error, {:already_started, pid}} -> - Logger.debug( - "Firmware reset complete. #{inspect(state.transport_args)}" - ) - {:noreply, %{state | reset_pid: pid}} error -> diff --git a/farmbot_os/lib/farmbot_os/sys_calls/farmware.ex b/farmbot_os/lib/farmbot_os/sys_calls/farmware.ex index 8f0bb358..bce83065 100644 --- a/farmbot_os/lib/farmbot_os/sys_calls/farmware.ex +++ b/farmbot_os/lib/farmbot_os/sys_calls/farmware.ex @@ -4,7 +4,7 @@ defmodule FarmbotOS.SysCalls.Farmware do require FarmbotCore.Logger alias FarmbotCore.{Asset, AssetSupervisor, FarmwareRuntime} alias FarmbotExt.API.ImageUploader - @farmware_timeout 30_000 + @farmware_timeout 60_000 def update_farmware(farmware_name) do with {:ok, installation} <- lookup_installation(farmware_name) do diff --git a/farmbot_os/lib/farmbot_os/sys_calls/flash_firmware.ex b/farmbot_os/lib/farmbot_os/sys_calls/flash_firmware.ex index 9e7c5c81..6df8287d 100644 --- a/farmbot_os/lib/farmbot_os/sys_calls/flash_firmware.ex +++ b/farmbot_os/lib/farmbot_os/sys_calls/flash_firmware.ex @@ -41,8 +41,8 @@ defmodule FarmbotOS.SysCalls.FlashFirmware do {:error, reason} when is_binary(reason) -> {:error, reason} - {_, exit_code} when is_number(exit_code) -> - {:error, "avrdude error: #{exit_code}"} + error -> + {:error, "flash_firmware misc error: #{inspect(error)}"} end end diff --git a/farmbot_os/lib/farmbot_os/sys_calls/movement.ex b/farmbot_os/lib/farmbot_os/sys_calls/movement.ex index ec30faf9..24c4a0c0 100644 --- a/farmbot_os/lib/farmbot_os/sys_calls/movement.ex +++ b/farmbot_os/lib/farmbot_os/sys_calls/movement.ex @@ -95,6 +95,9 @@ defmodule FarmbotOS.SysCalls.Movement do 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 msg = "Movement failed. #{inspect(reason)}" FarmbotCore.Logger.error(1, msg) diff --git a/farmbot_os/mix.exs b/farmbot_os/mix.exs index 9a0846c1..c7c030a6 100644 --- a/farmbot_os/mix.exs +++ b/farmbot_os/mix.exs @@ -126,7 +126,7 @@ defmodule FarmbotOS.MixProject do {:busybox, "~> 0.1.4", targets: @all_targets}, {:farmbot_system_rpi3, "1.10.0-farmbot.1", 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 diff --git a/farmbot_os/mix.lock b/farmbot_os/mix.lock index 865815a2..7f6cab1f 100644 --- a/farmbot_os/mix.lock +++ b/farmbot_os/mix.lock @@ -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_client": {:hex, :amqp_client, "3.8.2", "b50ac381c3c016a697d6ab8f08367043a08358cfeb8ee97832ccc7d101e59cef", [:make, :rebar3], [{:rabbit_common, "3.8.2", [hex: :rabbit_common, 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"}, - "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, 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"}, - "circuits_i2c": {:hex, :circuits_i2c, "0.3.5", "43e043d7efc3aead364061f8a7ed627f81ff7cef52bfa47cb629d8a68ca56a9f", [:make, :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"}, - "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, - "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "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"}, - "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"}, - "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, - "credentials_obfuscation": {:hex, :credentials_obfuscation, "1.1.0", "513793cc20c18afc9e03e584b436192a751a8344890e03a8741c65c8d6866fab", [:rebar3], [], "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"}, - "decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm"}, - "dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, - "dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"}, - "earmark": {:hex, :earmark, "1.4.3", "364ca2e9710f6bff494117dbbd53880d84bebb692dafc3a78eb50aa3183f2bfd", [:mix], [], "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"}, - "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm"}, - "erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm"}, - "esqlite": {:hex, :esqlite, "0.2.5", "cab6d87aeb5f33d848b9bb8a21129e9512ea608f930d4c63576942d8f7d72218", [:rebar3], [], "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"}, - "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"}, - "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"}, + "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", "f453386e02a7ae77e0aa88bf4cf071aa31cce54e1b8511ca805961b0d83f164e"}, + "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", "805abd97539caf89ec6d4732c91e62ba9da0cda51ac462380bbd28ee697a8c42"}, + "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", "37a301618b19fe82d311c9e2858e25a3c4f131fec44d9e0d0b18849e9434120e"}, + "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", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, + "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", "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", "e5580029080f3f1ad17436fb97b0d5ed2ed4e4815a96bac36b5a992e20f58db6"}, + "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"}, + "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", "5f0a16a58312a610d5eb0b07506280c65f5137868ad479045f2a2dc4ced80550"}, + "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", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"}, + "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", "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", "f1e20ddf41713b4db247443a3bea9045c4103b27c0e64b0c21ec50edde51fba8"}, + "elixir_make": {:hex, :elixir_make, "0.6.0", "38349f3e29aff4864352084fc736fa7fa0f2995a819a737554f7ebd28b85aaab", [:mix], [], "hexpm", "d522695b93b7f0b4c0fcb2dfe73a6b905b1c301226a5a55cb42e5b14d509e050"}, + "erlex": {:hex, :erlex, "0.2.2", "cb0e6878fdf86dc63509eaf2233a71fa73fc383c8362c8ff8e8b6f0c2bb7017c", [:mix], [], "hexpm", "423a8f6ac70b77f0001c18adbff2b10413afed6901c2975aa33151a9c1263307"}, + "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", "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", "b06c73492aa9940c4c29cfc1356bcf5540ae318f17b423749a0615a66ee3e049"}, + "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_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"}, - "fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm"}, - "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, - "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, - "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "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"}, - "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, - "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"}, - "lager": {:hex, :lager, "3.8.0", "3402b9a7e473680ca179fc2f1d827cab88dd37dd1e6113090c6f45ef05228a1c", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, 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", "d12990ebda7d485d0eb7502df7aa9a56e66f67b5eda158c352db1de48e3f0518"}, + "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm", "5cacd405e72b2609a7e1f891bddb80c53d0b3b7b0036d1648e7382ca108c41c8"}, + "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm", "e0b8598e802676c81e66b061a2148c37c03886b24a3ca86a1f98ed40693b94b3"}, + "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", "e0100f8ef7d1124222c11ad362c857d3df7cb5f4204054f9f0f4a728666591fc"}, + "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.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", "8ee1db1cabafdd578a2776a6aaae87c2a8ce54b47b59e9ec7dab5d7eb71cd8dc"}, + "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", []}, "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_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "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"}, - "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, - "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, - "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "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", "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", "e664f7ddacf7b811f9b7cec0c06c4e39788971bbdce24a19dc0f83c137aef8a0"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"}, + "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mimic": {:hex, :mimic, "1.1.3", "3bad83d5271b4faa7bbfef587417a6605cbbc802a353395d446a1e5f46fe7115", [: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"}, - "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_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.4", "12b0d9c84ec9f79c1b0ac0de1c575372ef972d0c58ce21c36bf354062c6222d9", [:mix], [{:nerves_runtime, "~> 0.6", [hex: :nerves_runtime, 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"}, - "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_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_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_system_br": {:hex, :nerves_system_br, "1.10.0", "29169ebad0415b916bf3b9890f821a91b70be9af93a7fd824aa661f17b193548", [:mix], [], "hexpm"}, - "nerves_system_linter": {:hex, :nerves_system_linter, "0.3.0", "84e0f63c8ac196b16b77608bbe7df66dcf352845c4e4fb394bffd2b572025413", [:mix], [], "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"}, - "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_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_ctng": {:hex, :nerves_toolchain_ctng, "1.6.0", "452f8589c1a58ac787477caab20a8cfc6671e345837ccc19beefe49ae35ba983", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm"}, - "nimble_csv": {:hex, :nimble_csv, "0.6.0", "a3673f26d41f986774fe6060e309615343d3cb83a6d435754d8b1fdbd5764879", [:mix], [], "hexpm"}, - "nimble_parsec": {:hex, :nimble_parsec, "0.5.3", "def21c10a9ed70ce22754fdeea0810dafd53c2db3219a0cd54cf5526377af1c6", [:mix], [], "hexpm"}, - "one_dhcpd": {:hex, :one_dhcpd, "0.2.4", "2664f2e1ac72cbae0474a88d1a3d55019ccc3ee582ded382589511627a8ed516", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, - "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "pbcs": {:hex, :pbcs, "0.1.1", "199c7fd4af3351758378355909145a2d187c565555ed16bde30b5055114652ed", [:mix], [], "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"}, - "phoenix_html": {:hex, :phoenix_html, "2.13.3", "850e292ff6e204257f5f9c4c54a8cb1f6fbc16ed53d360c2b780a3d0ba333867", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "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"}, - "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_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "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"}, - "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, - "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm"}, - "ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm"}, - "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, - "shoehorn": {:hex, :shoehorn, "0.6.0", "f9a1b7d6212cf18ba91c4f71c26076059df33cea4db2eb3c098bfa6673349412", [:mix], [{:distillery, "~> 2.1", [hex: :distillery, repo: "hexpm", optional: true]}], "hexpm"}, - "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "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"}, - "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"}, - "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"}, - "system_registry": {:hex, :system_registry, "0.8.2", "df791dc276652fcfb53be4dab823e05f8269b96ac57c26f86a67838dbc0eefe7", [:mix], [], "hexpm"}, - "table_rex": {:hex, :table_rex, "2.0.0", "712783cbc2decb4d644d2ab8ad9315294f960c41b2cf0539308164922e352084", [:mix], [], "hexpm"}, - "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "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"}, - "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"}, - "toolshed": {:hex, :toolshed, "0.2.11", "0cd5312bd6a48f5b654b6ffa9239a63af9f3d200da414790fe25f066e14842a9", [:mix], [{:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: true]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, - "uboot_env": {:hex, :uboot_env, "0.1.1", "b01e3ec0973e99473234f27839e29e63b5b81eba6a136a18a78d049d4813d6c5", [:mix], [], "hexpm"}, - "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, - "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "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"}, - "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_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_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"}, - "websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"}, - "x509": {:hex, :x509, "0.8.0", "b286b9dbb32801f76f48bdea476304d280c64ce172ea330c23d8df7ea9e75ce6", [:mix], [], "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.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", "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", "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", "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", "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", "1ed7d228490f2138a028bf293f9f492b716da10d33f43615e4318731961f3866"}, + "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", "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", "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", "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", "4e843f405d4b8e6137419f94e5b8f491bff5a87b02ac2223e126182e8cec4256"}, + "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", "b78e15f1c82fd963745ecc4073c47e1f0cdaf3622143342b8c30e9b5eaa546c6"}, + "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", "38f08c228d066153dbe352b150910345e395eacd2db3c19085d54c0baeeebacb"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"}, + "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", "9374a29a9f835125cec73a2b45086eedce8df6b4d7c5353fced11bb48a3d6800"}, + "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", "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", "6cd8ddd1bd1fbfa54d3fc61d4719c2057dae67615395d58d40437a919a46f132"}, + "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm", "73c1682f0e414cfb5d9b95c8e8cd6ffcfdae699e3b05e1db744e58b7be857759"}, + "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", "c36e9613093910ed3a86f43a21fddcd48c3015a81ba74586824bdc4b2ae5519b"}, + "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"}, + "recon": {:hex, :recon, "2.5.0", "2f7fcbec2c35034bade2f9717f77059dc54eb4e929a3049ca7ba6775c0bd66cd", [:mix, :rebar3], [], "hexpm", "72f3840fedd94f06315c523f6cecf5b4827233bed7ae3fe135b2a0ebeab5e196"}, + "ring_logger": {:hex, :ring_logger, "0.8.0", "b1baddc269099b2afe2ea3a87b8e2b71e57331c0000038ae55090068aac679db", [:mix], [], "hexpm", "9b2f482e4346c13c11ef555f898202d0ddbfda6e2354e5c6e0559d2b4e0cf781"}, + "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", "e54a1f58a121caf8f0f3a355686b2661258b1bc0d4fffef8923bd7b11c2f9d79"}, + "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", "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", "0e1974b48684ba85255d2fe16c6106d52f5e759b260c95f676b23aa13b708a96"}, + "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", "f7acdede22c73ab0b3735eead7f2095efb2a7a6198366564205274db2ca2a8f8"}, + "table_rex": {:hex, :table_rex, "2.0.0", "712783cbc2decb4d644d2ab8ad9315294f960c41b2cf0539308164922e352084", [:mix], [], "hexpm", "b183ff68abe9ace8764af4c2828767865d2c854d1d6f8cfd3660218166ed89a1"}, + "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", "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", "b8fd8c9fcfaef1fa9c415e0792e2e82783c7ec8a282dfceef7d48158d4cfb3e1"}, + "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", "69c9913feb9b9f84c7e70e448e1f425d450d95119800ba420c5121a59bdd12c6"}, + "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", "1d1848c40487cdb0b30e8ed975e34e025860c02e419cb615d255849f3427439d"}, + "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", "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", "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", "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", "a9dfac64705f0b7cd7e0fc2338cf891eef091a49385805d0a42c22fb0aaa7c84"}, + "websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm", "b864fa076f059b615da4ab99240e515b26132ce4d2d0f9df5d7f22f01fa04b65"}, + "x509": {:hex, :x509, "0.8.0", "b286b9dbb32801f76f48bdea476304d280c64ce172ea330c23d8df7ea9e75ce6", [:mix], [], "hexpm", "8aafaafdcafb1ea9f06bfc32c3b03ccc66f087e0faf36ef94c0195bb7a04157e"}, } diff --git a/farmbot_os/platform/target/nerves_hub_client.ex b/farmbot_os/platform/target/nerves_hub_client.ex index e5bf28bd..153889c5 100644 --- a/farmbot_os/platform/target/nerves_hub_client.ex +++ b/farmbot_os/platform/target/nerves_hub_client.ex @@ -2,7 +2,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do @moduledoc """ 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 might not be available in the environment. Environment is checked in this order: @@ -397,8 +397,8 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do %{is_applying_update: false, probably_connected: true} = state ) do if should_auto_apply_update?() && update_available?() do - FarmbotCore.Logger.busy(1, "Applying OTA update") - spawn_link(fn -> NervesHub.update() end) + FarmbotCore.Logger.busy(1, "Applying OTA update (1)") + run_update_but_only_once() {:noreply, %{state | is_applying_update: true}} else Process.send_after(self(), :checkup, @checkup_timeout_ms) @@ -423,7 +423,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do {:handle_nerves_hub_error, error}, %{is_applying_update: true} = state ) do - FarmbotCore.Logger.error(1, "Error applying OTA: #{inspect(error)}") + FarmbotCore.Logger.error(1, "Error applying OTA (1): #{inspect(error)}") {:noreply, state} end @@ -449,7 +449,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do def handle_cast({:handle_nerves_hub_fwup_message, {:error, _, reason}}, state) do _ = 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} end @@ -465,7 +465,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do ) do case should_auto_apply_update?() do true -> - FarmbotCore.Logger.busy(1, "Applying OTA update") + FarmbotCore.Logger.busy(1, "Applying OTA update (2)") _ = set_update_available_in_bot_state() _ = update_device_last_ota_checkup() _ = set_firmware_needs_flash() @@ -474,7 +474,6 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do _ -> _ = set_update_available_in_bot_state() _ = update_device_last_ota_checkup() - FarmbotCore.Logger.info(1, "New Farmbot OS is available!") Process.send_after(self(), :checkup, @checkup_timeout_ms) {:reply, :ignore, %{state | firmware_url: url}} end @@ -484,7 +483,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do _ = set_update_available_in_bot_state() _ = update_device_last_ota_checkup() _ = 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}} end @@ -498,9 +497,10 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do _ = set_update_available_in_bot_state() _ = update_device_last_ota_checkup() _ = set_firmware_needs_flash() - FarmbotCore.Logger.busy(1, "Applying OTA update") - - spawn_link(fn -> NervesHub.update() end) + FarmbotCore.Logger.busy(1, "Attempting OTA update...") + # This is where the NervesHub update gets called. + # Maybe we can check if the BotState has job progress for "FBOS_OTA" + run_update_but_only_once() {:reply, data, %{state | is_applying_update: true}} end end @@ -511,38 +511,41 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do ota_hour = Asset.device(:ota_hour) timezone = Asset.device(:timezone) # if ota_hour is nil, auto apply the update - if ota_hour && timezone do - # check that now.hour == device.ota_hour - case Timex.Timezone.convert(now, timezone) do - %{hour: ^ota_hour} -> - FarmbotCore.Logger.debug( - 3, - "current hour: #{ota_hour} (utc=#{now.hour}) == ota_hour #{ota_hour}. auto_update=#{ - auto_update - }" - ) + result = + if ota_hour && timezone do + # check that now.hour == device.ota_hour + case Timex.Timezone.convert(now, timezone) do + %{hour: ^ota_hour} -> + FarmbotCore.Logger.debug( + 3, + "current hour: #{ota_hour} (utc=#{now.hour}) == ota_hour #{ + ota_hour + }. auto_update=#{auto_update}" + ) - auto_update + auto_update - %{hour: now_hour} -> - FarmbotCore.Logger.debug( - 3, - "current hour: #{now_hour} (utc=#{now.hour}) != ota_hour: #{ - ota_hour - }. auto_update=#{auto_update}" - ) + %{hour: now_hour} -> + FarmbotCore.Logger.debug( + 3, + "current hour: #{now_hour} (utc=#{now.hour}) != ota_hour: #{ + ota_hour + }. auto_update=#{auto_update}" + ) - false + false + end + else + # ota_hour or timezone are nil + FarmbotCore.Logger.debug( + 3, + "ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}" + ) + + true end - else - # ota_hour or timezone are nil - FarmbotCore.Logger.debug( - 3, - "ota_hour = #{ota_hour || "null"} timezone = #{timezone || "null"}" - ) - true - end + result && !currently_downloading?() end def update_available?() do @@ -615,7 +618,7 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do Process.monitor(conn.pid) {: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 {:error, :unknown_host} = err -> err @@ -678,4 +681,18 @@ defmodule FarmbotOS.Platform.Target.NervesHubClient do 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 diff --git a/farmbot_os/platform/target/network.ex b/farmbot_os/platform/target/network.ex index b8dc0b03..ecd7c2fa 100644 --- a/farmbot_os/platform/target/network.ex +++ b/farmbot_os/platform/target/network.ex @@ -188,7 +188,7 @@ defmodule FarmbotOS.Platform.Target.Network do _meta}, state ) do - FarmbotCore.Logger.warn( + FarmbotCore.Logger.success( 1, "Interface #{ifname} connected to local area network" ) @@ -202,7 +202,7 @@ defmodule FarmbotOS.Platform.Target.Network do _meta}, state ) 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) FarmbotTelemetry.event(:network, :wan_connect, nil, interface: ifname) {:noreply, %{state | first_connect?: false}} diff --git a/farmbot_os/test/farmbot_os/syscalls/farmware_test.exs b/farmbot_os/test/farmbot_os/syscalls/farmware_test.exs index ca8294a4..3bec795a 100644 --- a/farmbot_os/test/farmbot_os/syscalls/farmware_test.exs +++ b/farmbot_os/test/farmbot_os/syscalls/farmware_test.exs @@ -9,7 +9,7 @@ defmodule FarmbotOS.SysCalls.FarmwareTest do expect(FarmbotCore.LogExecutor, :execute, fn log -> 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 :ok