Progress on refactoring data syncing (#643)

pull/974/head
Connor Rigby 2018-10-29 09:33:52 -07:00 committed by Connor Rigby
parent 9481d2ff24
commit 869d1ad1a4
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
171 changed files with 3771 additions and 4083 deletions

3
.formatter.exs 100644
View File

@ -0,0 +1,3 @@
[
inputs: ["*.{ex,exs}", "{test}/**/*.{ex,exs}"]
]

View File

@ -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 ; \

View File

@ -0,0 +1,3 @@
[
inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"],
]

View File

@ -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)

View File

@ -0,0 +1,5 @@
[
import_deps: [:ecto],
inputs: ["*.{ex,exs}", "{config,priv,test}/**/*.{ex,exs}"],
subdirectories: ["priv/*/migrations"]
]

View File

@ -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

View File

@ -1,9 +1,11 @@
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <erl_nif.h>
#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<MAX_GENERATED; j++) { if(events[j] > 0) { i++; } }
// Build the array to be returned.
ERL_NIF_TERM retArr [i];
for(j=0; j<i ; j++)
retArr[j] = enif_make_long(env, events[j]);
// we survived.
return enif_make_list_from_array(env, retArr, i);
return enif_make_long(env, startTimeSeconds);
}
static ErlNifFunc nif_funcs[] =

View File

@ -1,41 +1,57 @@
use Mix.Config
# config :logger, [
# utc_log: true,
# handle_otp_reports: true,
# handle_sasl_reports: true,
# ]
config :logger,
handle_otp_reports: true,
handle_sasl_reports: true
config :ecto, json_library: Farmbot.JSON
# Configure Farmbot Behaviours.
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, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 10_000
config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 30_000
if Mix.env() == :test do
config :farmbot_core, :behaviour,
celery_script_io_layer: Farmbot.TestSupport.CeleryScript.TestIOLayer
config :farmbot_core, Farmbot.AssetWorker.Farmbot.Asset.FarmEvent, checkup_time_ms: 1000
# must be lower than other timers
# To ensure other timers have time to timeout
config :farmbot_core, Farmbot.AssetMonitor, checkup_time_ms: 500
end
config :farmbot_core,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],
expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.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: "priv/config"
config :farmbot_core, Farmbot.Logger.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_logs.sqlite3",
database: ".#{Mix.env()}_logs.sqlite3",
priv: "priv/logger"
config :farmbot_core, Farmbot.Asset.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_assets.sqlite3",
database: ".#{Mix.env()}_assets.sqlite3",
priv: "priv/asset"

View File

@ -0,0 +1,102 @@
defmodule Farmbot.Asset do
alias Farmbot.Asset.{Repo,
Device,
FarmEvent,
FbosConfig,
FirmwareConfig,
PinBinding,
Regimen,
PersistentRegimen,
Sequence
}
import Ecto.Query
## Begin Device
def device() do
Repo.one(Device) || %Device{}
end
## End Device
## Begin FarmEvent
@doc "Returns a FarmEvent by its API id."
def get_farm_event(id) do
Repo.get_by(FarmEvent, id: id)
end
def update_farm_event!(farm_event, params) do
FarmEvent.changeset(farm_event, params)
|> 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,4 @@
defmodule Farmbot.Asset.Repo do
@moduledoc "Repo for storing Asset data."
use Ecto.Repo, otp_app: :farmbot_core
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,4 +0,0 @@
defmodule Farmbot.EctoTypes.ModuleType.FarmEvent do
@moduledoc false
use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(Sequence Regimen)
end

View File

@ -1,4 +0,0 @@
defmodule Farmbot.EctoTypes.ModuleType.Point do
@moduledoc false
use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(GenericPointer ToolSlot Plant)
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,3 @@
defprotocol Farmbot.AssetWorker do
def start_link(asset)
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"},

View File

@ -0,0 +1,4 @@
[
import_deps: [:ecto],
inputs: ["*.exs"]
]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

Some files were not shown because too many files have changed in this diff Show More