Merge pull request #445 from FarmBot/staging

6.2.0 Release
This commit is contained in:
Connor Rigby 2018-02-14 10:02:42 -08:00 committed by GitHub
commit a9391dfb05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 617 additions and 196 deletions

View file

@ -25,7 +25,6 @@ fetch_and_compile_deps: &fetch_and_compile_deps
name: Fetch and compile Elixir dependencies
command: |
mix deps.get
mix deps.compile
mix compile
install_arduino: &install_arduino
@ -48,7 +47,16 @@ jobs:
- v3-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
- <<: *install_arduino
- <<: *install_hex_archives
- restore_cache:
keys:
- v3-dep-cache-{{ checksum "mix.lock.host" }}
- <<: *fetch_and_compile_deps
- save_cache:
key: v3-dep-cache-{{ checksum "mix.lock.host" }}
paths:
- _build/host
- _build/arduino
- deps/host
- save_cache:
key: v3-arduino-cache-{{ checksum ".circleci/setup-arduino.sh" }}
paths:
@ -72,15 +80,27 @@ jobs:
- <<: *install_arduino
- restore_cache:
keys:
- v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- <<: *install_hex_archives
- <<: *fetch_and_compile_deps
- run: mix firmware
- save_cache:
key: v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
key: v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- deps/rpi3
- deps/host
- ~/.nerves
- run: mix firmware.slack --channels C58DCU4A3; echo "Hack."
- run:
name: Send firmware asset to slack.
command: |
if [ $(mix firmware.slack --channels C58DCU4A3) -eq 0 ]; then
echo "Firmware upload success."
else
echo "Firmware upload fail."
fi
- run: mkdir -p artifacts
- run:
name: Decode fwup priv key
@ -110,14 +130,18 @@ jobs:
- <<: *install_hex_archives
- restore_cache:
keys:
- v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- run: mix deps.get
- run: mix deps.compile
- run: mix compile
- run: mix firmware
- save_cache:
key: v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
key: v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- deps/rpi3
- deps/host
- ~/.nerves
- run: mkdir -p artifacts
- run:
@ -178,14 +202,18 @@ jobs:
- <<: *install_hex_archives
- restore_cache:
keys:
- v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
- run: mix deps.get
- run: mix deps.compile
- run: mix compile
- run: mix firmware
- save_cache:
key: v3-dependency-cache-{{ checksum "mix.lock.rpi3" }}
key: v4-dependency-cache-{{ checksum "mix.lock.rpi3" }}
paths:
- _build/rpi3
- _build/host
- _build/arduino
- deps/rpi3
- deps/host
- ~/.nerves
- run: mkdir -p artifacts
- run:

View file

@ -1,3 +1,10 @@
# 6.2.0
* Farmbot Settings are now synced with Farmbot API.
* Refactor Syncing to not make unnecessary HTTP requests.
* Estop status is now much faster.
* Add dns checkup for users with factory resetting disabled to make tokens refresh faster.
* Opting into beta updates will refresh farmbot's token.
# 6.1.2
* Fix fw hardware being reset on os upgrade.
* Bump arduino-firmware version to 6.0.1

View file

@ -1,11 +1,5 @@
# Erlang Nif Stuff
ifeq ($(ERL_EI_INCLUDE_DIR),)
ERL_ROOT_DIR = $(shell erl -eval "io:format(\"~s~n\", [code:root_dir()])" -s init stop -noshell)
ifeq ($(ERL_ROOT_DIR),)
$(error Could not find the Erlang installation. Check to see that 'erl' is in your PATH)
endif
ERL_EI_INCLUDE_DIR = "$(ERL_ROOT_DIR)/usr/include"
ERL_EI_LIBDIR = "$(ERL_ROOT_DIR)/usr/lib"
$(error ERL_EI_INCLUDE_DIR not set. Invoke via mix)
endif
# Set Erlang-specific compile and linker flags

View file

@ -1 +1 @@
6.1.2
6.2.0

View file

@ -6,6 +6,8 @@ config :logger,
config :farmbot, data_path: "/root"
config :farmbot, profile: System.get_env("FBOS_PROFILE")
# Disable tzdata autoupdates because it tries to dl the update file
# Before we have network or ntp.
config :tzdata, :autoupdate, :disabled
@ -81,3 +83,6 @@ config :nerves_firmware_ssh, authorized_keys: local_key
config :shoehorn,
init: [:nerves_runtime, :nerves_init_gadget],
app: :farmbot
config :nerves, :firmware,
rootfs_overlay: "overlay/"

16
docs/PROFILES.md Normal file
View file

