Progress on refactoring data syncing (#643)
parent
9481d2ff24
commit
869d1ad1a4
|
@ -0,0 +1,3 @@
|
|||
[
|
||||
inputs: ["*.{ex,exs}", "{test}/**/*.{ex,exs}"]
|
||||
]
|
5
Makefile
5
Makefile
|
@ -26,11 +26,14 @@ help:
|
|||
@echo " clean - clean all."
|
||||
|
||||
clean_other_branch:
|
||||
rm -rf _build deps c_src config tmp
|
||||
rm -rf _build deps c_src config tmp priv
|
||||
|
||||
clean: clean_other_branch
|
||||
@for project in $(PROJECTS) ; do \
|
||||
echo cleaning $$project ; \
|
||||
rm -rf $$project/erl_crash.dump ; \
|
||||
rm -rf $$project/.*.sqlite3* ; \
|
||||
rm -rf $$project/*.sqlite3* ; \
|
||||
rm -rf $$project/_build ; \
|
||||
rm -rf $$project/deps ; \
|
||||
rm -rf $$project/priv/*.so ; \
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[
|
||||
inputs: ["*.{ex,exs}", "{config,priv,lib,test}/**/*.{ex,exs}"],
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
[
|
||||
import_deps: [:ecto],
|
||||
inputs: ["*.{ex,exs}", "{config,priv,test}/**/*.{ex,exs}"],
|
||||
subdirectories: ["priv/*/migrations"]
|
||||
]
|
|
@ -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
|
||||
|
|
|
@ -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[] =
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
defmodule Farmbot.Asset.Repo do
|
||||
@moduledoc "Repo for storing Asset data."
|
||||
use Ecto.Repo, otp_app: :farmbot_core
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
|||
defmodule Farmbot.EctoTypes.ModuleType.FarmEvent do
|
||||
@moduledoc false
|
||||
use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(Sequence Regimen)
|
||||
end
|
|
@ -1,4 +0,0 @@
|
|||
defmodule Farmbot.EctoTypes.ModuleType.Point do
|
||||
@moduledoc false
|
||||
use Farmbot.EctoTypes.ModuleType, valid_mods: ~w(GenericPointer ToolSlot Plant)
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
defprotocol Farmbot.AssetWorker do
|
||||
def start_link(asset)
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
[
|
||||
import_deps: [:ecto],
|
||||
inputs: ["*.exs"]
|
||||
]
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue