diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 00000000..1cc2c6e1 --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["*.{ex,exs}", "{test}/**/*.{ex,exs}"] +] diff --git a/Makefile b/Makefile index 0893cfb6..7aa44bad 100644 --- a/Makefile +++ b/Makefile @@ -26,11 +26,14 @@ help: @echo " clean - clean all." clean_other_branch: - rm -rf _build deps c_src config tmp + rm -rf _build deps c_src config tmp priv clean: clean_other_branch @for project in $(PROJECTS) ; do \ echo cleaning $$project ; \ + rm -rf $$project/erl_crash.dump ; \ + rm -rf $$project/.*.sqlite3* ; \ + rm -rf $$project/*.sqlite3* ; \ rm -rf $$project/_build ; \ rm -rf $$project/deps ; \ rm -rf $$project/priv/*.so ; \ diff --git a/farmbot_celery_script/.formatter.exs b/farmbot_celery_script/.formatter.exs new file mode 100644 index 00000000..964fa581 --- /dev/null +++ b/farmbot_celery_script/.formatter.exs @@ -0,0 +1,3 @@ +[ + inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"], +] diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex index d16a7786..39c5db65 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex @@ -103,6 +103,9 @@ defmodule Farmbot.CeleryScript.RunTime.InstructionSet do @doc "Create a diagnostic dump of information." simple_io_instruction(:dump_info) + @doc false + simple_io_instruction(:debug) + @doc "Move to a location offset by another location." def move_absolute(%FarmProc{} = farm_proc) do pc = get_pc_ptr(farm_proc) diff --git a/farmbot_core/.formatter.exs b/farmbot_core/.formatter.exs new file mode 100644 index 00000000..ca0a0308 --- /dev/null +++ b/farmbot_core/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto], + inputs: ["*.{ex,exs}", "{config,priv,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] +] diff --git a/farmbot_core/.gitignore b/farmbot_core/.gitignore index 5fb0017e..d9b16a9e 100644 --- a/farmbot_core/.gitignore +++ b/farmbot_core/.gitignore @@ -22,7 +22,7 @@ erl_crash.dump # Ignore package tarball (built via "mix hex.build"). farmbot_ng-*.tar -*.sqlite3 +*.sqlite3* *.so *.hex !priv/eeprom_clear.ino.hex diff --git a/farmbot_core/c_src/build_calendar/build_calendar.c b/farmbot_core/c_src/build_calendar/build_calendar.c index 17c4a063..f849d53f 100644 --- a/farmbot_core/c_src/build_calendar/build_calendar.c +++ b/farmbot_core/c_src/build_calendar/build_calendar.c @@ -1,9 +1,11 @@ #include #include +#include #include -#define MAX_GENERATED 3 +// Enough space for one event every minute for 20 years. +#define MAX_GENERATED LONG_MAX static ERL_NIF_TERM do_build_calendar(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) { @@ -19,37 +21,20 @@ static ERL_NIF_TERM do_build_calendar(ErlNifEnv* env, int argc, const ERL_NIF_TE enif_get_long(env, argv[4], &frequencySeconds); // Data used to build the calendar. - long int gracePeriodSeconds; - gracePeriodSeconds = nowSeconds - 60; + long int gracePeriodSeconds = nowSeconds - 60; long int step = frequencySeconds * repeat; - // iterators for loops - long int i, j; + // iterator for loops + long int i; - // build our events array, fill it with zeroes. - long int events[MAX_GENERATED]; - for(i = 0; i < MAX_GENERATED; i++) - events[i] = 0; - - // put up to MAX_GENERATED events into the array - for(j = 0, i = startTimeSeconds; (i < endTimeSeconds) && (j < MAX_GENERATED); i += step) { - // if this event (i) is after the grace period, add it to the array. + // count up to MAX_GENERATED items + for(i = startTimeSeconds; i < endTimeSeconds; i += step) { + // if this event (i) is after the grace period this is the next event. if(i > gracePeriodSeconds) { - events[j] = i; - j++; + return enif_make_long(env, i); } } - - // Count up our total generated events - for(i=0, j=0; j 0) { i++; } } - - // Build the array to be returned. - ERL_NIF_TERM retArr [i]; - for(j=0; j Repo.update!() + end + + ## End FarmEvent + + ## Begin FbosConfig + + def fbos_config() do + Repo.one(FbosConfig) || %FbosConfig{} + end + + def fbos_config(field) do + Map.fetch!(fbos_config(), field) + end + + ## End FbosConfig + + ## Begin FirmwareConfig + + def firmware_config() do + Repo.one(FirmwareConfig) || %FirmwareConfig{} + end + + def firmware_config(field) do + Map.fetch!(firmware_config(), field) + end + + ## End FirmwareConfig + + ## Begin PersistentRegimen + + def upsert_persistent_regimen(%Regimen{} = regimen, %FarmEvent{} = farm_event, params \\ %{}) do + q = from pr in PersistentRegimen, where: pr.regimen_id == ^regimen.local_id and pr.farm_event_id == ^farm_event.local_id + pr = Repo.one(q) || %PersistentRegimen{} + pr + |> Repo.preload([:regimen, :farm_event]) + |> PersistentRegimen.changeset(params) + |> Ecto.Changeset.put_assoc(:regimen, regimen) + |> Ecto.Changeset.put_assoc(:farm_event, farm_event) + |> Repo.insert_or_update() + end + + ## End PersistentRegimen + + ## Begin PinBinding + + @doc "Lists all available pin bindings" + def list_pin_bindings do + Repo.all(PinBinding) + end + + ## End PinBinding + + ## Begin Regimen + + @doc "Get a regimen by it's API id and FarmEvent API id" + def get_regimen!(params) do + Repo.get_by!(Regimen, params) + end + + ## End Regimen + + ## Begin Sequence + + @doc "Get a sequence by it's API id" + def get_sequence!(params) do + Repo.get_by!(Sequence, params) + end + + ## End Sequence +end diff --git a/farmbot_core/lib/asset/device.ex b/farmbot_core/lib/asset/device.ex new file mode 100644 index 00000000..d8fc91fc --- /dev/null +++ b/farmbot_core/lib/asset/device.ex @@ -0,0 +1,36 @@ +defmodule Farmbot.Asset.Device do + @moduledoc """ + The current device. Should only ever be _one_ of these. If not there is a huge + problem probably higher up the stack. + """ + + use Farmbot.Asset.Schema, path: "/api/device" + + schema "devices" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:name, :string) + field(:timezone, :string) + timestamps() + end + + view device do + %{ + id: device.id, + name: device.name, + timezone: device.timezone + } + end + + def changeset(device, params \\ %{}) do + device + |> cast(params, [:id, :name, :timezone, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/diagnostic_dump.ex b/farmbot_core/lib/asset/diagnostic_dump.ex new file mode 100644 index 00000000..489ff6d5 --- /dev/null +++ b/farmbot_core/lib/asset/diagnostic_dump.ex @@ -0,0 +1,55 @@ +defmodule Elixir.Farmbot.Asset.DiagnosticDump do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/diagnostic_dumps" + + schema "diagnostic_dumps" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:ticket_identifier, :string) + field(:fbos_commit, :string) + field(:fbos_version, :string) + field(:firmware_commit, :string) + field(:firmware_state, :string) + field(:network_interface, :string) + field(:fbos_dmesg_dump, :string) + timestamps() + end + + view diagnostic_dump do + %{ + id: diagnostic_dump.id, + ticket_identifier: diagnostic_dump.ticket_identifier, + fbos_commit: diagnostic_dump.fbos_commit, + fbos_version: diagnostic_dump.fbos_version, + firmware_commit: diagnostic_dump.firmware_commit, + firmware_state: diagnostic_dump.firmware_state, + network_interface: diagnostic_dump.network_interface, + fbos_dmesg_dump: diagnostic_dump.fbos_dmesg_dump + } + end + + def changeset(diagnostic_dump, params \\ %{}) do + diagnostic_dump + |> cast(params, [ + :id, + :ticket_identifier, + :fbos_commit, + :fbos_version, + :firmware_commit, + :firmware_state, + :network_interface, + :fbos_dmesg_dump, + :created_at, + :updated_at + ]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/farm_event.ex b/farmbot_core/lib/asset/farm_event.ex new file mode 100644 index 00000000..f8f82d38 --- /dev/null +++ b/farmbot_core/lib/asset/farm_event.ex @@ -0,0 +1,101 @@ +defmodule Elixir.Farmbot.Asset.FarmEvent do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/farm_events" + + schema "farm_events" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:end_time, :utc_datetime) + field(:executable_type, :string) + field(:executable_id, :id) + field(:repeat, :integer) + field(:start_time, :utc_datetime) + field(:time_unit, :string) + + # Private + field(:last_executed, :utc_datetime) + + timestamps() + end + + view farm_event do + %{ + id: farm_event.id, + end_time: farm_event.end_time, + executable_type: farm_event.executable_type, + executable_id: farm_event.executable_id, + repeat: farm_event.repeat, + start_time: farm_event.start_time, + time_unit: farm_event.time_unit, + } + end + + def changeset(farm_event, params \\ %{}) do + farm_event + |> cast(params, [ + :id, + :end_time, + :executable_type, + :executable_id, + :repeat, + :start_time, + :time_unit, + :last_executed, + :created_at, + :updated_at + ]) + |> validate_required([]) + end + + @compile {:inline, [build_calendar: 2]} + def build_calendar(%__MODULE__{executable_type: "Regimen"} = fe, _), do: fe.start_time + + def build_calendar(%__MODULE__{time_unit: "never"} = fe, _), do: fe.start_time + + def build_calendar(%__MODULE__{} = fe, current_date_time) do + current_time_seconds = DateTime.to_unix(current_date_time) + start_time_seconds = DateTime.to_unix(fe.start_time, :second) + end_time_seconds = DateTime.to_unix(fe.end_time, :second) + + repeat = fe.repeat + repeat_frequency_seconds = time_unit_to_seconds(fe.time_unit) + + do_build_calendar( + current_time_seconds, + start_time_seconds, + end_time_seconds, + repeat, + repeat_frequency_seconds + ) |> DateTime.from_unix!() + end + + def do_build_calendar(_,_,_,_,_), do: :erlang.nif_error("NIF Not loaded") + + @on_load :load_nif + def load_nif do + require Logger + nif_file = '#{:code.priv_dir(:farmbot_core)}/build_calendar' + + case :erlang.load_nif(nif_file, 0) do + :ok -> :ok + {:error, {:reload, _}} -> :ok + {:error, reason} -> Logger.warn("Failed to load nif: #{inspect(reason)}") + end + end + + @compile {:inline, [time_unit_to_seconds: 1]} + defp time_unit_to_seconds("minutely"), do: 60 + defp time_unit_to_seconds("hourly"), do: 60 * 60 + defp time_unit_to_seconds("daily"), do: 60 * 60 * 24 + defp time_unit_to_seconds("weekly"), do: 60 * 60 * 24 * 7 + defp time_unit_to_seconds("monthly"), do: 60 * 60 * 24 * 30 + defp time_unit_to_seconds("yearly"), do: 60 * 60 * 24 * 365 +end diff --git a/farmbot_core/lib/asset/farmware_env.ex b/farmbot_core/lib/asset/farmware_env.ex new file mode 100644 index 00000000..48076392 --- /dev/null +++ b/farmbot_core/lib/asset/farmware_env.ex @@ -0,0 +1,34 @@ +defmodule Elixir.Farmbot.Asset.FarmwareEnv do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/farmware_envs" + + schema "farmware_envs" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:key, :string) + field(:value, :string) + timestamps() + end + + view farmware_env do + %{ + id: farmware_env.id, + key: farmware_env.key, + value: farmware_env.value + } + end + + def changeset(farmware_env, params \\ %{}) do + farmware_env + |> cast(params, [:id, :key, :value, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/farmware_installation.ex b/farmbot_core/lib/asset/farmware_installation.ex new file mode 100644 index 00000000..d9720dc3 --- /dev/null +++ b/farmbot_core/lib/asset/farmware_installation.ex @@ -0,0 +1,32 @@ +defmodule Elixir.Farmbot.Asset.FarmwareInstallation do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/farmware_installations" + + schema "farmware_installations" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:url, :string) + timestamps() + end + + view farmware_installation do + %{ + id: farmware_installation.id, + url: farmware_installation.url + } + end + + def changeset(farmware_installation, params \\ %{}) do + farmware_installation + |> cast(params, [:id, :url, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/fbos_config.ex b/farmbot_core/lib/asset/fbos_config.ex new file mode 100644 index 00000000..d4b7e94e --- /dev/null +++ b/farmbot_core/lib/asset/fbos_config.ex @@ -0,0 +1,70 @@ +defmodule Elixir.Farmbot.Asset.FbosConfig do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/fbos_config" + + schema "fbos_configs" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:arduino_debug_messages, :boolean) + field(:auto_sync, :boolean) + field(:beta_opt_in, :boolean) + field(:disable_factory_reset, :boolean) + field(:firmware_hardware, :string) + field(:firmware_input_log, :boolean) + field(:firmware_output_log, :boolean) + field(:network_not_found_timer, :integer) + field(:os_auto_update, :boolean) + field(:sequence_body_log, :boolean) + field(:sequence_complete_log, :boolean) + field(:sequence_init_log, :boolean) + timestamps() + end + + view fbos_config do + %{ + id: fbos_config.id, + arduino_debug_messages: fbos_config.arduino_debug_messages, + auto_sync: fbos_config.auto_sync, + beta_opt_in: fbos_config.beta_opt_in, + disable_factory_reset: fbos_config.disable_factory_reset, + firmware_hardware: fbos_config.firmware_hardware, + firmware_input_log: fbos_config.firmware_input_log, + firmware_output_log: fbos_config.firmware_output_log, + network_not_found_timer: fbos_config.network_not_found_timer, + os_auto_update: fbos_config.os_auto_update, + sequence_body_log: fbos_config.sequence_body_log, + sequence_complete_log: fbos_config.sequence_complete_log, + sequence_init_log: fbos_config.sequence_init_log + } + end + + def changeset(fbos_config, params \\ %{}) do + fbos_config + |> cast(params, [ + :id, + :arduino_debug_messages, + :auto_sync, + :beta_opt_in, + :disable_factory_reset, + :firmware_hardware, + :firmware_input_log, + :firmware_output_log, + :network_not_found_timer, + :os_auto_update, + :sequence_body_log, + :sequence_complete_log, + :sequence_init_log, + :created_at, + :updated_at + ]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/firmware_config.ex b/farmbot_core/lib/asset/firmware_config.ex new file mode 100644 index 00000000..a299927b --- /dev/null +++ b/farmbot_core/lib/asset/firmware_config.ex @@ -0,0 +1,298 @@ +defmodule Elixir.Farmbot.Asset.FirmwareConfig do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/firmware_config" + + schema "firmware_configs" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:pin_guard_4_time_out, :float) + field(:pin_guard_1_active_state, :float) + field(:encoder_scaling_y, :float) + field(:movement_invert_2_endpoints_x, :float) + field(:movement_min_spd_y, :float) + field(:pin_guard_2_time_out, :float) + field(:movement_timeout_y, :float) + field(:movement_home_at_boot_y, :float) + field(:movement_home_spd_z, :float) + field(:movement_invert_endpoints_z, :float) + field(:pin_guard_1_pin_nr, :float) + field(:movement_invert_endpoints_y, :float) + field(:movement_max_spd_y, :float) + field(:movement_home_up_y, :float) + field(:encoder_missed_steps_decay_z, :float) + field(:movement_home_spd_y, :float) + field(:encoder_use_for_pos_x, :float) + field(:movement_step_per_mm_x, :float) + field(:movement_home_at_boot_z, :float) + field(:movement_steps_acc_dec_z, :float) + field(:pin_guard_5_pin_nr, :float) + field(:movement_invert_motor_z, :float) + field(:movement_max_spd_x, :float) + field(:movement_enable_endpoints_y, :float) + field(:movement_enable_endpoints_z, :float) + field(:movement_stop_at_home_x, :float) + field(:movement_axis_nr_steps_y, :float) + field(:pin_guard_1_time_out, :float) + field(:movement_home_at_boot_x, :float) + field(:pin_guard_2_pin_nr, :float) + field(:encoder_scaling_z, :float) + field(:param_e_stop_on_mov_err, :float) + field(:encoder_enabled_x, :float) + field(:pin_guard_2_active_state, :float) + field(:encoder_missed_steps_decay_y, :float) + field(:movement_home_up_z, :float) + field(:movement_enable_endpoints_x, :float) + field(:movement_step_per_mm_y, :float) + field(:pin_guard_3_pin_nr, :float) + field(:param_mov_nr_retry, :float) + field(:movement_stop_at_home_z, :float) + field(:pin_guard_4_active_state, :float) + field(:movement_steps_acc_dec_y, :float) + field(:movement_home_spd_x, :float) + field(:movement_keep_active_x, :float) + field(:pin_guard_3_time_out, :float) + field(:movement_keep_active_y, :float) + field(:encoder_scaling_x, :float) + field(:movement_invert_2_endpoints_z, :float) + field(:encoder_missed_steps_decay_x, :float) + field(:movement_timeout_z, :float) + field(:encoder_missed_steps_max_z, :float) + field(:movement_min_spd_z, :float) + field(:encoder_enabled_y, :float) + field(:encoder_type_y, :float) + field(:movement_home_up_x, :float) + field(:pin_guard_3_active_state, :float) + field(:movement_invert_motor_x, :float) + field(:movement_keep_active_z, :float) + field(:movement_max_spd_z, :float) + field(:movement_secondary_motor_invert_x, :float) + field(:movement_stop_at_max_x, :float) + field(:movement_steps_acc_dec_x, :float) + field(:pin_guard_4_pin_nr, :float) + field(:encoder_type_x, :float) + field(:movement_invert_2_endpoints_y, :float) + field(:encoder_invert_y, :float) + field(:movement_axis_nr_steps_x, :float) + field(:movement_stop_at_max_z, :float) + field(:movement_invert_endpoints_x, :float) + field(:encoder_invert_z, :float) + field(:encoder_use_for_pos_z, :float) + field(:pin_guard_5_active_state, :float) + field(:movement_step_per_mm_z, :float) + field(:encoder_enabled_z, :float) + field(:movement_secondary_motor_x, :float) + field(:pin_guard_5_time_out, :float) + field(:movement_min_spd_x, :float) + field(:encoder_type_z, :float) + field(:movement_stop_at_max_y, :float) + field(:encoder_use_for_pos_y, :float) + field(:encoder_missed_steps_max_y, :float) + field(:movement_timeout_x, :float) + field(:movement_stop_at_home_y, :float) + field(:movement_axis_nr_steps_z, :float) + field(:encoder_invert_x, :float) + field(:encoder_missed_steps_max_x, :float) + field(:movement_invert_motor_y, :float) + timestamps() + end + + view firmware_config do + %{ + id: firmware_config.id, + pin_guard_4_time_out: firmware_config.pin_guard_4_time_out, + pin_guard_1_active_state: firmware_config.pin_guard_1_active_state, + encoder_scaling_y: firmware_config.encoder_scaling_y, + movement_invert_2_endpoints_x: firmware_config.movement_invert_2_endpoints_x, + movement_min_spd_y: firmware_config.movement_min_spd_y, + pin_guard_2_time_out: firmware_config.pin_guard_2_time_out, + movement_timeout_y: firmware_config.movement_timeout_y, + movement_home_at_boot_y: firmware_config.movement_home_at_boot_y, + movement_home_spd_z: firmware_config.movement_home_spd_z, + movement_invert_endpoints_z: firmware_config.movement_invert_endpoints_z, + pin_guard_1_pin_nr: firmware_config.pin_guard_1_pin_nr, + movement_invert_endpoints_y: firmware_config.movement_invert_endpoints_y, + movement_max_spd_y: firmware_config.movement_max_spd_y, + movement_home_up_y: firmware_config.movement_home_up_y, + encoder_missed_steps_decay_z: firmware_config.encoder_missed_steps_decay_z, + movement_home_spd_y: firmware_config.movement_home_spd_y, + encoder_use_for_pos_x: firmware_config.encoder_use_for_pos_x, + movement_step_per_mm_x: firmware_config.movement_step_per_mm_x, + movement_home_at_boot_z: firmware_config.movement_home_at_boot_z, + movement_steps_acc_dec_z: firmware_config.movement_steps_acc_dec_z, + pin_guard_5_pin_nr: firmware_config.pin_guard_5_pin_nr, + movement_invert_motor_z: firmware_config.movement_invert_motor_z, + movement_max_spd_x: firmware_config.movement_max_spd_x, + movement_enable_endpoints_y: firmware_config.movement_enable_endpoints_y, + movement_enable_endpoints_z: firmware_config.movement_enable_endpoints_z, + movement_stop_at_home_x: firmware_config.movement_stop_at_home_x, + movement_axis_nr_steps_y: firmware_config.movement_axis_nr_steps_y, + pin_guard_1_time_out: firmware_config.pin_guard_1_time_out, + movement_home_at_boot_x: firmware_config.movement_home_at_boot_x, + pin_guard_2_pin_nr: firmware_config.pin_guard_2_pin_nr, + encoder_scaling_z: firmware_config.encoder_scaling_z, + param_e_stop_on_mov_err: firmware_config.param_e_stop_on_mov_err, + encoder_enabled_x: firmware_config.encoder_enabled_x, + pin_guard_2_active_state: firmware_config.pin_guard_2_active_state, + encoder_missed_steps_decay_y: firmware_config.encoder_missed_steps_decay_y, + movement_home_up_z: firmware_config.movement_home_up_z, + movement_enable_endpoints_x: firmware_config.movement_enable_endpoints_x, + movement_step_per_mm_y: firmware_config.movement_step_per_mm_y, + pin_guard_3_pin_nr: firmware_config.pin_guard_3_pin_nr, + param_mov_nr_retry: firmware_config.param_mov_nr_retry, + movement_stop_at_home_z: firmware_config.movement_stop_at_home_z, + pin_guard_4_active_state: firmware_config.pin_guard_4_active_state, + movement_steps_acc_dec_y: firmware_config.movement_steps_acc_dec_y, + movement_home_spd_x: firmware_config.movement_home_spd_x, + movement_keep_active_x: firmware_config.movement_keep_active_x, + pin_guard_3_time_out: firmware_config.pin_guard_3_time_out, + movement_keep_active_y: firmware_config.movement_keep_active_y, + encoder_scaling_x: firmware_config.encoder_scaling_x, + movement_invert_2_endpoints_z: firmware_config.movement_invert_2_endpoints_z, + encoder_missed_steps_decay_x: firmware_config.encoder_missed_steps_decay_x, + movement_timeout_z: firmware_config.movement_timeout_z, + encoder_missed_steps_max_z: firmware_config.encoder_missed_steps_max_z, + movement_min_spd_z: firmware_config.movement_min_spd_z, + encoder_enabled_y: firmware_config.encoder_enabled_y, + encoder_type_y: firmware_config.encoder_type_y, + movement_home_up_x: firmware_config.movement_home_up_x, + pin_guard_3_active_state: firmware_config.pin_guard_3_active_state, + movement_invert_motor_x: firmware_config.movement_invert_motor_x, + movement_keep_active_z: firmware_config.movement_keep_active_z, + movement_max_spd_z: firmware_config.movement_max_spd_z, + movement_secondary_motor_invert_x: firmware_config.movement_secondary_motor_invert_x, + movement_stop_at_max_x: firmware_config.movement_stop_at_max_x, + movement_steps_acc_dec_x: firmware_config.movement_steps_acc_dec_x, + pin_guard_4_pin_nr: firmware_config.pin_guard_4_pin_nr, + encoder_type_x: firmware_config.encoder_type_x, + movement_invert_2_endpoints_y: firmware_config.movement_invert_2_endpoints_y, + encoder_invert_y: firmware_config.encoder_invert_y, + movement_axis_nr_steps_x: firmware_config.movement_axis_nr_steps_x, + movement_stop_at_max_z: firmware_config.movement_stop_at_max_z, + movement_invert_endpoints_x: firmware_config.movement_invert_endpoints_x, + encoder_invert_z: firmware_config.encoder_invert_z, + encoder_use_for_pos_z: firmware_config.encoder_use_for_pos_z, + pin_guard_5_active_state: firmware_config.pin_guard_5_active_state, + movement_step_per_mm_z: firmware_config.movement_step_per_mm_z, + encoder_enabled_z: firmware_config.encoder_enabled_z, + movement_secondary_motor_x: firmware_config.movement_secondary_motor_x, + pin_guard_5_time_out: firmware_config.pin_guard_5_time_out, + movement_min_spd_x: firmware_config.movement_min_spd_x, + encoder_type_z: firmware_config.encoder_type_z, + movement_stop_at_max_y: firmware_config.movement_stop_at_max_y, + encoder_use_for_pos_y: firmware_config.encoder_use_for_pos_y, + encoder_missed_steps_max_y: firmware_config.encoder_missed_steps_max_y, + movement_timeout_x: firmware_config.movement_timeout_x, + movement_stop_at_home_y: firmware_config.movement_stop_at_home_y, + movement_axis_nr_steps_z: firmware_config.movement_axis_nr_steps_z, + encoder_invert_x: firmware_config.encoder_invert_x, + encoder_missed_steps_max_x: firmware_config.encoder_missed_steps_max_x, + movement_invert_motor_y: firmware_config.movement_invert_motor_y + } + end + + def changeset(firmware_config, params \\ %{}) do + firmware_config + |> cast(params, [ + :id, + :pin_guard_4_time_out, + :pin_guard_1_active_state, + :encoder_scaling_y, + :movement_invert_2_endpoints_x, + :movement_min_spd_y, + :pin_guard_2_time_out, + :movement_timeout_y, + :movement_home_at_boot_y, + :movement_home_spd_z, + :movement_invert_endpoints_z, + :pin_guard_1_pin_nr, + :movement_invert_endpoints_y, + :movement_max_spd_y, + :movement_home_up_y, + :encoder_missed_steps_decay_z, + :movement_home_spd_y, + :encoder_use_for_pos_x, + :movement_step_per_mm_x, + :movement_home_at_boot_z, + :movement_steps_acc_dec_z, + :pin_guard_5_pin_nr, + :movement_invert_motor_z, + :movement_max_spd_x, + :movement_enable_endpoints_y, + :movement_enable_endpoints_z, + :movement_stop_at_home_x, + :movement_axis_nr_steps_y, + :pin_guard_1_time_out, + :movement_home_at_boot_x, + :pin_guard_2_pin_nr, + :encoder_scaling_z, + :param_e_stop_on_mov_err, + :encoder_enabled_x, + :pin_guard_2_active_state, + :encoder_missed_steps_decay_y, + :movement_home_up_z, + :movement_enable_endpoints_x, + :movement_step_per_mm_y, + :pin_guard_3_pin_nr, + :param_mov_nr_retry, + :movement_stop_at_home_z, + :pin_guard_4_active_state, + :movement_steps_acc_dec_y, + :movement_home_spd_x, + :movement_keep_active_x, + :pin_guard_3_time_out, + :movement_keep_active_y, + :encoder_scaling_x, + :movement_invert_2_endpoints_z, + :encoder_missed_steps_decay_x, + :movement_timeout_z, + :encoder_missed_steps_max_z, + :movement_min_spd_z, + :encoder_enabled_y, + :encoder_type_y, + :movement_home_up_x, + :pin_guard_3_active_state, + :movement_invert_motor_x, + :movement_keep_active_z, + :movement_max_spd_z, + :movement_secondary_motor_invert_x, + :movement_stop_at_max_x, + :movement_steps_acc_dec_x, + :pin_guard_4_pin_nr, + :encoder_type_x, + :movement_invert_2_endpoints_y, + :encoder_invert_y, + :movement_axis_nr_steps_x, + :movement_stop_at_max_z, + :movement_invert_endpoints_x, + :encoder_invert_z, + :encoder_use_for_pos_z, + :pin_guard_5_active_state, + :movement_step_per_mm_z, + :encoder_enabled_z, + :movement_secondary_motor_x, + :pin_guard_5_time_out, + :movement_min_spd_x, + :encoder_type_z, + :movement_stop_at_max_y, + :encoder_use_for_pos_y, + :encoder_missed_steps_max_y, + :movement_timeout_x, + :movement_stop_at_home_y, + :movement_axis_nr_steps_z, + :encoder_invert_x, + :encoder_missed_steps_max_x, + :movement_invert_motor_y, + :created_at, + :updated_at + ]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/peripheral.ex b/farmbot_core/lib/asset/peripheral.ex new file mode 100644 index 00000000..06bba9b8 --- /dev/null +++ b/farmbot_core/lib/asset/peripheral.ex @@ -0,0 +1,37 @@ +defmodule Farmbot.Asset.Peripheral do + @moduledoc """ + Peripherals are descriptors for pins/modes. + """ + + use Farmbot.Asset.Schema, path: "/api/peripherals" + + schema "peripherals" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:pin, :integer) + field(:mode, :integer) + field(:label, :string) + timestamps() + end + + view peripheral do + %{ + id: peripheral.id, + pin: peripheral.pin, + mode: peripheral.mode, + label: peripheral.label + } + end + + def changeset(peripheral, params \\ %{}) do + peripheral + |> cast(params, [:id, :pin, :mode, :label, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/persistent_regimen.ex b/farmbot_core/lib/asset/persistent_regimen.ex new file mode 100644 index 00000000..eaccff3a --- /dev/null +++ b/farmbot_core/lib/asset/persistent_regimen.ex @@ -0,0 +1,20 @@ +defmodule Farmbot.Asset.PersistentRegimen do + use Ecto.Schema + import Ecto.Changeset + @primary_key {:local_id, :binary_id, autogenerate: true} + @timestamps_opts inserted_at: :created_at, type: :utc_datetime + + schema "persistent_regimens" do + belongs_to(:regimen, Farmbot.Asset.Regimen, references: :local_id, type: :binary_id) + belongs_to(:farm_event, Farmbot.Asset.FarmEvent, references: :local_id, type: :binary_id) + field :started_at, :utc_datetime + timestamps() + end + + def changeset(persistent_regimen, params \\ %{}) do + persistent_regimen + |> cast(params, [:started_at]) + |> cast_assoc(:regimen) + |> cast_assoc(:farm_event) + end +end diff --git a/farmbot_core/lib/asset/pin_binding.ex b/farmbot_core/lib/asset/pin_binding.ex new file mode 100644 index 00000000..f450e05d --- /dev/null +++ b/farmbot_core/lib/asset/pin_binding.ex @@ -0,0 +1,72 @@ +defmodule Farmbot.Asset.PinBinding do + @moduledoc """ + When a pin binding is triggered a sequence fires. + """ + use Farmbot.Asset.Schema, path: "/api/pin_bindings" + + schema "pin_bindings" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:pin_num, :integer) + field(:sequence_id, :integer) + field(:special_action, :string) + timestamps() + end + + view pin_binding do + %{ + id: pin_binding.id, + pin_num: pin_binding.pin_num, + sequence_id: pin_binding.sequence_id, + special_action: pin_binding.special_action + } + end + + def changeset(pin_binding, params \\ %{}) do + pin_binding + |> cast(params, [:id, :pin_num, :sequence_id, :special_action, :created_at, :updated_at]) + |> validate_required([]) + |> validate_pin_num() + |> unique_constraint(:pin_num) + end + + def validate_pin_num(changeset) do + if get_field(changeset, :pin_num, -1) in [17, 23, 27, 06, 21, 24, 25, 12, 13] do + add_error(changeset, :pin_num, "in use") + else + changeset + end + end +end + +defimpl String.Chars, for: Farmbot.Asset.PinBinding do + def to_string(%Farmbot.Asset.PinBinding{pin_num: 16}) do + "Button 1" + end + + def to_string(%Farmbot.Asset.PinBinding{pin_num: 22}) do + "Button 2" + end + + def to_string(%Farmbot.Asset.PinBinding{pin_num: 26}) do + "Button 3" + end + + def to_string(%Farmbot.Asset.PinBinding{pin_num: 5}) do + "Button 4" + end + + def to_string(%Farmbot.Asset.PinBinding{pin_num: 20}) do + "Button 5" + end + + def to_string(%Farmbot.Asset.PinBinding{pin_num: num}) do + "Pi GPIO #{num}" + end +end diff --git a/farmbot_core/lib/asset/point.ex b/farmbot_core/lib/asset/point.ex new file mode 100644 index 00000000..5401fff9 --- /dev/null +++ b/farmbot_core/lib/asset/point.ex @@ -0,0 +1,61 @@ +defmodule Farmbot.Asset.Point do + @moduledoc """ + Points are data around an x,y,z + """ + use Farmbot.Asset.Schema, path: "/api/points" + + schema "points" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:meta, :map) + field(:name, :string) + field(:plant_stage, :string) + field(:planted_at, :utc_datetime) + field(:pointer_type, :string) + field(:radius, :float) + field(:x, :float) + field(:y, :float) + field(:z, :float) + timestamps() + end + + view point do + %{ + id: point.id, + meta: point.meta, + name: point.nama, + plant_stage: point.plant_stage, + planned_at: point.planned_at, + pointer_type: point.pointer_type, + radius: point.float, + x: point.x, + y: point.y, + z: point.z + } + end + + def changeset(point, params \\ %{}) do + point + |> cast(params, [ + :id, + :meta, + :name, + :plant_stage, + :planted_at, + :pointer_type, + :radius, + :x, + :y, + :z, + :created_at, + :updated_at + ]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/private.ex b/farmbot_core/lib/asset/private.ex new file mode 100644 index 00000000..f5aae289 --- /dev/null +++ b/farmbot_core/lib/asset/private.ex @@ -0,0 +1,47 @@ +defmodule Farmbot.Asset.Private do + alias Farmbot.Asset.Repo + alias Farmbot.Asset.Private.LocalMeta + + import Ecto.Query, warn: false + import Ecto.Changeset, warn: false + + @doc "Lists `module` objects that still need to be POSTed to the API." + def list_local(module) do + Repo.all(from(data in module, where: is_nil(data.id))) + end + + @doc "Lists `module` objects that have a `local_meta` object" + def list_dirty(module) do + table = table(module) + q = from(lm in LocalMeta, where: lm.table == ^table, select: lm.asset_local_id) + Repo.all(from(data in module, join: lm in subquery(q))) + end + + @doc "Mark a document as `dirty` by creating a `local_meta` object" + def mark_dirty!(asset, params) do + table = table(asset) + + local_meta = + Repo.one( + from(lm in LocalMeta, where: lm.asset_local_id == ^asset.local_id and lm.table == ^table) + ) || Ecto.build_assoc(asset, :local_meta) + + local_meta + |> LocalMeta.changeset(Map.merge(params, %{table: table, status: "dirty"})) + |> Repo.insert_or_update!() + end + + @doc "Remove the `local_meta` record from an object." + @spec mark_clean!(map) :: nil | map() + def mark_clean!(data) do + Repo.preload(data, :local_meta) + |> Map.fetch!(:local_meta) + |> case do + nil -> nil + local_meta -> Repo.delete!(local_meta) + end + end + + defp table(%module{}), do: table(module) + defp table(module), do: module.__schema__(:source) +end diff --git a/farmbot_core/lib/asset/private/local_meta.ex b/farmbot_core/lib/asset/private/local_meta.ex new file mode 100644 index 00000000..ee95bff0 --- /dev/null +++ b/farmbot_core/lib/asset/private/local_meta.ex @@ -0,0 +1,165 @@ +defmodule Farmbot.Asset.Private.LocalMeta do + @moduledoc """ + Existance of LocalMeta is a hint to Farmbot that + an Asset needs to be reconciled with the remote API. + """ + use Ecto.Schema + import Ecto.Changeset + + alias Farmbot.Asset.{ + Device, + DiagnosticDump, + FarmEvent, + FarmwareEnv, + FarmwareInstallation, + FbosConfig, + FirmwareConfig, + Peripheral, + PinBinding, + Point, + Regimen, + SensorReading, + Sensor, + Sequence, + Tool + } + + schema "local_metas" do + field(:status, :string) + field(:table, :string) + field(:asset_local_id, :binary_id) + + belongs_to(:device, Device, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:diagnostic_dump, DiagnosticDump, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:farm_event, FarmEvent, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:farmware_env, FarmwareEnv, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:farmware_installation, FarmwareInstallation, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:fbos_config, FbosConfig, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:firmware_config, FirmwareConfig, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:peripheral, Peripheral, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:pin_binding, PinBinding, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:point, Point, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:regimen, Regimen, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:sensor_reading, SensorReading, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:sensor, Sensor, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:sequence, Sequence, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + + belongs_to(:tool, Tool, + foreign_key: :asset_local_id, + type: :binary_id, + references: :local_id, + define_field: false + ) + end + + def changeset(local_meta, params \\ %{}) do + local_meta + |> cast(params, [:table, :status]) + |> validate_required([:asset_local_id, :table]) + |> validate_inclusion(:status, ~w(dirty)) + |> validate_inclusion(:table, [ + "devices", + "tools", + "peripherals", + "sensors", + "sensor_readings", + "sequences", + "regimens", + "pin_bindings", + "points", + "farm_events", + "firmware_configs", + "fbos_configs", + "farmware_installations", + "farmware_envs", + "diagnostic_dumps" + ]) + |> unsafe_validate_unique([:table, :asset_local_id], Farmbot.Repo, + message: "LocalMeta already exists." + ) + |> unique_constraint(:table, name: :local_metas_table_asset_local_id_index) + end +end diff --git a/farmbot_core/lib/asset/regimen.ex b/farmbot_core/lib/asset/regimen.ex new file mode 100644 index 00000000..6d8e8fdf --- /dev/null +++ b/farmbot_core/lib/asset/regimen.ex @@ -0,0 +1,63 @@ +defmodule Farmbot.Asset.Regimen do + @moduledoc """ + A Regimen is a schedule to run sequences on. + """ + + use Farmbot.Asset.Schema, path: "/api/regimens" + + defmodule Item do + use Ecto.Schema + + @primary_key false + @behaviour Farmbot.Asset.View + import Farmbot.Asset.View, only: [view: 2] + + view regimen_item do + %{ + time_offset: regimen_item.time_offset, + sequence_id: regimen_item.sequence_id + } + end + + embedded_schema do + field(:time_offset, :integer) + # Can't use real references here. + field(:sequence_id, :id) + end + + def changeset(item, params \\ %{}) do + item + |> cast(params, [:time_offset, :sequence_id]) + |> validate_required([]) + end + end + + schema "regimens" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:name, :string) + embeds_many(:regimen_items, Item, on_replace: :delete) + timestamps() + end + + view regimen do + %{ + id: regimen.id, + name: regimen.name, + regimen_items: Enum.map(regimen.items, &Item.render(&1)) + } + end + + def changeset(regimen, params \\ %{}) do + regimen + |> cast(params, [:id, :name, :created_at, :updated_at]) + |> cast_embed(:regimen_items) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/repo.ex b/farmbot_core/lib/asset/repo.ex new file mode 100644 index 00000000..de5ecbf7 --- /dev/null +++ b/farmbot_core/lib/asset/repo.ex @@ -0,0 +1,4 @@ +defmodule Farmbot.Asset.Repo do + @moduledoc "Repo for storing Asset data." + use Ecto.Repo, otp_app: :farmbot_core +end diff --git a/farmbot_core/lib/asset/schema.ex b/farmbot_core/lib/asset/schema.ex new file mode 100644 index 00000000..e69e8403 --- /dev/null +++ b/farmbot_core/lib/asset/schema.ex @@ -0,0 +1,29 @@ +defmodule Farmbot.Asset.Schema do + @moduledoc """ + Common Schema attributes. + """ + + @doc false + defmacro __using__(opts) do + quote do + use Ecto.Schema + import Ecto.Changeset + + @behaviour Farmbot.Asset.Schema + @behaviour Farmbot.Asset.View + + import Farmbot.Asset.View, only: [view: 2] + + @doc "Path on the Farmbot Web API" + def path, do: Keyword.fetch!(unquote(opts), :path) + @primary_key {:local_id, :binary_id, autogenerate: true} + @timestamps_opts inserted_at: :created_at, type: :utc_datetime + end + end + + @doc "API path for HTTP requests." + @callback path() :: Path.t() + + @doc "Apply params to a changeset or object." + @callback changeset(map, map) :: Ecto.Changeset.t() +end diff --git a/farmbot_core/lib/asset/sensor.ex b/farmbot_core/lib/asset/sensor.ex new file mode 100644 index 00000000..738294eb --- /dev/null +++ b/farmbot_core/lib/asset/sensor.ex @@ -0,0 +1,37 @@ +defmodule Farmbot.Asset.Sensor do + @moduledoc """ + Sensors are descriptors for pins/modes. + """ + + use Farmbot.Asset.Schema, path: "/api/sensors" + + schema "sensors" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:pin, :integer) + field(:mode, :integer) + field(:label, :string) + timestamps() + end + + view sensor do + %{ + id: sensor.id, + pin: sensor.pin, + mode: sensor.mode, + label: sensor.label + } + end + + def changeset(sensor, params \\ %{}) do + sensor + |> cast(params, [:id, :pin, :mode, :label, :created_at, :updated_at]) + |> validate_required([:id, :pin, :mode, :label]) + end +end diff --git a/farmbot_core/lib/asset/sensor_reading.ex b/farmbot_core/lib/asset/sensor_reading.ex new file mode 100644 index 00000000..685f6313 --- /dev/null +++ b/farmbot_core/lib/asset/sensor_reading.ex @@ -0,0 +1,44 @@ +defmodule Farmbot.Asset.SensorReading do + @moduledoc """ + SensorReadings are descriptors for pins/modes. + """ + + use Farmbot.Asset.Schema, path: "/api/sensor_readings" + + schema "sensor_readings" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:mode, :integer) + field(:pin, :integer) + field(:value, :integer) + field(:x, :float) + field(:y, :float) + field(:z, :float) + timestamps() + end + + view sensor_reading do + %{ + id: sensor_reading.id, + mode: sensor_reading.mode, + pin: sensor_reading.pin, + value: sensor_reading.value, + x: sensor_reading.x, + y: sensor_reading.y, + z: sensor_reading.z, + created_at: sensor_reading.created_at + } + end + + def changeset(sensor, params \\ %{}) do + sensor + |> cast(params, [:id, :mode, :pin, :value, :x, :y, :z, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/sequence.ex b/farmbot_core/lib/asset/sequence.ex new file mode 100644 index 00000000..18393349 --- /dev/null +++ b/farmbot_core/lib/asset/sequence.ex @@ -0,0 +1,39 @@ +defmodule Farmbot.Asset.Sequence do + @moduledoc """ + Sequences are "code" that FarmbotOS can Execute. + """ + + use Farmbot.Asset.Schema, path: "/api/sequences" + + schema "sequences" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:name, :string) + field(:kind, :string) + field(:args, :map) + field(:body, {:array, :map}) + timestamps() + end + + view sequence do + %{ + id: sequence.id, + name: sequence.name, + kind: sequence.kind, + args: sequence.args, + body: sequence.body + } + end + + def changeset(device, params \\ %{}) do + device + |> cast(params, [:id, :args, :name, :kind, :body, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/supervisor.ex b/farmbot_core/lib/asset/supervisor.ex new file mode 100644 index 00000000..95ba4e8f --- /dev/null +++ b/farmbot_core/lib/asset/supervisor.ex @@ -0,0 +1,20 @@ +defmodule Farmbot.Asset.Supervisor do + @moduledoc false + use Supervisor + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) + end + + def init([]) do + children = [ + Farmbot.Asset.Repo, + {Farmbot.AssetSupervisor, module: Farmbot.Asset.PersistentRegimen, preload: [:farm_event, :regimen]}, + {Farmbot.AssetSupervisor, module: Farmbot.Asset.FarmEvent}, + {Farmbot.AssetSupervisor, module: Farmbot.Asset.PinBinding}, + {Farmbot.AssetSupervisor, module: Farmbot.Asset.Peripheral}, + Farmbot.AssetMonitor + ] + Supervisor.init(children, [strategy: :one_for_one]) + end +end diff --git a/farmbot_core/lib/asset/sync.ex b/farmbot_core/lib/asset/sync.ex new file mode 100644 index 00000000..84bc440a --- /dev/null +++ b/farmbot_core/lib/asset/sync.ex @@ -0,0 +1,101 @@ +defmodule Elixir.Farmbot.Asset.Sync do + @moduledoc """ + """ + + use Farmbot.Asset.Schema, path: "/api/device/sync" + + defmodule Item do + @moduledoc false + use Ecto.Schema + + @primary_key false + @behaviour Farmbot.Asset.View + import Farmbot.Asset.View, only: [view: 2] + + view sync_item do + %{ + id: sync_item.id, + updated_at: sync_item.updated_at + } + end + + embedded_schema do + field(:id, :id) + field(:updated_at, :utc_datetime) + end + + def changeset(item, params \\ %{}) + + def changeset(item, [id, updated_at]) do + changeset(item, %{id: id, updated_at: updated_at}) + end + + def changeset(item, params) do + item + |> cast(params, [:id, :updated_at]) + |> validate_required([]) + end + end + + schema "syncs" do + embeds_many(:devices, Item) + embeds_many(:firmware_configs, Item) + embeds_many(:fbos_configs, Item) + embeds_many(:diagnostic_dumps, Item) + embeds_many(:farm_events, Item) + embeds_many(:farmware_envs, Item) + embeds_many(:farmware_installations, Item) + embeds_many(:peripherals, Item) + embeds_many(:pin_bindings, Item) + embeds_many(:points, Item) + embeds_many(:regimens, Item) + embeds_many(:sensor_readings, Item) + embeds_many(:sensors, Item) + embeds_many(:sequences, Item) + embeds_many(:tools, Item) + field(:now, :utc_datetime) + timestamps() + end + + view sync do + %{ + devices: Enum.map(sync.device, &Item.render/1), + fbos_configs: Enum.map(sync.fbos_config, &Item.render/1), + firmware_configs: Enum.map(sync.firmware_config, &Item.render/1), + diagnostic_dumps: Enum.map(sync.diagnostic_dumps, &Item.render/1), + farm_events: Enum.map(sync.farm_events, &Item.render/1), + farmware_envs: Enum.map(sync.farmware_envs, &Item.render/1), + farmware_installations: Enum.map(sync.farmware_installations, &Item.render/1), + peripherals: Enum.map(sync.peripherals, &Item.render/1), + pin_bindings: Enum.map(sync.pin_bindings, &Item.render/1), + points: Enum.map(sync.points, &Item.render/1), + regimens: Enum.map(sync.regimens, &Item.render/1), + sensor_readings: Enum.map(sync.sensor_readings, &Item.render/1), + sensors: Enum.map(sync.sensors, &Item.render/1), + sequences: Enum.map(sync.sequences, &Item.render/1), + tools: Enum.map(sync.tools, &Item.render/1), + now: sync.now + } + end + + def changeset(sync, params \\ %{}) do + sync + |> cast(params, [:now]) + |> cast_embed(:devices) + |> cast_embed(:fbos_configs) + |> cast_embed(:firmware_configs) + |> cast_embed(:diagnostic_dumps) + |> cast_embed(:farm_events) + |> cast_embed(:farmware_envs) + |> cast_embed(:farmware_installations) + |> cast_embed(:peripherals) + |> cast_embed(:pin_bindings) + |> cast_embed(:points) + |> cast_embed(:regimens) + |> cast_embed(:sensor_readings) + |> cast_embed(:sensors) + |> cast_embed(:sequences) + |> cast_embed(:tools) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/tool.ex b/farmbot_core/lib/asset/tool.ex new file mode 100644 index 00000000..ecfda1b1 --- /dev/null +++ b/farmbot_core/lib/asset/tool.ex @@ -0,0 +1,31 @@ +defmodule Farmbot.Asset.Tool do + @moduledoc "A Tool is an item that lives in a ToolSlot" + + use Farmbot.Asset.Schema, path: "/api/tools" + + schema "tools" do + field(:id, :id) + + has_one(:local_meta, Farmbot.Asset.Private.LocalMeta, + on_delete: :delete_all, + references: :local_id, + foreign_key: :asset_local_id + ) + + field(:name, :string) + timestamps() + end + + view tool do + %{ + id: tool.id, + name: tool.name + } + end + + def changeset(tool, params \\ %{}) do + tool + |> cast(params, [:id, :name, :created_at, :updated_at]) + |> validate_required([]) + end +end diff --git a/farmbot_core/lib/asset/view.ex b/farmbot_core/lib/asset/view.ex new file mode 100644 index 00000000..c85ecffa --- /dev/null +++ b/farmbot_core/lib/asset/view.ex @@ -0,0 +1,15 @@ +defmodule Farmbot.Asset.View do + @doc "Format data to be JSON encodable." + @callback render(map) :: map + + @doc "Delegates rendering to an asset's `render/1` function." + @spec render(module, map) :: map + def render(module, object), do: module.render(object) + + @doc "Helper to define a `render/1` function" + defmacro view(data, block) do + quote do + def render(unquote(data)), unquote(block) + end + end +end diff --git a/farmbot_core/lib/asset_monitor.ex b/farmbot_core/lib/asset_monitor.ex new file mode 100644 index 00000000..8c1dc93a --- /dev/null +++ b/farmbot_core/lib/asset_monitor.ex @@ -0,0 +1,80 @@ +defmodule Farmbot.AssetMonitor do + use GenServer + import Farmbot.TimeUtils, only: [compare_datetimes: 2] + alias Farmbot.Asset.{ + Repo, + FarmEvent, + Peripheral, + PersistentRegimen, + PinBinding + } + require Logger + + @checkup_time_ms Application.get_env(:farmbot_core, __MODULE__)[:checkup_time_ms] + @checkup_time_ms || Mix.raise(""" + config :farmbot_core, #{__MODULE__}, checkup_time_ms: 30_000 + """) + + @doc false + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + # This is helpful for tests, but should probably be avoided + @doc false + def force_checkup do + GenServer.call(__MODULE__, :force_checkup) + end + + def init(_args) do + state = Map.new(order(), fn(module) -> {module, %{}} end) + state = Map.put(state, :order, order()) + state = Map.put(state, :force_caller, nil) + {:ok, state, 0} + end + + def handle_call(:force_checkup, caller, state) do + {:noreply, %{state | force_caller: caller}, 0} + end + + def handle_info(:timeout, %{order: []} = state) do + if state.force_caller, do: GenServer.reply(state.force_caller, :ok) + {:noreply, %{state | order: order(), force_caller: nil}, @checkup_time_ms} + end + + def handle_info(:timeout, state) do + [kind | rest] = state.order + results = handle_kind(kind, state[kind]) + {:noreply, %{state | kind => results, order: rest}, 0} + end + + def handle_kind(kind, sub_state) do + expected = Repo.all(kind) + expected_ids = Enum.map(expected, &Map.fetch!(&1, :local_id)) + actual_ids = Enum.map(sub_state, fn({local_id, _}) -> local_id end) + deleted_ids = actual_ids -- expected_ids + sub_state = Map.drop(sub_state, deleted_ids) + Enum.each(deleted_ids, fn(local_id) -> + Logger.error "#{inspect kind} #{local_id} needs to be terminated" + Farmbot.AssetSupervisor.terminate_child(kind, local_id) + end) + + Enum.reduce(expected, sub_state, fn(%{local_id: id, updated_at: updated_at} = asset, sub_state) -> + cond do + is_nil(sub_state[id]) -> + Logger.debug "#{inspect kind} #{id} needs to be started" + Farmbot.AssetSupervisor.start_child(asset) + Map.put(sub_state, id, updated_at) + compare_datetimes(updated_at, sub_state[id]) == :gt -> + Logger.warn "#{inspect kind} #{id} needs to be updated" + Farmbot.AssetSupervisor.update_child(asset) + Map.put(sub_state, id, updated_at) + true -> + sub_state + end + end) + + end + + def order, do: [FarmEvent, Peripheral, PersistentRegimen, PinBinding] +end diff --git a/farmbot_core/lib/asset_storage/asset.ex b/farmbot_core/lib/asset_storage/asset.ex deleted file mode 100644 index becfc21f..00000000 --- a/farmbot_core/lib/asset_storage/asset.ex +++ /dev/null @@ -1,191 +0,0 @@ -defmodule Farmbot.Asset do - @moduledoc """ - API for inserting and retrieving assets. - """ - - alias Farmbot.Asset - - alias Asset.{ - Repo, - - Device, - FarmEvent, - FarmwareEnv, - FarmwareInstallation, - Peripheral, - PinBinding, - Point, - Regimen, - Sensor, - Sequence, - Tool, - - SyncCmd, - PersistentRegimen - } - - alias Repo.Snapshot - require Farmbot.Logger - import Ecto.Query - import Farmbot.Config, only: [update_config_value: 4] - require Logger - - defdelegate to_asset(data, kind), to: Farmbot.Asset.Converter - - def fragment_sync(_), do: :ok - def full_sync(_, _fun), do: :ok - - @doc "Information about _this_ device." - def device do - case Repo.all(Device) do - [device] -> device - [] -> nil - devices when is_list(devices) -> - Repo.delete_all(Device) - raise "There should only ever be 1 device!" - end - end - - @doc "Get all pin bindings." - def all_pin_bindings do - Repo.all(PinBinding) - end - - @doc "Get all Persistent Regimens" - def all_persistent_regimens do - Repo.all(PersistentRegimen) - end - - def persistent_regimens(%Regimen{id: id} = _regimen) do - Repo.all(from pr in PersistentRegimen, where: pr.regimen_id == ^id) - end - - def persistent_regimen(%Regimen{id: id, farm_event_id: fid} = _regimen) do - fid || raise "Can't look up persistent regimens without a farm_event id." - Repo.one(from pr in PersistentRegimen, where: pr.regimen_id == ^id and pr.farm_event_id == ^fid) - end - - @doc "Add a new Persistent Regimen." - def add_persistent_regimen(%Regimen{id: id, farm_event_id: fid} = _regimen, time) do - fid || raise "Can't save persistent regimens without a farm_event id." - PersistentRegimen.changeset(struct(PersistentRegimen, %{regimen_id: id, time: time, farm_event_id: fid})) - |> Repo.insert() - end - - @doc "Delete a persistent_regimen based on it's regimen id and farm_event id." - def delete_persistent_regimen(%Regimen{id: regimen_id, farm_event_id: fid} = _regimen) do - fid || raise "cannot delete persistent_regimen without farm_event_id" - itm = Repo.one(from pr in PersistentRegimen, where: pr.regimen_id == ^regimen_id and pr.farm_event_id == ^fid) - if itm, do: Repo.delete(itm), else: nil - end - - def update_persistent_regimen_time(%Regimen{id: _regimen_id, farm_event_id: _fid} = regimen, %DateTime{} = time) do - pr = persistent_regimen(regimen) - if pr do - pr = Ecto.Changeset.change pr, time: time - Repo.update!(pr) - else - nil - end - end - - @doc "Get a Peripheral by it's id." - def get_peripheral_by_id(peripheral_id) do - Repo.one(from(p in Peripheral, where: p.id == ^peripheral_id)) - end - - @doc "Get a peripheral by it's pin." - def get_peripheral_by_number(number) do - Repo.one(from(p in Peripheral, where: p.pin == ^number)) - end - - @doc "Get a Sensor by it's id." - def get_sensor_by_id(sensor_id) do - Repo.one(from(s in Sensor, where: s.id == ^sensor_id)) - end - - @doc "Get a peripheral by it's pin." - def get_sensor_by_number(number) do - Repo.one(from(s in Sensor, where: s.pin == ^number)) - end - - @doc "Get a Sequence by it's id." - def get_sequence_by_id(sequence_id) do - Repo.one(from(s in Sequence, where: s.id == ^sequence_id)) - end - - @doc "Same as `get_sequence_by_id/1` but raises if no Sequence is found." - def get_sequence_by_id!(sequence_id) do - case get_sequence_by_id(sequence_id) do - nil -> raise "Could not find sequence by id #{sequence_id}" - %Sequence{} = seq -> seq - end - end - - @doc "Get a Point by it's id." - def get_point_by_id(point_id) do - Repo.one(from(p in Point, where: p.id == ^point_id)) - end - - @doc "Get a Tool from a Point by `tool_id`." - def get_point_from_tool(tool_id) do - Repo.one(from(p in Point, where: p.tool_id == ^tool_id)) - end - - @doc "Get a Tool by it's id." - def get_tool_by_id(tool_id) do - Repo.one(from(t in Tool, where: t.id == ^tool_id)) - end - - @doc "Get a Regimen by it's id." - def get_regimen_by_id(regimen_id, farm_event_id) do - reg = Repo.one(from(r in Regimen, where: r.id == ^regimen_id)) - - if reg do - %{reg | farm_event_id: farm_event_id} - else - nil - end - end - - @doc "Same as `get_regimen_by_id/1` but raises if no Regimen is found." - def get_regimen_by_id!(regimen_id, farm_event_id) do - case get_regimen_by_id(regimen_id, farm_event_id) do - nil -> raise "Could not find regimen by id #{regimen_id}" - %Regimen{} = reg -> reg - end - end - - @doc "Fetches all regimens that use a particular sequence." - def get_regimens_using_sequence(sequence_id) do - uses_seq = &match?(^sequence_id, Map.fetch!(&1, "sequence_id")) - - Repo.all(Regimen) - |> Enum.filter(&Enum.find(Map.fetch!(&1, :regimen_items), uses_seq)) - end - - def get_farm_event_by_id(feid) do - Repo.one(from(fe in FarmEvent, where: fe.id == ^feid)) - end - - @doc "Clear all data stored in the Asset Repo." - def clear_all_data do - # remote assets. - Repo.delete_all(Device) - Repo.delete_all(FarmEvent) - Repo.delete_all(FarmwareEnv) - Repo.delete_all(FarmwareInstallation) - Repo.delete_all(Peripheral) - Repo.delete_all(PinBinding) - Repo.delete_all(Point) - Repo.delete_all(Regimen) - Repo.delete_all(Sensor) - Repo.delete_all(Sequence) - Repo.delete_all(Tool) - - # Interanal assets. - Repo.delete_all(PersistentRegimen) - Repo.delete_all(SyncCmd) - :ok - end -end diff --git a/farmbot_core/lib/asset_storage/asset_logger.ex b/farmbot_core/lib/asset_storage/asset_logger.ex deleted file mode 100644 index b67c604a..00000000 --- a/farmbot_core/lib/asset_storage/asset_logger.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.Asset.Logger do - use GenServer - require Logger - - def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - Farmbot.Registry.subscribe() - {:ok, %{status: :undefined}} - end - - def handle_info({Farmbot.Registry, {Farmbot.Asset, {:sync_status, status}}}, %{status: status} = state) do - {:noreply, state} - end - - def handle_info({Farmbot.Registry, {Farmbot.Asset, {:sync_status, status}}}, state) do - Logger.debug "Asset sync_status #{state.status} => #{status}" - {:noreply, %{state | status: status}} - end - - def handle_info({Farmbot.Registry, {Farmbot.Asset, {action, data}}}, state) do - Logger.debug "Asset #{action} #{inspect data}" - {:noreply, state} - end - - def handle_info({Farmbot.Registry, {_ns, _data}}, state) do - {:noreply, state} - end -end diff --git a/farmbot_core/lib/asset_storage/converter.ex b/farmbot_core/lib/asset_storage/converter.ex deleted file mode 100644 index f4a51617..00000000 --- a/farmbot_core/lib/asset_storage/converter.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Farmbot.Asset.Converter do - alias Asset.{ - Device, - FarmEvent, - FarmwareEnv, - FarmwareInstallation, - Peripheral, - PinBinding, - Point, - Regimen, - Sensor, - Sequence, - Tool, - } - - @device_fields ~W(id name timezone) - @farm_events_fields ~W(calendar end_time executable_id executable_type id repeat start_time time_unit) - @farmware_envs_fields ~W(id key value) - @farmware_installations_fields ~W(id url first_party) - @peripherals_fields ~W(id label mode pin) - @pin_bindings_fields ~W(id pin_num sequence_id special_action) - @points_fields ~W(id meta name pointer_type tool_id x y z) - @regimens_fields ~W(farm_event_id id name regimen_items) - @sensors_fields ~W(id label mode pin) - @sequences_fields ~W(args body id kind name) - @tools_fields ~W(id name) - - @doc "Converts data to Farmbot Asset types." - def to_asset(body, kind) when is_binary(kind) do - camel_kind = Module.concat(["Farmbot", "Asset", Macro.camelize(kind)]) - to_asset(body, camel_kind) - end - - def to_asset(body, Device), do: resource_decode(body, @device_fields, Device) - def to_asset(body, FarmEvent), do: resource_decode(body, @farm_events_fields, FarmEvent) - def to_asset(body, FarmwareEnv), do: resource_decode(body, @farmware_envs_fields, FarmwareEnv) - def to_asset(body, FarmwareInstallation), do: resource_decode(body, @farmware_installations_fields, FarmwareInstallation) - def to_asset(body, Peripheral), do: resource_decode(body, @peripherals_fields, Peripheral) - def to_asset(body, PinBinding), do: resource_decode(body, @pin_bindings_fields, PinBinding) - def to_asset(body, Point), do: resource_decode(body, @points_fields, Point) - def to_asset(body, Regimen), do: resource_decode(body, @regimens_fields, Regimen) - def to_asset(body, Sensor), do: resource_decode(body, @sensors_fields, Sensor) - def to_asset(body, Sequence), do: resource_decode(body, @sequences_fields, Sequence) - def to_asset(body, Tool), do: resource_decode(body, @tools_fields, Tool) - - defp resource_decode(data, fields, kind) when is_list(data), - do: Enum.map(data, &resource_decode(&1, fields, kind)) - - defp resource_decode(data, fields, kind) do - data - |> Map.take(fields) - |> Enum.map(&string_to_atom/1) - |> into_struct(kind) - end - - defp string_to_atom({k, v}), do: {String.to_atom(k), v} - defp into_struct(data, kind), do: struct(kind, data) -end diff --git a/farmbot_core/lib/asset_storage/device.ex b/farmbot_core/lib/asset_storage/device.ex deleted file mode 100644 index 9f3e2514..00000000 --- a/farmbot_core/lib/asset_storage/device.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Farmbot.Asset.Device do - @moduledoc """ - The current device. Should only ever be _one_ of these. If not there is a huge - problem probably higher up the stack. - """ - - alias Farmbot.Asset.Device - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "devices" do - field(:id, :integer) - field(:name, :string) - field(:timezone, :string) - end - - @required_fields [:id, :name] - - def changeset(%Device{} = device, params \\ %{}) do - device - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_storage/farm_event.ex b/farmbot_core/lib/asset_storage/farm_event.ex deleted file mode 100644 index e7474125..00000000 --- a/farmbot_core/lib/asset_storage/farm_event.ex +++ /dev/null @@ -1,125 +0,0 @@ -defmodule Farmbot.Asset.FarmEvent do - @moduledoc """ - FarmEvent's are events that happen on a schedule. - When it is time for the event to execute one of several things may happen: - - * A Regimen gets started. - * A Sequence will execute. - """ - - @on_load :load_nif - def load_nif do - require Elixir.Logger - nif_file = '#{:code.priv_dir(:farmbot_core)}/build_calendar' - - case :erlang.load_nif(nif_file, 0) do - :ok -> :ok - {:error, {:reload, _}} -> :ok - {:error, reason} -> Elixir.Logger.warn("Failed to load nif: #{inspect(reason)}") - end - end - - @callback schedule_event(map, DateTime.t) :: any - - alias Farmbot.Asset.FarmEvent - alias Farmbot.EctoTypes.ModuleType - alias Farmbot.EctoTypes.TermType - - use Ecto.Schema - import Ecto.Changeset - require Farmbot.Logger - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "farm_events" do - field(:id, :integer) - field(:start_time, :string) - field(:end_time, :string) - field(:repeat, :integer) - field(:time_unit, :string) - field(:executable_type, ModuleType.FarmEvent) - field(:executable_id, :integer) - field(:calendar, TermType) - end - - @required_fields [ - :id, - :start_time, - :end_time, - :repeat, - :time_unit, - :executable_type, - :executable_id - ] - - def changeset(%FarmEvent{} = farm_event, params \\ %{}) do - farm_event - |> build_calendar - |> cast(params, @required_fields ++ [:calendar]) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end - - @compile {:inline, [build_calendar: 1]} - def build_calendar(%FarmEvent{executable_type: Farmbot.Asset.Regimen} = fe), - do: fe - - def build_calendar(%FarmEvent{calendar: nil} = fe), - do: build_calendar(%{fe | calendar: []}) - - def build_calendar(%FarmEvent{time_unit: "never"} = fe), do: %{fe | calendar: [fe.start_time]} - - def build_calendar(%FarmEvent{calendar: calendar} = fe) - when is_list(calendar) do - current_time_seconds = :os.system_time(:second) - - start_time_seconds = - DateTime.from_iso8601(fe.start_time) - |> elem(1) - |> DateTime.to_unix(:second) - - end_time_seconds = - DateTime.from_iso8601(fe.end_time) |> elem(1) |> DateTime.to_unix(:second) - - repeat = fe.repeat - repeat_frequency_seconds = time_unit_to_seconds(fe.time_unit) - - new_calendar = - do_build_calendar( - current_time_seconds, - start_time_seconds, - end_time_seconds, - repeat, - repeat_frequency_seconds - ) - |> Enum.map(&DateTime.from_unix!(&1)) - |> Enum.map(&DateTime.to_iso8601(&1)) - - %{fe | calendar: new_calendar} - end - - # This should be replaced. YOU WILL KNOW if not. - def do_build_calendar( - now_seconds, - start_time_seconds, - end_time_seconds, - repeat, - repeat_frequency_seconds - ) do - Farmbot.Logger.error(1, "Using (very) slow calendar builder!") - grace_period_cutoff_seconds = now_seconds - 60 - - Range.new(start_time_seconds, end_time_seconds) - |> Enum.take_every(repeat * repeat_frequency_seconds) - |> Enum.filter(&Kernel.>(&1, grace_period_cutoff_seconds)) - |> Enum.take(3) - |> Enum.map(&Kernel.-(&1, div(&1, 60))) - end - - @compile {:inline, [time_unit_to_seconds: 1]} - defp time_unit_to_seconds("minutely"), do: 60 - defp time_unit_to_seconds("hourly"), do: 60 * 60 - defp time_unit_to_seconds("daily"), do: 60 * 60 * 24 - defp time_unit_to_seconds("weekly"), do: 60 * 60 * 24 * 7 - defp time_unit_to_seconds("monthly"), do: 60 * 60 * 24 * 30 - defp time_unit_to_seconds("yearly"), do: 60 * 60 * 24 * 365 -end diff --git a/farmbot_core/lib/asset_storage/farmware_env.ex b/farmbot_core/lib/asset_storage/farmware_env.ex deleted file mode 100644 index 7d4f9c53..00000000 --- a/farmbot_core/lib/asset_storage/farmware_env.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Farmbot.Asset.FarmwareEnv do - @moduledoc """ - Environment key/value store for Farmware. - """ - - alias Farmbot.Asset.FarmwareEnv - alias Farmbot.EctoTypes.JSONType - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "farmware_envs" do - field(:id, :integer) - field(:key, :string) - field(:value, JSONType) - end - - @required_fields [:id, :key, :value] - - def changeset(%FarmwareEnv{} = fwe, params \\ %{}) do - fwe - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_storage/farmware_installation.ex b/farmbot_core/lib/asset_storage/farmware_installation.ex deleted file mode 100644 index b27f655a..00000000 --- a/farmbot_core/lib/asset_storage/farmware_installation.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Farmbot.Asset.FarmwareInstallation do - @moduledoc """ - Farmware installation url. - """ - - alias Farmbot.Asset.FarmwareInstallation - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "farmware_installations" do - field(:id, :integer) - field(:first_party, :boolean) - field(:url, :string) - end - - @required_fields [:id, :url] - - def changeset(%FarmwareInstallation{} = fwi, params \\ %{}) do - fwi - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_storage/on_start_task.ex b/farmbot_core/lib/asset_storage/on_start_task.ex deleted file mode 100644 index d944f4b3..00000000 --- a/farmbot_core/lib/asset_storage/on_start_task.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Farmbot.Asset.OnStartTask do - alias Farmbot.Asset.Repo - alias Repo.Snapshot - -require Logger - - @doc false - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :dispatch, opts}, - type: :worker, - restart: :transient, - shutdown: 500 - } - end - - def dispatch do - # old = %Snapshot{} - # new = Repo.snapshot() - # diff = Snapshot.diff(old, new) - # Farmbot.Asset.dispatch_sync(diff) - :ignore - end -end diff --git a/farmbot_core/lib/asset_storage/peripheral.ex b/farmbot_core/lib/asset_storage/peripheral.ex deleted file mode 100644 index 37180bca..00000000 --- a/farmbot_core/lib/asset_storage/peripheral.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Farmbot.Asset.Peripheral do - @moduledoc """ - Peripherals are descriptors for pins/modes. - """ - - alias Farmbot.Asset.Peripheral - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "peripherals" do - field(:id, :integer) - field(:pin, :integer) - field(:mode, :integer) - field(:label, :string) - end - - @required_fields [:id, :pin, :mode, :label] - - def changeset(%Peripheral{} = peripheral, params \\ %{}) do - peripheral - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_storage/persistent_regimen.ex b/farmbot_core/lib/asset_storage/persistent_regimen.ex deleted file mode 100644 index ba62b655..00000000 --- a/farmbot_core/lib/asset_storage/persistent_regimen.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Farmbot.Asset.PersistentRegimen do - @moduledoc """ - A persistent regimen is a join between a started farm event and the regimen - it is set to operate on. These are stored in the database to persist reboots, - crashes etc. - """ - - alias Farmbot.Asset.PersistentRegimen - use Ecto.Schema - import Ecto.Changeset - - schema "persistent_regimens" do - field :regimen_id, :integer - field :farm_event_id, :integer - field :time, :utc_datetime - timestamps() - end - - @required_fields [:regimen_id, :farm_event_id, :time] - - def changeset(%PersistentRegimen{} = pr, params \\ %{}) do - pr - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:regimen_start_time, name: :regimen_start_time) - end -end diff --git a/farmbot_core/lib/asset_storage/pin_binding.ex b/farmbot_core/lib/asset_storage/pin_binding.ex deleted file mode 100644 index 89aaa472..00000000 --- a/farmbot_core/lib/asset_storage/pin_binding.ex +++ /dev/null @@ -1,34 +0,0 @@ -defmodule Farmbot.Asset.PinBinding do - @moduledoc """ - When a pin binding is triggered a sequence fires. - """ - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "pin_bindings" do - field(:id, :integer) - field(:pin_num, :integer) - field(:sequence_id, :integer) - field(:special_action, :string) - end - - @required_fields [:id, :pin_num] - - def changeset(pin_binding, params \\ %{}) do - pin_binding - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> validate_pin_num() - |> unique_constraint(:id) - |> unique_constraint(:pin_num) - end - - def validate_pin_num(changeset) do - if get_field(changeset, :pin_num, -1) in [17, 23, 27, 06, 21, 24, 25, 12, 13] do - add_error(changeset, :pin_num, "in use") - else - changeset - end - end -end diff --git a/farmbot_core/lib/asset_storage/point.ex b/farmbot_core/lib/asset_storage/point.ex deleted file mode 100644 index 85ca22f1..00000000 --- a/farmbot_core/lib/asset_storage/point.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Farmbot.Asset.Point do - @moduledoc "A Point is a location in the planting bed as denoted by X Y and Z." - - alias Farmbot.Asset.Point - use Ecto.Schema - import Ecto.Changeset - alias Farmbot.EctoTypes.ModuleType - alias Farmbot.EctoTypes.TermType - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "points" do - field(:id, :integer) - field(:name, :string) - field(:tool_id, :integer) - field(:x, :float) - field(:y, :float) - field(:z, :float) - field(:meta, TermType) - field(:pointer_type, ModuleType.Point) - end - - @required_fields [:id, :name, :x, :y, :z, :meta, :pointer_type] - @optional_fields [:tool_id] - - def changeset(%Point{} = point, params \\ %{}) do - point - |> cast(params, @required_fields ++ @optional_fields) - |> validate_required(@required_fields) - end -end diff --git a/farmbot_core/lib/asset_storage/regimen.ex b/farmbot_core/lib/asset_storage/regimen.ex deleted file mode 100644 index cbdd11c8..00000000 --- a/farmbot_core/lib/asset_storage/regimen.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Farmbot.Asset.Regimen do - @moduledoc """ - A Regimen is a schedule to run sequences on. - """ - - alias Farmbot.Asset.Regimen - alias Farmbot.EctoTypes.TermType - alias Farmbot.Regimen.NameProvider - alias Farmbot.Regimen.Supervisor, as: RegimenSupervisor - - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "regimens" do - field(:id, :integer) - field(:name, :string) - field(:farm_event_id, :integer, virtual: true) - field(:regimen_items, TermType) - end - - @type item :: %{ - name: String.t(), - time_offset: integer, - sequence_id: integer - } - - @type t :: %__MODULE__{ - name: String.t(), - regimen_items: [item] - } - - @required_fields [:id, :name, :regimen_items] - - def changeset(%Regimen{} = regimen, params \\ %{}) do - regimen - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end - - @behaviour Farmbot.Asset.FarmEvent - def schedule_event(%Regimen{} = regimen, now) do - name = NameProvider.via(regimen) - case GenServer.whereis(name) do - nil -> {:ok, _pid} = RegimenSupervisor.add_child(regimen, now) - pid -> {:ok, pid} - end - end -end diff --git a/farmbot_core/lib/asset_storage/repo/module_type/farm_event.ex b/farmbot_core/lib/asset_storage/repo/module_type/farm_event.ex deleted file mode 100644 index 43074d4f..00000000 --- a/farmbot_core/lib/asset_storage/repo/module_type/farm_event.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Farmbot.EctoTypes.ModuleType.FarmEvent do - @moduledoc false - use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(Sequence Regimen) -end diff --git a/farmbot_core/lib/asset_storage/repo/module_type/point.ex b/farmbot_core/lib/asset_storage/repo/module_type/point.ex deleted file mode 100644 index d1b12faf..00000000 --- a/farmbot_core/lib/asset_storage/repo/module_type/point.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Farmbot.EctoTypes.ModuleType.Point do - @moduledoc false - use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(GenericPointer ToolSlot Plant) -end diff --git a/farmbot_core/lib/asset_storage/repo/repo.ex b/farmbot_core/lib/asset_storage/repo/repo.ex deleted file mode 100644 index 80935e66..00000000 --- a/farmbot_core/lib/asset_storage/repo/repo.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Farmbot.Asset.Repo do - @moduledoc "Repo for storing Asset data." - require Farmbot.Logger - alias Farmbot.Asset.Repo.Snapshot - use Ecto.Repo, - otp_app: :farmbot_core, - adapter: Application.get_env(:farmbot_core, __MODULE__)[:adapter] - - alias Farmbot.Asset.{ - Device, - FarmEvent, - FarmwareEnv, - FarmwareInstallation, - Peripheral, - PinBinding, - Point, - Regimen, - Sensor, - Sequence, - Tool, - } - - def snapshot do - results = Farmbot.Asset.Repo.all(Device) ++ - Farmbot.Asset.Repo.all(FarmEvent) ++ - Farmbot.Asset.Repo.all(FarmwareEnv) ++ - Farmbot.Asset.Repo.all(FarmwareInstallation) ++ - Farmbot.Asset.Repo.all(Peripheral) ++ - Farmbot.Asset.Repo.all(PinBinding) ++ - Farmbot.Asset.Repo.all(Point) ++ - Farmbot.Asset.Repo.all(Regimen) ++ - Farmbot.Asset.Repo.all(Sensor) ++ - Farmbot.Asset.Repo.all(Sequence) ++ - Farmbot.Asset.Repo.all(Tool) - - %Snapshot{data: results} - |> Snapshot.md5() - end -end diff --git a/farmbot_core/lib/asset_storage/repo/snapshot.ex b/farmbot_core/lib/asset_storage/repo/snapshot.ex deleted file mode 100644 index e37b5881..00000000 --- a/farmbot_core/lib/asset_storage/repo/snapshot.ex +++ /dev/null @@ -1,102 +0,0 @@ -defmodule Farmbot.Asset.Repo.Snapshot do - @moduledoc "Opaque data type. Hash of the entire Repo." - alias Farmbot.Asset.Repo.Snapshot - - defmodule Diff do - @moduledoc false - defstruct [ - additions: [], - deletions: [], - updates: [], - ] - end - - defstruct [data: [], hash: nil] - - def diff(%Snapshot{} = old, %Snapshot{} = new) do - struct(Diff, [ - additions: calculate_additions(old.data, new.data), - deletions: calculate_deletions(old.data, new.data), - updates: calculate_updates(old.data, new.data) - ]) - end - - def diff(%Snapshot{} = data) do - struct(Diff, [ - additions: calculate_additions([], data.data), - deletions: calculate_deletions([], data.data), - updates: [] - ]) - end - - defp calculate_additions(old, new) do - Enum.reduce(new, [], fn(new_object, acc) -> - maybe_old_object = Enum.find(old, fn(old_object) -> - is_correct_mod? = old_object.__struct__ == new_object.__struct__ - is_correct_id? = old_object.id == new_object.id - is_correct_mod? && is_correct_id? - end) - if maybe_old_object do - acc - else - [new_object | acc] - end - end) - end - - # We need all the items that are not in `new`, but were in `old` - defp calculate_deletions(old, new) do - Enum.reduce(old, [], fn(old_object, acc) -> - maybe_new_object = Enum.find(new, fn(new_object) -> - is_correct_mod? = old_object.__struct__ == new_object.__struct__ - is_correct_id? = old_object.id == new_object.id - is_correct_mod? && is_correct_id? - end) - - if maybe_new_object do - acc - else - [old_object | acc] - end - end) - end - - # We need all items that weren't added, or deleted. - defp calculate_updates(old, new) do - index = fn(%{__struct__: mod, id: id} = data) -> - {{mod, id}, data} - end - - old_index = Map.new(old, index) - new_index = Map.new(new, index) - a = Map.take(new_index, Map.keys(old_index)) - Enum.reduce(a, [], fn({key, val}, acc) -> - if old_index[key] != val do - [val | acc] - else - acc - end - end) - end - - def md5(%Snapshot{data: data} = snapshot) do - data - |> Enum.map(&:crypto.hash(:md5, inspect(&1))) - |> fn(data) -> - :crypto.hash(:md5, data) |> Base.encode16() - end.() - |> fn(hash) -> - %{snapshot | hash: hash} - end.() - end - - defimpl Inspect, for: Snapshot do - def inspect(%Snapshot{data: []}, _) do - "#Snapshot<[NULL]>" - end - - def inspect(%Snapshot{hash: hash}, _) when is_binary(hash) do - "#Snapshot<#{hash}>" - end - end -end diff --git a/farmbot_core/lib/asset_storage/sensor.ex b/farmbot_core/lib/asset_storage/sensor.ex deleted file mode 100644 index 98fea52a..00000000 --- a/farmbot_core/lib/asset_storage/sensor.ex +++ /dev/null @@ -1,26 +0,0 @@ -defmodule Farmbot.Asset.Sensor do - @moduledoc """ - Sensors are descriptors for pins/modes. - """ - - alias Farmbot.Asset.Sensor - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "sensors" do - field(:id, :integer) - field(:pin, :integer) - field(:mode, :integer) - field(:label, :string) - end - - @required_fields [:id, :pin, :mode, :label] - - def changeset(%Sensor{} = sensor, params \\ %{}) do - sensor - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_storage/sequence.ex b/farmbot_core/lib/asset_storage/sequence.ex deleted file mode 100644 index 6e7c7fa4..00000000 --- a/farmbot_core/lib/asset_storage/sequence.ex +++ /dev/null @@ -1,42 +0,0 @@ -defmodule Farmbot.Asset.Sequence do - @moduledoc """ - A Sequence is a list of CeleryScript nodes. - """ - - alias Farmbot.Asset.Sequence - alias Farmbot.EctoTypes.TermType - use Ecto.Schema - import Ecto.Changeset - require Farmbot.Logger - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "sequences" do - field(:id, :integer) - field(:name, :string) - field(:kind, :string) - field(:args, TermType) - field(:body, TermType) - end - - @required_fields [:id, :name, :kind, :args, :body] - - def changeset(%Sequence{} = sequence, params \\ %{}) do - sequence - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end - - @behaviour Farmbot.Asset.FarmEvent - def schedule_event(%Sequence{} = sequence, _now) do - Farmbot.Logger.busy 1, "[#{sequence.name}] Sequence init." - Farmbot.Core.CeleryScript.sequence(sequence, fn(result) -> - case result do - :ok -> - Farmbot.Logger.success 1, "[#{sequence.name}] Sequence complete." - {:error, _} -> - Farmbot.Logger.error 1, "[#{sequence.nam}] Sequece failed!" - end - end) - end -end diff --git a/farmbot_core/lib/asset_storage/settings.ex b/farmbot_core/lib/asset_storage/settings.ex deleted file mode 100644 index 63d2f4f8..00000000 --- a/farmbot_core/lib/asset_storage/settings.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Farmbot.Asset.Settings do - @moduledoc """ - Responsible for turning FbosConfig and FirmwareConfig into - local Farmbot.Config settings. - """ - alias Farmbot.Asset.Settings.{ - FbosConfig, - FirmwareConfig - } - - import Farmbot.Config, only: [get_config_as_map: 0] - - def download_firmware(%{} = remote_fw_config) do - local_fw_config = get_config_as_map()["hardware_params"] - :ok = FirmwareConfig.download(remote_fw_config, local_fw_config) - end - - def download_os(%{} = remote_os_config) do - local_os_config = get_config_as_map()["settings"] - :ok = FbosConfig.download(remote_os_config, local_os_config) - end -end diff --git a/farmbot_core/lib/asset_storage/settings/fbos_config.ex b/farmbot_core/lib/asset_storage/settings/fbos_config.ex deleted file mode 100644 index 44d93b31..00000000 --- a/farmbot_core/lib/asset_storage/settings/fbos_config.ex +++ /dev/null @@ -1,49 +0,0 @@ -defmodule Farmbot.Asset.Settings.FbosConfig do - @moduledoc false - import Farmbot.Asset.Settings.Helpers - require Farmbot.Logger - @keys ~W(arduino_debug_messages - auto_sync - beta_opt_in - disable_factory_reset - firmware_hardware - firmware_input_log - firmware_output_log - network_not_found_timer - os_auto_update - sequence_body_log - sequence_complete_log - sequence_init_log) - - def download(new, old) do - new = Map.take(new, @keys) - for k <- @keys do - if old[k] != new[k] do - try do - apply_kv(k, new[k], old[k]) - rescue - _ -> Farmbot.Logger.error 1, "Failed to apply Fbos Config: #{k}" - end - end - end - :ok - end - - def log(key, new, old) do - Farmbot.Logger.info 3, "Fbos Config #{key} updated: #{new || "NULL"} => #{old || "NULL"}" - end - - bool("arduino_debug_messages") - bool("auto_sync") - bool("beta_opt_in") - bool("disable_factory_reset") - bool("firmware_input_log") - bool("firmware_output_log") - bool("os_auto_update") - bool("sequence_body_log") - bool("sequence_complete_log") - bool("sequence_init_log") - - string("firmware_hardware") - float("network_not_found_timer") -end diff --git a/farmbot_core/lib/asset_storage/settings/firmware_config.ex b/farmbot_core/lib/asset_storage/settings/firmware_config.ex deleted file mode 100644 index c76c1f86..00000000 --- a/farmbot_core/lib/asset_storage/settings/firmware_config.ex +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Farmbot.Asset.Settings.FirmwareConfig do - @moduledoc false - import Farmbot.Asset.Settings.Helpers - require Farmbot.Logger - @keys ~W(pin_guard_4_time_out - pin_guard_1_active_state - encoder_scaling_y - movement_invert_2_endpoints_x - movement_min_spd_y - pin_guard_2_time_out - movement_timeout_y - movement_home_at_boot_y - movement_home_spd_z - movement_invert_endpoints_z - pin_guard_1_pin_nr - movement_invert_endpoints_y - movement_max_spd_y - movement_home_up_y - encoder_missed_steps_decay_z - movement_home_spd_y - encoder_use_for_pos_x - movement_step_per_mm_x - movement_home_at_boot_z - movement_steps_acc_dec_z - pin_guard_5_pin_nr - movement_invert_motor_z - movement_max_spd_x - movement_enable_endpoints_y - movement_enable_endpoints_z - movement_stop_at_home_x - movement_axis_nr_steps_y - pin_guard_1_time_out - movement_home_at_boot_x - pin_guard_2_pin_nr - encoder_scaling_z - param_e_stop_on_mov_err - encoder_enabled_x - pin_guard_2_active_state - encoder_missed_steps_decay_y - movement_home_up_z - movement_enable_endpoints_x - movement_step_per_mm_y - pin_guard_3_pin_nr - param_mov_nr_retry - movement_stop_at_home_z - pin_guard_4_active_state - movement_steps_acc_dec_y - movement_home_spd_x - movement_keep_active_x - pin_guard_3_time_out - movement_keep_active_y - encoder_scaling_x - movement_invert_2_endpoints_z - encoder_missed_steps_decay_x - movement_timeout_z - encoder_missed_steps_max_z - movement_min_spd_z - encoder_enabled_y - encoder_type_y - movement_home_up_x - pin_guard_3_active_state - movement_invert_motor_x - movement_keep_active_z - movement_max_spd_z - movement_secondary_motor_invert_x - movement_stop_at_max_x - movement_steps_acc_dec_x - pin_guard_4_pin_nr - encoder_type_x - movement_invert_2_endpoints_y - encoder_invert_y - movement_axis_nr_steps_x - movement_stop_at_max_z - movement_invert_endpoints_x - encoder_invert_z - encoder_use_for_pos_z - pin_guard_5_active_state - movement_step_per_mm_z - encoder_enabled_z - movement_secondary_motor_x - pin_guard_5_time_out - movement_min_spd_x - encoder_type_z - movement_stop_at_max_y - encoder_use_for_pos_y - encoder_missed_steps_max_y - movement_timeout_x - movement_stop_at_home_y - movement_axis_nr_steps_z - encoder_invert_x - encoder_missed_steps_max_x - movement_invert_motor_y) - - def download(new, old) do - new = Map.take(new, @keys) - for k <- @keys do - if old[k] != new[k] do - try do - apply_kv(k, new[k], old[k]) - rescue - _ -> Farmbot.Logger.error 1, "Failed to apply Firmware Config: #{k}" - end - end - end - :ok - end - - def log(key, new, old) do - Farmbot.Logger.info 3, "Firmware Config #{key} updated: #{new || "NULL"} => #{old || "NULL"}" - end - - for key <- @keys do - fw_float(unquote(key)) - end -end diff --git a/farmbot_core/lib/asset_storage/settings/helpers.ex b/farmbot_core/lib/asset_storage/settings/helpers.ex deleted file mode 100644 index 1b474a9a..00000000 --- a/farmbot_core/lib/asset_storage/settings/helpers.ex +++ /dev/null @@ -1,58 +0,0 @@ -defmodule Farmbot.Asset.Settings.Helpers do - import Farmbot.Config, only: [update_config_value: 4] - defmacro bool(kind) do - quote do - def apply_kv(unquote(kind), nil = new, old) do - log(unquote(kind), old, new) - update_config_value(:bool, "settings", unquote(kind), nil) - end - - def apply_kv(unquote(kind), new, old) when is_boolean(new) do - log(unquote(kind), old, new) - update_config_value(:bool, "settings", unquote(kind), new) - end - end - end - - defmacro string(kind) do - quote do - def apply_kv(unquote(kind), nil = new, old) do - log(unquote(kind), old, new) - update_config_value(:string, "settings", unquote(kind), nil) - end - - def apply_kv(unquote(kind), new, old) when is_binary(new) do - log(unquote(kind), old, new) - update_config_value(:string, "settings", unquote(kind), new) - end - end - end - - defmacro float(kind) do - quote do - def apply_kv(unquote(kind), nil = new, old) do - log(unquote(kind), old, new) - update_config_value(:float, "settings", unquote(kind), nil) - end - - def apply_kv(unquote(kind), new, old) when is_number(new) do - log(unquote(kind), old, new) - update_config_value(:float, "settings", unquote(kind), new / 1) - end - end - end - - defmacro fw_float(kind) do - quote do - def apply_kv(unquote(kind), nil = new, old) do - log(unquote(kind), old, new) - update_config_value(:float, "hardware_params", unquote(kind), nil) - end - - def apply_kv(unquote(kind), new, old) when is_number(new) do - log(unquote(kind), old, new) - update_config_value(:float, "hardware_params", unquote(kind), new / 1) - end - end - end -end diff --git a/farmbot_core/lib/asset_storage/supervisor.ex b/farmbot_core/lib/asset_storage/supervisor.ex deleted file mode 100644 index f313e2a0..00000000 --- a/farmbot_core/lib/asset_storage/supervisor.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Farmbot.Asset.Supervisor do - @moduledoc false - use Supervisor - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - children = [ - {Farmbot.Asset.Logger, []}, - {Farmbot.Asset.Repo, []}, - {Farmbot.Regimen.NameProvider, []}, - {Farmbot.FarmEvent.Supervisor, []}, - {Farmbot.Regimen.Supervisor, []}, - {Farmbot.PinBinding.Supervisor, []}, - {Farmbot.Peripheral.Supervisor, []}, - {Farmbot.Asset.OnStartTask, []}, - ] - Supervisor.init(children, [strategy: :one_for_one]) - end -end diff --git a/farmbot_core/lib/asset_storage/sync.ex b/farmbot_core/lib/asset_storage/sync.ex deleted file mode 100644 index 66338a8d..00000000 --- a/farmbot_core/lib/asset_storage/sync.ex +++ /dev/null @@ -1,121 +0,0 @@ -defmodule Farmbot.Asset.Sync do - alias Farmbot.Asset.{ - Repo, - SyncCmd - } - require Farmbot.Logger - require Logger - import Ecto.Query, warn: false - - def sync(verbosity \\ 1) do - Farmbot.Logger.info(verbosity, "Syncing") - before_sync = Repo.snapshot() - - after_sync = Repo.snapshot() - diff = Repo.Snapshot.diff(before_sync, after_sync) - dispatch_sync(diff) - end - - @doc """ - Register a sync message from an external source. - This is like a snippit of the changes that have happened. - `sync_cmd`s should only be applied on `sync`ing. - `sync_cmd`s are _not_ a source of truth for transactions that have been applied. - Use the `Farmbot.Asset.Registry` for these types of events. - """ - def register_sync_cmd(remote_id, kind, body) when is_binary(kind) do - # Make sure to raise if this isn't valid. - _ = kind_to_module(kind) - new_sync_cmd(remote_id, kind, body) - |> SyncCmd.changeset() - |> Repo.insert!() - end - - @doc "Destroy all sync cmds locally." - def destroy_all_sync_cmds do - Repo.delete_all(SyncCmd) - end - - @doc "Returns all sync cmds stored locally." - def all_sync_cmds, do: Repo.all(SyncCmd) - - @doc "Delete a single cmd." - def destroy_sync_cmd(%SyncCmd{id: nil} = cmd), do: {:ok, cmd} - def destroy_sync_cmd(%SyncCmd{} = cmd), do: Repo.delete(cmd) - - defp apply_sync_cmd(%SyncCmd{} = cmd) do - mod = kind_to_module(kind) - Farmbot.Logger.debug(3, "Syncing #{cmd.kind}") - try do - do_apply_sync_cmd(cmd) - rescue - e -> - Farmbot.Logger.error(1, "Error syncing: #{mod}: #{Exception.message(e)}") - end - destroy_sync_cmd(cmd) - end - - # When `body` is nil, it means an object was deleted. - defp do_apply_sync_cmd(%{body: nil, remote_id: id, kind: kind}) do - mod = kind_to_module(kind) - case Repo.one(from m in mod, where: m.id == ^id) do - nil -> - :ok - - existing -> - Repo.delete!(existing) - :ok - end - end - - defp do_apply_sync_cmd(%{body: obj, remote_id: id, kind: kind}) do - not_struct = strip_struct(obj) - mod = kind_to_module(kind) - # We need to check if this object exists in the database. - case Repo.one(from m in mod, where: m.id == ^id) do - # If it does not, just return the newly created object. - nil -> - change = mod.changeset(struct(mod, not_struct), not_struct) - Repo.insert!(change) - :ok - # if there is an existing record, copy the ecto meta from the old - # record. This allows `insert_or_update` to work properly. - existing -> - existing - |> Ecto.Changeset.change(not_struct) - |> Repo.update!() - :ok - end - end - - defp strip_struct(%{__struct__: _, __meta__: _} = struct), - do: Map.from_struct(struct) |> Map.drop([:__struct__, :__meta__]) - defp strip_struct(%{} = already_map), do: already_map - - defp new_sync_cmd(remote_id, kind, body) - when is_integer(remote_id) when is_binary(kind) - do - _mod = Module.concat(["Farmbot", "Asset", kind]) - struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body}) - end - - defp kind_to_module(kind) do - mod = Module.concat(["Farmbot", "Asset", kind]) - if !Code.ensure_loaded?(mod), do: raise("Unknown kind: #{kind}") - mod - end - - defp dispatch_sync(diff) do - for deletion <- diff.deletions do - Farmbot.Registry.dispatch(__MODULE__, {:deletion, deletion}) - end - - for update <- diff.updates do - Farmbot.Registry.dispatch(__MODULE__, {:update, update}) - end - - for addition <- diff.additions do - Farmbot.Registry.dispatch(__MODULE__, {:addition, addition}) - end - end -end diff --git a/farmbot_core/lib/asset_storage/sync_cmd.ex b/farmbot_core/lib/asset_storage/sync_cmd.ex deleted file mode 100644 index 603c7229..00000000 --- a/farmbot_core/lib/asset_storage/sync_cmd.ex +++ /dev/null @@ -1,29 +0,0 @@ -defmodule Farmbot.Asset.SyncCmd do - @moduledoc """ - Describes an update to an API resource. - - * `remote_id` - ID of remote object change. - * `kind` - String camel case representation of the asset kind. - * `body` - Data for the change. - """ - - alias Farmbot.Asset.SyncCmd - use Ecto.Schema - import Ecto.Changeset - alias Farmbot.EctoTypes.TermType - - schema "sync_cmds" do - field(:remote_id, :integer) - field(:kind, :string) - field(:body, TermType) - timestamps() - end - - @required_fields [:kind, :remote_id] - - def changeset(%SyncCmd{} = cmd, params \\ %{}) do - cmd - |> cast(params, @required_fields) - |> validate_required(@required_fields) - end -end diff --git a/farmbot_core/lib/asset_storage/tool.ex b/farmbot_core/lib/asset_storage/tool.ex deleted file mode 100644 index 6c476cc8..00000000 --- a/farmbot_core/lib/asset_storage/tool.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Farmbot.Asset.Tool do - @moduledoc "A Tool is an item that lives in a ToolSlot" - - alias Farmbot.Asset.Tool - use Ecto.Schema - import Ecto.Changeset - - @primary_key {:local_id, :binary_id, autogenerate: true} - schema "tools" do - field(:id, :integer) - field(:name, :string) - end - - @required_fields [:id, :name] - - def changeset(%Tool{} = tool, params \\ %{}) do - tool - |> cast(params, @required_fields) - |> validate_required(@required_fields) - |> unique_constraint(:id) - end -end diff --git a/farmbot_core/lib/asset_supervisor.ex b/farmbot_core/lib/asset_supervisor.ex new file mode 100644 index 00000000..d8591e95 --- /dev/null +++ b/farmbot_core/lib/asset_supervisor.ex @@ -0,0 +1,82 @@ +defmodule Farmbot.AssetSupervisor do + use Supervisor + alias Farmbot.{Asset.Repo, AssetWorker} + + @doc "List all children for an asset" + def list_children(kind) do + name = Module.concat(__MODULE__, kind) + Supervisor.which_children(name) + end + + @doc "looks up a pid for an asset" + def whereis_child(%kind{local_id: id}) do + :ok = Protocol.assert_impl!(AssetWorker, kind) + name = Module.concat(__MODULE__, kind) + Supervisor.which_children(name) + |> Enum.find_value(fn({sup_id, pid, :worker, _}) -> + (sup_id == id) && pid + end) + end + + @doc "Start a process that manages an asset" + def start_child(%kind{local_id: id} = asset) when is_binary(id) do + :ok = Protocol.assert_impl!(AssetWorker, kind) + name = Module.concat(__MODULE__, kind) + spec = worker_spec(asset) + Supervisor.start_child(name, spec) + end + + @doc "Removes a child if it exists" + def terminate_child(kind, id) when is_binary(id) do + :ok = Protocol.assert_impl!(AssetWorker, kind) + name = Module.concat(__MODULE__, kind) + Supervisor.terminate_child(name, id) + end + + @doc "Updates a child if it exists" + def update_child(%kind{local_id: id} = asset) do + :ok = Protocol.assert_impl!(AssetWorker, kind) + name = Module.concat(__MODULE__, kind) + _ = terminate_child(kind, id) + _ = Supervisor.delete_child(name, id) + start_child(asset) + end + + # Non public supervisor stuff + + @doc false + def child_spec(args) do + module = Keyword.fetch!(args, :module) + id_and_name = Module.concat(__MODULE__, module) + %{ + id: id_and_name, + start: {__MODULE__, :start_link, [args]}, + } + end + + @doc false + def worker_spec(%{local_id: id} = asset) do + %{ + id: id, + start: {AssetWorker, :start_link, [asset]}, + } + end + + @doc false + def start_link(args) when is_list(args) do + module = Keyword.fetch!(args, :module) + Supervisor.start_link(__MODULE__, args, name: Module.concat(__MODULE__, module)) + end + + @doc false + def init(args) do + module = Keyword.fetch!(args, :module) + preload = Keyword.get(args, :preload, []) + + module + |> Repo.all() + |> Enum.map(&Repo.preload(&1, preload)) + |> Enum.map(&worker_spec/1) + |> Supervisor.init(strategy: :one_for_one) + end +end diff --git a/farmbot_core/lib/asset_worker.ex b/farmbot_core/lib/asset_worker.ex new file mode 100644 index 00000000..98e7e1d5 --- /dev/null +++ b/farmbot_core/lib/asset_worker.ex @@ -0,0 +1,3 @@ +defprotocol Farmbot.AssetWorker do + def start_link(asset) +end diff --git a/farmbot_core/lib/asset_workers/farm_event_worker.ex b/farmbot_core/lib/asset_workers/farm_event_worker.ex new file mode 100644 index 00000000..a225cf42 --- /dev/null +++ b/farmbot_core/lib/asset_workers/farm_event_worker.ex @@ -0,0 +1,125 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do + alias Farmbot.{ + Asset, + Asset.FarmEvent, + Asset.Regimen, + Asset.Sequence, + } + + require Logger + use GenServer + + defstruct [:farm_event, :datetime] + alias __MODULE__, as: State + + @checkup_time_ms Application.get_env(:farmbot_core, __MODULE__)[:checkup_time_ms] + @checkup_time_ms || Mix.raise(""" + config :farmbot_core, #{__MODULE__}, checkup_time_ms: 10_000 + """) + + def start_link(farm_event) do + GenServer.start_link(__MODULE__, [farm_event]) + end + + def init([farm_event]) do + Logger.disable(self()) + ensure_executable!(farm_event) + now = DateTime.utc_now() + state = %State{ + farm_event: farm_event, + datetime: farm_event.last_executed || DateTime.utc_now() + } + # check if now is _before_ start_time + case DateTime.compare(now, farm_event.start_time) do + :lt -> init_event_started(state, now) + _ -> + # check if now is _after_ end_time + case DateTime.compare(now, farm_event.end_time) do + :gt -> init_event_completed(state, now) + _ -> init_event_started(state, now) + end + end + end + + defp init_event_completed(_, _) do + Logger.warn "No future events" + :ignore + end + + def init_event_started(%State{} = state, _now) do + {:ok, state, 0} + end + + def handle_info(:timeout, %State{} = state) do + Logger.info "build_calendar" + next = FarmEvent.build_calendar(state.farm_event, state.datetime) + + if next do + # positive if the first date/time comes after the second. + diff = DateTime.compare(next, DateTime.utc_now()) + # if next_event is more than 0 milliseconds away, schedule that event. + case diff do + :gt -> + Logger.info "Event is still in the future" + {:noreply, state, @checkup_time_ms} + diff when diff in [:lt, :eq] -> + Logger.info "Event should be executed: #{Timex.from_now(next)}" + executable = ensure_executable!(state.farm_event) + event = ensure_executed!(state.farm_event, executable, next) + {:noreply, %{state | farm_event: event, datetime: DateTime.utc_now()}, @checkup_time_ms} + end + + else + Logger.warn "No more future events to execute." + {:stop, :normal, state} + end + end + + defp ensure_executed!(%FarmEvent{last_executed: nil} = event, %Sequence{} = exe, next_dt) do + # positive if the first date/time comes after the second. + comp = Timex.diff(DateTime.utc_now(), next_dt, :minutes) + cond do + # now is more than 2 minutes past expected execution time + comp > 2 -> + Logger.warn "Sequence: #{inspect exe} too late: #{comp} minutes difference." + event + true -> + Logger.warn "Sequence: #{inspect exe} has not run before: #{comp} minutes difference." + Farmbot.Core.CeleryScript.sequence(exe, fn(_) -> :ok end) + Asset.update_farm_event!(event, %{last_executed: next_dt}) + end + end + + defp ensure_executed!(%FarmEvent{} = event, %Sequence{} = exe, next_dt) do + # positive if the first date/time comes after the second. + comp = Timex.compare(event.last_executed, :minutes) + cond do + comp > 2 -> + Logger.warn("Sequence: #{inspect exe} needs executing") + Farmbot.Core.CeleryScript.sequence(exe, fn(_) -> :ok end) + Asset.update_farm_event!(event, %{last_executed: next_dt}) + 0 -> + Logger.warn("Sequence: #{inspect exe} already executed: #{Timex.from_now(next_dt)}") + event + end + end + + defp ensure_executed!(%FarmEvent{last_executed: nil} = event, %Regimen{} = exe, next_dt) do + Logger.warn "Regimen: #{inspect exe} has not run before. Executing it." + Asset.upsert_persistent_regimen(exe, event, %{started_at: next_dt}) + Asset.update_farm_event!(event, %{last_executed: next_dt}) + end + + defp ensure_executed!(%FarmEvent{} = event, %Regimen{} = exe, _next_dt) do + Asset.upsert_persistent_regimen(exe, event) + event + end + + defp ensure_executable!(%FarmEvent{executable_type: "Sequence", executable_id: id}) do + Asset.get_sequence!(id: id) + end + + defp ensure_executable!(%FarmEvent{executable_type: "Regimen", executable_id: id}) do + Asset.get_regimen!(id: id) + end +end diff --git a/farmbot_core/lib/asset_workers/peripheral_worker.ex b/farmbot_core/lib/asset_workers/peripheral_worker.ex new file mode 100644 index 00000000..3063c4f0 --- /dev/null +++ b/farmbot_core/lib/asset_workers/peripheral_worker.ex @@ -0,0 +1,46 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.Peripheral do + use GenServer + alias Farmbot.Core.CeleryScript + import Farmbot.CeleryScript.Utils + require Farmbot.Logger + @retry_ms 5_000 + + def start_link(peripheral) do + GenServer.start_link(__MODULE__, [peripheral]) + end + + def init([peripheral]) do + {:ok, peripheral, 0} + end + + def handle_info(:timeout, peripheral) do + # Farmbot.Logger.info 2, "Read peripheral: #{peripheral.label}" + CeleryScript.rpc_request(peripheral_to_rpc(peripheral), &handle_ast(&1, self())) + {:noreply, peripheral} + end + + def handle_cast(%{kind: :rpc_ok}, peripheral) do + # Farmbot.Logger.success 2, "Read peripheral: #{peripheral.label} ok" + {:stop, :normal, peripheral} + end + + def handle_cast(%{kind: :rpc_error} = rpc, peripheral) do + # Farmbot.Logger.error 1, "Read peripheral: #{peripheral.label} error" + # IO.inspect(rpc, label: "error") + {:noreply, peripheral, @retry_ms} + end + + def handle_ast(ast, pid) do + :ok = GenServer.cast(pid, ast) + end + + def peripheral_to_rpc(peripheral) do + ast(:rpc_request, %{label: peripheral.local_id}, [ + ast(:read_pin, %{ + pin_num: peripheral.pin, + label: peripheral.label, + pin_mode: peripheral.mode + }, []) + ]) + end +end diff --git a/farmbot_core/lib/asset_workers/persistent_regimen_worker.ex b/farmbot_core/lib/asset_workers/persistent_regimen_worker.ex new file mode 100644 index 00000000..a2cf4b5a --- /dev/null +++ b/farmbot_core/lib/asset_workers/persistent_regimen_worker.ex @@ -0,0 +1,13 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PersistentRegimen do + use GenServer + require Farmbot.Logger + import Farmbot.Config, only: [get_config_value: 3] + + def start_link(persistent_regimen) do + GenServer.start_link(__MODULE__, [persistent_regimen]) + end + + def init([persistent_regimen]) do + {:ok, persistent_regimen} + end +end diff --git a/farmbot_core/lib/asset_workers/pin_binding_worker.ex b/farmbot_core/lib/asset_workers/pin_binding_worker.ex new file mode 100644 index 00000000..bf0b96b5 --- /dev/null +++ b/farmbot_core/lib/asset_workers/pin_binding_worker.ex @@ -0,0 +1,11 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PinBinding do + use GenServer + + def start_link(pin_binding) do + GenServer.start_link(__MODULE__, [pin_binding]) + end + + def init([pin_binding]) do + {:ok, pin_binding} + end +end diff --git a/farmbot_core/lib/celery_script/celery_script.ex b/farmbot_core/lib/celery_script/celery_script.ex index c4d8a1f3..ee875237 100644 --- a/farmbot_core/lib/celery_script/celery_script.ex +++ b/farmbot_core/lib/celery_script/celery_script.ex @@ -2,11 +2,12 @@ defmodule Farmbot.Core.CeleryScript do @moduledoc """ Helpers for executing CeleryScript. """ - def rpc_request(data, fun) do - Farmbot.CeleryScript.RunTime.rpc_request(Farmbot.CeleryScript.RunTime, data, fun) + def rpc_request(ast, fun) do + Farmbot.CeleryScript.RunTime.rpc_request(Farmbot.CeleryScript.RunTime, ast, fun) end def sequence(%Farmbot.Asset.Sequence{} = seq, fun) do - Farmbot.CeleryScript.RunTime.sequence(Csvm, seq, seq.id, fun) + ast = Farmbot.CeleryScript.AST.decode(seq) + Farmbot.CeleryScript.RunTime.sequence(Farmbot.CeleryScript.RunTime, ast, seq.id, fun) end end diff --git a/farmbot_core/lib/config_storage/config_storage.ex b/farmbot_core/lib/config_storage/config_storage.ex index 9915734a..dc476afe 100644 --- a/farmbot_core/lib/config_storage/config_storage.ex +++ b/farmbot_core/lib/config_storage/config_storage.ex @@ -56,6 +56,17 @@ defmodule Farmbot.Config do end) end + def get_config_value(:string, "authorization", key_name) do + env = System.get_env("FARMBOT_#{String.upcase(key_name)}") + if env && env != "" do + env + else + __MODULE__ + |> apply(:get_string_value, ["authorization", key_name]) + |> Map.fetch!(:value) + end + end + def get_config_value(type, group_name, key_name) when type in [:bool, :float, :string] do __MODULE__ |> apply(:"get_#{type}_value", [group_name, key_name]) @@ -113,7 +124,6 @@ defmodule Farmbot.Config do def get_string_value(group_name, key_name) do group_id = get_group_id(group_name) - [type_id] = from( c in Config, diff --git a/farmbot_core/lib/farm_event/manager.ex b/farmbot_core/lib/farm_event/manager.ex deleted file mode 100644 index 7ce47625..00000000 --- a/farmbot_core/lib/farm_event/manager.ex +++ /dev/null @@ -1,472 +0,0 @@ -defmodule Farmbot.FarmEvent.Manager do - @moduledoc """ - Manages execution of FarmEvents. - - ## Rules for FarmEvent execution. - * Regimen - * ignore `end_time`. - * ignore calendar. - * if schedule_time is more than 60 seconds passed due, assume it already - scheduled, and don't schedule it again. - * Sequence - * if `schedule_time` is late, check the calendar. - * for each item in the calendar, check if it's event is more than - 60 seconds in the past. if not, execute it. - * if there is only one event in the calendar, ignore the `end_time` - """ - - # credo:disable-for-this-file Credo.Check.Refactor.FunctionArity - - use GenServer - require Farmbot.Logger - alias Farmbot.Asset - alias Farmbot.Asset.{FarmEvent, Sequence, Regimen} - alias Farmbot.Registry - - @checkup_time 1000 - # @checkup_time 15_000 - - ## GenServer - - defmodule State do - @moduledoc false - defstruct timer: nil, last_time_index: %{}, events: %{}, checkup: nil - end - - @doc false - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - Farmbot.Registry.subscribe() - send(self(), :checkup) - {:ok, struct(State)} - end - - def terminate(reason, _state) do - Farmbot.Logger.error(1, "FarmEvent Manager terminated: #{inspect(reason)}") - end - - def handle_info({Registry, {Asset, {:addition, %FarmEvent{} = data}}}, state) do - maybe_farm_event_log("Starting monitor on FarmEvent: #{data.id}.") - - Map.put(state.events, data.id, data) - |> reindex(state) - end - - def handle_info({Registry, {Asset, {:deletion, %FarmEvent{} = data}}}, state) do - maybe_farm_event_log("Destroying monitor on FarmEvent: #{data.id}.") - - if String.contains?(data.executable_type, "Regimen") do - reg = Farmbot.Asset.get_regimen_by_id(data.executable_id, data.id) - - if reg do - Farmbot.Regimen.Supervisor.stop_child(reg) - end - end - - Map.delete(state.events, data.id) - |> reindex(state) - end - - def handle_info({Registry, {Asset, {:update, %FarmEvent{} = data}}}, state) do - maybe_farm_event_log("Reindexing monitor on FarmEvent: #{data.id}.") - - if String.contains?(data.executable_type, "Regimen") do - reg = Farmbot.Asset.get_regimen_by_id(data.executable_id, data.id) - - if reg do - Farmbot.Regimen.Supervisor.reindex_all_managers( - reg, - Timex.parse!(data.start_time, "{ISO:Extended}") - ) - end - end - - Map.put(state.events, data.id, data) - |> reindex(state) - end - - def handle_info({Registry, {Asset, {:deletion, %Regimen{} = data}}}, state) do - Farmbot.Regimen.Supervisor.stop_all_managers(data) - {:noreply, state} - end - - def handle_info({Registry, {Asset, {:update, %Regimen{} = data}}}, state) do - Farmbot.Regimen.Supervisor.reindex_all_managers(data) - {:noreply, state} - end - - def handle_info({Registry, {Asset, {:update, %Sequence{} = sequence}}}, state) do - for reg <- Farmbot.Asset.get_regimens_using_sequence(sequence.id) do - Farmbot.Regimen.Supervisor.reindex_all_managers(reg) - end - - {:noreply, state} - end - - def handle_info({Registry, _}, state) do - {:noreply, state} - end - - def handle_info(:checkup, state) do - checkup = spawn_monitor(__MODULE__, :async_checkup, [self(), state]) - {:noreply, %{state | timer: nil, checkup: checkup}} - end - - def handle_info({:DOWN, _, :process, _, {:success, new_state}}, _old_state) do - timer = Process.send_after(self(), :checkup, @checkup_time) - {:noreply, %{new_state | timer: timer, checkup: nil}} - end - - def handle_info({:DOWN, _, :process, _, error}, state) do - Farmbot.Logger.error(1, "Farmevent checkup process died: #{inspect(error)}") - timer = Process.send_after(self(), :checkup, @checkup_time) - {:noreply, %{state | timer: timer, checkup: nil}} - end - - defp reindex(events, state) do - events = - Map.new(events, fn {id, event} -> - {id, FarmEvent.build_calendar(event)} - end) - - maybe_farm_event_log("Reindexed FarmEvents") - - if match?({_, _}, state.checkup) do - Process.exit( - state.checkup |> elem(0), - {:success, %{state | events: events}} - ) - end - - if state.timer do - Process.cancel_timer(state.timer) - timer = Process.send_after(self(), :checkup, @checkup_time) - {:noreply, %{state | events: events, timer: timer}} - else - {:noreply, %{state | events: events}} - end - end - - def async_checkup(_manager, state) do - now = get_now() - all_events = Enum.map(state.events, &FarmEvent.build_calendar(elem(&1, 1))) - - # do checkup is the bulk of the work. - {late_executables, new} = do_checkup(all_events, now, state) - - unless Enum.empty?(late_executables) do - # Map over the events for logging. - # Both Sequences and Regimens have a `name` field. - names = Enum.map(late_executables, &Map.get(elem(&1, 0), :name)) - Farmbot.Logger.debug(3, "Time for events: #{inspect(names)} to be scheduled.") - schedule_events(late_executables, now) - end - - exit( - {:success, - %{new | events: Map.new(all_events, fn event -> {event.id, event} end)}} - ) - end - - defp do_checkup(list, time, late_events \\ [], state) - - defp do_checkup([], _now, late_events, state), do: {late_events, state} - - defp do_checkup([farm_event | rest], now, late_exes, state) do - # new_late will be a executable event (Regimen or Sequence.) - {new_late_executable, last_time} = - check_event(farm_event, now, state.last_time_index[farm_event.id]) - - # update state. - new_state = %{ - state - | last_time_index: - Map.put(state.last_time_index, farm_event.id, last_time) - } - - case new_late_executable do - # if `new_late_executable` is nil, don't accumulate it. - nil -> - do_checkup(rest, now, late_exes, new_state) - - # if there is a new event, accumulate it. - late_executable -> - do_checkup( - rest, - now, - [{late_executable, farm_event} | late_exes], - new_state - ) - end - end - - defp check_event(%FarmEvent{} = f, now, last_time) do - # Get the executable out of the database this may fail. - mod = Module.safe_concat([f.executable_type]) - executable = lookup!(mod, f) - - # build a local schedule time and end time - schedule_time = Timex.parse!(f.start_time, "{ISO:Extended}") - end_time = Timex.parse!(f.end_time, "{ISO:Extended}") - - # get local bool of if the event is scheduled and finished. - scheduled? = Timex.after?(now, schedule_time) - finished? = Timex.after?(now, end_time) - - case mod do - Regimen -> - maybe_schedule_regimen( - scheduled?, - schedule_time, - last_time, - executable, - now - ) - - Sequence -> - maybe_schedule_sequence( - scheduled?, - finished?, - f, - last_time, - executable, - now - ) - end - end - - defp maybe_schedule_regimen( - scheduled?, - schedule_time, - last_time, - executable, - now - ) - - defp maybe_schedule_regimen( - true = _scheduled?, - schedule_time, - nil, - regimen, - _now - ) do - maybe_farm_event_log("regimen #{regimen.name} (#{regimen.id}) scheduling.") - process_via = Farmbot.Regimen.NameProvider.via(regimen) - pid = GenServer.whereis(process_via) - if pid, do: {nil, schedule_time}, else: {regimen, schedule_time} - end - - defp maybe_schedule_regimen( - true = _scheduled?, - _schedule_time, - last_time, - event, - _now - ) do - maybe_farm_event_log( - "regimen #{event.name} (#{event.id}) should already be scheduled." - ) - - {nil, last_time} - end - - defp maybe_schedule_regimen( - false = _scheduled?, - schedule_time, - last_time, - event, - _ - ) do - maybe_farm_event_log( - "regimen #{event.name} (#{event.id}) is not scheduled yet. (#{ - inspect(schedule_time) - }) (#{inspect(Timex.now())})" - ) - - {nil, last_time} - end - - defp lookup!(module, %FarmEvent{executable_id: exe_id, id: id}) - when is_atom(module) do - case module do - Sequence -> - Asset.get_sequence_by_id!(exe_id) - - Regimen -> - # We tag the looked up Regimen with the FarmEvent id here. - # This makes it easier to track the pid of it later when it - # needs to be scheduled or stopped. - Asset.get_regimen_by_id!(exe_id, id) - end - end - - # signals the start of a sequence based on the described logic. - defp maybe_schedule_sequence( - scheduled?, - finished?, - farm_event, - last_time, - event, - now - ) - - # We only want to check if the sequence is scheduled, and not finished. - defp maybe_schedule_sequence( - true = _scheduled?, - false = _finished?, - farm_event, - last_time, - event, - now - ) do - {run?, next_time} = - should_run_sequence?(farm_event.calendar, last_time, now) - - case run? do - true -> {event, next_time} - false -> {nil, last_time} - end - end - - # if `farm_event.time_unit` is "never" we can't use the `end_time`. - # if we have no `last_time`, time to execute. - defp maybe_schedule_sequence( - true = _scheduled?, - _, - %{time_unit: "never"} = f, - nil = _last_time, - event, - now - ) do - maybe_farm_event_log("Ignoring end_time.") - - case should_run_sequence?(f.calendar, nil, now) do - {true, next} -> {event, next} - {false, _} -> {nil, nil} - end - end - - # if scheduled is false, the event isn't ready to be executed. - defp maybe_schedule_sequence( - false = _scheduled?, - _fin, - _farm_event, - last_time, - event, - _now - ) do - maybe_farm_event_log( - "sequence #{event.name} (#{event.id}) is not scheduled yet." - ) - - {nil, last_time} - end - - # if the event is finished (but not a "never" time_unit), we don't execute. - defp maybe_schedule_sequence( - _scheduled?, - true = _finished?, - _farm_event, - last_time, - event, - _now - ) do - maybe_farm_event_log("sequence #{event.name} (#{event.id}) is finished.") - {nil, last_time} - end - - # Checks if we shoudl run a sequence or not. returns {event | nil, time | nil} - defp should_run_sequence?(calendar, last_time, now) - - # if there is no last time, check if time is passed now within 60 seconds. - defp should_run_sequence?([first_time | _], nil, now) do - maybe_farm_event_log( - "Checking sequence event that hasn't run before #{first_time}" - ) - - # convert the first_time to a DateTime - dt = Timex.parse!(first_time, "{ISO:Extended}") - # if now is after the time, we are in fact late - if Timex.after?(now, dt) do - {true, now} - else - # make sure to return nil as the last time because it stil hasnt executed yet. - maybe_farm_event_log("Sequence Event not ready yet.") - {false, nil} - end - end - - defp should_run_sequence?(nil, last_time, now) do - maybe_farm_event_log("Checking sequence with no calendar.") - - if is_nil(last_time) do - {true, now} - else - {false, last_time} - end - end - - defp should_run_sequence?(calendar, last_time, now) do - # get rid of all the items that happened before last_time - filtered_calendar = - Enum.filter(calendar, fn iso_time -> - dt = Timex.parse!(iso_time, "{ISO:Extended}") - # we only want this time if it happened after the last_time - Timex.after?(dt, last_time) - end) - - # if after filtering, there are events that need to be run - # check if they are older than a minute ago, - case filtered_calendar do - [iso_time | _] -> - dt = Timex.parse!(iso_time, "{ISO:Extended}") - - if Timex.after?(now, dt) do - {true, dt} - else - maybe_farm_event_log("Sequence Event not ready yet.") - {false, dt} - end - - [] -> - maybe_farm_event_log("No items in calendar.") - {false, last_time} - end - end - - # Enumeration is complete. - defp schedule_events([], _now), do: :ok - - # Enumerate the events to be scheduled. - defp schedule_events([{executable, farm_event} | rest], now) do - # Spawn to be non blocking here. Maybe link to this process? - time = Timex.parse!(farm_event.start_time, "{ISO:Extended}") - cond do - match?(%Regimen{}, executable) -> - spawn(Regimen, :schedule_event, [executable, time]) - match?(%Sequence{}, executable) -> - spawn(Sequence, :schedule_event, [executable, time]) - end - - # Continue enumeration. - schedule_events(rest, now) - end - - defp get_now(), do: Timex.now() - - defp maybe_farm_event_log(message) do - if Application.get_env(:farmbot_core, :farm_event_debug_log) do - Farmbot.Logger.debug(3, message) - else - :ok - end - end - - @doc "Enable or disbale debug logs for farmevents." - def debug_logs(bool \\ true) when is_boolean(bool) do - Application.put_env(:farmbot_core, :farm_event_debug_log, bool) - end -end diff --git a/farmbot_core/lib/farm_event/supervisor.ex b/farmbot_core/lib/farm_event/supervisor.ex deleted file mode 100644 index f1354074..00000000 --- a/farmbot_core/lib/farm_event/supervisor.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Farmbot.FarmEvent.Supervisor do - @moduledoc false - use Supervisor - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - children = [ - {Farmbot.FarmEvent.Manager, []} - ] - - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/farmbot_core/lib/json/jason_parser.ex b/farmbot_core/lib/json/jason_parser.ex index 167991d3..545da5c0 100644 --- a/farmbot_core/lib/json/jason_parser.ex +++ b/farmbot_core/lib/json/jason_parser.ex @@ -13,17 +13,4 @@ defmodule Farmbot.JSON.JasonParser do Protocol.derive Jason.Encoder, Farmbot.BotState.LocationData Protocol.derive Jason.Encoder, Farmbot.BotState.McuParams Protocol.derive Jason.Encoder, Farmbot.BotState.Pin - - # Assets - Protocol.derive Jason.Encoder, Farmbot.Asset.Device - Protocol.derive Jason.Encoder, Farmbot.Asset.FarmEvent - Protocol.derive Jason.Encoder, Farmbot.Asset.FarmwareEnv - Protocol.derive Jason.Encoder, Farmbot.Asset.FarmwareInstallation - Protocol.derive Jason.Encoder, Farmbot.Asset.Peripheral - Protocol.derive Jason.Encoder, Farmbot.Asset.PinBinding - Protocol.derive Jason.Encoder, Farmbot.Asset.Point - Protocol.derive Jason.Encoder, Farmbot.Asset.Regimen - Protocol.derive Jason.Encoder, Farmbot.Asset.Sensor - Protocol.derive Jason.Encoder, Farmbot.Asset.Sequence - Protocol.derive Jason.Encoder, Farmbot.Asset.Tool end diff --git a/farmbot_core/lib/log_storage/log.ex b/farmbot_core/lib/log_storage/log.ex index 7c7b6e1e..f12ea970 100644 --- a/farmbot_core/lib/log_storage/log.ex +++ b/farmbot_core/lib/log_storage/log.ex @@ -49,7 +49,7 @@ defmodule Farmbot.Log do field(:level, LogLevelType) field(:verbosity, :integer) field(:message, :string) - field(:meta, Farmbot.EctoTypes.TermType) + field(:meta, :map) field(:function, :string) field(:file, :string) field(:line, :integer) diff --git a/farmbot_core/lib/peripheral/supervisor.ex b/farmbot_core/lib/peripheral/supervisor.ex deleted file mode 100644 index 0e9f8738..00000000 --- a/farmbot_core/lib/peripheral/supervisor.ex +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Farmbot.Peripheral.Supervisor do - use Supervisor - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - children = [ - {Farmbot.Peripheral.Worker, []} - ] - Supervisor.init(children, [strategy: :one_for_one]) - end -end diff --git a/farmbot_core/lib/peripheral/worker.ex b/farmbot_core/lib/peripheral/worker.ex deleted file mode 100644 index b2f003fd..00000000 --- a/farmbot_core/lib/peripheral/worker.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Farmbot.Peripheral.Worker do - use GenServer - alias Farmbot.{Asset, Registry} - import Farmbot.CeleryScript.Utils - alias Asset.Peripheral - require Farmbot.Logger - - def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - Registry.subscribe() - {:ok, %{}} - end - - def handle_info({Registry, {Asset, {:deletion, %Peripheral{}}}}, state) do - {:noreply, state} - end - - def handle_info({Registry, {Asset, {_action, %Peripheral{label: label, id: id, mode: mode}}}}, state) do - # TODO Connor - this is a race condition on first sync since there is - # a transaction being appied. - # This needs to be queued up until `sync_status: :synced` or something.. - named_pin = ast(:named_pin, %{pin_type: "Peripheral", pin_id: id}) - read_pin = ast(:read_pin, %{pin_number: named_pin, label: label, pin_mode: mode}) - request = ast(:rpc_request, %{label: label}, [read_pin]) - Farmbot.Core.CeleryScript.rpc_request(request, fn(results) -> - case results do - %{kind: :rpc_ok} -> :ok - %{kind: :rpc_error, body: [%{args: %{message: message}}]} -> - Farmbot.Logger.error(1, "Error reading peripheral #{label} => #{message}") - end - end) - {:noreply, state} - end - - def handle_info({Registry, _}, state) do - {:noreply, state} - end -end diff --git a/farmbot_core/lib/pin_binding/handler.ex b/farmbot_core/lib/pin_binding/handler.ex deleted file mode 100644 index fb474ea0..00000000 --- a/farmbot_core/lib/pin_binding/handler.ex +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Farmbot.PinBinding.Handler do - @moduledoc "Behaviour for PinBinding handlers to implement." - - @doc "Start the handler." - @callback start_link :: GenServer.on_start() - - @doc "Register a pin." - @callback register_pin(integer) :: :ok | {:error, term} - - @doc "Unregister a pin." - @callback unregister_pin(integer) :: :ok | {:error, term} -end diff --git a/farmbot_core/lib/pin_binding/manager.ex b/farmbot_core/lib/pin_binding/manager.ex deleted file mode 100644 index 789b8612..00000000 --- a/farmbot_core/lib/pin_binding/manager.ex +++ /dev/null @@ -1,191 +0,0 @@ -defmodule Farmbot.PinBinding.Manager do - @moduledoc "Handles PinBinding inputs and outputs" - use GenServer - require Farmbot.Logger - alias __MODULE__, as: State - alias Farmbot.Asset - alias Asset.{PinBinding, Sequence} - @handler Application.get_env(:farmbot_core, :behaviour)[:pin_binding_handler] - @handler || Mix.raise("No pin binding handler.") - - defstruct registered: %{}, - signal: %{}, - handler: nil - - # Should be called by a handler - @doc false - def trigger(pin, signal) do - GenServer.cast(__MODULE__, {:pin_trigger, pin, signal}) - end - - @doc false - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - case @handler.start_link() do - {:ok, handler} -> - Farmbot.Registry.subscribe(self()) - all = Asset.all_pin_bindings() - {:ok, initial_state(all, %State{handler: handler})} - err -> - err - end - end - - def terminate(reason, state) do - if state.handler do - if Process.alive?(state.handler) do - GenStage.stop(state.handler, reason) - end - end - end - - defp initial_state([], state), do: state - - defp initial_state([%PinBinding{pin_num: pin} = binding | rest], state) do - case @handler.register_pin(pin) do - :ok -> - new_state = do_register(state, binding) - initial_state(rest, new_state) - _ -> - initial_state(rest, state) - end - end - - def handle_cast({:pin_trigger, pin, :falling}, state) do - binding = state.registered[pin] - if binding do - do_usr_led(binding, :off) - if state.signal[pin] do - Process.cancel_timer(state.signal[pin]) - {:noreply, %{state | signal: Map.put(state.signal, pin, debounce_timer(pin))}} - else - Farmbot.Logger.busy(1, "Pin Binding #{binding} triggered #{binding.special_action || "execute_sequence"}") - do_execute(binding) - {:noreply, %{state | signal: Map.put(state.signal, pin, debounce_timer(pin))}} - end - else - Farmbot.Logger.warn(3, "No Pin Binding assosiated with: #{pin}") - {:noreply, state} - end - end - - def handle_cast({:pin_trigger, pin, :rising}, state) do - binding = state.registered[pin] - if binding do - do_usr_led(binding, :solid) - end - {:noreply, state} - end - - def handle_info({pin, :ok}, state) do - {:noreply, %{state | signal: Map.put(state.signal, pin, nil)}} - end - - def handle_info({Farmbot.Registry, {Asset, {:addition, %PinBinding{} = binding}}}, state) do - state = register_pin(state, binding) - {:noreply, state} - end - - def handle_info({Farmbot.Registry, {Asset, {:deletion, %PinBinding{} = binding}}}, state) do - state = unregister_pin(state, binding) - {:noreply, state} - end - - def handle_info({Farmbot.Registry, {Asset, {:update, %PinBinding{} = binding}}}, state) do - state = state - |> unregister_pin(binding) - |> register_pin(binding) - {:noreply, state} - end - - def handle_info({Farmbot.Registry, _}, state) do - {:noreply, state} - end - - defp register_pin(state, %PinBinding{pin_num: pin_num} = binding) do - case state.registered[pin_num] do - nil -> - case @handler.register_pin(pin_num) do - :ok -> do_register(state, binding) - - {:error, reason} -> - error_log("registering", binding, inspect reason) - state - end - - _ -> - error_log("registering", binding, "already registered") - state - end - end - - def unregister_pin(state, %PinBinding{pin_num: pin_num} = binding) do - case state.registered[pin_num] do - nil -> - error_log("unregistering", binding, "not registered") - state - - %PinBinding{} = old -> - case @handler.unregister_pin(pin_num) do - :ok -> - do_unregister(state, old) - - {:error, reason} -> - error_log("unregistering", binding, inspect reason) - state - end - end - end - - defp error_log(action_verb, binding , reason) do - Farmbot.Logger.error 1, "Error #{action_verb} Pin Binding #{binding} (#{reason})" - end - - defp do_register(state, %PinBinding{pin_num: pin} = binding) do - Farmbot.Logger.debug 1, "Pin Binding #{binding} registered." - %{state | registered: Map.put(state.registered, pin, binding), signal: Map.put(state.signal, pin, nil)} - end - - defp do_unregister(state, %PinBinding{pin_num: pin_num} = binding) do - Farmbot.Logger.debug 1, "Pin Binding #{binding} unregistered." - %{state | registered: Map.delete(state.registered, pin_num), signal: Map.delete(state.signal, pin_num)} - end - - defp do_execute(%PinBinding{sequence_id: sequence_id} = binding) when is_number(sequence_id) do - sequence_id - |> Farmbot.Asset.get_sequence_by_id!() - |> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding)) - end - - defp do_execute(%PinBinding{special_action: action} = binding) when is_binary(action) do - %Sequence{ - id: 0, - name: action, - kind: action, - args: %{}, - body: [] } - |> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding)) - end - - @doc false - def execute_results(:ok, binding) do - Farmbot.Logger.success(1, "Pin Binding #{binding} execution complete.") - end - - def execute_results({:error, _}, binding) do - Farmbot.Logger.error(1, "Pin Binding #{binding} execution failed.") - end - - defp debounce_timer(pin) do - Process.send_after(self(), {pin, :ok}, 200) - end - - defp do_usr_led(%PinBinding{pin_num: 26}, signal), do: do_write(:white1, signal) - defp do_usr_led(%PinBinding{pin_num: 5}, signal), do: do_write(:white2, signal) - defp do_usr_led(%PinBinding{pin_num: 20}, signal), do: do_write(:white3, signal) - defp do_usr_led(_, _), do: :ok - defp do_write(led, signal), do: apply(Farmbot.Leds, led, [signal]) -end diff --git a/farmbot_core/lib/pin_binding/stub_handler.ex b/farmbot_core/lib/pin_binding/stub_handler.ex deleted file mode 100644 index 7bd782f5..00000000 --- a/farmbot_core/lib/pin_binding/stub_handler.ex +++ /dev/null @@ -1,50 +0,0 @@ -defmodule Farmbot.PinBinding.StubHandler do - @moduledoc "Stub for handling PinBinding." - @behaviour Farmbot.PinBinding.Handler - use GenServer - - def test_fire(pin) do - GenServer.call(__MODULE__, {:test_fire, pin}) - end - - def register_pin(num) do - GenServer.call(__MODULE__, {:register_pin, num}) - end - - def unregister_pin(num) do - GenServer.call(__MODULE__, {:unregister_pin, num}) - end - - def start_link do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init([]) do - {:ok, %{}} - end - - def handle_call({:register_pin, num}, _from, state) do - {:reply, :ok, Map.put(state, num, :enabled)} - end - - def handle_call({:unregister_pin, num}, _from, state) do - {:reply, :ok, Map.delete(state, num)} - end - - def handle_call({:test_fire, pin}, _from, state) do - case state[pin] do - nil -> - {:reply, :error, state} - - :enabled -> - send(self(), {:do_test_fire, pin}) - {:reply, :ok, state} - end - end - - def handle_info({:do_test_fire, pin}, state) do - Farmbot.PinBinding.Manager.trigger(pin, :rising) - Farmbot.PinBinding.Manager.trigger(pin, :falling) - {:noreply, state} - end -end diff --git a/farmbot_core/lib/pin_binding/supervisor.ex b/farmbot_core/lib/pin_binding/supervisor.ex deleted file mode 100644 index 616a67fd..00000000 --- a/farmbot_core/lib/pin_binding/supervisor.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Farmbot.PinBinding.Supervisor do - @moduledoc false - use Supervisor - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - children = [ - {Farmbot.PinBinding.Manager, []}, - ] - Supervisor.init(children, [strategy: :one_for_one]) - end -end diff --git a/farmbot_core/lib/pin_binding/to_string.ex b/farmbot_core/lib/pin_binding/to_string.ex deleted file mode 100644 index d7b92c7f..00000000 --- a/farmbot_core/lib/pin_binding/to_string.ex +++ /dev/null @@ -1,26 +0,0 @@ -alias Farmbot.Asset.PinBinding -defimpl String.Chars, for: PinBinding do - def to_string(%PinBinding{pin_num: 16}) do - "Button 1" - end - - def to_string(%PinBinding{pin_num: 22}) do - "Button 2" - end - - def to_string(%PinBinding{pin_num: 26}) do - "Button 3" - end - - def to_string(%PinBinding{pin_num: 5}) do - "Button 4" - end - - def to_string(%PinBinding{pin_num: 20}) do - "Button 5" - end - - def to_string(%PinBinding{pin_num: num}) do - "Pi GPIO #{num}" - end -end diff --git a/farmbot_core/lib/regimen/manager.ex b/farmbot_core/lib/regimen/manager.ex deleted file mode 100644 index fdc44d71..00000000 --- a/farmbot_core/lib/regimen/manager.ex +++ /dev/null @@ -1,198 +0,0 @@ -defmodule Farmbot.Regimen.Manager do - @moduledoc "Manages a Regimen" - - require Farmbot.Logger - use GenServer - alias Farmbot.Core.CeleryScript - alias Farmbot.Asset - alias Asset.Regimen - import Farmbot.Regimen.NameProvider - - import Farmbot.Config, - only: [ - get_config_value: 3 - ] - - defmodule Error do - @moduledoc false - defexception [:epoch, :regimen, :message] - end - - defmodule Item do - @moduledoc false - @type t :: %__MODULE__{ - time_offset: integer, - sequence_id: integer, - ref: reference - } - - defstruct [:time_offset, :sequence, :sequence_id, :name, :ref] - - def parse(%{time_offset: offset, sequence_id: sequence_id}) do - %Item{ - time_offset: offset, - sequence_id: sequence_id, - ref: make_ref() - } - end - end - - def filter_items(regimen) do - regimen.regimen_items - |> Enum.map(&Item.parse(&1)) - |> Enum.sort(&(&1.time_offset <= &2.time_offset)) - end - - @doc false - def start_link(regimen, time) do - regimen.farm_event_id || raise "Starting a regimen requires a farm_event id" - GenServer.start_link(__MODULE__, [regimen, time], name: via(regimen)) - end - - def init([regimen, time]) do - # parse and sort the regimen items - items = filter_items(regimen) - first_item = List.first(items) - regimen = %{regimen | regimen_items: items} - - epoch = - Farmbot.TimeUtils.build_epoch(time) - - initial_state = %{ - next_execution: nil, - regimen: regimen, - epoch: epoch, - timer: nil - } - - if first_item do - state = build_next_state(regimen, first_item, self(), initial_state) - {:ok, state} - else - Farmbot.Logger.warn(2, "[#{regimen.name} #{regimen.farm_event_id}] has no items on regimen.") - {:ok, initial_state} - end - end - - def handle_call({:reindex, regimen, time}, _from, state) do - Farmbot.Logger.debug(3, "Reindexing regimen by id: #{regimen.id}") - regimen.farm_event_id || raise "Can't reindex without farm_event_id" - # parse and sort the regimen items - items = filter_items(regimen) - first_item = List.first(items) - regimen = %{regimen | regimen_items: items} - epoch = if time, do: Farmbot.TimeUtils.build_epoch(time), else: state.epoch - - initial_state = %{ - regimen: regimen, - epoch: epoch, - # Leave these so they get cleaned up - next_execution: state.next_execution, - timer: state.timer - } - - if first_item do - state = build_next_state(regimen, first_item, self(), initial_state) - {:reply, :ok, state} - else - Farmbot.Logger.warn(2, "[#{regimen.name} #{regimen.farm_event_id}] has no items on regimen.") - {:reply, :ok, initial_state} - end - end - - def handle_info(:execute, state) do - {item, regimen} = pop_item(state.regimen) - - if item do - do_item(item, regimen, state) - else - complete(regimen, state) - end - end - - def handle_info(:skip, state) do - {item, regimen} = pop_item(state.regimen) - - if item do - do_item(nil, regimen, state) - else - complete(regimen, state) - end - end - - defp complete(regimen, state) do - Farmbot.Logger.success( - 2, - "[#{regimen.name} #{regimen.farm_event_id}] has executed all current items!" - ) - - items = filter_items(state.regimen) - regimen = %{state.regimen | regimen_items: items} - {:noreply, %{state | regimen: regimen}} - end - - defp do_item(item, regimen, state) do - if item do - sequence = Farmbot.Asset.get_sequence_by_id!(item.sequence_id) - CeleryScript.sequence(sequence, fn(results) -> - case results do - :ok -> - Farmbot.Logger.success(1, "[#{sequence.name}] executed by [#{regimen.name}] complete.") - {:error, _} -> - Farmbot.Logger.error(1, "[#{sequence.name}] executed by [#{regimen.name}] failed.") - end - end) - end - - next_item = List.first(regimen.regimen_items) - - if next_item do - new_state = build_next_state(regimen, next_item, self(), state) - {:noreply, new_state} - else - complete(regimen, state) - end - end - - def build_next_state(%Regimen{} = regimen, %Item{} = nx_itm, pid, state) do - if state.timer do - Process.cancel_timer(state.timer) - end - - next_dt = Timex.shift(state.epoch, milliseconds: nx_itm.time_offset) - timezone = get_config_value(:string, "settings", "timezone") - now = Timex.now(timezone) - offset_from_now = Timex.diff(next_dt, now, :milliseconds) - - timer = - if offset_from_now < 0 and offset_from_now < -60_000 do - Process.send_after(pid, :skip, 1) - else - {msg, real_offset} = ensure_not_negative(offset_from_now) - Process.send_after(pid, msg, real_offset) - end - - if offset_from_now > 0 do - timestr = Farmbot.TimeUtils.format_time(next_dt) - from_now = Timex.from_now(next_dt, Farmbot.Asset.device().timezone) - - msg = - "[#{regimen.name}] scheduled by FarmEvent (#{regimen.farm_event_id}) " <> - "will execute next item #{from_now} (#{timestr})" - - Farmbot.Logger.info(3, msg) - end - - %{state | timer: timer, regimen: regimen, next_execution: next_dt} - end - - defp ensure_not_negative(offset) when offset < -60_000, do: {:skip, 1} - defp ensure_not_negative(offset) when offset < 0, do: {:execute, 1000} - defp ensure_not_negative(offset), do: {:execute, offset} - - @spec pop_item(Regimen.t()) :: {Item.t() | nil, Regimen.t()} - # when there is more than one item pop the top one - defp pop_item(%Regimen{regimen_items: [do_this_one | items]} = r) do - {do_this_one, %Regimen{r | regimen_items: items}} - end -end diff --git a/farmbot_core/lib/regimen/name_provider.ex b/farmbot_core/lib/regimen/name_provider.ex deleted file mode 100644 index 57b62e59..00000000 --- a/farmbot_core/lib/regimen/name_provider.ex +++ /dev/null @@ -1,89 +0,0 @@ -defmodule Farmbot.Regimen.NameProvider do - @moduledoc """ - Provides global names for running regimens as started by the - RegimenSupervisor. - - # Example - ``` - %Regimen{} = reg = Farmbot.Asset.get_regimen_by_id(123, 100) - via = Farmbot.Regimen.NameProvider.via(reg) - pid = GenServer.whereis(via) - ``` - """ - - alias Farmbot.Asset.Regimen - import Farmbot.Asset, only: [persistent_regimen: 1, delete_persistent_regimen: 1] - use GenServer - require Farmbot.Logger - - @checkup 45_000 - - def start_link(args) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def via(%Regimen{} = regimen) do - regimen.farm_event_id || raise "Regimen lookups require a farm_event_id" - {:via, __MODULE__, regimen} - end - - def whereis_name(%Regimen{} = regimen) do - GenServer.call(__MODULE__, {:whereis_name, regimen}) - end - - def register_name(%Regimen{} = regimen, pid) do - GenServer.call(__MODULE__, {:register_name, regimen, pid}) - end - - def unregister_name(%Regimen{} = regimen) do - GenServer.call(__MODULE__, {:unregister_name, regimen}) - end - - def init([]) do - start_timer() - {:ok, %{}} - end - - def handle_call({:whereis_name, regimen}, _, state) do - # Farmbot.Logger.info 3, "whereis_name: #{regimen.name} #{regimen.farm_event_id}" - case persistent_regimen(regimen) do - nil -> - {:reply, :undefined, state} - %{id: id} -> - {:reply, Map.get(state, id) || :undefined, state} - end - end - - def handle_call({:register_name, regimen, pid}, _, state) do - # Farmbot.Logger.info 3, "register_name: #{regimen.name} #{regimen.farm_event_id}" - case persistent_regimen(regimen) do - nil -> - Farmbot.Logger.error 1, "No persistent regimen for #{regimen.name} #{regimen.farm_event_id}" - {:reply, :no, state} - %{id: id} -> - {:reply, :yes, Map.put(state, id, pid)} - end - end - - def handle_call({:unregister_name, regimen}, _, state) do - # Farmbot.Logger.info 3, "unregister_name: #{regimen.name}" - case delete_persistent_regimen(regimen) do - {:ok, id} -> {:reply, :yes, Map.delete(state, id)} - {:error, reason} -> - Farmbot.Logger.error 1, "Failed to unregister #{regimen.name}: #{inspect reason}" - {:reply, :no, state} - end - end - - def handle_info(:checkup, state) do - new_state = Enum.filter(state, fn({_pr_id, pid}) -> - Process.alive?(pid) - end) |> Map.new() - start_timer() - {:noreply, new_state} - end - - defp start_timer do - Process.send_after(self(), :checkup, @checkup) - end -end diff --git a/farmbot_core/lib/regimen/supervisor.ex b/farmbot_core/lib/regimen/supervisor.ex deleted file mode 100644 index 5b3e597b..00000000 --- a/farmbot_core/lib/regimen/supervisor.ex +++ /dev/null @@ -1,185 +0,0 @@ -defmodule Farmbot.Regimen.Supervisor do - @moduledoc false - use Supervisor - alias Farmbot.Asset - alias Asset.PersistentRegimen - alias Farmbot.Regimen.NameProvider - require Farmbot.Logger - - @doc "Debug function to see what regimens are running." - def whats_going_on do - IO.warn("THIS SHOULD NOT BE USED IN PRODUCTION") - prs = Asset.all_persistent_regimens() - - Enum.map(prs, fn %PersistentRegimen{regimen_id: rid, farm_event_id: fid, time: start_time} = - pr -> - r = Farmbot.Asset.get_regimen_by_id!(rid, fid) - server_name = NameProvider.via(r) - pid = GenServer.whereis(server_name) - alive = if pid, do: "is alive", else: "is not alive" - state = if pid, do: :sys.get_state(pid) - - info = %{ - _status: - "[#{r.id}] scheduled by FarmEvent: [#{fid}] #{Timex.from_now(start_time)}, #{alive}", - _id: r.id, - _farm_event_id: r.farm_event_id, - pid: pid, - persistent_regimen: pr - } - - if state do - timezone = Farmbot.Asset.device().timezone - - next = state.next_execution - timer_ms = state.timer |> Process.read_timer() || 0 - from_now = Timex.from_now(next, timezone) - next_tick = Timex.from_now(Timex.shift(Timex.now(), milliseconds: timer_ms), timezone) - - state = %{ - state - | regimen: %{ - state.regimen - | regimen_items: "#{Enum.count(state.regimen.regimen_items)} items." - } - } - - Map.put(info, :state, state) - |> Map.put(:_next_execution, from_now) - |> Map.put(:_next_tick, next_tick) - else - info - end - end) - end - - @doc "Stops all running instances of a regimen." - def stop_all_managers(regimen) do - Farmbot.Logger.info(3, "Stopping all running regimens by id: #{inspect(regimen.id)}") - prs = Asset.persistent_regimens(regimen) - - for %PersistentRegimen{farm_event_id: feid} <- prs do - reg_with_fe_id = %{regimen | farm_event_id: feid} - name = NameProvider.via(reg_with_fe_id) - - case GenServer.whereis(name) do - nil -> - Farmbot.Logger.info(3, "Could not find regimen by id: #{reg_with_fe_id.id} and tag: #{feid}") - - regimen_server -> - GenServer.stop(regimen_server) - end - - Asset.delete_persistent_regimen(reg_with_fe_id) - end - end - - @doc "Looks up all regimen instances that are running, and reindexes them." - def reindex_all_managers(regimen, time \\ nil) do - prs = Asset.persistent_regimens(regimen) - Farmbot.Logger.debug(3, "Reindexing #{Enum.count(prs)} running regimens by id: #{regimen.id}") - - for %{farm_event_id: feid} <- prs do - reg_with_fe_id = %{regimen | farm_event_id: feid} - name = NameProvider.via(reg_with_fe_id) - - case GenServer.whereis(name) do - nil -> - Farmbot.Logger.info(3, "Could not find regimen by id: #{reg_with_fe_id.id} and tag: #{feid}") - - regimen_server -> - if time do - Asset.update_persistent_regimen_time(regimen, time) - end - - GenServer.call(regimen_server, {:reindex, reg_with_fe_id, time}) - end - end - end - - @doc false - def start_link(args) do - Supervisor.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - prs = Asset.all_persistent_regimens() - children = build_children(prs) - opts = [strategy: :one_for_one] - supervise(children, opts) - end - - def add_child(regimen, time) do - regimen.farm_event_id || raise "Starting a regimen process requires a farm event id tag." - - # Farmbot.Logger.debug 3, "Starting regimen: #{regimen.name} #{regimen.farm_event_id} at #{inspect time}" - Asset.add_persistent_regimen(regimen, time) - args = [regimen, time] - opts = [restart: :transient, id: regimen.farm_event_id] - spec = worker(Farmbot.Regimen.Manager, args, opts) - Supervisor.start_child(__MODULE__, spec) - end - - def stop_child(regimen) do - regimen.farm_event_id || raise "Stopping a regimen process requires a farm event id tag." - name = NameProvider.via(regimen) - - case GenServer.whereis(name) do - nil -> - Farmbot.Logger.info( - 3, - "Could not find regimen by id: #{regimen.id} and tag: #{regimen.farm_event_id}" - ) - - _regimen_server -> - Farmbot.Logger.debug(3, "Stopping regimen: #{regimen.name} (#{regimen.farm_event_id})") - Supervisor.terminate_child(Farmbot.Regimen.Supervisor, regimen.farm_event_id) - Supervisor.delete_child(Farmbot.Regimen.Supervisor, regimen.farm_event_id) - end - - Asset.delete_persistent_regimen(regimen) - end - - @doc "Builds a list of supervisor children. Will also delete and not build a child from stale data." - @spec build_children([%PersistentRegimen{}]) :: [Supervisor.child_spec()] - def build_children(prs) do - Enum.reject(prs, fn %PersistentRegimen{regimen_id: rid, farm_event_id: feid} -> - reg = Asset.get_regimen_by_id(rid, feid) - - if Asset.get_farm_event_by_id(feid) && reg do - _rejected = false - else - Farmbot.Logger.debug( - 3, - "Deleting stale persistent regimen: regimen_id: #{rid} farm_event_id: #{feid}" - ) - - # Build a fake regimen to allow the deletion of the persistent regimen - # if reg above is nil. - backup = %Farmbot.Asset.Regimen{ - farm_event_id: feid, - id: rid, - name: "Not Real", - regimen_items: [] - } - - Asset.delete_persistent_regimen(reg || backup) - _rejected = true - end - end) - |> Enum.map(fn %PersistentRegimen{regimen_id: id, time: time, farm_event_id: feid} -> - regimen = Asset.get_regimen_by_id!(id, feid) - farm_event = Asset.get_farm_event_by_id(feid) - fe_time = Timex.parse!(farm_event.start_time, "{ISO:Extended}") - - if Timex.compare(fe_time, time) != 0 do - Asset.update_persistent_regimen_time(regimen, fe_time) - Farmbot.Logger.debug(1, "FarmEvent start time and stored regimen start time are different.") - end - - args = [regimen, fe_time] - opts = [restart: :transient, id: feid] - worker(Farmbot.Regimen.Manager, args, opts) - end) - end -end diff --git a/farmbot_core/lib/time_utils.ex b/farmbot_core/lib/time_utils.ex index 7e0beccc..773dc6de 100644 --- a/farmbot_core/lib/time_utils.ex +++ b/farmbot_core/lib/time_utils.ex @@ -16,4 +16,22 @@ defmodule Farmbot.TimeUtils do n = Timex.Timezone.convert(time, tz) Timex.shift(n, hours: -n.hour, seconds: -n.second, minutes: -n.minute) end + + @doc """ + Compares a datetime with another. + • -1 -- the first date comes before the second one + • 0 -- both arguments represent the same date when coalesced to the same + timezone. + • 1 -- the first date comes after the second one + + Returns :gt if the first datetime is later than the second and :lt for vice + versa. If the two datetimes are equal :eq is returned. + """ + def compare_datetimes(left, right) do + case Timex.compare(left, right, :seconds) do + -1 -> :lt + 0 -> :eq + 1 -> :gt + end + end end diff --git a/farmbot_core/mix.exs b/farmbot_core/mix.exs index d0495d63..35c39ca7 100644 --- a/farmbot_core/mix.exs +++ b/farmbot_core/mix.exs @@ -11,6 +11,7 @@ defmodule FarmbotCore.MixProject do defp arduino_commit do opts = [cd: Path.join("c_src", "farmbot-arduino-firmware")] + System.cmd("git", ~w"rev-parse --verify HEAD", opts) |> elem(0) |> String.trim() @@ -25,6 +26,7 @@ defmodule FarmbotCore.MixProject do make_env: make_env(), make_cwd: __DIR__, compilers: [:elixir_make] ++ Mix.compilers(), + elixirc_paths: elixirc_paths(Mix.env()), version: @version, target: @target, branch: @branch, @@ -32,6 +34,7 @@ defmodule FarmbotCore.MixProject do arduino_commit: arduino_commit(), build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, + aliases: aliases(), deps: deps(), dialyzer: [ plt_add_deps: :transitive, @@ -39,9 +42,14 @@ defmodule FarmbotCore.MixProject do flags: [] ], test_coverage: [tool: ExCoveralls], - preferred_cli_env: [coveralls: :test, "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test], + preferred_cli_env: [ + coveralls: :test, + "coveralls.detail": :test, + "coveralls.post": :test, + "coveralls.html": :test + ], source_url: "https://github.com/Farmbot/farmbot_os", - homepage_url: "http://farmbot.io", + homepage_url: "http://farmbot.io" ] end @@ -74,10 +82,8 @@ defmodule FarmbotCore.MixProject do nil -> %{ "MAKE_CWD" => __DIR__, - "ERL_EI_INCLUDE_DIR" => - Path.join([:code.root_dir(), "usr", "include"]), - "ERL_EI_LIBDIR" => - Path.join([:code.root_dir(), "usr", "lib"]), + "ERL_EI_INCLUDE_DIR" => Path.join([:code.root_dir(), "usr", "include"]), + "ERL_EI_LIBDIR" => Path.join([:code.root_dir(), "usr", "lib"]), "MIX_TARGET" => @target } @@ -85,4 +91,15 @@ defmodule FarmbotCore.MixProject do %{"MAKE_CWD" => __DIR__} end end + + defp elixirc_paths(:test) do + ["lib", "../test/support"] + end + + defp elixirc_paths(_), do: ["lib"] + + defp aliases, + do: [ + test: ["ecto.drop", "ecto.migrate", "test"] + ] end diff --git a/farmbot_core/mix.lock b/farmbot_core/mix.lock index 3133f882..96ca8171 100644 --- a/farmbot_core/mix.lock +++ b/farmbot_core/mix.lock @@ -27,7 +27,7 @@ "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, "ring_logger": {:hex, :ring_logger, "0.4.1", "db972365bfda705288d7629e80af5704a1aafdbe9da842712c3cdd587639c72e", [:mix], [], "hexpm"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, - "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.5", "f111a48188b0640effb7f2952071c4cf285501d3ce090820a7c2fc20af3867e9", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, "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.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/farmbot_core/priv/asset/migrations/.formatter.exs b/farmbot_core/priv/asset/migrations/.formatter.exs new file mode 100644 index 00000000..16173310 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/.formatter.exs @@ -0,0 +1,4 @@ +[ + import_deps: [:ecto], + inputs: ["*.exs"] +] diff --git a/farmbot_core/priv/asset/migrations/20170919150432_add_farm_events_table.exs b/farmbot_core/priv/asset/migrations/20170919150432_add_farm_events_table.exs deleted file mode 100644 index b30bb144..00000000 --- a/farmbot_core/priv/asset/migrations/20170919150432_add_farm_events_table.exs +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddFarmEventsTable do - use Ecto.Migration - - def change do - create table("farm_events", primary_key: false) do - add(:id, :integer) - add(:start_time, :string) - add(:end_time, :string) - add(:repeat, :integer) - add(:time_unit, :string) - add(:executable_type, :string) - add(:executable_id, :integer) - add(:calendar, :string) - end - - create(unique_index("farm_events", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920025503_add_peripherals_table.exs b/farmbot_core/priv/asset/migrations/20170920025503_add_peripherals_table.exs deleted file mode 100644 index 6140dbb2..00000000 --- a/farmbot_core/priv/asset/migrations/20170920025503_add_peripherals_table.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddPeripheralsTable do - use Ecto.Migration - - def change do - create table("peripherals", primary_key: false) do - add(:id, :integer) - add(:pin, :integer) - add(:mode, :integer) - add(:label, :string) - end - - create(unique_index("peripherals", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920182759_add_sequences_table.exs b/farmbot_core/priv/asset/migrations/20170920182759_add_sequences_table.exs deleted file mode 100644 index 47893e2b..00000000 --- a/farmbot_core/priv/asset/migrations/20170920182759_add_sequences_table.exs +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddSequencesTable do - use Ecto.Migration - - def change do - create table("sequences", primary_key: false) do - add(:id, :integer) - add(:name, :string) - add(:kind, :string, default: "sequence") - add(:args, :text) - add(:body, :text) - end - - create(unique_index("sequences", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920182805_add_regimens_table.exs b/farmbot_core/priv/asset/migrations/20170920182805_add_regimens_table.exs deleted file mode 100644 index b0293a81..00000000 --- a/farmbot_core/priv/asset/migrations/20170920182805_add_regimens_table.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddRegimensTable do - use Ecto.Migration - - def change do - create table("regimens", primary_key: false) do - add(:id, :integer) - add(:name, :string) - add(:regimen_items, :string) - end - - create(unique_index("regimens", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920182817_add_tools_table.exs b/farmbot_core/priv/asset/migrations/20170920182817_add_tools_table.exs deleted file mode 100644 index 08ba608b..00000000 --- a/farmbot_core/priv/asset/migrations/20170920182817_add_tools_table.exs +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddToolsTable do - use Ecto.Migration - - def change do - create table("tools", primary_key: false) do - add(:id, :integer) - add(:name, :string) - end - - create(unique_index("tools", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920182827_add_tool_slots_table.exs b/farmbot_core/priv/asset/migrations/20170920182827_add_tool_slots_table.exs deleted file mode 100644 index 84ef61fa..00000000 --- a/farmbot_core/priv/asset/migrations/20170920182827_add_tool_slots_table.exs +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddToolSlotsTable do - use Ecto.Migration - - def change do - create table("tool_slots", primary_key: false) do - add(:id, :integer) - add(:tool_id, :integer) - end - - create(unique_index("tool_slots", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920182853_add_points_table.exs b/farmbot_core/priv/asset/migrations/20170920182853_add_points_table.exs deleted file mode 100644 index 9126b7bb..00000000 --- a/farmbot_core/priv/asset/migrations/20170920182853_add_points_table.exs +++ /dev/null @@ -1,18 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddPointsTable do - use Ecto.Migration - - def change do - create table("points", primary_key: false) do - add(:id, :integer) - add(:name, :string) - add(:x, :float) - add(:y, :float) - add(:z, :float) - add(:meta, :text) - add(:tool_id, :integer) - add(:pointer_type, :string) - end - - create(unique_index("points", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20170920183526_add_generic_pointers_table.exs b/farmbot_core/priv/asset/migrations/20170920183526_add_generic_pointers_table.exs deleted file mode 100644 index 9e0de194..00000000 --- a/farmbot_core/priv/asset/migrations/20170920183526_add_generic_pointers_table.exs +++ /dev/null @@ -1,11 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddGenericPointersTable do - use Ecto.Migration - - def change do - create table("generic_pointers", primary_key: false) do - add(:id, :integer) - end - - create(unique_index("generic_pointers", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20171025231225_add_devices_table.exs b/farmbot_core/priv/asset/migrations/20171025231225_add_devices_table.exs deleted file mode 100644 index dbfa1e6a..00000000 --- a/farmbot_core/priv/asset/migrations/20171025231225_add_devices_table.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddDevicesTable do - use Ecto.Migration - - def change do - create table("devices", primary_key: false) do - add(:id, :integer) - add(:name, :string) - add(:timezone, :string) - end - - create(unique_index("devices", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180309203638_add_sensors_table.exs b/farmbot_core/priv/asset/migrations/20180309203638_add_sensors_table.exs deleted file mode 100644 index cd70b267..00000000 --- a/farmbot_core/priv/asset/migrations/20180309203638_add_sensors_table.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddSensorsTable do - use Ecto.Migration - - def change do - create table("sensors", primary_key: false) do - add(:id, :integer) - add(:pin, :integer) - add(:mode, :integer) - add(:label, :string) - end - - create(unique_index("sensors", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180322010355_fix_namespaces.exs b/farmbot_core/priv/asset/migrations/20180322010355_fix_namespaces.exs deleted file mode 100644 index 200d8fd8..00000000 --- a/farmbot_core/priv/asset/migrations/20180322010355_fix_namespaces.exs +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.FixNamespaces do - use Ecto.Migration - import Ecto.Query - - def change do - repo = Application.get_env(:farmbot_core, :repo_hack) - if repo do - do_update(repo) - else - IO.puts "Not migrating." - end - end - - defp do_update(repo) do - fe_needs_change = repo.all(from e in Farmbot.Asset.FarmEvent) - |> Enum.filter(fn(asset) -> - String.contains?(asset.executable_type, "Repo") - end) - - fe_needs_change |> Enum.map(fn(a) -> - String.split(a.executable_type, ".") |> List.last - Ecto.Changeset.change(a, executable_type: String.split(a.executable_type, ".") |> List.last) - end) |> Enum.map(fn(cs) -> repo.update!(cs) end) - |> fn(updated) -> - IO.puts "FarmEvents updated: #{Enum.count(updated)}\n\n\n" - end.() - - point_needs_change = repo.all(from p in Farmbot.Asset.Point) - |> Enum.filter(fn(asset) -> - String.contains?(asset.pointer_type, "Repo") - end) - - point_needs_change |> Enum.map(fn(a) -> - String.split(a.pointer_type, ".") |> List.last - Ecto.Changeset.change(a, pointer_type: String.split(a.pointer_type, ".") |> List.last) - end) |> Enum.map(fn(cs) -> repo.update!(cs) end) - |> fn(updated) -> - IO.puts "Points updated: #{Enum.count(updated)}" - end.() - end -end diff --git a/farmbot_core/priv/asset/migrations/20180621184803_add_sync_cmd_table.exs b/farmbot_core/priv/asset/migrations/20180621184803_add_sync_cmd_table.exs deleted file mode 100644 index e83cd84e..00000000 --- a/farmbot_core/priv/asset/migrations/20180621184803_add_sync_cmd_table.exs +++ /dev/null @@ -1,12 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddSyncCmdTable do - use Ecto.Migration - - def change do - create table("sync_cmds") do - add(:remote_id, :integer) - add(:kind, :string) - add(:body, :string) - timestamps() - end - end -end diff --git a/farmbot_core/priv/asset/migrations/20180621184905_add_regimen_persistence_table.exs b/farmbot_core/priv/asset/migrations/20180621184905_add_regimen_persistence_table.exs deleted file mode 100644 index 1d81a41f..00000000 --- a/farmbot_core/priv/asset/migrations/20180621184905_add_regimen_persistence_table.exs +++ /dev/null @@ -1,14 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddRegimenPersistenceTable do - use Ecto.Migration - - def change do - create table("persistent_regimens") do - add :regimen_id, :integer - add :time, :utc_datetime - add :farm_event_id, :integer - timestamps() - end - unique_index("persistent_regimens", :regimen_id) - create unique_index("persistent_regimens", [:regimen_id, :time, :farm_event_id], name: :regimen_start_time) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180713180958_pin_bindings_special_action.exs b/farmbot_core/priv/asset/migrations/20180713180958_pin_bindings_special_action.exs deleted file mode 100644 index 1efc1e9b..00000000 --- a/farmbot_core/priv/asset/migrations/20180713180958_pin_bindings_special_action.exs +++ /dev/null @@ -1,9 +0,0 @@ -defmodule Farmbot.Repo.Migrations.PinBindingsSpecialAction do - use Ecto.Migration - - def change do - alter table("pin_bindings") do - add(:special_action, :string) - end - end -end diff --git a/farmbot_core/priv/asset/migrations/20180717220240_pin_bindings_unique_index.exs b/farmbot_core/priv/asset/migrations/20180717220240_pin_bindings_unique_index.exs deleted file mode 100644 index d047ebc2..00000000 --- a/farmbot_core/priv/asset/migrations/20180717220240_pin_bindings_unique_index.exs +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Farmbot.Repo.Migrations.PinBindingsUniqueIndex do - use Ecto.Migration - - def change do - create unique_index("pin_bindings", [:pin_num]) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180718180123_pin_binding_delete.exs b/farmbot_core/priv/asset/migrations/20180718180123_pin_binding_delete.exs deleted file mode 100644 index 43127032..00000000 --- a/farmbot_core/priv/asset/migrations/20180718180123_pin_binding_delete.exs +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Farmbot.Repo.Migrations.PinBindingDelete do - use Ecto.Migration - - def change do - execute("DELETE FROM pin_bindings") - end -end diff --git a/farmbot_core/priv/asset/migrations/20180814173955_create_farmware_env_table.exs b/farmbot_core/priv/asset/migrations/20180814173955_create_farmware_env_table.exs deleted file mode 100644 index 4d6d8a51..00000000 --- a/farmbot_core/priv/asset/migrations/20180814173955_create_farmware_env_table.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.CreateFarmwareEnvTable do - use Ecto.Migration - - def change do - create table("farmware_envs", primary_key: false) do - add(:id, :integer) - add(:key, :string) - add(:value, :text) - end - - create(unique_index("farmware_envs", [:id])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180814174016_create_farmware_installations_table.exs b/farmbot_core/priv/asset/migrations/20180814174016_create_farmware_installations_table.exs deleted file mode 100644 index 1c7e94b5..00000000 --- a/farmbot_core/priv/asset/migrations/20180814174016_create_farmware_installations_table.exs +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.CreateFarmwareInstallationsTable do - use Ecto.Migration - - def change do - create table("farmware_installations", primary_key: false) do - add(:id, :integer) - add(:url, :string) - add(:first_party, :boolean) - end - - create(unique_index("farmware_installations", [:id, :url])) - end -end diff --git a/farmbot_core/priv/asset/migrations/20180815120750_add_local_id_and_dirty_fields.exs b/farmbot_core/priv/asset/migrations/20180815120750_add_local_id_and_dirty_fields.exs deleted file mode 100644 index 8af2835c..00000000 --- a/farmbot_core/priv/asset/migrations/20180815120750_add_local_id_and_dirty_fields.exs +++ /dev/null @@ -1,65 +0,0 @@ -defmodule Farmbot.Asset.Repo.Migrations.AddLocalIdAndDirtyFields do - use Ecto.Migration - - def change do - alter table("devices") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("farm_events") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("farmware_installations") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("farmware_envs") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("peripherals") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("pin_bindings") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("points") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("regimens") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("sensors") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("sequences") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - alter table("tools") do - add(:local_id, :uuid, primary: true) - add(:dirty, :boolean, default: false) - end - - # TODO(Connor) - 2018-08-15 All things without a UUID will probably need one? - # However first HTTP sync will wipe out all assets, and redownload them - # So i guess not? - end - -end diff --git a/farmbot_core/priv/asset/migrations/20181017210730_create_local_metas_table.exs b/farmbot_core/priv/asset/migrations/20181017210730_create_local_metas_table.exs new file mode 100644 index 00000000..ab50b332 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017210730_create_local_metas_table.exs @@ -0,0 +1,13 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateLocalMetasTable do + use Ecto.Migration + + def change do + create table("local_metas") do + add(:status, :string) + add(:table, :string) + add(:asset_local_id, :binary_id) + end + + create(unique_index("local_metas", [:table, :asset_local_id])) + end +end diff --git a/farmbot_core/priv/asset/migrations/20181017210735_create_devices_table.exs b/farmbot_core/priv/asset/migrations/20181017210735_create_devices_table.exs new file mode 100644 index 00000000..e3363afd --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017210735_create_devices_table.exs @@ -0,0 +1,13 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateDevicesTable do + use Ecto.Migration + + def change do + create table("devices", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:name, :string) + add(:timezone, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181017223659_create_tools_table.exs b/farmbot_core/priv/asset/migrations/20181017223659_create_tools_table.exs new file mode 100644 index 00000000..7c012f9f --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017223659_create_tools_table.exs @@ -0,0 +1,12 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateToolsTable do + use Ecto.Migration + + def change do + create table("tools", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:name, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181017225445_create_peripherals_table.exs b/farmbot_core/priv/asset/migrations/20181017225445_create_peripherals_table.exs new file mode 100644 index 00000000..5722e93b --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017225445_create_peripherals_table.exs @@ -0,0 +1,14 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreatePeripheralsTable do + use Ecto.Migration + + def change do + create table("peripherals", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:pin, :integer) + add(:mode, :integer) + add(:label, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181017230326_create_sensors_table.exs b/farmbot_core/priv/asset/migrations/20181017230326_create_sensors_table.exs new file mode 100644 index 00000000..b57d3f54 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017230326_create_sensors_table.exs @@ -0,0 +1,14 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateSensorsTable do + use Ecto.Migration + + def change do + create table("sensors", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:pin, :integer) + add(:mode, :integer) + add(:label, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181017230333_create_sensor_readings_table.exs b/farmbot_core/priv/asset/migrations/20181017230333_create_sensor_readings_table.exs new file mode 100644 index 00000000..7440941b --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181017230333_create_sensor_readings_table.exs @@ -0,0 +1,17 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateSensorReadingsTable do + use Ecto.Migration + + def change do + create table("sensor_readings", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:mode, :integer) + add(:pin, :integer) + add(:value, :integer) + add(:x, :float) + add(:y, :float) + add(:z, :float) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181018005030_create_sequences_table.exs b/farmbot_core/priv/asset/migrations/20181018005030_create_sequences_table.exs new file mode 100644 index 00000000..6607a515 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181018005030_create_sequences_table.exs @@ -0,0 +1,15 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateSequencesTable do + use Ecto.Migration + + def change do + create table("sequences", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:name, :string) + add(:kind, :string) + add(:args, :map) + add(:body, {:array, :map}) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181018022229_create_regimens_table.exs b/farmbot_core/priv/asset/migrations/20181018022229_create_regimens_table.exs new file mode 100644 index 00000000..36728058 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181018022229_create_regimens_table.exs @@ -0,0 +1,13 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateRegimensTable do + use Ecto.Migration + + def change do + create table("regimens", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:regimen_items, {:array, :map}) + add(:name, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20180710160707_create_pin_bindings_table.exs b/farmbot_core/priv/asset/migrations/20181018040229_create_pin_bindings_table.exs similarity index 58% rename from farmbot_core/priv/asset/migrations/20180710160707_create_pin_bindings_table.exs rename to farmbot_core/priv/asset/migrations/20181018040229_create_pin_bindings_table.exs index 48a286d1..b927bdbe 100644 --- a/farmbot_core/priv/asset/migrations/20180710160707_create_pin_bindings_table.exs +++ b/farmbot_core/priv/asset/migrations/20181018040229_create_pin_bindings_table.exs @@ -3,11 +3,12 @@ defmodule Farmbot.Asset.Repo.Migrations.CreatePinBindingsTable do def change do create table("pin_bindings", primary_key: false) do - add(:id, :integer) + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) add(:pin_num, :integer) add(:sequence_id, :integer) + add(:special_action, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) end - - create(unique_index("pin_bindings", [:id])) end end diff --git a/farmbot_core/priv/asset/migrations/20181019165701_create_points_table.exs b/farmbot_core/priv/asset/migrations/20181019165701_create_points_table.exs new file mode 100644 index 00000000..1c3f4750 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019165701_create_points_table.exs @@ -0,0 +1,20 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreatePointsTable do + use Ecto.Migration + + def change do + create table("points", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:meta, :map) + add(:name, :string) + add(:plant_stage, :string) + add(:planted_at, :utc_datetime) + add(:pointer_type, :string) + add(:radius, :float) + add(:x, :float) + add(:y, :float) + add(:z, :float) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019172740_create_farm_events_table.exs b/farmbot_core/priv/asset/migrations/20181019172740_create_farm_events_table.exs new file mode 100644 index 00000000..c47e6446 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019172740_create_farm_events_table.exs @@ -0,0 +1,18 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateFarmEventsTable do + use Ecto.Migration + + def change do + create table("farm_events", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:end_time, :utc_datetime) + add(:executable_type, :string) + add(:executable_id, :id) + add(:repeat, :integer) + add(:start_time, :utc_datetime) + add(:time_unit, :string) + add(:last_executed, :utc_datetime) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019180739_create_firmware_configs_table.exs b/farmbot_core/priv/asset/migrations/20181019180739_create_firmware_configs_table.exs new file mode 100644 index 00000000..325a645a --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019180739_create_firmware_configs_table.exs @@ -0,0 +1,99 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateFirmwareConfigsTable do + use Ecto.Migration + + def change do + create table("firmware_configs", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:pin_guard_4_time_out, :float) + add(:pin_guard_1_active_state, :float) + add(:encoder_scaling_y, :float) + add(:movement_invert_2_endpoints_x, :float) + add(:movement_min_spd_y, :float) + add(:pin_guard_2_time_out, :float) + add(:movement_timeout_y, :float) + add(:movement_home_at_boot_y, :float) + add(:movement_home_spd_z, :float) + add(:movement_invert_endpoints_z, :float) + add(:pin_guard_1_pin_nr, :float) + add(:movement_invert_endpoints_y, :float) + add(:movement_max_spd_y, :float) + add(:movement_home_up_y, :float) + add(:encoder_missed_steps_decay_z, :float) + add(:movement_home_spd_y, :float) + add(:encoder_use_for_pos_x, :float) + add(:movement_step_per_mm_x, :float) + add(:movement_home_at_boot_z, :float) + add(:movement_steps_acc_dec_z, :float) + add(:pin_guard_5_pin_nr, :float) + add(:movement_invert_motor_z, :float) + add(:movement_max_spd_x, :float) + add(:movement_enable_endpoints_y, :float) + add(:movement_enable_endpoints_z, :float) + add(:movement_stop_at_home_x, :float) + add(:movement_axis_nr_steps_y, :float) + add(:pin_guard_1_time_out, :float) + add(:movement_home_at_boot_x, :float) + add(:pin_guard_2_pin_nr, :float) + add(:encoder_scaling_z, :float) + add(:param_e_stop_on_mov_err, :float) + add(:encoder_enabled_x, :float) + add(:pin_guard_2_active_state, :float) + add(:encoder_missed_steps_decay_y, :float) + add(:movement_home_up_z, :float) + add(:movement_enable_endpoints_x, :float) + add(:movement_step_per_mm_y, :float) + add(:pin_guard_3_pin_nr, :float) + add(:param_mov_nr_retry, :float) + add(:movement_stop_at_home_z, :float) + add(:pin_guard_4_active_state, :float) + add(:movement_steps_acc_dec_y, :float) + add(:movement_home_spd_x, :float) + add(:movement_keep_active_x, :float) + add(:pin_guard_3_time_out, :float) + add(:movement_keep_active_y, :float) + add(:encoder_scaling_x, :float) + add(:movement_invert_2_endpoints_z, :float) + add(:encoder_missed_steps_decay_x, :float) + add(:movement_timeout_z, :float) + add(:encoder_missed_steps_max_z, :float) + add(:movement_min_spd_z, :float) + add(:encoder_enabled_y, :float) + add(:encoder_type_y, :float) + add(:movement_home_up_x, :float) + add(:pin_guard_3_active_state, :float) + add(:movement_invert_motor_x, :float) + add(:movement_keep_active_z, :float) + add(:movement_max_spd_z, :float) + add(:movement_secondary_motor_invert_x, :float) + add(:movement_stop_at_max_x, :float) + add(:movement_steps_acc_dec_x, :float) + add(:pin_guard_4_pin_nr, :float) + add(:encoder_type_x, :float) + add(:movement_invert_2_endpoints_y, :float) + add(:encoder_invert_y, :float) + add(:movement_axis_nr_steps_x, :float) + add(:movement_stop_at_max_z, :float) + add(:movement_invert_endpoints_x, :float) + add(:encoder_invert_z, :float) + add(:encoder_use_for_pos_z, :float) + add(:pin_guard_5_active_state, :float) + add(:movement_step_per_mm_z, :float) + add(:encoder_enabled_z, :float) + add(:movement_secondary_motor_x, :float) + add(:pin_guard_5_time_out, :float) + add(:movement_min_spd_x, :float) + add(:encoder_type_z, :float) + add(:movement_stop_at_max_y, :float) + add(:encoder_use_for_pos_y, :float) + add(:encoder_missed_steps_max_y, :float) + add(:movement_timeout_x, :float) + add(:movement_stop_at_home_y, :float) + add(:movement_axis_nr_steps_z, :float) + add(:encoder_invert_x, :float) + add(:encoder_missed_steps_max_x, :float) + add(:movement_invert_motor_y, :float) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019180816_create_fbos_configs_table.exs b/farmbot_core/priv/asset/migrations/20181019180816_create_fbos_configs_table.exs new file mode 100644 index 00000000..ea2dedf3 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019180816_create_fbos_configs_table.exs @@ -0,0 +1,23 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateFbosConfigsTable do + use Ecto.Migration + + def change do + create table("fbos_configs", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:arduino_debug_messages, :boolean) + add(:auto_sync, :boolean) + add(:beta_opt_in, :boolean) + add(:disable_factory_reset, :boolean) + add(:firmware_hardware, :string) + add(:firmware_input_log, :boolean) + add(:firmware_output_log, :boolean) + add(:network_not_found_timer, :integer) + add(:os_auto_update, :boolean) + add(:sequence_body_log, :boolean) + add(:sequence_complete_log, :boolean) + add(:sequence_init_log, :boolean) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019180911_create_farmware_installations_table.exs b/farmbot_core/priv/asset/migrations/20181019180911_create_farmware_installations_table.exs new file mode 100644 index 00000000..895056c1 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019180911_create_farmware_installations_table.exs @@ -0,0 +1,12 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateFarmwareInstallationsTable do + use Ecto.Migration + + def change do + create table("farmware_installations", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:url, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019180925_create_farmware_envs_table.exs b/farmbot_core/priv/asset/migrations/20181019180925_create_farmware_envs_table.exs new file mode 100644 index 00000000..b6eca102 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019180925_create_farmware_envs_table.exs @@ -0,0 +1,13 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateFarmwareEnvsTable do + use Ecto.Migration + + def change do + create table("farmware_envs", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:key, :string) + add(:value, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181019180955_create_diagnostic_dumps_table.exs b/farmbot_core/priv/asset/migrations/20181019180955_create_diagnostic_dumps_table.exs new file mode 100644 index 00000000..bd424193 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181019180955_create_diagnostic_dumps_table.exs @@ -0,0 +1,18 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateDiagnosticDumpsTable do + use Ecto.Migration + + def change do + create table("diagnostic_dumps", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:id, :id) + add(:ticket_identifier, :string) + add(:fbos_commit, :string) + add(:fbos_version, :string) + add(:firmware_commit, :string) + add(:firmware_state, :string) + add(:network_interface, :string) + add(:fbos_dmesg_dump, :string) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181022002449_create_syncs_table.exs b/farmbot_core/priv/asset/migrations/20181022002449_create_syncs_table.exs new file mode 100644 index 00000000..77f5d8b4 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181022002449_create_syncs_table.exs @@ -0,0 +1,26 @@ +defmodule Elixir.Farmbot.Asset.Repo.Migrations.CreateSyncsTable do + use Ecto.Migration + + def change do + create table("syncs", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:devices, {:array, :map}) + add(:diagnostic_dumps, {:array, :map}) + add(:farm_events, {:array, :map}) + add(:farmware_envs, {:array, :map}) + add(:farmware_installations, {:array, :map}) + add(:fbos_configs, {:array, :map}) + add(:firmware_configs, {:array, :map}) + add(:peripherals, {:array, :map}) + add(:pin_bindings, {:array, :map}) + add(:points, {:array, :map}) + add(:regimens, {:array, :map}) + add(:sensor_readings, {:array, :map}) + add(:sensors, {:array, :map}) + add(:sequences, {:array, :map}) + add(:tools, {:array, :map}) + add(:now, :utc_datetime) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + end +end diff --git a/farmbot_core/priv/asset/migrations/20181022002450_create_persistent_regimens_table.exs b/farmbot_core/priv/asset/migrations/20181022002450_create_persistent_regimens_table.exs new file mode 100644 index 00000000..97436fc9 --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181022002450_create_persistent_regimens_table.exs @@ -0,0 +1,16 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreatePersistentRegimensTable do + use Ecto.Migration + + def change do + create table("persistent_regimens", primary_key: false) do + add(:local_id, :binary_id, primary_key: true) + add(:started_at, :utc_datetime) + add(:regimen_id, references("regimens", type: :binary_id, column: :local_id)) + add(:farm_event_id, references("farm_events", type: :binary_id, column: :local_id)) + timestamps(inserted_at: :created_at, type: :utc_datetime) + end + + create(unique_index("persistent_regimens", [:local_id, :regimen_id, :farm_event_id])) + create(unique_index("persistent_regimens", :started_at)) + end +end diff --git a/farmbot_core/priv/asset/migrations/20181022002455_create_id_unique_indexes.exs b/farmbot_core/priv/asset/migrations/20181022002455_create_id_unique_indexes.exs new file mode 100644 index 00000000..78292f0e --- /dev/null +++ b/farmbot_core/priv/asset/migrations/20181022002455_create_id_unique_indexes.exs @@ -0,0 +1,21 @@ +defmodule Farmbot.Asset.Repo.Migrations.CreateIdUniqueIndexes do + use Ecto.Migration + + def change do + create(unique_index("devices", :id)) + create(unique_index("tools", :id)) + create(unique_index("peripherals", :id)) + create(unique_index("sensors", :id)) + create(unique_index("sensor_readings", :id)) + create(unique_index("sequences", :id)) + create(unique_index("regimens", :id)) + create(unique_index("pin_bindings", :id)) + create(unique_index("points", :id)) + create(unique_index("farm_events", :id)) + create(unique_index("firmware_configs", :id)) + create(unique_index("fbos_configs", :id)) + create(unique_index("farmware_installations", :id)) + create(unique_index("farmware_envs", :id)) + create(unique_index("diagnostic_dumps", :id)) + end +end diff --git a/farmbot_core/priv/config/migrations/20170922221449_seed_groups.exs b/farmbot_core/priv/config/migrations/20170922221449_seed_groups.exs index 265252ae..7538e3a2 100644 --- a/farmbot_core/priv/config/migrations/20170922221449_seed_groups.exs +++ b/farmbot_core/priv/config/migrations/20170922221449_seed_groups.exs @@ -23,8 +23,7 @@ defmodule Farmbot.Config.Repo.Migrations.SeedGroups do defp populate_config_values do for name <- @group_names do - [group_id] = - from(g in Group, where: g.group_name == ^name, select: g.id) |> Repo.all() + [group_id] = from(g in Group, where: g.group_name == ^name, select: g.id) |> Repo.all() populate_config_values(name, group_id) end @@ -38,15 +37,15 @@ defmodule Farmbot.Config.Repo.Migrations.SeedGroups do end defp populate_config_values("settings", group_id) do - create_value(BoolValue, true) |> create_config(group_id, "os_auto_update") - create_value(BoolValue, true) |> create_config(group_id, "ignore_external_logs") - create_value(BoolValue, true) |> create_config(group_id, "first_boot") - create_value(BoolValue, true) |> create_config(group_id, "first_sync") - create_value(StringValue, "A") |> create_config(group_id, "current_repo") - create_value(BoolValue, true) |> create_config(group_id, "first_party_farmware") - create_value(BoolValue, false) |> create_config(group_id, "auto_sync") - create_value(StringValue, nil) |> create_config(group_id, "firmware_hardware") - create_value(StringValue, nil) |> create_config(group_id, "timezone") + create_value(BoolValue, true) |> create_config(group_id, "os_auto_update") + create_value(BoolValue, true) |> create_config(group_id, "ignore_external_logs") + create_value(BoolValue, true) |> create_config(group_id, "first_boot") + create_value(BoolValue, true) |> create_config(group_id, "first_sync") + create_value(StringValue, "A") |> create_config(group_id, "current_repo") + create_value(BoolValue, true) |> create_config(group_id, "first_party_farmware") + create_value(BoolValue, false) |> create_config(group_id, "auto_sync") + create_value(StringValue, nil) |> create_config(group_id, "firmware_hardware") + create_value(StringValue, nil) |> create_config(group_id, "timezone") create_value(StringValue, "{}") |> create_config(group_id, "user_env") fpf_url = Application.get_env(:farmbot_core, :farmware)[:first_part_farmware_manifest_url] create_value(StringValue, fpf_url) |> create_config(group_id, "first_party_farmware_url") diff --git a/farmbot_core/priv/config/migrations/20180403180047_add_regimen_persistence_table.exs b/farmbot_core/priv/config/migrations/20180403180047_add_regimen_persistence_table.exs index f2484669..ac166877 100644 --- a/farmbot_core/priv/config/migrations/20180403180047_add_regimen_persistence_table.exs +++ b/farmbot_core/priv/config/migrations/20180403180047_add_regimen_persistence_table.exs @@ -3,9 +3,10 @@ defmodule Farmbot.Config.Repo.Migrations.AddRegimenPersistenceTable do def change do create table("persistent_regimens") do - add :regimen_id, :integer - add :time, :utc_datetime + add(:regimen_id, :integer) + add(:time, :utc_datetime) end + unique_index("persistent_regimens", :regimen_id) end end diff --git a/farmbot_core/priv/config/migrations/20180724200909_migrate_secret.exs b/farmbot_core/priv/config/migrations/20180724200909_migrate_secret.exs index d73e6992..92ba9ec6 100644 --- a/farmbot_core/priv/config/migrations/20180724200909_migrate_secret.exs +++ b/farmbot_core/priv/config/migrations/20180724200909_migrate_secret.exs @@ -3,12 +3,31 @@ defmodule Farmbot.Config.Repo.Migrations.MigrateSecret do import Ecto.Query def change do - group = Farmbot.Config.Repo.one!(from(g in Farmbot.Config.Group, where: g.group_name == "authorization")) - pass_ref = Farmbot.Config.Repo.one!(from(c in Farmbot.Config.Config, where: c.key == "password" and c.group_id == ^group.id)) - sec_ref = Farmbot.Config.Repo.one!(from(c in Farmbot.Config.Config, where: c.key == "secret" and c.group_id == ^group.id)) + group = + Farmbot.Config.Repo.one!( + from(g in Farmbot.Config.Group, where: g.group_name == "authorization") + ) + + pass_ref = + Farmbot.Config.Repo.one!( + from(c in Farmbot.Config.Config, where: c.key == "password" and c.group_id == ^group.id) + ) + + sec_ref = + Farmbot.Config.Repo.one!( + from(c in Farmbot.Config.Config, where: c.key == "secret" and c.group_id == ^group.id) + ) + + pass = + Farmbot.Config.Repo.one!( + from(s in Farmbot.Config.StringValue, where: s.id == ^pass_ref.string_value_id) + ) + + sec = + Farmbot.Config.Repo.one!( + from(s in Farmbot.Config.StringValue, where: s.id == ^sec_ref.string_value_id) + ) - pass = Farmbot.Config.Repo.one!(from(s in Farmbot.Config.StringValue, where: s.id == ^pass_ref.string_value_id)) - sec = Farmbot.Config.Repo.one!(from(s in Farmbot.Config.StringValue, where: s.id == ^sec_ref.string_value_id)) if pass.value do Ecto.Changeset.change(sec, %{value: pass.value}) |> Farmbot.Config.Repo.update!() diff --git a/farmbot_core/priv/config/migrations/20180814180226_add_farmware_migration.exs b/farmbot_core/priv/config/migrations/20180814180226_add_farmware_migration.exs index f009273a..2f57c069 100644 --- a/farmbot_core/priv/config/migrations/20180814180226_add_farmware_migration.exs +++ b/farmbot_core/priv/config/migrations/20180814180226_add_farmware_migration.exs @@ -2,7 +2,7 @@ defmodule Farmbot.Config.Repo.Migrations.AddFarmwareMigration do use Ecto.Migration import Farmbot.Config.MigrationHelpers - @default Farmbot.Project.version == "6.5.0" + @default Farmbot.Project.version() == "6.5.0" def change do create_settings_config("firmware_needs_migration", :bool, @default) diff --git a/farmbot_core/priv/config/migrations/20180814202716_add_ignore_fw_config.exs b/farmbot_core/priv/config/migrations/20180814202716_add_ignore_fw_config.exs index fa93aefa..46ce3d56 100644 --- a/farmbot_core/priv/config/migrations/20180814202716_add_ignore_fw_config.exs +++ b/farmbot_core/priv/config/migrations/20180814202716_add_ignore_fw_config.exs @@ -1,6 +1,7 @@ defmodule Farmbot.System.ConfigStorage.Migrations.AddIgnoreFwConfig do use Ecto.Migration import Farmbot.Config.MigrationHelpers + def change do create_settings_config("ignore_fw_config", :bool, false) end diff --git a/farmbot_core/priv/logger/migrations/20180620135642_add_log_buffer.exs b/farmbot_core/priv/logger/migrations/20180620135642_add_log_buffer.exs index 348b224c..a8cc9e21 100644 --- a/farmbot_core/priv/logger/migrations/20180620135642_add_log_buffer.exs +++ b/farmbot_core/priv/logger/migrations/20180620135642_add_log_buffer.exs @@ -14,7 +14,7 @@ defmodule Farmbot.Logger.Repo.Migrations.AddLogBuffer do add(:version, :string) add(:commit, :string) add(:target, :string) - add(:env, :string) + add(:env, :string) timestamps() end end diff --git a/farmbot_core/test/asset_monitor_test.exs b/farmbot_core/test/asset_monitor_test.exs new file mode 100644 index 00000000..ec96b12d --- /dev/null +++ b/farmbot_core/test/asset_monitor_test.exs @@ -0,0 +1,57 @@ +defmodule Farmbot.AssetMonitorTest do + use ExUnit.Case + alias Farmbot.Asset + alias Farmbot.AssetSupervisor + import Farmbot.TestSupport.AssetFixtures + alias Farmbot.TestSupport.CeleryScript.TestIOLayer + + describe "persistent regimens" do + test "adding a persistent regimen starts a process" do + seq = sequence() + reg = regimen(%{regimen_items: [%{time_offset: 100, sequence_id: seq.id}]}) + event = regimen_event(reg) + {:ok, pr} = Asset.upsert_persistent_regimen(reg, event) + + Farmbot.AssetMonitor.force_checkup() + + pid = AssetSupervisor.whereis_child(pr) + assert is_pid(pid) + assert Process.alive?(pid) + + Asset.Repo.delete!(pr) + + Farmbot.AssetMonitor.force_checkup() + refute Process.alive?(pid) + end + end + + describe "farm events" do + test "adding a farm event starts a process" do + ast = TestIOLayer.debug_ast() + seq = sequence(%{body: [ast]}) + now = DateTime.utc_now() + start_time = Timex.shift(now, seconds: 2) + end_time = Timex.shift(now, minutes: 10) + + params = %{ + start_time: start_time, + end_time: end_time, + repeat: 1, + time_unit: "minutely" + } + + event = sequence_event(seq, params) + + Farmbot.AssetMonitor.force_checkup() + + pid = AssetSupervisor.whereis_child(event) + assert is_pid(pid) + assert Process.alive?(pid) + + Asset.Repo.delete!(event) + + Farmbot.AssetMonitor.force_checkup() + refute Process.alive?(pid) + end + end +end diff --git a/farmbot_core/test/asset_test.exs b/farmbot_core/test/asset_test.exs index 7858f9c8..41867a4b 100644 --- a/farmbot_core/test/asset_test.exs +++ b/farmbot_core/test/asset_test.exs @@ -1,183 +1,26 @@ defmodule Farmbot.AssetTest do - use ExUnit.Case, async: false + use ExUnit.Case + alias Farmbot.Asset.{Repo, Regimen, PersistentRegimen} alias Farmbot.Asset - alias Asset.{ - SyncCmd, - Sequence, - } - alias Asset.Repo.Snapshot.Diff + import Farmbot.TestSupport.AssetFixtures - defp id, do: :rand.uniform(16384) - - test "registers sync commands and syncs" do - id = id() - refute Asset.get_sequence_by_id(id) - Farmbot.Registry.subscribe() - seq = %Sequence{ - name: "test sequence", - kind: "sequence", - args: %{}, - body: [], - id: id - } - %SyncCmd{} = sync_cmd = Asset.register_sync_cmd(id, "Sequence", seq) - assert sync_cmd.remote_id == id - assert sync_cmd.kind == "Sequence" - assert sync_cmd.body == seq - Asset.apply_sync_cmd(sync_cmd) - from_db = Asset.get_sequence_by_id(id) - # make sure the status and diff is dispatched. - assert_receive {Farmbot.Registry, {Asset, {:sync_status, :syncing}}} - assert_receive {Farmbot.Registry, {Asset, {:sync_diff, %Diff{additions: [^from_db]}}}} - assert_receive {Farmbot.Registry, {Asset, {:sync_status, :synced}}} - # make sure the sync cmd was deleted. - assert sync_cmd.id not in Enum.map(Asset.all_sync_cmds(), &Map.get(&1, :id)) - end - - test "won't apply sync_cmd of unknown kind" do - asset = %{id: id(), data: "hey!", value: []} - cmd = Asset.register_sync_cmd(asset.id, "Whoops", asset) - Farmbot.Registry.subscribe() - Asset.apply_sync_cmd(cmd) - # make sure sync status doesn't change to syncing and no diff was dispatched. - refute_receive {Farmbot.Registry, {Asset, {:sync_status, :syncing}}} - refute_receive {Farmbot.Registry, {Asset, {:sync_status, :synced}}} - refute_receive {Farmbot.Registry, {Asset, {:sync_status, :synced}}} - refute_receive {Farmbot.Registry, {Asset, {:sync_diff, %Diff{}}}} - # make sure the sync cmd was deleted. - assert cmd.id not in Enum.map(Asset.all_sync_cmds(), &Map.get(&1, :id)) - end - - test "applies multiple sync cmds" do - seq = %{id: id(), name: "test", kind: "sequence", args: %{}, body: []} - tool = %{id: id(), name: "testtool"} - Asset.register_sync_cmd(seq.id, "Sequence", seq) - Asset.register_sync_cmd(tool.id, "Tool", tool) - Farmbot.Registry.subscribe() - Asset.fragment_sync() - assert_receive {Farmbot.Registry, {Asset, {:sync_status, :syncing}}} - assert_receive {Farmbot.Registry, {Asset, {:sync_status, :synced}}} - assert_receive {Farmbot.Registry, {Asset, {:sync_status, :synced}}} - assert_receive {Farmbot.Registry, {Asset, {:sync_diff, %Diff{additions: [tool_from_db, seq_from_db]}}}} - assert seq.id == seq_from_db.id - assert tool.id == tool_from_db.id - end - - test "sync cmds with empty body remove asset" do - seq = %{id: id(), name: "test", kind: "sequence", args: %{}, body: []} - cmd_insert = Asset.register_sync_cmd(seq.id, "Sequence", seq) - Asset.apply_sync_cmd(cmd_insert) - cmd_delete = Asset.register_sync_cmd(seq.id, "Sequence", nil) - Asset.apply_sync_cmd(cmd_delete) - refute Asset.get_sequence_by_id(seq.id) - end - - test "Sync cmd updates existing data" do - seq = %{id: id(), name: "test", kind: "sequence", args: %{}, body: []} - cmd_insert = Asset.register_sync_cmd(seq.id, "Sequence", seq) - Asset.apply_sync_cmd(cmd_insert) - cmd_update = Asset.register_sync_cmd(seq.id, "Sequence", %{name: "New name!"}) - Asset.apply_sync_cmd(cmd_update) - assert Asset.get_sequence_by_id(seq.id).name == "New name!" - end - - describe "Sequence" do - test "inserts a sequence" do - seq = %{id: id(), name: "test", kind: "sequence", args: %{}, body: []} - cmd_insert = Asset.register_sync_cmd(seq.id, "Sequence", seq) - Asset.apply_sync_cmd(cmd_insert) - assert data = Asset.get_sequence_by_id(seq.id) - assert data == Asset.get_sequence_by_id!(seq.id) + describe "persistent regimens" do + test "creates a persistent regimen" do + seq = sequence() + reg = regimen(%{regimen_items: [%{time_offset: 100, sequence_id: seq.id}]}) + event = regimen_event(reg) + assert {:ok, %PersistentRegimen{}} = Asset.upsert_persistent_regimen(reg, event) end - test "raises when no sequence is to be found" do - assert_raise RuntimeError, ~r"Could not find sequence by id", fn() -> - Asset.get_sequence_by_id!(id()) - end + test "updates a persisten regimen" do + seq = sequence() + reg = regimen(%{name: "old", regimen_items: [%{time_offset: 100, sequence_id: seq.id}]}) + event = regimen_event(reg) + {:ok, pr} = Asset.upsert_persistent_regimen(reg, event) + assert pr.regimen.name == "old" + reg = Regimen.changeset(reg, %{name: "new"}) |> Repo.update!() + {:ok, pr} = Asset.upsert_persistent_regimen(reg, event) + assert pr.regimen.name == "new" end end - - describe "Device" do - test "only allows one device total" do - Farmbot.Asset.Repo.delete_all(Farmbot.Asset.Device) - device = %{id: id(), name: "Magice Device!", timezone: nil} - cmd_insert = Asset.register_sync_cmd(device.id, "Device", device) - Asset.apply_sync_cmd(cmd_insert) - assert Asset.device() - device_2 = %{device | id: id()} - cmd_insert_2 = Asset.register_sync_cmd(device_2.id, "Device", device_2) - Asset.apply_sync_cmd(cmd_insert_2) - assert_raise RuntimeError, ~r"There should only ever be 1 device!", fn() -> - Asset.device() - end - # should delete all devices just in case. - refute Asset.device() - end - end - - describe "Regimens" do - test "inserts a regimen" - test "inserts a regemin that uses a sequence" - test "persistent regimens need to be unique by their start time." - end - - describe "FarmEvents" do - test "inserts a FarmEvent" - end - - describe "Peripherals" do - test "inserts a Peripheral" do - per = %{id: id(), pin: 1, mode: 0, label: "laser beam"} - cmd_insert = Asset.register_sync_cmd(per.id, "Peripheral", per) - Asset.apply_sync_cmd(cmd_insert) - assert data = Asset.get_peripheral_by_id(per.id) - assert data.label == per.label - end - end - - describe "Sensors" do - test "inserts a Sensor" do - sensor = %{id: id(), pin: 1, mode: 0, label: "lightning"} - cmd_insert = Asset.register_sync_cmd(sensor.id, "Sensor", sensor) - Asset.apply_sync_cmd(cmd_insert) - assert data = Asset.get_sensor_by_id(sensor.id) - assert data.label == sensor.label - end - end - - describe "Points" do - test "inserts a Point" do - pnt = %{id: id(), name: "potato", x: 0, y: 101, z: -20, meta: %{}, pointer_type: "GenericPointer"} - cmd_insert = Asset.register_sync_cmd(pnt.id, "Point", pnt) - Asset.apply_sync_cmd(cmd_insert) - assert data = Asset.get_point_by_id(pnt.id) - assert data.name == pnt.name - end - end - - describe "Tools" do - test "inserts a tool" do - tool = %{id: id(), name: "warp drive"} - tool_insert = Asset.register_sync_cmd(tool.id, "Tool", tool) - Asset.apply_sync_cmd(tool_insert) - assert data = Asset.get_tool_by_id(tool.id) - assert data.name == "warp drive" - end - - test "gets the point assosiated with a tool" do - tool = %{id: id(), name: "trench digger"} - point = %{id: id(), tool_id: tool.id, name: "trench digger location", x: 0, y: 0, z: 0, meta: %{}, pointer_type: "ToolSlot"} - - _tool_insert = Asset.register_sync_cmd(tool.id, "Tool", tool) - _point_insert = Asset.register_sync_cmd(point.id, "Point", point) - Asset.fragment_sync() - assert point = Asset.get_point_from_tool(tool.id) - assert point.id == point.id - assert point.name == "trench digger location" - end - end - - test "Delets all data." - test "Snapshot from a single diff works." - test "Inspecting a snapshot returns a nice hash." end diff --git a/farmbot_core/test/asset_workers/farm_event_worker_test.exs b/farmbot_core/test/asset_workers/farm_event_worker_test.exs new file mode 100644 index 00000000..d5468b2e --- /dev/null +++ b/farmbot_core/test/asset_workers/farm_event_worker_test.exs @@ -0,0 +1,94 @@ +defmodule Farmbot.FarmEventWorkerTest do + use ExUnit.Case + alias Farmbot.Asset.FarmEvent + + import Farmbot.TestSupport.AssetFixtures + alias Farmbot.TestSupport.CeleryScript.TestIOLayer + import Farmbot.TestSupport + + describe "regimens" do + test "always ensure a regimen is started" do + seq = sequence() + reg = regimen(%{regimen_items: [%{time_offset: 100, sequence_id: seq.id}]}) + + now = DateTime.utc_now() + start_time = Timex.shift(now, minutes: -20) + end_time = Timex.shift(now, minutes: 10) + + params = %{ + start_time: start_time, + end_time: end_time, + repeat: 1, + time_unit: "never" + } + + _event = regimen_event(reg, params) + refute :lookup_persistent_regimen_after_sleep? + end + end + + describe "sequences" do + # TODO(Connor) - this test isn't really that good + # Because it is timeout based.. + test "doesn't execute a sequence more than 2 mintues late" do + TestIOLayer.subscribe() + ast = TestIOLayer.debug_ast() + seq = sequence(%{body: [ast]}) + now = DateTime.utc_now() + start_time = Timex.shift(now, minutes: -20) + end_time = Timex.shift(now, minutes: 10) + + params = %{ + start_time: start_time, + end_time: end_time, + repeat: 1, + time_unit: "never" + } + + assert %FarmEvent{} = sequence_event(seq, params) + # This is not really that useful. + refute_receive ^ast, farm_event_timeout() + end + end + + describe "common" do + test "schedules an event in the future" do + TestIOLayer.subscribe() + ast = TestIOLayer.debug_ast() + seq = sequence(%{body: [ast]}) + now = DateTime.utc_now() + start_time = Timex.shift(now, seconds: 2) + end_time = Timex.shift(now, minutes: 10) + + params = %{ + start_time: start_time, + end_time: end_time, + repeat: 1, + time_unit: "minutely" + } + + assert %FarmEvent{} = sequence_event(seq, params) + assert_receive ^ast, farm_event_timeout() + end + + test "wont start an event after end_time" do + TestIOLayer.subscribe() + ast = TestIOLayer.debug_ast() + seq = sequence(%{body: [ast]}) + now = DateTime.utc_now() + start_time = Timex.shift(now, seconds: 2) + end_time = Timex.shift(now, minutes: -10) + + params = %{ + start_time: start_time, + end_time: end_time, + repeat: 1, + time_unit: "minutely" + } + + assert %FarmEvent{} = sequence_event(seq, params) + # This is not really that useful. + refute_receive ^ast + end + end +end diff --git a/farmbot_core/test/botstate_test.exs b/farmbot_core/test/botstate_test.exs index eb8a156c..2a07d1c3 100644 --- a/farmbot_core/test/botstate_test.exs +++ b/farmbot_core/test/botstate_test.exs @@ -8,6 +8,5 @@ defmodule Farmbot.BotStateTest do Config.update_config_value(:bool, "settings", "log_amqp_connected", false) refute BotState.fetch().configuration.log_amqp_connected - end end diff --git a/farmbot_core/test/celery_script_test.exs b/farmbot_core/test/celery_script_test.exs new file mode 100644 index 00000000..ee3c9629 --- /dev/null +++ b/farmbot_core/test/celery_script_test.exs @@ -0,0 +1,21 @@ +defmodule Farmbot.Core.CeleryScriptTest do + use ExUnit.Case + import Farmbot.TestSupport.AssetFixtures + alias Farmbot.Core.CeleryScript + alias Farmbot.TestSupport.CeleryScript.TestIOLayer + + test "rpc_request" do + TestIOLayer.subscribe() + debug_ast = TestIOLayer.debug_ast() + CeleryScript.rpc_request(debug_ast, &TestIOLayer.debug_fun/1) + assert_receive ^debug_ast + end + + test "sequence" do + TestIOLayer.subscribe() + debug_ast = TestIOLayer.debug_ast() + seq = sequence(%{args: %{}, body: [debug_ast]}) + CeleryScript.sequence(seq, &TestIOLayer.debug_fun/1) + assert_receive ^debug_ast + end +end diff --git a/farmbot_ext/.formatter.exs b/farmbot_ext/.formatter.exs new file mode 100644 index 00000000..3f6d5c68 --- /dev/null +++ b/farmbot_ext/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto], + inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] +] diff --git a/farmbot_ext/config/config.exs b/farmbot_ext/config/config.exs index 2d5911cb..55363e93 100644 --- a/farmbot_ext/config/config.exs +++ b/farmbot_ext/config/config.exs @@ -16,41 +16,47 @@ config :lager, :handlers, [] config :ssl, protocol_version: :"tlsv1.2" +config :ecto, json_library: Farmbot.JSON + +config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 10_000 + +config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 30_000 + config :farmbot_core, :behaviour, firmware_handler: Farmbot.Firmware.StubHandler, leds_handler: Farmbot.Leds.StubHandler, pin_binding_handler: Farmbot.PinBinding.StubHandler, - celery_script_io_layer: Farmbot.CeleryScript.StubIOLayer, + celery_script_io_layer: Farmbot.Core.CeleryScript.StubIOLayer, json_parser: Farmbot.JSON.JasonParser config :farmbot_core, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], expected_fw_versions: ["6.4.0.F", "6.4.0.R", "6.4.0.G"], default_server: "https://my.farm.bot", - default_currently_on_beta: String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta"), + default_currently_on_beta: + String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta"), firmware_io_logs: false, farm_event_debug_log: false config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: ".#{Mix.env}_configs.sqlite3", + database: ".#{Mix.env()}_configs.sqlite3", priv: "../farmbot_core/priv/config" config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: ".#{Mix.env}_logs.sqlite3", + database: ".#{Mix.env()}_logs.sqlite3", priv: "../farmbot_core/priv/logger" config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: ".#{Mix.env}_assets.sqlite3", + database: ".#{Mix.env()}_assets.sqlite3", priv: "../farmbot_core/priv/asset" -config :farmbot_ext, :behaviour, - authorization: Farmbot.Bootstrap.Authorization +config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization config :farmbot_ext, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], diff --git a/farmbot_ext/lib/amqp/amqp_supervisor.ex b/farmbot_ext/lib/amqp/amqp_supervisor.ex index 31c5e0dc..22d2affb 100644 --- a/farmbot_ext/lib/amqp/amqp_supervisor.ex +++ b/farmbot_ext/lib/amqp/amqp_supervisor.ex @@ -10,10 +10,12 @@ defmodule Farmbot.AMQP.Supervisor do def init([]) do token = get_config_value(:string, "authorization", "token") email = get_config_value(:string, "authorization", "email") + children = [ {Farmbot.AMQP.ConnectionWorker, [token: token, email: email]}, {Farmbot.AMQP.ChannelSupervisor, [token]} ] - Supervisor.init(children, [strategy: :one_for_all]) + + Supervisor.init(children, strategy: :one_for_all) end end diff --git a/farmbot_ext/lib/amqp/auto_sync_transport.ex b/farmbot_ext/lib/amqp/auto_sync_transport.ex index 6e7e70fe..10bb01bf 100644 --- a/farmbot_ext/lib/amqp/auto_sync_transport.ex +++ b/farmbot_ext/lib/amqp/auto_sync_transport.ex @@ -1,10 +1,25 @@ defmodule Farmbot.AMQP.AutoSyncTransport do use GenServer use AMQP + + alias AMQP.{ + Channel, + Queue + } + require Farmbot.Logger require Logger import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] + alias Farmbot.{ + API.EagerLoader, + Asset.Repo, + Asset.Device, + Asset.FbosConfig, + Asset.FirmwareConfig, + JSON + } + @exchange "amq.topic" defstruct [:conn, :chan, :bot] @@ -12,17 +27,20 @@ defmodule Farmbot.AMQP.AutoSyncTransport do @doc false def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([conn, jwt]) do Process.flag(:sensitive, true) - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, [global: true]) - {:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_auto_sync", [auto_delete: false]) - :ok = AMQP.Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, [routing_key: "bot.#{jwt.bot}.sync.#"]) - {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), [no_ack: true]) - {:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])} + {:ok, chan} = Channel.open(conn) + :ok = Basic.qos(chan, global: true) + {:ok, _} = Queue.declare(chan, jwt.bot <> "_auto_sync", auto_delete: false) + + :ok = + Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, routing_key: "bot.#{jwt.bot}.sync.#") + + {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), no_ack: true) + {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)} end def terminate(_reason, _state) do @@ -46,32 +64,63 @@ defmodule Farmbot.AMQP.AutoSyncTransport do end def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do + auto_sync? = get_config_value(:bool, "settings", "auto_sync") device = state.bot ["bot", ^device, "sync", asset_kind, id_str] = String.split(key, ".") - data = Farmbot.JSON.decode!(payload) - body = data["body"] - case asset_kind do - "FbosConfig" when is_nil(body) -> - Farmbot.Logger.error 1, "FbosConfig deleted via API?" - "FbosConfig" -> - Farmbot.HTTP.SettingsWorker.download_os(data) - "FirmwareConfig" when is_nil(body) -> - Farmbot.Logger.error 1, "FirmwareConfig deleted via API?" - "FirmwareConfig" -> - Farmbot.HTTP.SettingsWorker.download_firmware(data) - _ -> - if !get_config_value(:bool, "settings", "needs_http_sync") do - id = data["id"] || String.to_integer(id_str) - # Body might be nil if a resource was deleted. - body = if body, do: Farmbot.Asset.to_asset(body, asset_kind) - _cmd = Farmbot.Asset.register_sync_cmd(id, asset_kind, body) - else - Logger.warn "not accepting sync_cmd from amqp because bot needs http sync first." + asset_kind = Module.concat([Farmbot, Asset, asset_kind]) + data = JSON.decode!(payload) + id = data["id"] || String.to_integer(id_str) + params = data["body"] + + cond do + # TODO(Connor) no way to cache a deletion yet + is_nil(params) && !auto_sync? -> + :ok + + asset_kind == Device -> + Repo.get_by!(Device, id: id) + |> Device.changeset(params) + |> Repo.update!() + |> Farmbot.Bootstrap.APITask.device_to_config_storage() + + :ok + + asset_kind == FbosConfig -> + Repo.get_by!(FbosConfig, id: id) + |> FbosConfig.changeset(params) + |> Repo.update!() + |> Farmbot.Bootstrap.APITask.fbos_config_to_config_storage() + + :ok + + asset_kind == FirmwareConfig -> + raise("FIXME") + + is_nil(params) && auto_sync? -> + old = Repo.get_by(asset_kind, id: id) + old && Repo.delete!(old) + :ok + + auto_sync? -> + case Repo.get_by(asset_kind, id: id) do + nil -> + struct(asset_kind) + |> asset_kind.changeset(params) + |> Repo.insert!() + + asset -> + asset_kind.changeset(asset, params) + |> Repo.update!() end + + true -> + asset = Repo.get_by(asset_kind, id: id) || struct(asset_kind) + changeset = asset_kind.changeset(asset, params) + :ok = EagerLoader.cache(changeset) end - json = Farmbot.JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"}) - :ok = AMQP.Basic.publish state.chan, @exchange, "bot.#{device}.from_device", json + json = JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"}) + :ok = Basic.publish(state.chan, @exchange, "bot.#{device}.from_device", json) {:noreply, state} end end diff --git a/farmbot_ext/lib/amqp/bot_state_transport.ex b/farmbot_ext/lib/amqp/bot_state_transport.ex index 6549696f..16de196a 100644 --- a/farmbot_ext/lib/amqp/bot_state_transport.ex +++ b/farmbot_ext/lib/amqp/bot_state_transport.ex @@ -14,15 +14,15 @@ defmodule Farmbot.AMQP.BotStateTransport do @doc false def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([conn, jwt]) do Process.flag(:sensitive, true) Farmbot.Registry.subscribe() - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, [global: true]) - {:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])} + {:ok, chan} = AMQP.Channel.open(conn) + :ok = Basic.qos(chan, global: true) + {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)} end def handle_cast(:force, %{state_cache: bot_state} = state) do @@ -30,7 +30,10 @@ defmodule Farmbot.AMQP.BotStateTransport do {:noreply, state} end - def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, %{state_cache: bot_state} = state) do + def handle_info( + {Farmbot.Registry, {Farmbot.BotState, bot_state}}, + %{state_cache: bot_state} = state + ) do # IO.puts "no state change" {:noreply, state} end @@ -46,7 +49,7 @@ defmodule Farmbot.AMQP.BotStateTransport do defp push_bot_state(chan, bot, state) do json = Farmbot.JSON.encode!(state) - :ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.status", json + :ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.status", json) state end end diff --git a/farmbot_ext/lib/amqp/celery_script_transport.ex b/farmbot_ext/lib/amqp/celery_script_transport.ex index 726fbfc4..61e0e9d3 100644 --- a/farmbot_ext/lib/amqp/celery_script_transport.ex +++ b/farmbot_ext/lib/amqp/celery_script_transport.ex @@ -7,23 +7,32 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do @exchange "amq.topic" + # FIXME + require Protocol + Protocol.derive(Jason.Encoder, Farmbot.CeleryScript.AST) + defstruct [:conn, :chan, :bot] alias __MODULE__, as: State @doc false def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([conn, jwt]) do Process.flag(:sensitive, true) - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, [global: true]) - {:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_from_clients", [auto_delete: true]) - {:ok, _} = AMQP.Queue.purge(chan, jwt.bot <> "_from_clients") - :ok = AMQP.Queue.bind(chan, jwt.bot <> "_from_clients", @exchange, [routing_key: "bot.#{jwt.bot}.from_clients"]) - {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_from_clients", self(), [no_ack: true]) - {:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])} + {:ok, chan} = AMQP.Channel.open(conn) + :ok = Basic.qos(chan, global: true) + {:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_from_clients", auto_delete: true) + {:ok, _} = AMQP.Queue.purge(chan, jwt.bot <> "_from_clients") + + :ok = + AMQP.Queue.bind(chan, jwt.bot <> "_from_clients", @exchange, + routing_key: "bot.#{jwt.bot}.from_clients" + ) + + {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_from_clients", self(), no_ack: true) + {:ok, struct(State, conn: conn, chan: chan, bot: jwt.bot)} end # Confirmation sent by the broker after registering this process as a consumer @@ -32,6 +41,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do Farmbot.Logger.success(1, "Farmbot is up and running!") update_config_value(:bool, "settings", "log_amqp_connected", false) end + {:noreply, state} end @@ -49,10 +59,12 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do device = state.bot ["bot", ^device, "from_clients"] = String.split(key, ".") - spawn_link fn() -> - {_us, _results} = :timer.tc __MODULE__, :handle_celery_script, [payload, state] - # IO.puts "#{results.args.label} took: #{us}µs" - end + + spawn_link(fn -> + {_us, _results} = :timer.tc(__MODULE__, :handle_celery_script, [payload, state]) + # IO.puts("#{results.args.label} took: #{us}µs") + end) + {:noreply, state} end @@ -60,13 +72,15 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do def handle_celery_script(payload, state) do json = Farmbot.JSON.decode!(payload) # IO.inspect(json, label: "RPC_REQUEST") - Farmbot.Core.CeleryScript.rpc_request(json, fn(results_ast) -> + Farmbot.Core.CeleryScript.rpc_request(json, fn results_ast -> reply = Farmbot.JSON.encode!(results_ast) + if results_ast.kind == :rpc_error do [%{args: %{message: message}}] = results_ast.body Logger.error(message) end - AMQP.Basic.publish state.chan, @exchange, "bot.#{state.bot}.from_device", reply + + AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.bot}.from_device", reply) results_ast end) end diff --git a/farmbot_ext/lib/amqp/channel_supervisor.ex b/farmbot_ext/lib/amqp/channel_supervisor.ex index 0726a5b9..ac4a5eae 100644 --- a/farmbot_ext/lib/amqp/channel_supervisor.ex +++ b/farmbot_ext/lib/amqp/channel_supervisor.ex @@ -1,6 +1,15 @@ defmodule Farmbot.AMQP.ChannelSupervisor do @moduledoc false use Supervisor + alias Farmbot.JWT + + alias Farmbot.AMQP.{ + ConnectionWorker, + LogTransport, + BotStateTransport, + AutoSyncTransport, + CeleryScriptTransport + } def start_link(args) do Supervisor.start_link(__MODULE__, args, name: __MODULE__) @@ -8,14 +17,16 @@ defmodule Farmbot.AMQP.ChannelSupervisor do def init([token]) do Process.flag(:sensitive, true) - conn = Farmbot.AMQP.ConnectionWorker.connection() - jwt = Farmbot.Jwt.decode!(token) + conn = ConnectionWorker.connection() + jwt = JWT.decode!(token) + children = [ - {Farmbot.AMQP.LogTransport, [conn, jwt]}, - {Farmbot.AMQP.BotStateTransport, [conn, jwt]}, - {Farmbot.AMQP.AutoSyncTransport, [conn, jwt]}, - {Farmbot.AMQP.CeleryScriptTransport, [conn, jwt]} + {LogTransport, [conn, jwt]}, + {BotStateTransport, [conn, jwt]}, + {AutoSyncTransport, [conn, jwt]}, + {CeleryScriptTransport, [conn, jwt]} ] - Supervisor.init(children, [strategy: :one_for_one]) + + Supervisor.init(children, strategy: :one_for_one) end end diff --git a/farmbot_ext/lib/amqp/connection_worker.ex b/farmbot_ext/lib/amqp/connection_worker.ex index 750952bc..56da9f4f 100644 --- a/farmbot_ext/lib/amqp/connection_worker.ex +++ b/farmbot_ext/lib/amqp/connection_worker.ex @@ -1,11 +1,12 @@ defmodule Farmbot.AMQP.ConnectionWorker do use GenServer + alias Farmbot.JWT require Farmbot.Logger require Logger import Farmbot.Config, only: [update_config_value: 4] def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + GenServer.start_link(__MODULE__, args, name: __MODULE__) end def connection do @@ -17,8 +18,10 @@ defmodule Farmbot.AMQP.ConnectionWorker do email = Keyword.fetch!(opts, :email) Process.flag(:sensitive, true) Process.flag(:trap_exit, true) - jwt = Farmbot.Jwt.decode!(token) + jwt = JWT.decode!(token) + IO.puts("OPEN") {:ok, conn} = open_connection(token, email, jwt.bot, jwt.mqtt, jwt.vhost) + IO.puts("OPENED") Process.link(conn.pid) Process.monitor(conn.pid) {:ok, conn} @@ -27,13 +30,13 @@ defmodule Farmbot.AMQP.ConnectionWorker do def terminate(_, conn) do if Process.alive?(conn.pid) do try do - Logger.info "Closing AMQP connection." + Logger.info("Closing AMQP connection.") :ok = AMQP.Connection.close(conn) rescue ex -> message = Exception.message(ex) - Logger.error "Could not close AMQP connection: #{message}" - end + Logger.error("Could not close AMQP connection: #{message}") + end end end @@ -42,16 +45,18 @@ defmodule Farmbot.AMQP.ConnectionWorker do update_config_value(:bool, "settings", "ignore_fbos_config", false) if reason not in ok_reasons do - Farmbot.Logger.error 1, "AMQP Connection closed: #{inspect reason}" + Farmbot.Logger.error(1, "AMQP Connection closed: #{inspect(reason)}") update_config_value(:bool, "settings", "log_amqp_connected", true) end + {:stop, reason, conn} end def handle_call(:connection, _, conn), do: {:reply, conn, conn} defp open_connection(token, email, bot, mqtt_server, vhost) do - Logger.info "Opening new AMQP connection." + Logger.info("Opening new AMQP connection.") + opts = [ client_properties: [ {"version", :longstr, Farmbot.Project.version()}, @@ -61,16 +66,20 @@ defmodule Farmbot.AMQP.ConnectionWorker do {"product", :longstr, "farmbot_os"}, {"bot", :longstr, bot}, {"email", :longstr, email}, - {"node", :longstr, to_string(node())}, + {"node", :longstr, to_string(node())} ], host: mqtt_server, username: bot, password: token, - virtual_host: vhost] + virtual_host: vhost + ] + case AMQP.Connection.open(opts) do - {:ok, conn} -> {:ok, conn} + {:ok, conn} -> + {:ok, conn} + {:error, reason} -> - Logger.error "Error connecting to AMPQ: #{inspect reason}" + Logger.error("Error connecting to AMPQ: #{inspect(reason)}") Process.sleep(5000) open_connection(token, email, bot, mqtt_server, vhost) end diff --git a/farmbot_ext/lib/amqp/log_transport.ex b/farmbot_ext/lib/amqp/log_transport.ex index a8448c17..cbaa3e9a 100644 --- a/farmbot_ext/lib/amqp/log_transport.ex +++ b/farmbot_ext/lib/amqp/log_transport.ex @@ -11,18 +11,20 @@ defmodule Farmbot.AMQP.LogTransport do @doc false def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) + GenServer.start_link(__MODULE__, args, name: __MODULE__) end def init([conn, jwt]) do Process.flag(:sensitive, true) Farmbot.Registry.subscribe() - {:ok, chan} = AMQP.Channel.open(conn) - :ok = Basic.qos(chan, [global: true]) - state = struct(State, [conn: conn, chan: chan, bot: jwt.bot]) + {:ok, chan} = AMQP.Channel.open(conn) + :ok = Basic.qos(chan, global: true) + state = struct(State, conn: conn, chan: chan, bot: jwt.bot) + for l <- Farmbot.Logger.handle_all_logs() do do_handle_log(l, state) end + {:ok, state} end @@ -31,7 +33,7 @@ defmodule Farmbot.AMQP.LogTransport do update_config_value(:bool, "settings", "ignore_fbos_config", false) if reason not in ok_reasons do - Farmbot.Logger.error 1, "Logger amqp client Died: #{inspect reason}" + Farmbot.Logger.error(1, "Logger amqp client Died: #{inspect(reason)}") update_config_value(:bool, "settings", "log_amqp_connected", true) end @@ -43,6 +45,7 @@ defmodule Farmbot.AMQP.LogTransport do if log = Farmbot.Logger.handle_log(id) do do_handle_log(log, state) end + {:noreply, state} end @@ -58,9 +61,12 @@ defmodule Farmbot.AMQP.LogTransport do if Farmbot.Logger.should_log?(log.module, log.verbosity) do fb = %{position: %{x: -1, y: -1, z: -1}} location_data = Map.get(state.state_cache || %{}, :location_data, fb) + log_without_pos = %{ type: log.level, - x: nil, y: nil, z: nil, + x: nil, + y: nil, + z: nil, verbosity: log.verbosity, major_version: log.version.major, minor_version: log.version.minor, @@ -69,6 +75,7 @@ defmodule Farmbot.AMQP.LogTransport do channels: log.meta[:channels] || [], message: log.message } + log = add_position_to_log(log_without_pos, location_data) push_bot_log(state.chan, state.bot, log) end @@ -76,7 +83,7 @@ defmodule Farmbot.AMQP.LogTransport do defp push_bot_log(chan, bot, log) do json = Farmbot.JSON.encode!(log) - :ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json + :ok = AMQP.Basic.publish(chan, @exchange, "bot.#{bot}.logs", json) end defp add_position_to_log(%{} = log, %{position: %{} = pos}) do diff --git a/farmbot_ext/lib/api.ex b/farmbot_ext/lib/api.ex new file mode 100644 index 00000000..2bdd1113 --- /dev/null +++ b/farmbot_ext/lib/api.ex @@ -0,0 +1,81 @@ +defmodule Farmbot.API do + alias Farmbot.{API, JSON, JWT} + import Farmbot.Config, only: [get_config_value: 3] + use Tesla + + plug(Tesla.Middleware.JSON, decode: &JSON.decode/1, encode: &JSON.encode/1) + plug(Tesla.Middleware.FollowRedirects) + # plug(Tesla.Middleware.Logger) + + @doc false + def client do + binary_token = get_config_value(:string, "authorization", "token") + {:ok, tkn} = JWT.decode(binary_token) + + uri = Map.fetch!(tkn, :iss) |> URI.parse() + url = (uri.scheme || "https") <> "://" <> uri.host <> ":" <> to_string(uri.port) + + Tesla.build_client([ + {Tesla.Middleware.BaseUrl, url}, + {Tesla.Middleware.Headers, + [ + {"content-type", "application/json"}, + {"authorization", "Bearer: " <> binary_token}, + {"user-agent", "farmbot-os"} + ]} + ]) + end + + @doc "helper for `GET`ing a path." + def get_body!(path) do + API.get!(API.client(), path) + |> Map.fetch!(:body) + end + + @doc "helper for `GET`ing api resources." + def get_changeset(module) when is_atom(module) do + get_changeset(struct(module)) + end + + def get_changeset(%module{} = data) do + get_body!(module.path()) + |> case do + %{} = single -> + module.changeset(data, single) + + many when is_list(many) -> + Enum.map(many, &module.changeset(data, &1)) + end + end + + @doc "helper for `GET`ing api resources." + def get_changeset(asset, path) + + # Hacks for dealing with these resources not having #show + def get_changeset(Farmbot.Asset.FbosConfig, _), + do: get_changeset(Farmbot.Asset.FbosConfig) + + def get_changeset(Farmbot.Asset.FirmwareConfig, _), + do: get_changeset(Farmbot.Asset.FirmwareConfig) + + def get_changeset(%Farmbot.Asset.FbosConfig{} = data, _), + do: get_changeset(data) + + def get_changeset(%Farmbot.Asset.FirmwareConfig{} = data, _), + do: get_changeset(data) + + def get_changeset(module, path) when is_atom(module) do + get_changeset(struct(module), path) + end + + def get_changeset(%module{} = data, path) do + get_body!(Path.join(module.path(), to_string(path))) + |> case do + %{} = single -> + module.changeset(data, single) + + many when is_list(many) -> + Enum.map(many, &module.changeset(data, &1)) + end + end +end diff --git a/farmbot_ext/lib/api/dirty_worker.ex b/farmbot_ext/lib/api/dirty_worker.ex new file mode 100644 index 00000000..d70a6a7d --- /dev/null +++ b/farmbot_ext/lib/api/dirty_worker.ex @@ -0,0 +1,109 @@ +defmodule Farmbot.API.DirtyWorker do + @moduledoc "Handles uploading/downloading of data from the API." + alias Farmbot.Asset.{Private, Repo} + alias Farmbot.API + import API.View, only: [render: 2] + + require Logger + use GenServer + @timeout 10000 + + @doc false + def child_spec(module) when is_atom(module) do + %{ + id: {Farmbot.API.DirtyWorker, module}, + start: {__MODULE__, :start_link, [[module: module, timeout: @timeout]]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end + + @doc "Start an instance of a DirtyWorker" + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + @impl GenServer + def init(args) do + Logger.disable(self()) + module = Keyword.fetch!(args, :module) + timeout = Keyword.get(args, :timeout, @timeout) + {:ok, %{module: module, timeout: timeout}, timeout} + end + + @impl GenServer + def handle_info(:timeout, %{module: module} = state) do + dirty = Private.list_dirty(module) + local = Private.list_local(module) + {:noreply, state, {:continue, dirty ++ local}} + end + + @impl GenServer + def handle_continue([], state) do + {:noreply, state, state.timeout} + end + + def handle_continue([dirty | rest], %{module: module} = state) do + case http_request(dirty, state) do + # Valid data + {:ok, %{status: s, body: body}} when s > 199 and s < 300 -> + dirty |> module.changeset(body) |> handle_changeset(rest, state) + + # Invalid data + {:ok, %{status: s, body: %{} = body}} when s > 399 and s < 500 -> + changeset = module.changeset(dirty) + + Enum.reduce(body, changeset, fn {key, val}, changeset -> + Ecto.Changeset.add_error(changeset, key, val) + end) + |> handle_changeset(rest, state) + + # Invalid data, but the API didn't say why + {:ok, %{status: s, body: _body}} when s > 399 and s < 500 -> + module.changeset(dirty) + |> Map.put(:valid?, false) + |> handle_changeset(rest, state) + + # HTTP Error. (500, network error, timeout etc.) + _ -> + {:noreply, state, @timeout} + end + end + + # If the changeset was valid, update the record. + def handle_changeset(%{valid?: true} = changeset, rest, state) do + Logger.info("Successfully synced: #{state.module}", changeset: changeset) + + Repo.update!(changeset) + |> Private.mark_clean!() + + {:noreply, state, {:continue, rest}} + end + + # If the changeset was invalid, delete the record. + # TODO(Connor) - Update the dirty field here, upload to rollbar? + def handle_changeset(%{valid?: false, data: data} = changeset, rest, state) do + message = + Enum.map(changeset.errors, fn {key, val} -> + "#{key}: #{val}" + end) + |> Enum.join("\n") + + Logger.error("Failed to sync: #{state.module} #{message}", changeset: changeset) + _ = Repo.delete!(data) + {:noreply, state, {:continue, rest}} + end + + defp http_request(%{id: nil} = dirty, state) do + path = state.module.path() + data = render(state.module, dirty) + API.post(API.client(), path, data) + end + + defp http_request(dirty, state) do + path = state.module.path() + data = render(state.module, dirty) + API.patch(API.client(), path, data) + end +end diff --git a/farmbot_ext/lib/api/dirty_worker/supervisor.ex b/farmbot_ext/lib/api/dirty_worker/supervisor.ex new file mode 100644 index 00000000..eb5c105c --- /dev/null +++ b/farmbot_ext/lib/api/dirty_worker/supervisor.ex @@ -0,0 +1,48 @@ +defmodule Farmbot.API.DirtyWorker.Supervisor do + use Supervisor + alias Farmbot.API.DirtyWorker + + alias Farmbot.Asset.{ + Device, + DiagnosticDump, + FarmEvent, + FarmwareEnv, + FarmwareInstallation, + FbosConfig, + FirmwareConfig, + Peripheral, + PinBinding, + Point, + Regimen, + SensorReading, + Sensor, + Sequence, + Tool + } + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(_args) do + children = [ + {DirtyWorker, Device}, + {DirtyWorker, DiagnosticDump}, + {DirtyWorker, FarmEvent}, + {DirtyWorker, FarmwareEnv}, + {DirtyWorker, FarmwareInstallation}, + {DirtyWorker, FbosConfig}, + {DirtyWorker, FirmwareConfig}, + {DirtyWorker, Peripheral}, + {DirtyWorker, PinBinding}, + {DirtyWorker, Point}, + {DirtyWorker, Regimen}, + {DirtyWorker, SensorReading}, + {DirtyWorker, Sensor}, + {DirtyWorker, Sequence}, + {DirtyWorker, Tool} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/farmbot_ext/lib/api/eager_loader.ex b/farmbot_ext/lib/api/eager_loader.ex new file mode 100644 index 00000000..6edf4921 --- /dev/null +++ b/farmbot_ext/lib/api/eager_loader.ex @@ -0,0 +1,108 @@ +defmodule Farmbot.API.EagerLoader do + @moduledoc "Handles caching of asset changes" + alias Farmbot.API + alias API.{SyncGroup, EagerLoader} + alias Farmbot.Asset.{Repo, Sync} + alias Ecto.Changeset + import Ecto.Query + require Logger + use GenServer + + @doc "Does a ton of HTTP requests to preload the cache" + def preload do + sync = API.get_changeset(Sync) |> Changeset.apply_changes() + + SyncGroup.all_groups() + |> Enum.map(fn asset_module -> + table = asset_module.__schema__(:source) |> String.to_existing_atom() + {asset_module, Map.fetch!(sync, table)} + end) + |> Enum.map(fn {asset_module, sync_items} -> + Enum.map(sync_items, fn sync_item -> + Task.async(__MODULE__, :preload, [asset_module, sync_item]) + end) + end) + |> List.flatten() + |> Enum.map(&Task.await(&1, :infinity)) + + :ok + end + + def preload(asset_module, %{id: id}) when is_atom(asset_module) do + local = Repo.one(from(m in asset_module, where: m.id == ^id)) || asset_module + :ok = API.get_changeset(local, id) |> cache() + end + + @doc "Get a Changeset by module and id. May return nil" + def get_cache(module, id) do + pid(module) + |> GenServer.call({:get_cache, id}) + end + + @doc "Don't use this in production" + def inspect_cache(module) do + pid(module) + |> GenServer.call(:get_cache) + end + + @doc """ + Cache a Changeset. + This Changeset _must_ be complete. This includes: + * Existing data if this is an update + * a remote `id` field. + """ + def cache(%Changeset{data: %module{}} = changeset) do + 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") + updated_at || change_error(changeset, "Can't cache a changeset with no :updated_at attribute") + + pid(module) + |> GenServer.cast({:cache, id, changeset}) + end + + defp change_error(changeset, message) do + raise(Ecto.ChangeError, message: message <> ": #{inspect(changeset)}") + end + + defp pid(module) do + Supervisor.which_children(EagerLoader.Supervisor) + |> Enum.find_value(fn {{EagerLoader, child_module}, pid, :worker, _} -> + module == child_module && pid + end) + end + + @doc false + def child_spec(module) when is_atom(module) do + %{ + id: {EagerLoader, module}, + start: {__MODULE__, :start_link, [[module: module]]}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end + + @doc false + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(args) do + module = Keyword.fetch!(args, :module) + {:ok, %{module: module, cache: %{}}} + end + + def handle_cast({:cache, id, changeset}, state) do + {:noreply, %{state | cache: Map.put(state.cache, id, changeset)}} + end + + def handle_call({:get_cache, id}, _, state) do + {result, cache} = Map.pop(state.cache, id) + {:reply, result, %{state | cache: cache}} + end + + def handle_call(:get_cache, _, state) do + {:reply, state.cache, state} + end +end diff --git a/farmbot_ext/lib/api/eager_loader/supervisor.ex b/farmbot_ext/lib/api/eager_loader/supervisor.ex new file mode 100644 index 00000000..7897d4c1 --- /dev/null +++ b/farmbot_ext/lib/api/eager_loader/supervisor.ex @@ -0,0 +1,48 @@ +defmodule Farmbot.API.EagerLoader.Supervisor do + use Supervisor + alias Farmbot.API.EagerLoader + + alias Farmbot.Asset.{ + Device, + DiagnosticDump, + FarmEvent, + FarmwareEnv, + FarmwareInstallation, + FbosConfig, + FirmwareConfig, + Peripheral, + PinBinding, + Point, + Regimen, + SensorReading, + Sensor, + Sequence, + Tool + } + + def start_link(args) do + Supervisor.start_link(__MODULE__, args, name: __MODULE__) + end + + def init(_args) do + children = [ + {EagerLoader, Device}, + {EagerLoader, DiagnosticDump}, + {EagerLoader, FarmEvent}, + {EagerLoader, FarmwareEnv}, + {EagerLoader, FarmwareInstallation}, + {EagerLoader, FbosConfig}, + {EagerLoader, FirmwareConfig}, + {EagerLoader, Peripheral}, + {EagerLoader, PinBinding}, + {EagerLoader, Point}, + {EagerLoader, Regimen}, + {EagerLoader, SensorReading}, + {EagerLoader, Sensor}, + {EagerLoader, Sequence}, + {EagerLoader, Tool} + ] + + Supervisor.init(children, strategy: :one_for_one) + end +end diff --git a/farmbot_ext/lib/api/reconciler.ex b/farmbot_ext/lib/api/reconciler.ex new file mode 100644 index 00000000..9be5d0ae --- /dev/null +++ b/farmbot_ext/lib/api/reconciler.ex @@ -0,0 +1,164 @@ +defmodule Farmbot.API.Reconciler do + @moduledoc """ + Handles remote additions and changes. + """ + require Logger + alias Ecto.{Changeset, Multi} + import Ecto.Query + + alias Farmbot.API + alias Farmbot.Asset.{Repo, Sync} + alias API.{SyncGroup, EagerLoader} + import Farmbot.TimeUtils, only: [compare_datetimes: 2] + + @doc """ + Reconcile remote updates. The following steps are wrapped in a tranaction + that is treated as an `all or nothing` sync. + + * get sync object from API + * start a new Transaction + * `sync_group` for groups 1-4, aborting the transaction if there are failures. + * add the `sync` to the Transaction + * apply the Transaction. + """ + def sync do + # Get the sync changeset + sync_changeset = API.get_changeset(Sync) + sync = Changeset.apply_changes(sync_changeset) + + multi = Multi.new() + + with {:ok, multi} <- sync_group(multi, sync, SyncGroup.group_0()), + {:ok, multi} <- sync_group(multi, sync, SyncGroup.group_1()), + {:ok, multi} <- sync_group(multi, sync, SyncGroup.group_2()), + {:ok, multi} <- sync_group(multi, sync, SyncGroup.group_3()), + {:ok, multi} <- sync_group(multi, sync, SyncGroup.group_4()) do + Multi.insert(multi, :syncs, sync_changeset) + |> Repo.transaction() + end + end + + @doc """ + Sync a group (list) of modules into a transaction. + For each item in the `sync` object, belonging to a `module` does the following: + + * checks EagerLoader cache + * if no cache exists: + * downloads changeset via HTTP + * if cache exists: + * check if cache matches data on the `sync` object + * if cache is valid: uses cached changeset + * if cache is _not_ valid, falls back to http + * applies changeset if there was any changes from cache or http + + """ + def sync_group(multi, sync, [module | rest]) do + multi + |> do_sync_group(sync, module) + |> sync_group(sync, rest) + end + + def sync_group(multi, _sync, []), do: {:ok, multi} + + defp do_sync_group(multi, sync, module) do + table = module.__schema__(:source) |> String.to_atom() + items = Map.fetch!(sync, table) + + ids_fbos_knows_about = + Repo.all(from(d in module, where: not is_nil(d.id), select: d.id)) + |> Enum.sort() + + ids_the_api_knows_about = + Enum.map(items, &Map.fetch!(&1, :id)) + |> Enum.sort() + + ids_that_were_deleted = ids_fbos_knows_about -- ids_the_api_knows_about + + multi = + Enum.reduce(ids_that_were_deleted, multi, fn id, multi -> + Logger.info("delete: #{module} #{inspect(id)}") + local_item = Repo.one!(from(d in module, where: d.id == ^id)) + Multi.delete(multi, {table, id}, local_item) + end) + + # TODO(Connor) make this reduce async with Task/Agent + Enum.reduce(items, multi, &multi_reduce(module, table, &1, &2)) + end + + @doc false + def multi_reduce(module, table, item, multi) do + cached_cs = EagerLoader.get_cache(module, item.id) + local_item = Repo.one(from(d in module, where: d.id == ^item.id)) + + case get_changeset(local_item || module, item, cached_cs) do + {:insert, %Changeset{} = cs} -> + Logger.info("insert: #{inspect(cs)}") + Multi.insert(multi, {table, item.id}, cs) + + {:update, %Changeset{} = cs} -> + Logger.info("update: #{inspect(cs)}") + Multi.update(multi, {table, item.id}, cs) + + nil -> + Logger.info("Local data: #{local_item.__struct__} is current.") + multi + end + end + + defp get_changeset(local_item, sync_item, cached_cs) + + # A module is passed in if there is no local copy of the data. + defp get_changeset(module, sync_item, nil) when is_atom(module) do + Logger.info("Local data: #{module} does not exist. Using HTTP to get data.") + {:insert, API.get_changeset(module, "#{sync_item.id}")} + end + + defp get_changeset(module, sync_item, %Changeset{} = cached) when is_atom(module) do + cached_updated_at = Changeset.get_field(cached, :updated_at) + + if compare_datetimes(sync_item.updated_at, cached_updated_at) == :eq do + {:insert, cached} + else + Logger.info("Cached item is out of date") + get_changeset(module, sync_item, nil) + end + end + + # no cache available + # If the `sync_item.updated_at` is newer than `local_item.updated_at` + # HTTP get the data. + defp get_changeset(%{} = local_item, sync_item, nil) do + # Check if remote data is newer + if compare_datetimes(sync_item.updated_at, local_item.updated_at) == :gt do + raise("#{inspect(local_item)} #{sync_item.updated_at} :gt #{local_item.updated_at}") + + Logger.info( + "Local data: #{local_item.__struct__} is out of date. Using HTTP to get newer data." + ) + + {:update, API.get_changeset(local_item, "#{sync_item.id}")} + end + end + + # We have a cache. + # First check if it is the same `updated_at` as what the API has. + # If the cache is the same `updated_at` as the API, check if the cache + # is newer than `local_item.updated_at` + # if the cache is not the same `updated_at` as the API, fallback to HTTP. + defp get_changeset(%{} = local_item, sync_item, %Changeset{} = cached) do + cached_updated_at = Changeset.get_field(cached, :updated_at) + + if compare_datetimes(sync_item.updated_at, cached_updated_at) == :eq do + if compare_datetimes(cached_updated_at, local_item.updated_at) == :gt do + Logger.info( + "Local data: #{local_item.__struct__} is out of date. Using cache do get newer data." + ) + + {:update, cached} + end + else + Logger.info("Cached item is out of date") + get_changeset(local_item, sync_item, nil) + end + end +end diff --git a/farmbot_ext/lib/api/sync_group.ex b/farmbot_ext/lib/api/sync_group.ex new file mode 100644 index 00000000..3fd9d105 --- /dev/null +++ b/farmbot_ext/lib/api/sync_group.ex @@ -0,0 +1,65 @@ +defmodule Farmbot.API.SyncGroup do + @moduledoc "Handles dependency ordering." + + alias Farmbot.Asset.{ + Device, + FarmEvent, + FarmwareEnv, + FarmwareInstallation, + FbosConfig, + FirmwareConfig, + Peripheral, + PinBinding, + Point, + Regimen, + SensorReading, + Sensor, + Sequence, + Tool + } + + def all_groups, do: group_0() ++ group_1() ++ group_2() ++ group_3() ++ group_4() + + @doc "Assets in Group 0 are required for FarmBot to operate." + def group_0, + do: [ + Device, + FbosConfig, + FirmwareConfig + ] + + @doc "Group 1 should have no external requirements" + def group_1, + do: [ + FarmwareInstallation, + FarmwareEnv, + Peripheral, + Point, + SensorReading, + Sensor, + Tool + ] + + @doc "Group 2 relies on assets in Group 1" + def group_2, + do: [ + # Requires Peripheral, Point, Sensor, SensorReading, Tool + Sequence + ] + + @doc "Group 3 relies on assets in Group 3" + def group_3, + do: [ + # Requires Sequence + Regimen, + # Requires Sequence + PinBinding + ] + + @doc "Group 4 relies on assets in Group 3" + def group_4, + do: [ + # Requires Regimen and Sequence + FarmEvent + ] +end diff --git a/farmbot_ext/lib/api/view.ex b/farmbot_ext/lib/api/view.ex new file mode 100644 index 00000000..f88e9f61 --- /dev/null +++ b/farmbot_ext/lib/api/view.ex @@ -0,0 +1,15 @@ +defmodule Farmbot.API.View do + @doc "Format data to be JSON encodable." + @callback render(map) :: map + + @doc "Delegates rendering to an asset's `render/1` function." + @spec render(module, map) :: map + def render(module, object), do: module.render(object) + + @doc "Helper to define a `render/1` function" + defmacro view(data, block) do + quote do + def render(unquote(data)), unquote(block) + end + end +end diff --git a/farmbot_ext/lib/auto_sync_task.ex b/farmbot_ext/lib/auto_sync_task.ex deleted file mode 100644 index 8ee7b880..00000000 --- a/farmbot_ext/lib/auto_sync_task.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Farmbot.AutoSyncTask do - @moduledoc false - require Farmbot.Logger - - @rpc %{ - kind: :rpc_request, - args: %{label: "auto_sync_task"}, - body: [ - %{kind: :sync, args: %{}} - ] - } - - @doc false - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :maybe_auto_sync, opts}, - type: :worker, - restart: :transient, - shutdown: 500 - } - end - - def maybe_auto_sync() do - if Farmbot.Config.get_config_value(:bool, "settings", "auto_sync") do - Farmbot.Core.CeleryScript.rpc_request(@rpc, &handle_rpc/1) - end - :ignore - end - - @doc false - def handle_rpc(%{kind: :rpc_ok}), do: :ok - def handle_rpc(%{kind: :rpc_error, body: [%{args: %{message: msg}}]}) do - Farmbot.Logger.error 1, "AutoSyncTask failed: #{msg}" - end -end diff --git a/farmbot_ext/lib/bootstrap/api_task.ex b/farmbot_ext/lib/bootstrap/api_task.ex new file mode 100644 index 00000000..ac32f8b0 --- /dev/null +++ b/farmbot_ext/lib/bootstrap/api_task.ex @@ -0,0 +1,97 @@ +defmodule Farmbot.Bootstrap.APITask do + @moduledoc """ + Task to ensure Farmbot has synced: + * Farmbot.Asset.Device + * Farmbot.Asset.FbosConfig + * Farmbot.Asset.FirmwareConfig + """ + require Farmbot.Logger + import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] + alias Farmbot.API + alias Farmbot.Asset.{Repo, Device, FbosConfig, FirmwareConfig} + + def child_spec(_) do + %{ + id: __MODULE__, + start: {__MODULE__, :sync_all, []}, + type: :worker, + restart: :permanent, + shutdown: 500 + } + end + + @doc false + def sync_all() do + _ = sync_device() + _ = sync_fbos_config() + _ = sync_firmware_config() + + if get_config_value(:bool, "settings", "auto_sync") do + try do + API.Reconciler.sync() + catch + _, _ -> + Farmbot.Logger.error(1, "Faild to bootup sync.") + end + end + + :ignore + end + + def sync_device do + device = Repo.one(Device) || Device + + API.get_changeset(device) + |> Repo.insert_or_update!() + |> device_to_config_storage() + end + + def device_to_config_storage(%Device{timezone: tz} = device) do + update_config_value(:string, "settings", "timezone", tz) + device + end + + def sync_fbos_config do + fbos_config = Repo.one(FbosConfig) || FbosConfig + + API.get_changeset(fbos_config) + |> Repo.insert_or_update!() + |> fbos_config_to_config_storage() + end + + def fbos_config_to_config_storage(%FbosConfig{} = config) do + update_config_value( + :bool, + "settings", + "arduino_debug_messages", + config.arduino_debug_messages + ) + + update_config_value(:bool, "settings", "auto_sync", config.auto_sync) + update_config_value(:bool, "settings", "beta_opt_in", config.beta_opt_in) + update_config_value(:bool, "settings", "disable_factory_reset", config.disable_factory_reset) + update_config_value(:string, "settings", "firmware_hardware", config.firmware_hardware) + update_config_value(:bool, "settings", "firmware_input_log", config.firmware_input_log) + update_config_value(:bool, "settings", "firmware_output_log", config.firmware_output_log) + + update_config_value( + :float, + "settings", + "network_not_found_timer", + config.network_not_found_timer && config.network_not_found_timer / 1 + ) + + update_config_value(:bool, "settings", "os_auto_update", config.os_auto_update) + update_config_value(:bool, "settings", "sequence_body_log", config.sequence_body_log) + update_config_value(:bool, "settings", "sequence_complete_log", config.sequence_complete_log) + update_config_value(:bool, "settings", "sequence_init_log", config.sequence_init_log) + config + end + + def sync_firmware_config do + firmware_config = Repo.one(FirmwareConfig) || FirmwareConfig + + API.get_changeset(firmware_config) + |> Repo.insert_or_update!() + end +end diff --git a/farmbot_ext/lib/bootstrap/auth_task.ex b/farmbot_ext/lib/bootstrap/auth_task.ex deleted file mode 100644 index d963b9b4..00000000 --- a/farmbot_ext/lib/bootstrap/auth_task.ex +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Farmbot.Bootstrap.AuthTask do - @moduledoc "Background worker that refreshes a token every 30 minutes." - use GenServer - require Farmbot.Logger - alias Farmbot.Config - import Config, only: [update_config_value: 4, get_config_value: 3] - - # 30 minutes. - @refresh_time 1.8e+6 |> round() - # @refresh_time 5_000 - - @doc false - def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) - end - - @doc "Force the token to refresh. Restarts any transports?" - def force_refresh do - GenServer.call(__MODULE__, :force_refresh) - end - - def init([]) do - timer = Process.send_after(self(), :refresh, @refresh_time) - {:ok, timer, :hibernate} - end - - def terminate(reason, _state) do - unless reason == {:shutdown, :normal} do - Farmbot.Logger.error 1, "Token Refresh failed: #{inspect reason}" - end - end - - defp do_refresh do - auth_task = Application.get_env(:farmbot_ext, :behaviour)[:authorization] - email = get_config_value(:string, "authorization", "email") - server = get_config_value(:string, "authorization", "server") - password = get_config_value(:string, "authorization", "password") - secret = get_config_value(:string, "authorization", "secret") - Farmbot.Logger.busy(3, "refreshing token: #{email} - #{server}") - cond do - is_nil(email) -> exit("No email") - is_nil(server) -> exit("No server") - is_nil(secret) && is_nil(password) -> exit("No password or secret.") - secret -> auth_task.authorize_with_secret(email, secret, server) - password -> auth_task.authorize_with_password(email, password, server) - end - |> case do - {:ok, token} -> - Farmbot.Logger.success(3, "Successful authorization: #{email} - #{server}") - update_config_value(:bool, "settings", "first_boot", false) - update_config_value(:string, "authorization", "token", token) - update_config_value(:bool, "settings", "needs_http_sync", true) - restart_transports() - refresh_timer(self()) - {:error, err} -> - msg = "Token failed to reauthorize: #{email} - #{server} #{inspect err}" - Farmbot.Logger.error(1, msg) - # 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_call(:force_refresh, _, old_timer) do - Farmbot.Logger.info 1, "Forcing a token refresh." - if Process.read_timer(old_timer) do - Process.cancel_timer(old_timer) - end - send self(), :refresh - {:reply, :ok, nil} - end - - defp restart_transports do - bootstrap_sup = Farmbot.Bootstrap.Supervisor - transport_sup = Farmbot.AMQP.Supervisor - :ok = Supervisor.terminate_child(bootstrap_sup, transport_sup) - {:ok, _} = Supervisor.restart_child(bootstrap_sup, transport_sup) - end - - defp refresh_timer(pid, ms \\ @refresh_time) do - timer = Process.send_after(pid, :refresh, ms) - {:noreply, timer, :hibernate} - end -end diff --git a/farmbot_ext/lib/bootstrap/authorization.ex b/farmbot_ext/lib/bootstrap/authorization.ex index 59710aa0..aac446bf 100644 --- a/farmbot_ext/lib/bootstrap/authorization.ex +++ b/farmbot_ext/lib/bootstrap/authorization.ex @@ -2,7 +2,7 @@ defmodule Farmbot.Bootstrap.Authorization do @moduledoc "Functionality responsible for getting a JWT." @typedoc "Email used to configure this bot." - @type email :: String.t + @type email :: String.t() @typedoc "Password used to configure this bot." @type password :: binary @@ -11,7 +11,7 @@ defmodule Farmbot.Bootstrap.Authorization do @type secret :: binary @typedoc "Server used to configure this bot." - @type server :: String.t + @type server :: String.t() @typedoc "Token that was fetched with the credentials." @type token :: binary @@ -23,23 +23,25 @@ defmodule Farmbot.Bootstrap.Authorization do @data_path Application.get_env(:farmbot_ext, :data_path) @data_path || raise("No appdata path configured.") - @spec authorize_with_secret(email, secret, server) :: {:ok, binary} | {:error, String.t | atom} + @spec authorize_with_secret(email, secret, server) :: + {:ok, binary} | {:error, String.t() | atom} def authorize_with_secret(_email, secret, server) do with {:ok, payload} <- build_payload(secret), - {:ok, resp} <- request_token(server, payload), - {:ok, body} <- Farmbot.JSON.decode(resp) do - get_encoded(body) - end + {:ok, resp} <- request_token(server, payload), + {:ok, body} <- Farmbot.JSON.decode(resp) do + get_encoded(body) + end end - @spec authorize_with_password(email, password, server) :: {:ok, binary} | {:error, String.t | atom} + @spec authorize_with_password(email, password, server) :: + {:ok, binary} | {:error, String.t() | atom} def authorize_with_password(email, password, server) do with {:ok, {:RSAPublicKey, _, _} = rsa_key} <- fetch_rsa_key(server), {:ok, payload} <- build_payload(email, password, rsa_key), - {:ok, resp} <- request_token(server, payload), - {:ok, body} <- Farmbot.JSON.decode(resp) do - get_encoded(body) - end + {:ok, resp} <- request_token(server, payload), + {:ok, body} <- Farmbot.JSON.decode(resp) do + get_encoded(body) + end end defp get_encoded(%{"token" => %{"encoded" => encoded}}), do: {:ok, encoded} @@ -66,27 +68,34 @@ defmodule Farmbot.Bootstrap.Authorization do {"Content-Type", "application/json"} ] - @spec fetch_rsa_key(server) :: {:ok, term} | {:error, String.t | atom} + @spec fetch_rsa_key(server) :: {:ok, term} | {:error, String.t() | atom} def fetch_rsa_key(server) do url = "#{server}/api/public_key" + with {:ok, body} <- do_request({:get, url, "", @headers}) do {:ok, RSA.decode_key(body)} end end - @spec request_token(server, binary) :: {:ok, binary} | {:error, String.t | atom} + @spec request_token(server, binary) :: {:ok, binary} | {:error, String.t() | atom} def request_token(server, payload, tries_remaining \\ 10) do url = "#{server}/api/tokens" + case do_request({:post, url, payload, @headers}) do # Don't try more times if we have an ok request. - {:ok, _} = ok -> ok + {:ok, _} = ok -> + ok + # Don't try more times if there was a 4xx error - {:error, {:authorization, reason}} -> {:error, reason} + {:error, {:authorization, reason}} -> + {:error, reason} + # Network error such such as wifi disconnect, dns down etc. # Try again. {:error, reason} when tries_remaining == 0 -> - Farmbot.Logger.error 1, "Farmbot failed to request token: #{inspect reason}" + Farmbot.Logger.error(1, "Farmbot failed to request token: #{inspect(reason)}") {:error, reason} + {:error, _reason} -> Process.sleep(2500) request_token(server, payload, tries_remaining - 1) @@ -96,29 +105,45 @@ defmodule Farmbot.Bootstrap.Authorization do def do_request(request, state \\ %{backoff: 5000, log_dispatch_flag: false}) def do_request({method, url, payload, headers}, state) do - headers = Enum.map(headers, fn({k, v}) -> {to_charlist(k), to_charlist(v)} end) + headers = Enum.map(headers, fn {k, v} -> {to_charlist(k), to_charlist(v)} end) opts = [{:body_format, :binary}] - request = if method == :get, do: {to_charlist(url), headers}, else: {to_charlist(url), headers, 'Application/JSON', payload} + + request = + if method == :get, + do: {to_charlist(url), headers}, + else: {to_charlist(url), headers, 'Application/JSON', payload} + resp = :httpc.request(method, request, [], opts) + case resp do - {:ok, {{_, c, _}, _headers, body}} when (c >= 200) and (c <= 299) -> + {:ok, {{_, c, _}, _headers, body}} when c >= 200 and c <= 299 -> {:ok, body} - {:ok, {{_, c, _}, _headers, body}} when (c >= 400) and (c <= 499) -> + + {:ok, {{_, c, _}, _headers, body}} when c >= 400 and c <= 499 -> err = get_error_message(body) - Farmbot.Logger.error 1, "Authorization error for url: #{url} #{err}" + Farmbot.Logger.error(1, "Authorization error for url: #{url} #{err}") {:error, {:authorization, err}} - {:ok, {{_, c, _}, _headers, body}} when (c >= 500) and (c <= 599) -> + + {:ok, {{_, c, _}, _headers, body}} when c >= 500 and c <= 599 -> Process.sleep(state.backoff) + unless state.log_dispatch_flag do err = get_error_message(body) - Farmbot.Logger.warn 1, "Farmbot web app failed complete request for url: #{url} #{err}" + Farmbot.Logger.warn(1, "Farmbot web app failed complete request for url: #{url} #{err}") end - do_request({method, url, payload, headers}, %{state | backoff: state.backoff + 1000, log_dispatch_flag: true}) - {:error, reason} -> {:error, reason} + + do_request({method, url, payload, headers}, %{ + state + | backoff: state.backoff + 1000, + log_dispatch_flag: true + }) + + {:error, reason} -> + {:error, reason} end end - @spec get_error_message(binary) :: String.t + @spec get_error_message(binary) :: String.t() defp get_error_message(bin) when is_binary(bin) do case Farmbot.JSON.decode(bin) do {:ok, %{"auth" => reason}} when is_binary(reason) -> reason diff --git a/farmbot_ext/lib/bootstrap/supervisor.ex b/farmbot_ext/lib/bootstrap/supervisor.ex index 5cb2adbc..04995e0d 100644 --- a/farmbot_ext/lib/bootstrap/supervisor.ex +++ b/farmbot_ext/lib/bootstrap/supervisor.ex @@ -70,17 +70,17 @@ defmodule Farmbot.Bootstrap.Supervisor do @doc "Start Bootstrap services." def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) + Supervisor.start_link(__MODULE__, args, name: __MODULE__) end def init([]) do # Make sure we log when amqp is connected. update_config_value(:bool, "settings", "log_amqp_connected", true) - - email = get_config_value(:string, "authorization", "email") - server = get_config_value(:string, "authorization", "server") + email = get_config_value(:string, "authorization", "email") + server = get_config_value(:string, "authorization", "server") password = get_config_value(:string, "authorization", "password") - secret = get_config_value(:string, "authorization", "secret") + secret = get_config_value(:string, "authorization", "secret") + cond do is_nil(email) -> exit("No email") is_nil(server) -> exit("No server") @@ -103,16 +103,17 @@ defmodule Farmbot.Bootstrap.Supervisor do update_config_value(:string, "authorization", "token", token) children = [ - {Farmbot.HTTP.Supervisor, []}, - {Farmbot.AMQP.Supervisor , []}, - {Farmbot.Bootstrap.AuthTask, []}, - {Farmbot.AutoSyncTask, []}, + Farmbot.API.EagerLoader.Supervisor, + Farmbot.API.DirtyWorker.Supervisor, + Farmbot.Bootstrap.APITask, + Farmbot.AMQP.Supervisor ] opts = [strategy: :one_for_one] Supervisor.init(children, opts) - {:error, reason} -> exit(reason) + {:error, reason} -> + exit(reason) end end @@ -125,12 +126,12 @@ defmodule Farmbot.Bootstrap.Supervisor do end defp auth_with_secret(e, sec, s) do - Farmbot.Logger.debug 3, "Using secret to authorize." + Farmbot.Logger.debug(3, "Using secret to authorize.") @auth_task.authorize_with_secret(e, sec, s) end defp auth_with_password(e, pass, s) do - Farmbot.Logger.debug 3, "Using password to authorize." + Farmbot.Logger.debug(3, "Using password to authorize.") @auth_task.authorize_with_password(e, pass, s) end end diff --git a/farmbot_ext/lib/http/helpers.ex b/farmbot_ext/lib/http/helpers.ex deleted file mode 100644 index 3f3c6c24..00000000 --- a/farmbot_ext/lib/http/helpers.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Farmbot.HTTP.Helpers do - @moduledoc false - - @doc "Helper for checking status codes" - defmacro is_2xx(number) do - quote do - unquote(number) > 199 and unquote(number) < 300 - end - end - - # Defines a function to fetch and decode an api resource. - @doc "Helper to define fetch and decode resource function in Farmbot.HTTP" - defmacro fadr(plural, kind) do - quote do - def unquote(plural)(), - do: fetch_and_decode("/api/#{unquote(plural)}.json", unquote(kind)) - end - end -end diff --git a/farmbot_ext/lib/http/http.ex b/farmbot_ext/lib/http/http.ex deleted file mode 100644 index 4c6d259b..00000000 --- a/farmbot_ext/lib/http/http.ex +++ /dev/null @@ -1,110 +0,0 @@ -defmodule Farmbot.HTTP do - import Farmbot.Config, only: [get_config_value: 3] - import Farmbot.HTTP.Helpers, only: [fadr: 2] - alias Farmbot.Asset.{ - Device, - FarmEvent, - FarmwareEnv, - FarmwareInstallation, - Peripheral, - PinBinding, - Point, - Regimen, - Sensor, - Sequence, - Tool, - } - - use Tesla, docs: false - @version Farmbot.Project.version() - @target Farmbot.Project.target() - - plug Tesla.Middleware.JSON, engine: Farmbot.JSON - plug Tesla.Middleware.FollowRedirects, max_redirects: 10 - plug Tesla.Middleware.Logger, log_level: :info - - fadr :device, Device - fadr :farm_events, FarmEvent - fadr :farmware_installations, FarmwareInstallation - fadr :farmware_envs, FarmwareEnv - fadr :peripherals, Peripheral - fadr :pin_bindings, PinBinding - fadr :points, Point - fadr :regimens, Regimen - fadr :sensors, Sensor - fadr :sequences, Sequence - fadr :tools, Tool - - def firmware_config do - client() - |> get!("/api/firmware_config") - |> Map.fetch!(:body) - end - - def update_firmware_config(%{} = data) do - client() - |> patch!("/api/firmware_config", data) - end - - def fbos_config do - client() - |> get!("/api/fbos_config") - |> Map.fetch!(:body) - end - - def update_fbos_config(%{} = data) do - client() - |> patch!("/api/fbos_config", data) - |> Map.fetch!(:body) - end - - @doc "Upload a file to Farmbot GCS." - def upload_file(filename, meta) do - client() - |> get!("/api/storage_auth") - |> finish_upload(filename, meta) - end - - defp finish_upload(%{status: 200, body: body, url: beep}, filename, meta) do - # Farmbot API doesn't supply the scheme - # So we extract it from the previous http request url. - url = URI.parse(beep).scheme <> ":" <> body["url"] |> IO.inspect(label: "url") - form_data = body["form_data"] - attachment_url = url <> form_data["key"] - - alias Tesla.Multipart - mp = Enum.reduce(form_data, Multipart.new(), fn({key, v}, mp) -> - case key do - "file" -> - Multipart.add_file(mp, filename, name: "file") - _ -> - Multipart.add_field(mp, key, v) - end - end) - - # Post the image to GCS. - post!(url, mp) - body = %{"attachment_url" => attachment_url, "meta" => meta} - post!(client(), "/api/images", body) - end - - defp fetch_and_decode(url, kind) do - client() - |> get!(url) - |> Map.fetch!(:body) - |> Farmbot.Asset.to_asset(kind) - end - - def client do - token = get_config_value(:string, "authorization", "token") - server = get_config_value(:string, "authorization", "server") - headers = [ - {"authorization", "token: " <> token}, - {"user-agent", "FarmbotOS/#{@version} (#{@target}) #{@target} ()"} - ] - Tesla.build_client [ - {Tesla.Middleware.BaseUrl, server}, - {Tesla.Middleware.Headers, headers} - ] - end -end diff --git a/farmbot_ext/lib/http/image_uploader.ex b/farmbot_ext/lib/http/image_uploader.ex deleted file mode 100644 index 577da6fe..00000000 --- a/farmbot_ext/lib/http/image_uploader.ex +++ /dev/null @@ -1,113 +0,0 @@ -defmodule Farmbot.HTTP.ImageUploader do - @moduledoc """ - Watches a directory on the File System and uploads images - """ - use GenServer - require Farmbot.Logger - - @images_path Path.join(["/", "tmp", "images"]) - - @doc """ - Starts the Image Watcher - """ - def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - Farmbot.Logger.debug 3, "Ensuring #{@images_path} exists." - Application.stop(:fs) - Application.put_env(:fs, :path, @images_path) - File.rm_rf! @images_path - File.mkdir_p! @images_path - Farmbot.Registry.subscribe() - :fs_app.start(:normal, []) - :fs.subscribe() - {:ok, %{uploads: %{}, state_cache: Farmbot.BotState.fetch()}} - end - - def terminate(reason, _state) do - Farmbot.Logger.debug 3, "Image uploader terminated: #{inspect reason}" - end - - def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do - {:noreply, %{state | state_cache: bot_state}} - end - - def handle_info({_pid, {:fs, :file_event}, {path, _}}, state) do - matches? = matches_any_pattern?(path, [~r{/tmp/images/.*(jpg|jpeg|png|gif)}]) - already_uploading? = Enum.find(state.uploads, fn({_pid, {find_path, _meta, _count}}) -> - find_path == path - end) |> is_nil() |> Kernel.!() - if matches? and (not already_uploading?) do - Farmbot.Logger.info 2, "Uploading: #{path}" - %{x: x, y: y, z: z} = state.state_cache.location_data.position - meta = %{x: x, y: y, z: z, name: Path.rootname(path)} - pid = spawn __MODULE__, :upload, [path, meta] - Process.monitor(pid) - {:noreply, %{state | uploads: Map.put(state.uploads, pid, {path, meta, 0})}} - else - # Farmbot.Logger.warn 3, "Not uploading: match: #{matches?} already_uploading?: #{already_uploading?}" - {:noreply, state} - end - end - - def handle_info({:DOWN, _, :process, pid, :normal}, state) do - case state.uploads[pid] do - nil -> {:noreply, state} - {path, _meta, _} -> - Farmbot.Logger.success 1, "Image Watcher successfully uploaded: #{path}" - File.rm path - {:noreply, %{state | uploads: Map.delete(state.uploads, pid)}} - end - end - - def handle_info({:DOWN, _, :process, pid, reason}, state) do - case state.uploads[pid] do - nil -> {:noreply, state} - {path, _meta, 3 = ret} -> - Farmbot.Logger.error 1, "Failed to upload #{path} #{ret} times. Giving up." - File.rm path - {:noreply, %{state | uploads: Map.delete(state.uploads, pid)}} - {path, meta, retries} -> - if File.exists?(path) do - Farmbot.Logger.warn 2, "Failed to upload #{path} #{inspect reason}. Going to retry." - Process.sleep(1000 * retries) - new_pid = spawn __MODULE__, :upload, [path, meta] - new_uploads = state.uploads - |> Map.delete(pid) - |> Map.put(new_pid, {path, meta, retries + 1}) - Process.monitor(new_pid) - {:noreply, %{state | uploads: new_uploads}} - else - {:noreply, %{state | uploads: Map.delete(state.uploads, pid)}} - end - end - end - - def handle_info(_info, state), do: {:noreply, state} - - # Stolen from - # https://github.com/phoenixframework/ - # phoenix_live_reload/blob/151ce9e17c1b4ead79062098b70d4e6bc7c7e528 - # /lib/phoenix_live_reload/channel.ex#L27 - defp matches_any_pattern?(path, patterns) do - path = to_string(path) - if String.contains?(path, "~") do - false - else - Enum.any?(patterns, fn pattern -> - String.match?(path, pattern) - end) - end - end - - def upload(file_path, meta) do - Farmbot.Logger.busy 3, "Image Watcher trying to upload #{file_path}" - case Farmbot.HTTP.upload_file(file_path, meta) do - {:ok, %{status_code: 200}} -> exit(:normal) - {:ok, %{body: body}} -> exit({:http_error, body}) - {:error, reason} -> exit(reason) - end - end -end diff --git a/farmbot_ext/lib/http/settings_worker.ex b/farmbot_ext/lib/http/settings_worker.ex deleted file mode 100644 index e562dc44..00000000 --- a/farmbot_ext/lib/http/settings_worker.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Farmbot.HTTP.SettingsWorker do - @moduledoc """ - Watches the local database for changes to the following resources: - * FbosConfig - * FirmwareConfig - * FarmwareEnv - - When a change is detected, the asset is uploaded to the API. - """ - - alias Farmbot.Asset.Settings - import Farmbot.Config, only: [get_config_as_map: 0] - require Farmbot.Logger - use GenServer - - def download_all_settings do - Farmbot.Logger.debug 3, "Syncing all settings." - - remote_fw_config = HTTP.firmware_config() - Settings.download_firmware(remote_fw_config) - - # Make sure that the API has the correct firmware hardware. - patch = %{"firmware_hardware" => get_config_as_map()["settings"]["firmware_hardware"]} - remote_os_config = HTTP.update_fbos_config(patch) - Settings.download_os(remote_os_config) - - Farmbot.Logger.debug 3, "Done syncing all settings." - :ok - end - - def start_link(args) do - GenServer.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - :ok = download_all_settings() - {:ok, %{}} - end -end diff --git a/farmbot_ext/lib/http/supervisor.ex b/farmbot_ext/lib/http/supervisor.ex deleted file mode 100644 index e90ccc20..00000000 --- a/farmbot_ext/lib/http/supervisor.ex +++ /dev/null @@ -1,20 +0,0 @@ -defmodule Farmbot.HTTP.Supervisor do - @moduledoc false - - use Supervisor - - @doc false - def start_link(args) do - Supervisor.start_link(__MODULE__, args, [name: __MODULE__]) - end - - def init([]) do - children = [ - {Farmbot.HTTP.ImageUploader, []}, - {Farmbot.HTTP.SettingsWorker, []} - ] - - opts = [strategy: :one_for_all] - Supervisor.init(children, opts) - end -end diff --git a/farmbot_ext/lib/jwt.ex b/farmbot_ext/lib/jwt.ex index d126121c..39641ae3 100644 --- a/farmbot_ext/lib/jwt.ex +++ b/farmbot_ext/lib/jwt.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.Jwt do +defmodule Farmbot.JWT do @moduledoc "Functions for dealing with the Farmbot JSON Web Token" defstruct [ @@ -12,9 +12,6 @@ defmodule Farmbot.Jwt do :interim_email ] - require Protocol - Protocol.derive Jason.Encoder, Farmbot.Jwt - @typedoc "Type def for Farmbot Web Token." @type t :: %__MODULE__{ bot: binary, @@ -53,7 +50,7 @@ defmodule Farmbot.Jwt do defp decode_map(%{} = map) do {:ok, struct( - Farmbot.Jwt, + Farmbot.JWT, bot: map["bot"], exp: map["exp"], iss: map["iss"], diff --git a/farmbot_ext/lib/settings_sync.ex b/farmbot_ext/lib/settings_sync.ex deleted file mode 100644 index ffcc643e..00000000 --- a/farmbot_ext/lib/settings_sync.ex +++ /dev/null @@ -1,304 +0,0 @@ -defmodule Farmbot.SettingsSync do - @moduledoc "Handles uploading and downloading of Farmbot OS and Firmware configs." - require Farmbot.Logger - import Farmbot.Config, - only: [get_config_value: 3, update_config_value: 4, get_config_as_map: 0] - - @fbos_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", - "update_channel" - ] - - @firmware_keys [ - "param_version", - "param_test", - "param_config_ok", - "param_use_eeprom", - "param_e_stop_on_mov_err", - "param_mov_nr_retry", - "movement_timeout_x", - "movement_timeout_y", - "movement_timeout_z", - "movement_keep_active_x", - "movement_keep_active_y", - "movement_keep_active_z", - "movement_home_at_boot_x", - "movement_home_at_boot_y", - "movement_home_at_boot_z", - "movement_invert_endpoints_x", - "movement_invert_endpoints_y", - "movement_invert_endpoints_z", - "movement_enable_endpoints_x", - "movement_enable_endpoints_y", - "movement_enable_endpoints_z", - "movement_invert_motor_x", - "movement_invert_motor_y", - "movement_invert_motor_z", - "movement_secondary_motor_x", - "movement_secondary_motor_invert_x", - "movement_steps_acc_dec_x", - "movement_steps_acc_dec_y", - "movement_steps_acc_dec_z", - "movement_stop_at_home_x", - "movement_stop_at_home_y", - "movement_stop_at_home_z", - "movement_home_up_x", - "movement_home_up_y", - "movement_home_up_z", - "movement_step_per_mm_x", - "movement_step_per_mm_y", - "movement_step_per_mm_z", - "movement_min_spd_x", - "movement_min_spd_y", - "movement_min_spd_z", - "movement_home_spd_x", - "movement_home_spd_y", - "movement_home_spd_z", - "movement_max_spd_x", - "movement_max_spd_y", - "movement_max_spd_z", - "movement_invert_2_endpoints_x", - "movement_invert_2_endpoints_y", - "movement_invert_2_endpoints_z", - "encoder_enabled_x", - "encoder_enabled_y", - "encoder_enabled_z", - "encoder_type_x", - "encoder_type_y", - "encoder_type_z", - "encoder_missed_steps_max_x", - "encoder_missed_steps_max_y", - "encoder_missed_steps_max_z", - "encoder_scaling_x", - "encoder_scaling_y", - "encoder_scaling_z", - "encoder_missed_steps_decay_x", - "encoder_missed_steps_decay_y", - "encoder_missed_steps_decay_z", - "encoder_use_for_pos_x", - "encoder_use_for_pos_y", - "encoder_use_for_pos_z", - "encoder_invert_x", - "encoder_invert_y", - "encoder_invert_z", - "movement_axis_nr_steps_x", - "movement_axis_nr_steps_y", - "movement_axis_nr_steps_z", - "movement_stop_at_max_x", - "movement_stop_at_max_y", - "movement_stop_at_max_z", - "pin_guard_1_pin_nr", - "pin_guard_1_time_out", - "pin_guard_1_active_state", - "pin_guard_2_pin_nr", - "pin_guard_2_time_out", - "pin_guard_2_active_state", - "pin_guard_3_pin_nr", - "pin_guard_3_time_out", - "pin_guard_3_active_state", - "pin_guard_4_pin_nr", - "pin_guard_4_time_out", - "pin_guard_4_active_state", - "pin_guard_5_pin_nr", - "pin_guard_5_time_out", - "pin_guard_5_active_state", - ] - - # 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", - "update_channel" - ] - - @float_keys @firmware_keys ++ [ - "network_not_found_timer" - ] - - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, [opts]}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - - - @doc false - def start_link(_) do - run() - :ignore - end - - @doc false - def run() do - try do - do_sync_fw_configs() - do_sync_fbos_configs() - Farmbot.Logger.debug 1, "Synced Farmbot OS and Firmware settings with API" - :ok - rescue - err -> - message = Exception.message(err) - err_msg = "#{message} #{inspect System.stacktrace()}" - Farmbot.Logger.error 1, "Error syncing settings: #{err_msg}" - update_config_value(:bool, "settings", "ignore_fbos_config", false) - update_config_value(:bool, "settings", "ignore_fw_config", false) - {:error, message} - end - end - - def apply_fbos_map(old_map, new_map) do - old_map = take_valid_fbos(old_map) - new_map = take_valid_fbos(new_map) - Map.new(new_map, fn({key, new_value}) -> - if old_map[key] !== new_value do - Farmbot.Logger.debug 3, "Got new config update: #{key} => #{new_value}" - apply_to_config_storage key, new_value - end - {key, new_value} - end) - end - - def apply_fw_map(old_map, new_map) do - old_map = take_valid_fw(old_map) - new_map = take_valid_fw(new_map) - new_stuff = Map.new(new_map, fn({key, new_value}) -> - if old_map[key] != new_value do - IO.puts "1 #{key} #{old_map[key]} != #{new_value}" - apply_to_config_storage key, new_value - {key, new_value} - else - {key, old_map[key]} - end - end) - - if Process.whereis(Farmbot.Firmware) do - Map.new(new_map, fn({param, new_value}) -> - if old_map[param] != new_value do - IO.puts "2 #{param} #{old_map[param]} != #{new_value}" - Farmbot.Firmware.update_param(String.to_atom(param), new_value) - Farmbot.Firmware.read_param(String.to_atom(param)) - end - {param, get_config_value(:float, "hardware_params", param)} - end) - else - new_stuff - end - end - - defp apply_to_config_storage(key, val) - when key in @bool_keys and (is_nil(val) or is_boolean(val)) do - Farmbot.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 - Farmbot.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 @firmware_keys do - if val do - Farmbot.Logger.success 2, "Updating FW param: #{key}: #{get_config_value(:float, "hardware_params", key)} => #{val}" - update_config_value(:float, "hardware_params", key, val / 1) - else - Farmbot.Logger.warn 2, "Not allowing #{key} to be set to null" - end - end - - defp apply_to_config_storage(key, val) - when key in @float_keys and (is_nil(val) or is_number(val)) do - Farmbot.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 - Farmbot.Logger.error 1, "Unknown pair: #{key} => #{inspect val}" - {:error, {:unknown_pair, {key, val}}} - end - - @doc "Sync the settings related to the Firmware." - def do_sync_fw_configs do - Farmbot.HTTP.firmware_config() - |> do_sync_fw_configs() - end - - def do_sync_fw_configs(api_data) do - update_config_value(:bool, "settings", "ignore_fw_config", true) - Farmbot.Logger.info 3, "API is source of truth for fw configs." - current = get_config_as_map()["hardware_params"] - apply_fw_map(current, api_data) - update_config_value(:bool, "settings", "ignore_fw_config", false) - :ok - end - - @doc "Uploads a single key value pair to the firmware config endpoint." - def upload_fw_kv(key, val) when is_binary(key) and key in @firmware_keys and is_number(val) - do - Farmbot.Logger.debug 3, "Uploading #{key} => #{val} to api." - Farmbot.HTTP.update_firmware_config(%{key => val}) - :ok - end - - @doc "Sync the settings related to Farmbot OS and Firmware" - def do_sync_fbos_configs do - Farmbot.HTTP.fbos_config() - |> Map.put("firmware_hardware", get_config_value(:string, "settings", "firmware_hardware")) - |> do_sync_fbos_configs() - end - - def do_sync_fbos_configs(api_data) do - update_config_value(:bool, "settings", "ignore_fbos_config", true) - Farmbot.Logger.info 3, "API is the source of truth for Farmbot OS configs. Downloading data." - old_config = get_config_as_map()["settings"] - apply_fbos_map(old_config, api_data) - update_config_value(:bool, "settings", "ignore_fbos_config", false) - - :ok - end - - def take_valid_fbos(map) do - Map.take(map, @fbos_keys ++ Enum.map(@fbos_keys, &String.to_atom(&1))) - end - - def take_valid_fw(%{param_config_ok: _} = atom_map) do - Map.new(atom_map, fn({key, val}) -> {to_string(key), val} end) - end - - def take_valid_fw(map) do - Map.take(map, @firmware_keys ++ Enum.map(@firmware_keys, &String.to_atom(&1))) - |> Map.drop(["param_version", "param_test", "param_config_ok", "param_use_eeprom"]) - end -end diff --git a/farmbot_ext/mix.exs b/farmbot_ext/mix.exs index bb39fc9d..c644010c 100644 --- a/farmbot_ext/mix.exs +++ b/farmbot_ext/mix.exs @@ -32,10 +32,9 @@ defmodule Farmbot.Ext.MixProject do {:uuid, "~> 1.1"}, {:amqp, "~> 1.0"}, {:fs, "~> 3.4"}, - {:excoveralls, "~> 0.10", only: [:test]}, {:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false}, - {:ex_doc, "~> 0.19", only: [:dev], runtime: false}, + {:ex_doc, "~> 0.19", only: [:dev], runtime: false} ] end end diff --git a/farmbot_ext/mix.lock b/farmbot_ext/mix.lock index ac92d33e..b66f59bb 100644 --- a/farmbot_ext/mix.lock +++ b/farmbot_ext/mix.lock @@ -1,6 +1,6 @@ %{ "amqp": {:hex, :amqp, "1.0.3", "06a6d909abc71d82b7c3133ca491899ca18fce857d0697dd060c29de1ef498d8", [:mix], [{:amqp_client, "~> 3.7.3", [hex: :amqp_client, repo: "hexpm", optional: false]}, {:goldrush, "~> 0.1.0", [hex: :goldrush, repo: "hexpm", optional: false]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "~> 3.5", [hex: :lager, repo: "hexpm", optional: false]}, {:rabbit_common, "~> 3.7.3", [hex: :rabbit_common, repo: "hexpm", optional: false]}, {:ranch, "~> 1.4", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "~> 1.4", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "~> 2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, - "amqp_client": {:hex, :amqp_client, "3.7.6", "e85a5688edf75d2f786ea66303f5b0f2b196bc4c5f82495f738ce19570cf8748", [:make, :rebar3], [{:rabbit_common, "3.7.6", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"}, + "amqp_client": {:hex, :amqp_client, "3.7.8", "5ec44ad152aed8519ef557189fa21e779f60578d21bcab36cabe381b451728ee", [:make, :rebar3], [{:rabbit_common, "3.7.8", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, @@ -8,7 +8,7 @@ "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, "erlex": {:hex, :erlex, "0.1.6", "c01c889363168d3fdd23f4211647d8a34c0f9a21ec726762312e08e083f3d47e", [:mix], [], "hexpm"}, "esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"}, @@ -23,7 +23,7 @@ "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.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, - "lager": {:hex, :lager, "3.5.1", "63897a61af646c59bb928fee9756ce8bdd02d5a1a2f3551d4a5e38386c2cc071", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"}, + "lager": {:hex, :lager, "3.6.3", "fe78951d174616273f87f0dbc3374d1430b1952e5efc4e1c995592d30a207294", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"}, "makeup": {:hex, :makeup, "0.5.6", "da47b331b1fe0a5f0380cc3a6967200eac5e1daaa9c6bff4b0310b3fcc12b98f", [:mix], [{:nimble_parsec, "~> 0.4.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, @@ -33,14 +33,14 @@ "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, - "rabbit_common": {:hex, :rabbit_common, "3.7.6", "fe6b34db6a98ceef40852e55dd782e944a63c7e855794ab58d3c3845091a6db0", [:make, :rebar3], [{:jsx, "2.8.2", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.5.1", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "1.5.0", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "rabbit_common": {:hex, :rabbit_common, "3.7.8", "e1410371c5814f85092b6dda85aa25fad945e7a41a9c4e34a59e61bd59a8c3b2", [:make, :rebar3], [{:jsx, "2.8.2", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.6.3", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "1.5.0", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, "ranch": {:hex, :ranch, "1.5.0", "f04166f456790fee2ac1aa05a02745cc75783c2bfb26d39faf6aefc9a3d3a58a", [:rebar3], [], "hexpm"}, "ranch_proxy_protocol": {:hex, :ranch_proxy_protocol, "2.0.0", "623c732025f9d66d123a8ccc1735e5f43d7eb9b20aa09457c9609ef05f7e8ace", [:rebar3], [{:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "recon": {:hex, :recon, "2.3.2", "4444c879be323b1b133eec5241cb84bd3821ea194c740d75617e106be4744318", [:rebar3], [], "hexpm"}, "rsa": {:hex, :rsa, "0.0.1", "a63069f88ce342ffdf8448b7cdef4b39ba7dee3c1510644a39385c7e63ba246f", [:mix], [], "hexpm"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, - "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.4", "e192c243750511efc1369d74c15d65a014e176e4aba9f67ca7852e9826b2cba4", [:mix], [{:connection, "~> 1.0.3", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 2.2", [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.3.2 or ~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, - "sqlitex": {:hex, :sqlitex, "1.4.2", "b18f2b53cefbc9cca0bd17d51386f9caa7cf341144cb314e5cd9fd2a1f9b0845", [:mix], [{:decimal, "~> 1.1", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "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.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.4.2", "d74649c93ad0e12ce5b17cf5e11fbd1fb1b24a3d114643e86dba194b64439547", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/farmbot_os/.formatter.exs b/farmbot_os/.formatter.exs new file mode 100644 index 00000000..ca0a0308 --- /dev/null +++ b/farmbot_os/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [:ecto], + inputs: ["*.{ex,exs}", "{config,priv,test}/**/*.{ex,exs}"], + subdirectories: ["priv/*/migrations"] +] diff --git a/farmbot_os/mix.lock.host b/farmbot_os/mix.lock.host index f3be8925..204806ed 100644 --- a/farmbot_os/mix.lock.host +++ b/farmbot_os/mix.lock.host @@ -54,7 +54,7 @@ "rsa": {:hex, :rsa, "0.0.1", "a63069f88ce342ffdf8448b7cdef4b39ba7dee3c1510644a39385c7e63ba246f", [:mix], [], "hexpm"}, "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, "shoehorn": {:hex, :shoehorn, "0.4.0", "f3830e22e1c58b502e8c436623804c4eb6ed15f5d0bdbacdeb448cddf4795951", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}], "hexpm"}, - "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.5", "f111a48188b0640effb7f2952071c4cf285501d3ce090820a7c2fc20af3867e9", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.3.1", "fe58926854c3962c4c8710bd1070dd4ba3717ba77250387794cb7a65f77006aa", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, "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.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, diff --git a/test/support/asset_fixtures.ex b/test/support/asset_fixtures.ex new file mode 100644 index 00000000..4c9a66c1 --- /dev/null +++ b/test/support/asset_fixtures.ex @@ -0,0 +1,65 @@ +defmodule Farmbot.TestSupport.AssetFixtures do + alias Farmbot.Asset.{Repo, Sequence, Regimen, FarmEvent} + + def sequence(params \\ %{}) do + Sequence + |> struct() + |> Sequence.changeset( + Map.merge(%{id: :rand.uniform(10000), kind: "sequence", args: %{}, body: []}, params) + ) + |> Repo.insert!() + end + + def regimen(params \\ %{}) do + Regimen + |> struct() + |> Regimen.changeset(Map.merge(%{id: :rand.uniform(10000), regimen_items: []}, params)) + |> Repo.insert!() + end + + def regimen_event(regimen, params \\ %{}) do + now = DateTime.utc_now() + + params = + Map.merge( + %{ + id: :rand.uniform(1_000_000), + executable_type: "Regimen", + executable_id: regimen.id, + start_time: now, + end_time: now, + repeat: 0, + time_unit: "never" + }, + params + ) + + FarmEvent + |> struct() + |> FarmEvent.changeset(params) + |> Repo.insert!() + end + + def sequence_event(sequence, params \\ %{}) do + now = DateTime.utc_now() + + params = + Map.merge( + %{ + id: :rand.uniform(1_000_000), + executable_type: "Sequence", + executable_id: sequence.id, + start_time: now, + end_time: now, + repeat: 0, + time_unit: "never" + }, + params + ) + + FarmEvent + |> struct() + |> FarmEvent.changeset(params) + |> Repo.insert!() + end +end diff --git a/test/support/test_io_layer.ex b/test/support/test_io_layer.ex new file mode 100644 index 00000000..4decfe21 --- /dev/null +++ b/test/support/test_io_layer.ex @@ -0,0 +1,87 @@ +defmodule Farmbot.TestSupport.CeleryScript.TestIOLayer do + @behaviour Farmbot.Core.CeleryScript.IOLayer + def calibrate(args, body), do: dispatch(:calibrate, args, body) + def change_ownership(args, body), do: dispatch(:change_ownership, args, body) + def check_updates(args, body), do: dispatch(:check_updates, args, body) + def config_update(args, body), do: dispatch(:config_update, args, body) + def dump_info(args, body), do: dispatch(:dump_info, args, body) + def emergency_lock(args, body), do: dispatch(:emergency_lock, args, body) + def emergency_unlock(args, body), do: dispatch(:emergency_unlock, args, body) + def execute(args, body), do: dispatch(:execute, args, body) + def execute_script(args, body), do: dispatch(:execute_script, args, body) + def factory_reset(args, body), do: dispatch(:factory_reset, args, body) + def find_home(args, body), do: dispatch(:find_home, args, body) + def home(args, body), do: dispatch(:home, args, body) + def move_absolute(args, body), do: dispatch(:move_absolute, args, body) + def move_relative(args, body), do: dispatch(:move_relative, args, body) + def power_off(args, body), do: dispatch(:power_off, args, body) + def read_pin(args, body), do: dispatch(:read_pin, args, body) + def read_status(args, body), do: dispatch(:read_status, args, body) + def reboot(args, body), do: dispatch(:reboot, args, body) + def send_message(args, body), do: dispatch(:send_message, args, body) + def set_servo_angle(args, body), do: dispatch(:set_servo_angle, args, body) + def set_user_env(args, body), do: dispatch(:set_user_env, args, body) + def sync(args, body), do: dispatch(:sync, args, body) + def take_photo(args, body), do: dispatch(:take_photo, args, body) + def toggle_pin(args, body), do: dispatch(:toggle_pin, args, body) + def wait(args, body), do: dispatch(:wait, args, body) + def write_pin(args, body), do: dispatch(:write_pin, args, body) + def zero(args, body), do: dispatch(:zero, args, body) + def _if(args, body), do: dispatch(:_if, args, body) + def debug(args, body), do: dispatch(:debug, args, body) + + defmodule Tracker do + use GenServer + + def dispatch(msg) do + GenServer.cast(__MODULE__, {:dispatch, msg}) + end + + def subscribe do + GenServer.cast(__MODULE__, {:subscribe, self()}) + end + + def start_link do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init(subs) do + {:ok, subs} + end + + def handle_cast({:dispatch, msg}, subs) do + for pid <- subs do + Process.alive?(pid) && send(pid, msg) + end + + {:noreply, subs} + end + + def handle_cast({:subscribe, pid}, subs), do: {:noreply, [pid | subs]} + end + + def dispatch(kind, args, body) do + Tracker.start_link() + ast = %{kind: kind, args: args, body: body} + Tracker.dispatch(ast) + {:error, to_string(kind)} + end + + def subscribe do + Tracker.start_link() + Tracker.subscribe() + end + + def debug_ast(params \\ %{}) do + %{ + kind: :debug, + args: %{label: uuid()}, + body: [] + } + |> Map.merge(params) + end + + def debug_fun(_), do: :ok + + def uuid, do: Ecto.UUID.generate() +end diff --git a/test/support/test_support.ex b/test/support/test_support.ex new file mode 100644 index 00000000..8e9706ce --- /dev/null +++ b/test/support/test_support.ex @@ -0,0 +1,15 @@ +defmodule Farmbot.TestSupport do + def farm_event_timeout do + Application.get_env(:farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent)[ + :checkup_time_ms + ] + asset_monitor_timeout() + end + + def asset_monitor_timeout do + Application.get_env(:farmbot_core, Farmbot.AssetMonitor)[:checkup_time_ms] + grace() + end + + def grace do + 5000 + end +end