@ -0,0 +1,16 @@
# Development Profiles
Profiles are loaded synchronously right after the local settings database is setup.
A profile can modify any global setting to farmbot os. Profiles are *NOT* loaded
at compile time. This means, that if deployed to a target such as Raspberry Pi 3,
your host environment variables will not be loaded.
# Selecting a profile
Load a profile by doing:
```bash
FBOS_PROFILE=profile_name iex -S mix
```
## Chaining profiles
Profiles can be chained with commas. They will be evaluated in order supplied.
```bash
FBOS_PROFILE=admin,setup_arduino iex -S mix

View file

@ -20,6 +20,7 @@ defmodule Farmbot.Bootstrap.AuthTask do
def init([]) do
timer = Process.send_after(self(), :refresh, @refresh_time)
Farmbot.System.Registry.subscribe(self())
{:ok, timer, :hibernate}
end
@ -29,7 +30,7 @@ defmodule Farmbot.Bootstrap.AuthTask do
end
end
def handle_info(:refresh, _old_timer) do
defp do_refresh do
auth_task = Application.get_env(:farmbot, :behaviour)[:authorization]
{email, pass, server} = {fetch_email(), fetch_pass(), fetch_server()}
# Logger.busy(3, "refreshing token: #{email} - #{server}")
@ -43,15 +44,27 @@ defmodule Farmbot.Bootstrap.AuthTask do
if get_config_value(:bool, "settings", "auto_sync") do
Farmbot.Repo.flip()
end
Farmbot.System.Registry.dispatch :authorization, :new_token
restart_transports()
refresh_timer(self())
{:error, err} ->
msg = "Token failed to reauthorize: #{email} - #{server} #{inspect err}"
Logger.error(1, msg)
refresh_timer(self(), 30_000)
# If refresh failed, try again more often
refresh_timer(self(), 15_000)
end
end
def handle_info(:refresh, _old_timer) do
do_refresh()
end
def handle_info({Farmbot.System.Registry, {:network, :dns_up}}, _old_timer) do
do_refresh()
end
def handle_info({Farmbot.System.Registry, _}, timer), do: {:noreply, timer}
def handle_call(:force_refresh, _, old_timer) do
Logger.info 1, "Forcing a token refresh."
if Process.read_timer(old_timer) do

View file

@ -0,0 +1,153 @@
defmodule Farmbot.Bootstrap.SettingsSync do
@moduledoc "Handles uploading and downloading of FBOS configs."
use Task, restart: :transient
use Farmbot.Logger
import Farmbot.System.ConfigStorage, only: [get_config_value: 3, update_config_value: 4, get_config_as_map: 0]
def start_link() do
run()
:ignore
end
def run() do
with {:ok, %{body: body, status_code: 200}} <- Farmbot.HTTP.get("/api/fbos_config"),
{:ok, data} <- Poison.decode(body)
do
do_sync_settings(data)
update_config_value(:bool, "settings", "ignore_fbos_config", false)
:ok
else
{:ok, status_code: code} ->
Logger.error 1, "HTTP error syncing settings: #{code}"
:ok
err ->
Logger.error 1, "Error syncing settings: #{inspect err}"
:ok
end
rescue
err ->
update_config_value(:bool, "settings", "ignore_fbos_config", false)
Logger.error 1, "Error syncing settings: #{Exception.message(err)} #{inspect System.stacktrace()}"
end
def apply_map(old_map, new_map) do
old_map = take_valid(old_map)
new_map = take_valid(new_map)
Map.new(new_map, fn({key, new_value}) ->
# Logger.debug 1, "Applying #{key} #{inspect old_map[key]} over #{inspect new_value}"
if old_map[key] != new_value do
apply_to_config_storage key, new_value
end
{key, new_value}
end)
end
# TODO: This should be moved to ConfigStorage module maybe?
@bool_keys [
"auto_sync",
"beta_opt_in",
"disable_factory_reset",
"firmware_output_log",
"firmware_input_log",
"sequence_body_log",
"sequence_complete_log",
"sequence_init_log",
"arduino_debug_messages",
"os_auto_update"
]
@string_keys [
"firmware_hardware"
]
@float_keys [
"network_not_found_timer"
]
defp apply_to_config_storage(key, val)
when key in @bool_keys and (is_nil(val) or is_boolean(val)) do
Logger.success 2, "Updating: #{key} => #{inspect val}"
update_config_value(:bool, "settings", key, val)
end
defp apply_to_config_storage(key, val)
when key in @string_keys and (is_nil(val) or is_binary(val)) do
Logger.success 2, "Updating: #{key} => #{inspect val}"
update_config_value(:string, "settings", key, val)
end
defp apply_to_config_storage(key, val)
when key in @float_keys and (is_nil(val) or is_number(val)) do
Logger.success 2, "Updating: #{key} => #{inspect val}"
if val do
update_config_value(:float, "settings", key, val / 1)
else
update_config_value(:float, "settings", key, val)
end
end
defp apply_to_config_storage(key, val) do
Logger.error 1, "Unknown pair: #{key} => #{inspect val}"
end
def do_sync_settings(%{"api_migrated" => true} = api_data) do
Logger.info 3, "API is the source of truth; Downloading data."
old_config = get_config_as_map()["settings"]
apply_map(old_config, api_data)
:ok
end
def do_sync_settings(_unimportant_data) do
Logger.info 3, "FBOS is the source of truth; Uploading data."
update_config_value(:bool, "settings", "ignore_fbos_config", true)
auto_sync = get_config_value(:bool, "settings", "auto_sync")
beta_opt_in = get_config_value(:bool, "settings", "beta_opt_in")
disable_factory_reset = get_config_value(:bool, "settings", "disable_factory_reset")
firmware_output_log = get_config_value(:bool, "settings", "firmware_output_log")
firmware_input_log = get_config_value(:bool, "settings", "firmware_input_log")
sequence_body_log = get_config_value(:bool, "settings", "sequence_body_log")
sequence_complete_log = get_config_value(:bool, "settings", "sequence_complete_log")
sequence_init_log = get_config_value(:bool, "settings", "sequence_init_log")
arduino_debug_messages = get_config_value(:bool, "settings", "arduino_debug_messages")
os_auto_update = get_config_value(:bool, "settings", "os_auto_update")
firmware_hardware = get_config_value(:string, "settings", "firmware_hardware")
network_not_found_timer = get_config_value(:float, "settings", "network_not_found_timer")
payload = %{
api_migrated: true,
auto_sync: auto_sync,
beta_opt_in: beta_opt_in,
disable_factory_reset: disable_factory_reset,
firmware_output_log: firmware_output_log,
firmware_input_log: firmware_input_log,
sequence_body_log: sequence_body_log,
sequence_complete_log: sequence_complete_log,
sequence_init_log: sequence_init_log,
arduino_debug_messages: arduino_debug_messages,
os_auto_update: os_auto_update,
firmware_hardware: firmware_hardware,
network_not_found_timer: network_not_found_timer,
} |> Poison.encode!()
Farmbot.HTTP.delete!("/api/fbos_config")
Farmbot.HTTP.put!("/api/fbos_config", payload)
update_config_value(:bool, "settings", "ignore_fbos_config", false)
:ok
end
@keys [
"auto_sync",
"beta_opt_in",
"disable_factory_reset",
"firmware_output_log",
"firmware_input_log",
"sequence_body_log",
"sequence_complete_log",
"sequence_init_log",
"arduino_debug_messages",
"os_auto_update",
"firmware_hardware",
"network_not_found_timer"
]
def take_valid(map) do
Map.take(map, @keys ++ Enum.map(@keys, &String.to_atom(&1)))
end
end

View file

@ -125,11 +125,12 @@ defmodule Farmbot.Bootstrap.Supervisor do
children = [
worker(Farmbot.Bootstrap.AuthTask, []),
supervisor(Farmbot.HTTP.Supervisor, []),
worker(Farmbot.Bootstrap.SettingsSync, [], [restart: :transient]),
supervisor(Farmbot.Firmware.Supervisor, []),
supervisor(Farmbot.BotState.Supervisor, []),
supervisor(Farmbot.BotState.Transport.Supervisor, []),
supervisor(Farmbot.FarmEvent.Supervisor, []),
supervisor(Farmbot.HTTP.Supervisor, []),
supervisor(Farmbot.Repo.Supervisor, []),
supervisor(Farmbot.Farmware.Supervisor, []),
supervisor(Farmbot.Regimen.Supervisor, []),

View file

@ -276,6 +276,7 @@ defmodule Farmbot.BotState do
end
defp do_handle([{:config, "settings", key, val} | rest], state) do
# Logger.debug 1, "Got config update: #{inspect key} => #{inspect val}"
new_config = Map.put(state.configuration, key, val)
new_state = %{state | configuration: new_config}
do_handle(rest, new_state)

View file

@ -178,6 +178,8 @@ defmodule Farmbot.BotState.Transport.AMQP do
["bot", ^device, "sync", resource, _]
when resource in ["Log", "User", "Image", "WebcamFeed"] ->
{:noreply, [], state}
["bot", ^device, "sync", "FbosConfig", id] ->
handle_fbos_config(id, payload, state)
["bot", ^device, "sync", resource, id] ->
handle_sync_cmd(resource, id, payload, state)
["bot", ^device, "logs"] -> {:noreply, [], state}
@ -231,6 +233,28 @@ defmodule Farmbot.BotState.Transport.AMQP do
{:noreply, [], state}
end
def handle_fbos_config(_, _, %{state_cache: nil} = state) do
# Don't update fbos config, if we don't have a state cache for whatever reason.
{:noreply, [], state}
end
def handle_fbos_config(_id, payload, state) do
if get_config_value(:bool, "settings", "ignore_fbos_config") do
{:noreply, [], state}
else
case Poison.decode(payload) do
# TODO(Connor) What do I do with deletes?
{:ok, %{"body" => nil}} -> {:noreply, [], state}
{:ok, %{"body" => config}} ->
# Logger.info 1, "Got fbos config from amqp: #{inspect config}"
old = state.state_cache.configuration
updated = Farmbot.Bootstrap.SettingsSync.apply_map(old, config)
push_bot_state(state.chan, state.bot, %{state.state_cache | configuration: updated})
{:noreply, [], state}
end
end
end
defp push_bot_log(chan, bot, log) do
json = Poison.encode!(log)
:ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json

View file

@ -4,9 +4,10 @@ defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
use Farmbot.Logger
allow_args [:package]
def execute(%{package: :farmbot_os}, body, env) do
def execute(%{package: :farmbot_os}, _body, env) do
Logger.warn 2, "`config_update` for FBOS is depricated."
env = mutate_env(env)
do_reduce_os(body, env)
{:ok, env}
end
def execute(%{package: :arduino_firmware}, body, env) do
@ -14,21 +15,6 @@ defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
do_reduce_fw(body, env)
end
defp do_reduce_os([%{args: %{label: key, value: value}} | rest], env) do
case lookup_os_config(key, value) do
{:ok, {type, group, value}} ->
Farmbot.System.ConfigStorage.update_config_value(type, group, key, value)
Logger.success 3, "Updating: #{inspect key}: #{value}"
do_reduce_os(rest, env)
{:error, reason} ->
{:error, reason, env}
end
end
defp do_reduce_os([], env) do
{:ok, env}
end
defp do_reduce_fw([%{args: %{label: key, value: value}} | rest], env) do
case Farmbot.Firmware.update_param(:"#{key}", value) do
:ok -> do_reduce_fw(rest, env)
@ -37,41 +23,4 @@ defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdate do
end
defp do_reduce_fw([], env), do: {:ok, env}
defp lookup_os_config("os_auto_update", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("auto_sync", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("timezone", val), do: {:ok, {:string, "settings", val}}
defp lookup_os_config("disable_factory_reset", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("sequence_init_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("sequence_body_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("sequence_complete_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("arduino_debug_messages", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("firmware_input_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("firmware_output_log", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("beta_opt_in", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("email_on_estop", val), do: {:ok, {:bool, "settings", format_bool_for_os(val)}}
defp lookup_os_config("network_not_found_timer", val) when val > 0, do: {:ok, {:float, "settings", to_float(val)}}
defp lookup_os_config("network_not_found_timer", _val), do: {:error, "network_not_found_timer must be greater than zero"}
defp lookup_os_config("firmware_hardware", "farmduino"), do: {:ok, {:string, "settings", "farmduino"}}
defp lookup_os_config("firmware_hardware", "arduino"), do: {:ok, {:string, "settings", "arduino"}}
defp lookup_os_config("firmware_hardware", unknown), do: {:error, "unknown hardware: #{unknown}"}
defp lookup_os_config(unknown_config, _), do: {:error, "unknown config: #{unknown_config}"}
defp format_bool_for_os(1), do: true
defp format_bool_for_os(0), do: false
defp format_bool_for_os(true), do: true
defp format_bool_for_os(false), do: false
defp to_float(int) when is_integer(int) do
int / 1
end
defp to_float(float) when is_float(float) do
float
end
end

View file

@ -9,6 +9,7 @@ defmodule Farmbot.CeleryScript.AST.Node.FactoryReset do
Farmbot.BotState.set_sync_status(:maintenance)
Farmbot.BotState.force_state_push()
Farmbot.System.ConfigStorage.update_config_value(:bool, "settings", "disable_factory_reset", false)
Farmbot.HTTP.delete("/api/fbos_config")
Logger.warn 1, "Farmbot OS going down for factory reset!"
Farmbot.System.factory_reset "CeleryScript request."
{:ok, env}

View file

@ -16,7 +16,7 @@ defmodule Farmbot.CeleryScript.AST.Node.InstallFirstPartyFarmware do
defp do_sync_repo(env) do
case Farmbot.Farmware.Installer.sync_repo(@fpf_url) do
:ok -> {:ok, env}
{:ok, _} -> {:ok, env}
{:error, reason} -> {:error, reason, env}
end
end

View file

@ -217,9 +217,14 @@ defmodule Farmbot.Firmware do
# Logger.busy 3, "FW Starting: #{fun}: #{inspect from}"
case apply(state.handler_mod, fun, [state.handler | args]) do
:ok ->
if fun == :emergency_unlock, do: Farmbot.System.GPIO.Leds.led_status_ok()
timer = Process.send_after(self(), :timeout, state.timeout_ms)
{:noreply, dispatch, %{state | current: current, timer: timer}}
if fun == :emergency_unlock do
Farmbot.System.GPIO.Leds.led_status_ok()
new_dispatch = [{:informational_settings, %{busy: false, locked: false}} | dispatch]
{:noreply, new_dispatch, %{state | current: current, timer: timer}}
else
{:noreply, dispatch, %{state | current: current, timer: timer}}
end
{:error, _} = res ->
do_reply(%{state | current: current}, res)
{:noreply, dispatch, %{state | current: nil}}

View file

@ -41,7 +41,7 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do
:ignore
end
defp update_fw_handler(fw_handler) do
def update_fw_handler(fw_handler) do
old = Application.get_all_env(:farmbot)[:behaviour]
new = Keyword.put(old, :firmware_handler, fw_handler)
Application.put_env(:farmbot, :behaviour, new)

View file

@ -88,6 +88,24 @@ defmodule Farmbot.HTTP do
request(:put, url, body, headers, opts)
end
def put!(url, body, headers \\ [], opts \\ [])
def put!(url, body, headers, opts) do
request!(:put, url, body, headers, opts)
end
def delete(url, headers \\ [], opts \\ [])
def delete(url, headers, opts) do
request!(:delete, url, "", headers, opts)
end
def delete!(url, headers \\ [], opts \\ [])
def delete!(url, headers, opts) do
request!(:delete, url, "", headers, opts)
end
@doc "Download a file to the filesystem."
def download_file(url,
path,

View file

@ -298,7 +298,7 @@ defmodule Farmbot.Repo do
mod = Module.concat(["Farmbot", "Repo", kind])
# an object was deleted.
if Code.ensure_loaded?(mod) do
Logger.busy(3, "Applying sync_cmd (#{mod}: delete)")
Logger.debug(3, "Applying sync_cmd (#{mod}: delete)")
case repo.get(mod, id) do
nil ->
@ -319,7 +319,7 @@ defmodule Farmbot.Repo do
mod = Module.concat(["Farmbot", "Repo", kind])
if Code.ensure_loaded?(mod) do
Logger.busy(3, "Applying sync_cmd (#{mod}): insert_or_update")
Logger.debug(3, "Applying sync_cmd (#{mod}): insert_or_update")
# We need to check if this object exists in the database.
case repo.get(mod, id) do
@ -367,23 +367,65 @@ defmodule Farmbot.Repo do
end
defp do_sync_both(repo_a, repo_b) do
case do_sync_all_resources(repo_a) do
:ok ->
do_sync_all_resources(repo_b)
err ->
err
end
{time, res} = :timer.tc(fn() ->
with {:ok, cache} <- do_http_requests(),
:ok <- do_sync_all_resources(repo_a, cache),
:ok <- do_sync_all_resources(repo_b, cache) do
Farmbot.Bootstrap.SettingsSync.run()
end
end)
Logger.debug 3, "Entire sync took: #{time}µs."
res
end
defp do_sync_all_resources(repo) do
with :ok <- sync_resource(repo, Device, "/api/device"),
:ok <- sync_resource(repo, FarmEvent, "/api/farm_events"),
:ok <- sync_resource(repo, Peripheral, "/api/peripherals"),
:ok <- sync_resource(repo, Point, "/api/points"),
:ok <- sync_resource(repo, Regimen, "/api/regimens"),
:ok <- sync_resource(repo, Sequence, "/api/sequences"),
:ok <- sync_resource(repo, Tool, "/api/tools") do
defp do_http_requests do
initial_err = {:error, :request_not_started}
acc = %{
Device => initial_err,
FarmEvent => initial_err,
Peripheral => initial_err,
Point => initial_err,
Regimen => initial_err,
Sequence => initial_err,
Tool => initial_err
}
device_task = Task.async(__MODULE__, :do_get_resource, [Device, "/api/device"])
farm_events_task = Task.async(__MODULE__, :do_get_resource, [FarmEvent, "/api/farm_events"])
peripherals_task = Task.async(__MODULE__, :do_get_resource, [Peripheral, "/api/peripherals"])
points_task = Task.async(__MODULE__, :do_get_resource, [Point, "/api/points"])
regimens_task = Task.async(__MODULE__, :do_get_resource, [Regimen, "/api/regimens"])
sequences_task = Task.async(__MODULE__, :do_get_resource, [Sequence, "/api/sequences"])
tools_task = Task.async(__MODULE__, :do_get_resource, [Tool, "/api/tools"])
res = %{acc |
Device => Task.await(device_task, 30_000),
FarmEvent => Task.await(farm_events_task, 30_000),
Peripheral => Task.await(peripherals_task, 30_000),
Point => Task.await(points_task, 30_000),
Regimen => Task.await(regimens_task, 30_000),
Sequence => Task.await(sequences_task, 30_000),
Tool => Task.await(tools_task, 30_000),
}
{:ok, res}
end
def do_get_resource(resource, slug) do
resource = Module.split(resource) |> List.last()
Logger.debug 3, "Fetching #{resource}"
maybe_debug_log("[#{resource}] Downloading: (#{slug})")
{time, res} = :timer.tc(fn -> Farmbot.HTTP.get(slug) end)
maybe_debug_log("[#{resource}] HTTP Request took: #{time}µs")
res
end
defp do_sync_all_resources(repo, cache) do
with :ok <- sync_resource(repo, Device, cache),
:ok <- sync_resource(repo, FarmEvent, cache),
:ok <- sync_resource(repo, Peripheral, cache),
:ok <- sync_resource(repo, Point, cache),
:ok <- sync_resource(repo, Regimen, cache),
:ok <- sync_resource(repo, Sequence, cache),
:ok <- sync_resource(repo, Tool, cache) do
:ok
else
err ->
@ -392,13 +434,17 @@ defmodule Farmbot.Repo do
end
end
defp sync_resource(repo, resource, slug) do
Logger.debug(3, "syncing: #{resource} (#{slug})")
defp sync_resource(repo, resource, cache) do
human_readable_resource_name = Module.split(resource) |> List.last()
maybe_debug_log("[#{human_readable_resource_name}] Entering into DB.")
as = if resource in @singular_resources, do: struct(resource), else: [struct(resource)]
with {:ok, %{status_code: 200, body: body}} <- Farmbot.HTTP.get(slug),
{:ok, obj_or_list} <- Poison.decode(body, as: as) do
case do_insert_or_update(repo, obj_or_list) do
with {:ok, %{status_code: 200, body: body}} <- cache[resource],
{json_time, {:ok, obj_or_list}} <- :timer.tc(fn -> Poison.decode(body, as: as) end) do
maybe_debug_log("[#{human_readable_resource_name}] JSON Decode took: #{json_time}µs")
{insert_time, res} = :timer.tc(fn -> do_insert_or_update(repo, obj_or_list) end)
maybe_debug_log("[#{human_readable_resource_name}] DB Operations took: #{insert_time}µs")
case res do
{:ok, _} when resource in @singular_resources -> :ok
:ok -> :ok
err -> err
@ -450,6 +496,22 @@ defmodule Farmbot.Repo do
end
end
def enable_debug_logs do
Application.put_env(:farmbot, :repo_debug_logs, true)
end
def disable_debug_logs do
Application.put_env(:farmbot, :repo_debug_logs, false)
end
defp maybe_debug_log(msg) do
if Application.get_env(:farmbot, :repo_debug_logs, false) do
Logger.debug 3, msg
else
:ok
end
end
@doc false
defmacro __using__(_) do
quote do

View file

@ -26,6 +26,7 @@ defmodule Farmbot.System.ConfigStorage.Dispatcher do
end
def handle_call({:dispatch, group, key, val}, _, state) do
Farmbot.System.Registry.dispatch(:config_storage, {group, key, val})
{:reply, :ok, [{:config, group, key, val}], state}
end
end

View file

@ -0,0 +1,39 @@
defmodule Farmbot.System.Profile do
@moduledoc File.read!("docs/PROFILES.md")
use GenServer
use Farmbot.Logger
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
def init([]) do
profile = Application.get_env(:farmbot, :profile) || System.get_env("FBOS_PROFILE")
if profile do
try do
do_load_profiles(profile)
rescue
error ->
IO.warn "Failed to load profile #{profile}: #{inspect Exception.message(error)}\n\n"
end
end
:ignore
end
def profile_dir do
case Farmbot.Project.target() do
"host" -> Path.join(["overlay", "profiles"])
_ -> "/profiles"
end
end
defp do_load_profiles(bin) do
profiles = String.split(bin, ",")
for profile <- profiles do
Logger.busy 1, "Loading profile: #{profile}"
Code.eval_file("#{profile}.exs", profile_dir())
Logger.success 1, "Profile #{profile} loaded."
end
end
end

View file

@ -0,0 +1,31 @@
defmodule Farmbot.System.Registry do
@moduledoc "Farmbot System Global Registry"
@reg FarmbotRegistry
@doc false
def start_link do
GenServer.start_link(__MODULE__, [], [name: __MODULE__])
end
@doc "Dispatch a global event from a namespace."
def dispatch(namespace, event) do
GenServer.call(__MODULE__, {:dispatch, namespace, event})
end
def subscribe(pid) do
Elixir.Registry.register(@reg, __MODULE__, pid)
end
def init([]) do
opts = [keys: :duplicate, partitions: System.schedulers_online, name: @reg]
{:ok, reg} = Elixir.Registry.start_link(opts)
{:ok, %{reg: reg}}
end
def handle_call({:dispatch, ns, event}, _from, state) do
Elixir.Registry.dispatch(@reg, __MODULE__, fn(entries) ->
for {pid, _} <- entries, do: send(pid, {__MODULE__, {ns, event}})
end)
{:reply, :ok, state}
end
end

View file

@ -12,12 +12,14 @@ defmodule Farmbot.System.Supervisor do
def init([]) do
before_init_children = [
worker(Farmbot.System.Registry, []),
worker(Farmbot.System.Init.KernelMods, [[], []]),
worker(Farmbot.System.Init.FSCheckup, [[], []]),
supervisor(Farmbot.System.Init.Ecto, [[], []]),
supervisor(Farmbot.System.ConfigStorage, []),
worker(Farmbot.System.ConfigStorage.Dispatcher, []),
worker(Farmbot.System.GPIO.Leds, [])
worker(Farmbot.System.GPIO.Leds, []),
worker(Farmbot.System.Profile, [])
]
init_mods =

View file

@ -25,13 +25,24 @@ defmodule Farmbot.System.UpdateTimer do
def init([]) do
spawn __MODULE__, :wait_for_http, [self()]
{:ok, [], :hibernate}
Farmbot.System.Registry.subscribe(self())
{:ok, []}
end
def handle_info(:checkup, state) do
osau = Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "os_auto_update")
Farmbot.System.Updates.check_updates(osau)
Process.send_after(self(), :checkup, @twelve_hours)
{:noreply, state, :hibernate}
{:noreply, state}
end
def handle_info({Farmbot.System.Registry, {:config_storage, {"settings", "beta_opt_in", true}}}, state) do
Logger.debug 3, "Opted into beta updates. Refreshing token."
Farmbot.Bootstrap.AuthTask.force_refresh()
{:noreply, state}
end
def handle_info({Farmbot.System.Registry, _info}, state) do
{:noreply, state}
end
end

View file

@ -82,7 +82,6 @@ defmodule Farmbot.System.Updates do
end
if should_apply_update(@env, prerelease, needs_update) do
Logger.busy 1, "Downloading FarmbotOS over the air update"
IO.puts cl
@ -103,6 +102,10 @@ defmodule Farmbot.System.Updates do
{:error, reason} ->
Logger.error 1, "Failed to fetch update data: #{inspect reason}"
{:ok, %{status_code: 400}} ->
Logger.info 2, "Had out of date token. Try that again."
Farmbot.Bootstrap.AuthTask.force_refresh()
{:ok, %{body: body, status_code: code}} ->
reason = case Poison.decode(body) do
{:ok, res} -> res

19
mix.exs
View file

@ -27,21 +27,21 @@ defmodule Farmbot.Mixfile do
app: :farmbot,
description: "The Brains of the Farmbot Project",
package: package(),
compilers: compilers(),
make_clean: ["clean"],
make_env: make_env(),
compilers: [:elixir_make] ++ Mix.compilers,
test_coverage: [tool: ExCoveralls],
version: @version,
target: @target,
commit: commit(),
arduino_commit: arduino_commit(),
archives: [nerves_bootstrap: "~> 0.7.0"],
archives: [nerves_bootstrap: "~> 0.8.0"],
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
deps_path: "deps/#{@target}",
build_path: "_build/#{@target}",
lockfile: "mix.lock.#{@target}",
config_path: "config/config.exs",
lockfile: "mix.lock",
elixirc_paths: elixirc_paths(Mix.env(), @target),
aliases: aliases(Mix.env(), @target),
deps: deps() ++ deps(@target),
@ -81,11 +81,14 @@ defmodule Farmbot.Mixfile do
]
end
defp compilers do
case :init.get_plain_arguments() |> List.last() do
a when a in ['mix', 'compile', 'firmware'] ->
[:elixir_make] ++ Mix.compilers
_ -> Mix.compilers
defp make_env do
case System.get_env("ERL_EI_INCLUDE_DIR") do
nil ->
%{
"ERL_EI_INCLUDE_DIR" => "#{:code.root_dir()}/usr/include",
"ERL_EI_LIBDIR" => "#{:code.root_dir()}/usr/lib"
}
_ -> %{}
end
end

View file

@ -4,7 +4,7 @@ defmodule Farmbot.Target.Network.Manager do
alias Farmbot.System.ConfigStorage
alias Nerves.Network
alias Farmbot.Target.Network.Ntp
import Farmbot.Target.Network, only: [test_dns: 0]
import Farmbot.Target.Network, only: [test_dns: 0, test_dns: 1]
def get_ip_addr(interface) do
GenServer.call(:"#{__MODULE__}-#{interface}", :ip)
@ -22,11 +22,11 @@ defmodule Farmbot.Target.Network.Manager do
init(args)
end
SystemRegistry.register()
{:ok, _} = Registry.register(Nerves.NetworkInterface, interface, [])
{:ok, _} = Registry.register(Nerves.Udhcpc, interface, [])
{:ok, _} = Registry.register(Nerves.WpaSupplicant, interface, [])
{:ok, _} = Elixir.Registry.register(Nerves.NetworkInterface, interface, [])
{:ok, _} = Elixir.Registry.register(Nerves.Udhcpc, interface, [])
{:ok, _} = Elixir.Registry.register(Nerves.WpaSupplicant, interface, [])
Network.setup(interface, opts)
{:ok, %{interface: interface, ip_address: nil, connected: false, not_found_timer: nil, ntp_timer: nil}}
{:ok, %{interface: interface, ip_address: nil, connected: false, not_found_timer: nil, ntp_timer: nil, dns_timer: nil}}
end
def handle_call(:ip, _, state) do
@ -45,8 +45,9 @@ defmodule Farmbot.Target.Network.Manager do
connected = match?({:ok, {:hostent, 'nerves-project.org', [], :inet, 4, _}}, test_dns())
if connected do
not_found_timer = cancel_not_found_timer(state.not_found_timer)
{:noreply, %{state | ip_address: ip, connected: true, not_found_timer: not_found_timer, ntp_timer: ntp_timer}}
not_found_timer = cancel_timer(state.not_found_timer)
dns_timer = restart_dns_timer(state.dns_timer, 45_000)
{:noreply, %{state | dns_timer: dns_timer, ip_address: ip, connected: true, not_found_timer: not_found_timer, ntp_timer: ntp_timer}}
else
{:noreply, %{state | connected: false, ntp_timer: ntp_timer, ip_address: ip}}
end
@ -98,24 +99,46 @@ defmodule Farmbot.Target.Network.Manager do
{:noreply, %{state | ntp_timer: new_timer}}
end
def handle_info(:dns_timer, state) do
case test_dns('my.farmbot.io') do
{:ok, {:hostent, _host_name, aliases, :inet, 4, _}} ->
# If we weren't previously connected, send a log.
unless state.connected do
Logger.success 3, "Farmbot was reconnected to the internet: #{inspect aliases}"
Farmbot.System.Registry.dispatch(:network, :dns_up)
end
{:noreply, %{state | connected: true, dns_timer: restart_dns_timer(nil, 45_000)}}
{:error, err} ->
Farmbot.System.Registry.dispatch(:network, :dns_down)
Logger.warn 3, "Farmbot was disconnected from the internet: #{inspect err}"
{:noreply, %{state | connected: false, dns_timer: restart_dns_timer(nil, 10_000)}}
end
end
def handle_info(_event, state) do
# Logger.warn 3, "unhandled network event: #{inspect event}"
{:noreply, state}
end
defp cancel_not_found_timer(timer) do
defp cancel_timer(timer) do
# If there was a timer, cancel it.
if timer do
Logger.warn 3, "Cancelling Network timer"
# Logger.warn 3, "Cancelling Network timer"
Process.cancel_timer(timer)
end
nil
end
defp restart_dns_timer(timer, time) when is_number(time) do
cancel_timer(timer)
Process.send_after(self(), :dns_timer, time)
end
defp maybe_cancel_and_reset_ntp_timer(timer) do
if timer do
Process.cancel_timer(timer)
end
# introduce a bit of randomness to avoid dosing ntp servers.
# I don't think this would ever happen but the default ntpd implementation
# does this..
@ -130,7 +153,6 @@ defmodule Farmbot.Target.Network.Manager do
if Farmbot.System.ConfigStorage.get_config_value(:bool, "settings", "first_boot") do
Process.send_after(self(), :ntp_timer, 10_000 + rand)
else
Process.send_after(self(), :ntp_timer, 300000 + rand)
end
end

1
overlay/profiles/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
*.exs

View file

@ -0,0 +1,9 @@
defmodule Farmbot.System.ConfigStorage.Migrations.AddApiMigratedFlag do
use Ecto.Migration
import Farmbot.System.ConfigStorage.MigrationHelpers
def change do
create_settings_config("api_migrated", :bool, false)
end
end

View file

@ -0,0 +1,9 @@
defmodule Farmbot.System.ConfigStorage.Migrations.IgnoreFbosConfig do
use Ecto.Migration
import Farmbot.System.ConfigStorage.MigrationHelpers
def change do
create_settings_config("ignore_fbos_config", :bool, true)
end
end

View file

@ -1,6 +1,7 @@
defmodule Farmbot.Bootstrap.AuthorizationTest do
@moduledoc "Tests the default authorization implementation"
alias Farmbot.Bootstrap.Authorization, as: Auth
alias Farmbot.Bootstrap.AuthTask
use ExUnit.Case
@moduletag :farmbot_api
@ -37,4 +38,15 @@ defmodule Farmbot.Bootstrap.AuthorizationTest do
res = Auth.authorize("yolo@mtv.org", "123password", ctx.server)
assert match?({:error, _}, res)
end
test "internet off and back on refreshes token" do
Farmbot.System.Registry.subscribe(self())
old = Farmbot.System.ConfigStorage.get_config_value(:string, "authorization", "token")
send AuthTask, {Farmbot.System.Registry, {:network, :dns_up}}
assert_receive {Farmbot.System.Registry, {:authorization, :new_token}}, 1000
new = Farmbot.System.ConfigStorage.get_config_value(:string, "authorization", "token")
assert old != new
end
end

View file

@ -0,0 +1,44 @@
defmodule Farmbot.Bootstrap.SettingsSyncTest do
use ExUnit.Case, async: false
alias Farmbot.Bootstrap.SettingsSync
import Farmbot.System.ConfigStorage, only: [update_config_value: 4, get_config_value: 3]
test "Applies new configs in the form of a map." do
SettingsSync.apply_map(%{"firmware_output_log" => true}, %{"firmware_output_log" => false})
refute get_config_value(:bool, "settings", "firmware_output_log")
SettingsSync.apply_map(%{"firmware_hardware" => "arduino"}, %{"firmware_hardware" => "farmduino"})
assert get_config_value(:string, "settings", "firmware_hardware") == "farmduino"
SettingsSync.apply_map(%{"network_not_found_timer" => nil}, %{"network_not_found_timer" => 100})
assert get_config_value(:float, "settings", "network_not_found_timer") == 100.0
end
test "doesn't crash on unknown key value pairs when applying a map" do
Farmbot.System.Registry.subscribe(self())
bad_map = %{
"some_random_float" => 1.0,
"some_random_string" => "hello world",
"some_random_bool" => false
}
SettingsSync.apply_map(bad_map, %{})
SettingsSync.apply_map(%{}, bad_map)
refute_receive {Farmbot.System.Registry, {:config_storage, {"settings", "some_random_float", _}}}
refute_receive {Farmbot.System.Registry, {:config_storage, {"settings", "some_random_string", _}}}
refute_receive {Farmbot.System.Registry, {:config_storage, {"settings", "some_random_bool", _}}}
end
test "Updating configs externally will update in fbos" do
Farmbot.System.Registry.subscribe(self())
config_bin = %{
"os_auto_update" => true
} |> Poison.encode!
update_config_value(:bool, "settings", "os_auto_update", false)
assert_receive {Farmbot.System.Registry, {:config_storage, {"settings", "os_auto_update", false}}}
%{status_code: 200} = Farmbot.HTTP.put!("/api/fbos_config", config_bin)
Farmbot.Bootstrap.SettingsSync.run()
assert_receive {Farmbot.System.Registry, {:config_storage, {"settings", "os_auto_update", true}}}, 2000
end
end

View file

@ -1,81 +1,9 @@
defmodule Farmbot.CeleryScript.AST.Node.ConfigUpdateTest do
alias Farmbot.CeleryScript.AST.Node.{ConfigUpdate, Pair}
alias Farmbot.CeleryScript.AST.Node.ConfigUpdate
use FarmbotTestSupport.AST.NodeTestCase, async: false
alias Farmbot.System.ConfigStorage
test "mutates env", %{env: env} do
{:ok, env} = ConfigUpdate.execute(%{package: :farmbot_os}, [], env)
assert_cs_env_mutation(ConfigUpdate, env)
end
test "sets network_not_found_timer", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("network_not_found_timer", 1000), env)
|> assert_cs_success()
assert ConfigStorage.get_config_value(:float, "settings", "network_not_found_timer") == 1000.0
end
test "Wont set network_not_found_timer to a negative number", %{env: env} do
old = ConfigStorage.get_config_value(:float, "settings", "network_not_found_timer")
ConfigUpdate.execute(%{package: :farmbot_os}, pair("network_not_found_timer", -1), env)
|> assert_cs_fail("network_not_found_timer must be greater than zero")
assert ConfigStorage.get_config_value(:float, "settings", "network_not_found_timer") == old
end
test "sets os auto update", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("os_auto_update", true), env) |> assert_cs_success()
assert ConfigStorage.get_config_value(:bool, "settings", "os_auto_update")
ConfigUpdate.execute(%{package: :farmbot_os}, pair("os_auto_update", false), env) |> assert_cs_success()
refute ConfigStorage.get_config_value(:bool, "settings", "os_auto_update")
end
test "sets auto sync", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("auto_sync", true), env) |> assert_cs_success()
assert ConfigStorage.get_config_value(:bool, "settings", "auto_sync")
ConfigUpdate.execute(%{package: :farmbot_os}, pair("auto_sync", false), env) |> assert_cs_success()
refute ConfigStorage.get_config_value(:bool, "settings", "auto_sync")
end
test "can not set arduino hardware to unknown setting", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("firmware_hardware", "whoops"), env)
|> assert_cs_fail("unknown hardware: whoops")
refute ConfigStorage.get_config_value(:string, "settings", "firmware_hardware") == "whoops"
end
test "gives decent error on unknown onfig files.", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("some_other_config", "whoops"), env)
|> assert_cs_fail("unknown config: some_other_config")
end
test "can set float values with an integer", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("network_not_found_timer", 1000), env)
|> assert_cs_success()
end
test "can set float values with a float", %{env: env} do
ConfigUpdate.execute(%{package: :farmbot_os}, pair("network_not_found_timer", 1000.00), env)
|> assert_cs_success()
end
test "allows setting multiple configs", %{env: env} do
pairs = pairs([{"network_not_found_timer", 10.0}, {"firmware_hardware", "farmduino"}])
ConfigUpdate.execute(%{package: :farmbot_os}, pairs, env) |> assert_cs_success()
assert ConfigStorage.get_config_value(:string, "settings", "firmware_hardware") == "farmduino"
assert ConfigStorage.get_config_value(:float, "settings", "network_not_found_timer") == 10.0
end
defp pair(key, val) do
{:ok, pair, _} = Pair.execute(%{label: key, value: val}, [], struct(Macro.Env, []))
[pair]
end
defp pairs(pairs) do
Enum.map(pairs, fn({key, val}) -> pair(key, val) end) |> List.flatten()
end
end

View file

@ -0,0 +1,10 @@
defmodule Farmbot.System.RegistryTest do
use ExUnit.Case
alias Farmbot.System.Registry
test "subscribes and dispatches global events" do
Registry.subscribe(self())
Registry.dispatch(:hello, :world)
assert_receive {Registry, {:hello, :world}}
end
end

View file

@ -0,0 +1,19 @@
defmodule Farmbot.System.UpdateTimerTest do
use ExUnit.Case, async: false
alias Farmbot.System.ConfigStorage
test "Opting into beta updates should refresh token" do
Farmbot.System.Registry.subscribe(self())
old = ConfigStorage.get_config_value(:string, "authorization", "token")
ConfigStorage.update_config_value(:bool, "settings", "beta_opt_in", false)
ConfigStorage.update_config_value(:bool, "settings", "beta_opt_in", true)
assert_receive {Farmbot.System.Registry, {:config_storage, {"settings", "beta_opt_in", true}}}
assert_receive {Farmbot.System.Registry, {:authorization, :new_token}}, 1000
new = ConfigStorage.get_config_value(:string, "authorization", "token")
assert old != new
end
end