diff --git a/farmbot_celery_script/config/config.exs b/farmbot_celery_script/config/config.exs new file mode 100644 index 00000000..4bbfd1f8 --- /dev/null +++ b/farmbot_celery_script/config/config.exs @@ -0,0 +1,9 @@ +use Mix.Config + +if Mix.env() == :test do + config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, + sys_calls: Farmbot.CeleryScript.TestSupport.TestSysCalls +else + config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, + sys_calls: Farmbot.CeleryScript.SysCalls.Stubs +end diff --git a/farmbot_celery_script/coveralls.json b/farmbot_celery_script/coveralls.json new file mode 100644 index 00000000..fb304945 --- /dev/null +++ b/farmbot_celery_script/coveralls.json @@ -0,0 +1,5 @@ +{ + "skip_files": [ + "lib/farmbot_celery_script/compiler/tools.ex" + ] +} diff --git a/farmbot_celery_script/fixture/master_sequence.term b/farmbot_celery_script/fixture/master_sequence.term deleted file mode 100644 index 44a7643e..00000000 Binary files a/farmbot_celery_script/fixture/master_sequence.term and /dev/null differ diff --git a/farmbot_celery_script/fixture/paramater_sequence.json b/farmbot_celery_script/fixture/paramater_sequence.json new file mode 100644 index 00000000..1060c641 --- /dev/null +++ b/farmbot_celery_script/fixture/paramater_sequence.json @@ -0,0 +1,142 @@ +{ + "kind": "sequence", + "name": "Test Sequence (TM)", + "color": "red", + "id": 2, + "comment": "This is the root", + "args": { + "version": 20180209, + "locals": { + "kind": "scope_declaration", + "args": {}, + "body": [ + { + "kind": "parameter_declaration", + "args": { + "label": "abc", + "default_value": { + "kind": "point", + "args": { + "pointer_type": "Plant", + "pointer_id": 123 + } + } + } + }, + { + "kind": "parameter_declaration", + "args": { + "label": "def", + "default_value": { + "kind": "point", + "args": { + "pointer_type": "Plant", + "pointer_id": 456 + } + } + } + }, + { + "kind": "parameter_application", + "args": { + "label": "ghi", + "data_value": { + "kind": "coordinate", + "args": { + "x": 100.0, + "y": 200.0, + "z": 300.0 + } + } + } + } + ] + } + }, + "body": [ + { + "kind": "move_absolute", + "args": { + "location": { + "kind": "identifier", + "args": { + "label": "abc" + } + }, + "offset": { + "kind": "identifier", + "args": { + "label": "def" + } + }, + "speed": 100 + } + }, + { + "kind": "move_absolute", + "args": { + "location": { + "kind": "identifier", + "args": { + "label": "ghi" + } + }, + "offset": { + "kind": "coordinate", + "args": { + "x": 100, + "y": 10000, + "z": 55 + } + }, + "speed": 100 + } + }, + { + "kind": "execute", + "args": { + "sequence_id": 9 + }, + "body": [ + { + "kind": "variable_declaration", + "args": { + "label": "jkl", + "data_value": { + "kind": "point", + "args": { + "pointer_type": "Plant", + "pointer_id": 789 + } + } + } + }, + { + "kind": "variable_declaration", + "args": { + "label": "pqr", + "data_value": { + "kind": "identifier", + "args": { + "label": "abc" + } + } + } + }, + { + "kind": "variable_declaration", + "args": { + "label": "mno", + "data_value": { + "kind": "point", + "args": { + "pointer_type": "Plant", + "pointer_id": 555 + } + } + } + } + ] + } + ] +} diff --git a/farmbot_celery_script/lib/address.ex b/farmbot_celery_script/lib/address.ex deleted file mode 100644 index 32e744e9..00000000 --- a/farmbot_celery_script/lib/address.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Address do - @moduledoc "Address on the heap." - - defstruct [:value] - - @type value :: integer - - @type t :: %Address{value: value} - - @typedoc "Null address." - @type null :: %Address{value: 0} - - @doc "New heap address." - @spec new(integer) :: t() - def new(num) when is_integer(num), do: %Address{value: num} - - @spec null :: null() - def null, do: %Address{value: 0} - - @doc "Increment an address." - @spec inc(t) :: t() - def inc(%Address{value: num}), do: %Address{value: num + 1} - - @doc "Decrement an address." - @spec dec(t) :: t() - def dec(%Address{value: num}), do: %Address{value: num - 1} - - defimpl Inspect, for: Address do - def inspect(%Address{value: val}, _), do: "#Address<#{val}>" - end -end diff --git a/farmbot_celery_script/lib/circular_list.ex b/farmbot_celery_script/lib/circular_list.ex deleted file mode 100644 index c7c4ae8f..00000000 --- a/farmbot_celery_script/lib/circular_list.ex +++ /dev/null @@ -1,93 +0,0 @@ -defmodule CircularList do - defstruct current_index: 0, items: %{}, autoinc: -1 - - @opaque index :: number - @type data :: any - - @type t :: %CircularList{ - current_index: index, - autoinc: index, - items: %{optional(index) => data} - } - - @spec new :: %CircularList{ - current_index: 0, - autoinc: -1, - items: %{} - } - def new do - %CircularList{} - end - - @spec get_index(t) :: index - def get_index(this) do - this.current_index - end - - @spec current(t) :: data() - def current(this) do - at(this, get_index(this)) - end - - @spec at(t(), index) :: data() - def at(this, index) do - this.items[index] - end - - @spec is_empty?(t()) :: boolean() - def is_empty?(%{items: items}) when map_size(items) == 0, do: true - def is_empty?(_this), do: false - - @spec reduce(t(), (index, data -> data)) :: t() - def reduce(this, fun) do - results = Enum.reduce(this.items, %{}, fun) - - %{this | items: results} - |> rotate() - end - - @spec update_current(t, (data -> data)) :: t - def update_current(this, fun) do - index = get_index(this) - current_value = at(this, index) - - if current_value do - result = fun.(current_value) - %{this | items: Map.put(this.items, index, result)} - else - fun.(:noop) - this - end - end - - @spec rotate(t) :: t - def rotate(this) do - current = this.current_index - keys = Enum.sort(Map.keys(this.items)) - # Grab first where index > this.current_index, or keys.first - next_key = Enum.find(keys, List.first(keys), fn key -> key > current end) - %CircularList{this | current_index: next_key} - end - - @spec push(t, data) :: t - def push(this, item) do - # Bump autoinc - next_autoinc = this.autoinc + 1 - next_items = Map.put(this.items, next_autoinc, item) - # Add the item - %CircularList{this | autoinc: next_autoinc, items: next_items} - end - - @spec remove(t, index) :: t - def remove(this, index) do - if index in Map.keys(this.items) do - this - |> rotate() - |> Map.update(:items, %{}, fn old_items -> - Map.delete(old_items, index) - end) - else - this - end - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script.ex b/farmbot_celery_script/lib/farmbot_celery_script.ex new file mode 100644 index 00000000..61457673 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script.ex @@ -0,0 +1,4 @@ +defmodule Farmbot.CeleryScript do + @moduledoc """ + """ +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex index 88392ab4..0f0d35e6 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex @@ -3,8 +3,7 @@ defmodule Farmbot.CeleryScript.AST do Handy functions for turning various data types into Farbot Celery Script Ast nodes. """ - alias Farmbot.CeleryScript.{AST, Compiler} - alias AST.{Heap, Slicer, Unslicer} + alias Farmbot.CeleryScript.AST @typedoc "Arguments to a ast node." @type args :: map @@ -26,9 +25,13 @@ defmodule Farmbot.CeleryScript.AST do defstruct [:args, :body, :kind, :comment] @doc "Decode a base map into CeleryScript AST." - @spec decode(t() | map) :: t() + @spec decode(t() | map | [t() | map]) :: t() def decode(map_or_list_of_maps) + def decode(list) when is_list(list) do + decode_body(list) + end + def decode(%{__struct__: _} = thing) do thing |> Map.from_struct() |> decode() end @@ -39,7 +42,7 @@ defmodule Farmbot.CeleryScript.AST do body = thing["body"] || thing[:body] || [] comment = thing["comment"] || thing[:comment] || nil - %__MODULE__{ + %AST{ kind: String.to_atom(to_string(kind)), args: decode_args(args), body: decode_body(body), @@ -72,7 +75,7 @@ defmodule Farmbot.CeleryScript.AST do @spec new(atom, map, [map]) :: t() def new(kind, args, body, comment \\ nil) when is_map(args) and is_list(body) do - %__MODULE__{ + %AST{ kind: String.to_atom(to_string(kind)), args: args, body: body, @@ -80,13 +83,4 @@ defmodule Farmbot.CeleryScript.AST do } |> decode() end - - @spec slice(AST.t()) :: Heap.t() - def slice(%AST{} = ast), do: Slicer.run(ast) - - @spec unslice(Heap.t(), Address.t()) :: AST.t() - def unslice(%Heap{} = heap, %Address{} = addr), - do: Unslicer.run(heap, addr) - - defdelegate compile(ast), to: Compiler end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex new file mode 100644 index 00000000..436b2784 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/factory.ex @@ -0,0 +1,68 @@ +defmodule Farmbot.CeleryScript.AST.Factory do + @moduledoc """ + Helpers for creating ASTs. + """ + + alias Farmbot.CeleryScript.AST + + def new do + %AST{body: []} + end + + def new(kind, args \\ %{}, body \\ []) do + AST.new(kind, Map.new(args), body) + end + + def rpc_request(%AST{} = ast, label) when is_binary(label) do + %AST{ast | kind: :rpc_request, args: %{label: label}, body: []} + end + + def read_pin(%AST{} = ast, pin_number, pin_mode) do + ast + |> add_body_node(new(:read_pin, %{pin_number: pin_number, pin_mode: pin_mode})) + end + + def dump_info(%AST{} = ast) do + ast + |> add_body_node(new(:dump_info)) + end + + def emergency_lock(%AST{} = ast) do + ast + |> add_body_node(new(:emergency_lock)) + end + + def emergency_unlock(%AST{} = ast) do + ast + |> add_body_node(new(:emergency_unlock)) + end + + def read_status(%AST{} = ast) do + ast + |> add_body_node(new(:read_status)) + end + + def power_off(%AST{} = ast) do + ast + |> add_body_node(new(:power_off)) + end + + def reboot(%AST{} = ast) do + ast + |> add_body_node(new(:reboot)) + end + + def sync(%AST{} = ast) do + ast + |> add_body_node(new(:sync)) + end + + def take_photo(%AST{} = ast) do + ast + |> add_body_node(new(:take_photo)) + end + + def add_body_node(%AST{body: body} = ast, %AST{} = body_node) do + %{ast | body: body ++ [body_node]} + end +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex deleted file mode 100644 index 81fe453f..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex +++ /dev/null @@ -1,103 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.Heap do - @moduledoc """ - A heap-ish data structure required when converting canonical CeleryScript AST - nodes into the Flat IR form. - This data structure is useful because it addresses each node in the - CeleryScript tree via a unique numerical index, rather than using mutable - references. - MORE INFO: https://github.com/FarmBot-Labs/Celery-Slicer - """ - alias Farmbot.CeleryScript.AST.Heap - - # Constants and key names. - - @link "__" - @body String.to_atom(@link <> "body") - @next String.to_atom(@link <> "next") - @parent String.to_atom(@link <> "parent") - @kind String.to_atom(@link <> "kind") - - @primary_fields [@parent, @body, @kind, @next] - - @null Address.new(0) - @nothing %{ - @kind => :nothing, - @parent => @null, - @body => @null, - @next => @null - } - - # A special char. When a node has an attribute - # that starts with the `@link` char, it - # indicates a "link" to another node. - # Ex: "__parent" points to another node. - def link, do: @link - def parent, do: @parent - def body, do: @body - def next, do: @next - def kind, do: @kind - # Fields found on every heap entry. - def primary_fields, do: @primary_fields - def null, do: @null - - defstruct [:entries, :here] - - @type t :: %Heap{ - entries: %{Address.t() => cell()}, - here: here() - } - - @type here :: Address.t() - - @typedoc "this is actually an atom that starts with __" - @type link :: atom - - @typedoc "individual heap entry." - @type cell :: %{ - required(:__kind) => atom, - required(:__body) => Address.t(), - required(:__next) => Address.t(), - required(:__parent) => Address.t() - } - - @doc "Initialize a new heap." - @spec new() :: t() - def new do - %{struct(Heap) | here: @null, entries: %{@null => @nothing}} - end - - @doc "Alot a new kind on the heap. Increments `here` on the heap." - @spec alot(t(), atom) :: t() - def alot(%Heap{} = heap, kind) do - here_plus_one = Address.inc(heap.here) - - new_entries = Map.put(heap.entries, here_plus_one, %{@kind => kind}) - - %Heap{heap | here: here_plus_one, entries: new_entries} - end - - @doc "Puts a key/value pair at `here` on the heap." - @spec put(t(), any, any) :: t() - def put(%Heap{here: addr} = heap, key, value) do - put(heap, addr, key, value) - end - - @doc "Puts a key/value pair at an arbitrary address on the heap." - @spec put(t(), Address.t(), any, any) :: t() - def put(%Heap{} = heap, %Address{} = addr, key, value) do - block = heap[addr] || raise "Bad node address: #{inspect(addr)}" - new_block = Map.put(block, String.to_atom(to_string(key)), value) - new_entries = Map.put(heap.entries, addr, new_block) - %{heap | entries: new_entries} - end - - @doc "Gets the values of the heap entries." - @spec values(t()) :: %{Address.t() => cell()} - def values(%Heap{entries: entries}), do: entries - - # Access behaviour. - @doc false - @spec fetch(t, Address.t()) :: {:ok, cell()} - def fetch(%Heap{} = heap, %Address{} = adr), - do: Map.fetch(Heap.values(heap), adr) -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex deleted file mode 100644 index 59103000..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex +++ /dev/null @@ -1,115 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.Slicer do - @moduledoc """ - ORIGINAL IMPLEMENTATION HERE: https://github.com/FarmBot-Labs/Celery-Slicer - Take a nested ("canonical") representation of a CeleryScript sequence and - transofrms it to a flat/homogenous intermediate representation which is better - suited for storage in a relation database. - """ - alias Farmbot.CeleryScript.AST - alias AST.Heap - - @doc "Slice the canonical AST format into a AST Heap." - @spec run(AST.t()) :: Heap.t() - def run(canonical) - - def run(%AST{} = canonical) do - Heap.new() - |> allocate(canonical, Heap.null()) - |> elem(1) - |> Map.update(:entries, :error, fn entries -> - Map.new(entries, fn {key, entry} -> - entry = - Map.put( - entry, - Heap.body(), - Map.get(entry, Heap.body(), Heap.null()) - ) - - entry = - Map.put( - entry, - Heap.next(), - Map.get(entry, Heap.next(), Heap.null()) - ) - - {key, entry} - end) - end) - end - - @doc false - @spec allocate(Heap.t(), AST.t(), Address.t()) :: {Heap.here(), Heap.t()} - def allocate(%Heap{} = heap, %AST{} = ast, %Address{} = parent_addr) do - %Heap{here: addr} = heap = Heap.alot(heap, ast.kind) - - new_heap = - Heap.put(heap, Heap.parent(), parent_addr) - |> iterate_over_body(ast, addr) - |> iterate_over_args(ast, addr) - - {addr, new_heap} - end - - @spec iterate_over_args(Heap.t(), AST.t(), Address.t()) :: Heap.t() - defp iterate_over_args( - %Heap{} = heap, - %AST{} = canonical_node, - parent_addr - ) do - keys = Map.keys(canonical_node.args) - - Enum.reduce(keys, heap, fn key, %Heap{} = heap -> - case canonical_node.args[key] do - %AST{} = another_node -> - k = Heap.link() <> to_string(key) - {addr, new_heap} = allocate(heap, another_node, parent_addr) - Heap.put(new_heap, parent_addr, k, addr) - - val -> - Heap.put(heap, parent_addr, key, val) - end - end) - end - - @spec iterate_over_body(Heap.t(), AST.t(), Address.t()) :: Heap.t() - defp iterate_over_body( - %Heap{} = heap, - %AST{} = canonical_node, - parent_addr - ) do - recurse_into_body(heap, canonical_node.body, parent_addr) - end - - @spec recurse_into_body(Heap.t(), [AST.t()], Address.t(), integer) :: Heap.t() - defp recurse_into_body(heap, body, parent_addr, index \\ 0) - - defp recurse_into_body( - %Heap{} = heap, - [body_item | rest], - prev_addr, - 0 - ) do - {my_heap_address, %Heap{} = new_heap} = - heap - |> Heap.put(prev_addr, Heap.body(), Address.inc(prev_addr)) - |> allocate(body_item, prev_addr) - - new_heap - |> Heap.put(prev_addr, Heap.next(), Heap.null()) - |> recurse_into_body(rest, my_heap_address, 1) - end - - defp recurse_into_body( - %Heap{} = heap, - [body_item | rest], - prev_addr, - index - ) do - {my_heap_address, %Heap{} = heap} = allocate(heap, body_item, prev_addr) - - new_heap = Heap.put(heap, prev_addr, Heap.next(), my_heap_address) - recurse_into_body(new_heap, rest, my_heap_address, index + 1) - end - - defp recurse_into_body(%Heap{} = heap, [], _, _), do: heap -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex deleted file mode 100644 index f564ff79..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex +++ /dev/null @@ -1,78 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.Unslicer do - @moduledoc """ - Turn an AST Heap back into an AST. - """ - alias Farmbot.CeleryScript.AST - alias Farmbot.CeleryScript.AST.Heap - - @link Heap.link() - @parent Heap.parent() - @body Heap.body() - @next Heap.next() - @kind Heap.kind() - - @typedoc "Ast with String Keys" - @type pre_ast :: map - - @doc "Unslices a Heap struct back to cannonical celeryscript." - @spec run(Heap.t(), Address.t()) :: AST.t() - def run(%Heap{} = heap, %Address{} = addr) do - heap - |> unslice(addr) - |> AST.decode() - end - - @spec unslice(Heap.t(), Address.t()) :: pre_ast - defp unslice(heap, addr) do - here_cell = heap[addr] || raise "No cell at address: #{inspect(addr)}" - - Enum.reduce(here_cell, %{"args" => %{}}, fn {key, value}, acc -> - if is_link?(key) do - do_unslice(heap, key, value, acc) - else - %{acc | "args" => Map.put(acc["args"], to_string(key), value)} - end - end) - end - - @spec do_unslice(Heap.t(), Heap.link(), any, acc :: map) :: acc :: map - defp do_unslice(_heap, @parent, _value, acc), do: acc - defp do_unslice(_heap, @next, _value, acc), do: acc - - defp do_unslice(_heap, @kind, value, acc), - do: Map.put(acc, "kind", to_string(value)) - - defp do_unslice(heap, @body, value, acc) do - if heap[value][@kind] == :nothing do - acc - else - next_addr = value - n = heap[next_addr] - body = reduce_body(n, next_addr, heap, []) - Map.put(acc, "body", body) - end - end - - defp do_unslice(heap, key, value, acc) do - key = String.replace(to_string(key), @link, "") - args = Map.put(acc["args"], key, unslice(heap, value)) - %{acc | "args" => args} - end - - @spec reduce_body(Heap.cell(), Address.t(), Heap.t(), [pre_ast]) :: [pre_ast] - defp reduce_body(%{__kind: :nothing}, _next_addr, _heap, acc), - do: acc - - defp reduce_body(%{} = cell, %Address{} = next_addr, heap, acc) do - item = unslice(heap, next_addr) - new_acc = acc ++ [item] - next_addr = cell[@next] - next_cell = heap[next_addr] - reduce_body(next_cell, next_addr, heap, new_acc) - end - - @spec is_link?(atom) :: boolean() - defp is_link?(key) do - String.starts_with?(to_string(key), @link) - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex b/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex index 37d16998..d475d4f5 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex @@ -4,16 +4,51 @@ defmodule Farmbot.CeleryScript.Compiler do Elixir AST. """ - alias Farmbot.CeleryScript.{AST, Compiler} + alias Farmbot.CeleryScript.{AST, Compiler, Compiler.IdentifierSanitizer, SysCalls} use Compiler.Tools @valid_entry_points [:sequence, :rpc_request] + # TODO(Connor) - Delete this when the new corpus is published + @kinds "regisiter_gpio" + @kinds "unregisiter_gpio" + @kinds "config_update" + + @typedoc """ + Compiled CeleryScript node should compile to an anon function. + Entrypoint nodes such as + * `rpc_request` + * `sequence` + will compile to a function that takes a Keyword list of variables. This function + needs to be executed before scheduling/executing. + + Non entrypoint nodes compile to a function that symbolizes one individual step. + + ## Examples + + `rpc_request` will be compiled to something like: + ``` + fn params -> + [ + # Body of the `rpc_request` compiled in here. + ] + end + ``` + + as compared to a "simple" node like `wait` will compile to something like: + ``` + fn() -> wait(200) end + ``` + """ + @type compiled :: (Keyword.t() -> [(() -> any())]) | (() -> any()) + @doc """ Recursive function that will emit Elixir AST from CeleryScript AST. """ + @spec compile(AST.t(), Keyword.t()) :: [compiled()] def compile(%AST{kind: kind} = ast, env \\ []) when kind in @valid_entry_points do # compile the ast {_, _, _} = compiled = compile_ast(ast) + delete_me(compiled) # entry points must be evaluated once more with the calling `env` # to return a list of compiled `steps` case Code.eval_quoted(compiled, []) do @@ -22,42 +57,66 @@ defmodule Farmbot.CeleryScript.Compiler do end end + # The compile macro right here is generated by the Compiler.Tools module. + # The goal of the macro is to do two things: + # 1) take out all the common code between each node impl. + # Example: + # compile :fire_laser, %{after: 100}, targets, do: quote, do: fire_at(targets) + # will compile down to: + # def compile_ast(%AST{kind: :fire_laser, args: %{after: 100}, body: targets}) + # + # 2) Accumulate implemented nodes behind the scenes. + # This allows for the Corpus to throw warnings when a new node + # is added. + # Compiles a `sequence` into an Elixir `fn`. - def compile_ast(%AST{kind: :sequence, args: %{locals: %{body: params}}, body: block}) do + compile :sequence, %{locals: %{body: params}}, block do # Sort the args.body into two arrays. # The `params` side gets turned into # a keyword list. These `params` are passed in from a previous sequence. # The `body` side declares variables in _this_ scope. - {params, body} = + {params_fetch, body} = Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc -> case ast do + # declares usage of a paramater as defined by variable_declaration %{kind: :parameter_declaration} -> {params ++ [compile_param_declaration(ast)], body} + # declares usage of a variable as defined inside the body of itself + %{kind: :parameter_application} -> {params ++ [compile_param_application(ast)], body} + # defines a variable exists %{kind: :variable_declaration} -> {params, body ++ [ast]} end end) - assignments = compile_block(body) + {:__block__, [], assignments} = compile_block(body) steps = compile_block(block) |> decompose_block_to_steps() quote do fn params -> - import Farmbot.CeleryScript.Syscalls + import SysCalls # Fetches variables from the previous execute() # example: # parent = Keyword.fetch!(params, :parent) - unquote_splicing(params) + unquote_splicing(params_fetch) + unquote_splicing(assignments) # Unquote the remaining sequence steps. - unquote(assignments) + unquote(steps) + end + end + end + + compile :rpc_request, %{label: _label}, block do + steps = compile_block(block) |> decompose_block_to_steps() + + quote do + fn params -> + import SysCalls unquote(steps) end end end # Compiles a variable asignment. - def compile_ast(%AST{ - kind: :variable_declaration, - args: %{label: var_name, data_value: data_value_ast} - }) do + compile :variable_declaration, %{label: var_name, data_value: data_value_ast} do # Compiles the `data_value` # and assigns the result to a variable named `label` # Example: @@ -78,14 +137,15 @@ defmodule Farmbot.CeleryScript.Compiler do # parent = point("Plant", 456) # NOTE: This needs to be Elixir AST syntax, not quoted # because var! doesn't do what what we need. - {:=, [], [{String.to_atom(var_name), [], __MODULE__}, compile_ast(data_value_ast)]} + var_name = IdentifierSanitizer.to_variable(var_name) + + quote do + unquote({var_name, [], nil}) = unquote(compile_ast(data_value_ast)) + end end # Compiles an if statement. - def compile_ast(%AST{ - kind: :_if, - args: %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs} - }) do + compile :_if, %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs} do # Turns the left hand side arg into # a number. x, y, z, and pin{number} are special that need to be # evaluated before evaluating the if statement. @@ -157,42 +217,76 @@ defmodule Farmbot.CeleryScript.Compiler do end # Compiles an `execute` block. - def compile_ast(%AST{kind: :execute, args: %{sequence_id: id}, body: variable_declarations}) do + compile :execute, %{sequence_id: id}, variable_declarations do quote do # We have to lookup the sequence by it's id. %Farmbot.CeleryScript.AST{} = ast = get_sequence(unquote(id)) # compile the ast - compiled_fun = unquote(__MODULE__).compile(ast) - - # And call it, serializing all the variables it expects. - # see the `compile_param_application/1` docs for more info. - compiled_fun.(unquote(compile_param_application(variable_declarations))) + env = unquote(compile_params_to_function_args(variable_declarations)) + unquote(__MODULE__).compile(ast, env) end end # Compiles `execute_script` # TODO(Connor) - make this actually usable - def compile_ast(%AST{kind: :execute_script, args: %{label: package}, body: params}) do + compile :execute_script, %{label: package}, params do env = Enum.map(params, fn %{args: %{label: key, value: value}} -> {to_string(key), value} end) quote do - execute_script(unquote(package), unquote(Macro.escape(Map.new(env)))) + execute_script(unquote(compile_ast(package)), unquote(Macro.escape(Map.new(env)))) end end # TODO(Connor) - see above TODO - def compile_ast(%AST{kind: :take_photo}) do + compile :take_photo do # {:execute_script, [], ["take_photo", {:%{}, [], []}]} quote do execute_script("take_photo", %{}) end end + compile :set_user_env, _args, pairs do + kvs = + Enum.map(pairs, fn %{kind: :pair, args: %{label: key, value: value}} -> + quote do + set_user_env(unquote(key), unquote(value)) + end + end) + + quote do + (unquote_splicing(kvs)) + end + end + + compile :install_farmware, %{url: url} do + quote do + install_farmware(unquote(compile_ast(url))) + end + end + + compile :update_farmware, %{package: package} do + quote do + update_farmware(unquote(compile_ast(package))) + end + end + + compile :remove_farmware, %{package: package} do + quote do + remove_farmware(unquote(compile_ast(package))) + end + end + + compile :install_first_party_farmware, _ do + quote do + install_first_party_farmware() + end + end + # Compiles a nothing block. - def compile_ast(%AST{kind: :nothing}) do + compile :nothing do # AST looks like: {:nothing, [], []} quote do nothing() @@ -200,10 +294,7 @@ defmodule Farmbot.CeleryScript.Compiler do end # Compiles move_absolute - def compile_ast(%AST{ - kind: :move_absolute, - args: %{location: location, offset: offset, speed: speed} - }) do + compile :move_absolute, %{location: location, offset: offset, speed: speed} do quote do # Extract the location arg %{x: locx, y: locy, z: locz} = unquote(compile_ast(location)) @@ -218,7 +309,7 @@ defmodule Farmbot.CeleryScript.Compiler do end # compiles move_relative into move absolute - def compile_ast(%AST{kind: :move_relative, args: %{x: x, y: y, z: z, speed: speed}}) do + compile :move_relative, %{x: x, y: y, z: z, speed: speed} do quote do # build a vec3 of passed in args %{x: locx, y: locy, z: locz} = %{ @@ -242,7 +333,7 @@ defmodule Farmbot.CeleryScript.Compiler do end # compiles write_pin - def compile_ast(%AST{kind: :write_pin, args: %{pin_number: num, pin_mode: mode, pin_value: val}}) do + compile :write_pin, %{pin_number: num, pin_mode: mode, pin_value: val} do quote do write_pin( unquote(compile_ast(num)), @@ -253,14 +344,21 @@ defmodule Farmbot.CeleryScript.Compiler do end # compiles read_pin - def compile_ast(%AST{kind: :read_pin, args: %{pin_number: num, pin_mode: mode}}) do + compile :read_pin, %{pin_number: num, pin_mode: mode} do quote do read_pin(unquote(compile_ast(num)), unquote(compile_ast(mode))) end end + # compiles set_servo_angle + compile :set_servo_angle, %{pin_number: pin_number, pin_value: pin_value} do + quote do + set_servo_angle(unquote(compile_ast(pin_number)), unquote(compile_ast(pin_value))) + end + end + # Expands find_home(all) into three find_home/1 calls - def compile_ast(%AST{kind: :find_home, args: %{axis: "all", speed: speed}}) do + compile :find_home, %{axis: "all", speed: speed} do quote do find_home("x", unquote(compile_ast(speed))) find_home("y", unquote(compile_ast(speed))) @@ -269,50 +367,65 @@ defmodule Farmbot.CeleryScript.Compiler do end # compiles find_home - def compile_ast(%AST{kind: :find_home, args: %{axis: axis, speed: speed}}) do + compile :find_home, %{axis: axis, speed: speed} do quote do find_home(unquote(compile_ast(axis)), unquote(compile_ast(speed))) end end - # compiles wait - # def compile_ast(%AST{kind: :wait, args: %{milliseconds: millis}}) do - # quote do - # find_home(unquote(compile_ast(millis))) - # end - # end - - compile :wait, %{milliseconds: millis} do + # Expands home(all) into three home/1 calls + compile :home, %{axis: "all", speed: speed} do quote do - find_home(unquote(compile_ast(millis))) + home("x", unquote(compile_ast(speed))) + home("y", unquote(compile_ast(speed))) + home("z", unquote(compile_ast(speed))) end end - # # compiles send_message - # def compile_ast(%AST{ - # kind: :send_message, - # args: %{message: msg, message_type: type}, - # body: channels - # }) do - # # body gets turned into a list of atoms. - # # Example: - # # [{kind: "channel", args: {channel_name: "email"}}] - # # is turned into: - # # [:email] - # channels = - # Enum.map(channels, fn %{kind: :channel, args: %{channel_name: channel_name}} -> - # String.to_atom(channel_name) - # end) + # compiles home + compile :home, %{axis: axis, speed: speed} do + quote do + home(unquote(compile_ast(axis)), unquote(compile_ast(speed))) + end + end - # quote do - # # send_message("success", "Hello world!", [:email, :toast]) - # send_message( - # unquote(compile_ast(type)), - # unquote(compile_ast(msg)), - # unquote(channels) - # ) - # end - # end + # Expands zero(all) into three zero/1 calls + compile :zero, %{axis: "all", speed: speed} do + quote do + zero("x", unquote(compile_ast(speed))) + zero("y", unquote(compile_ast(speed))) + zero("z", unquote(compile_ast(speed))) + end + end + + # compiles zero + compile :zero, %{axis: axis, speed: speed} do + quote do + zero(unquote(compile_ast(axis)), unquote(compile_ast(speed))) + end + end + + # Expands calibrate(all) into three calibrate/1 calls + compile :calibrate, %{axis: "all", speed: speed} do + quote do + calibrate("x", unquote(compile_ast(speed))) + calibrate("y", unquote(compile_ast(speed))) + calibrate("z", unquote(compile_ast(speed))) + end + end + + # compiles calibrate + compile :calibrate, %{axis: axis, speed: speed} do + quote do + calibrate(unquote(compile_ast(axis)), unquote(compile_ast(speed))) + end + end + + compile :wait, %{milliseconds: millis} do + quote do + wait(unquote(compile_ast(millis))) + end + end compile :send_message, %{message: msg, message_type: type}, channels do # body gets turned into a list of atoms. @@ -337,7 +450,7 @@ defmodule Farmbot.CeleryScript.Compiler do # compiles coordinate # Coordinate should return a vec3 - def compile_ast(%AST{kind: :coordinate, args: %{x: x, y: y, z: z}}) do + compile :coordinate, %{x: x, y: y, z: z} do quote do coordinate( unquote(compile_ast(x)), @@ -348,29 +461,154 @@ defmodule Farmbot.CeleryScript.Compiler do end # compiles point - def compile_ast(%AST{kind: :point, args: %{pointer_type: type, pointer_id: id}}) do + compile :point, %{pointer_type: type, pointer_id: id} do quote do point(unquote(compile_ast(type)), unquote(compile_ast(id))) end end # compile a named pin - def compile_ast(%AST{kind: :named_pin, args: %{pin_id: id, pin_type: type}}) do + compile :named_pin, %{pin_id: id, pin_type: type} do quote do - pin(unquote(compile_ast(type)), unquote(compile_ast(id))) + named_pin(unquote(compile_ast(type)), unquote(compile_ast(id))) end end # compiles identifier into a variable. # We have to use Elixir ast syntax here because # var! doesn't work quite the way we want. - def compile_ast(%AST{kind: :identifier, args: %{label: var_name}}) do - {String.to_atom(var_name), [], __MODULE__} + compile :identifier, %{label: var_name} do + var_name = IdentifierSanitizer.to_variable(var_name) + + quote do + unquote({var_name, [], nil}) + end end - # Numbers and strings are treated as literals. - def compile_ast(lit) when is_number(lit), do: lit - def compile_ast(lit) when is_binary(lit), do: lit + compile :tool, %{tool_id: tool_id} do + quote do + get_tool(unquote(compile_ast(tool_id))) + end + end + + compile :emergency_lock do + quote do + emergency_lock() + end + end + + compile :emergency_unlock do + quote do + emergency_unlock() + end + end + + compile :read_status do + quote do + read_status() + end + end + + compile :sync do + quote do + sync() + end + end + + compile :check_updates, %{package: "farmbot_os"} do + quote do + check_update() + end + end + + compile :power_off do + quote do + power_off() + end + end + + compile :reboot, %{package: "farmbot_os"} do + quote do + reboot() + end + end + + compile :reboot, %{package: "arduino_firmware"} do + quote do + firmware_reboot() + end + end + + compile :factory_reset, %{package: "farmbot_os"} do + quote do + factory_reset() + end + end + + compile :change_ownership, %{}, _body do + quote do + # Add code here + end + end + + compile :dump_info do + quote do + dump_info() + end + end + + compile :toggle_pin, %{pin_number: pin_number} do + quote do + # mode 0 = digital + case read_pin(unquote(compile_ast(pin_number)), 0) do + 0 -> write_pin(unquote(compile_ast(pin_number)), 0, 1) + _ -> write_pin(unquote(compile_ast(pin_number)), 0, 0) + end + end + end + + # not actually used + compile :channel, %{channel_name: _channel_name} do + quote do + nothing() + end + end + + # not actually used + compile :explanation, %{message: _message} do + quote do + nothing() + end + end + + # not actually used + compile :rpc_ok, %{label: _label} do + quote do + nothing() + end + end + + # not actually used + compile :rpc_error, %{label: _label}, _body do + quote do + nothing() + # Add code here + end + end + + # not actually used + compile :pair, %{label: _label, value: _value} do + quote do + nothing() + end + end + + # not actually used + compile :scope_declaration, _args, _body do + quote do + nothing() + end + end @doc """ Recursively compiles a list or single Celery AST into an Elixir `__block__` @@ -430,20 +668,32 @@ defmodule Farmbot.CeleryScript.Compiler do [variable_in_next_scope: variable_in_this_scope] """ - def compile_param_application(list, acc \\ []) + def compile_params_to_function_args(list, acc \\ []) - def compile_param_application( - [%{args: %{label: next_scope, data_value: %{args: %{label: current_scope}}}} | rest], + def compile_params_to_function_args( + [%{kind: :variable_declaration, args: args} | rest], acc ) do - var = {String.to_atom(next_scope), {String.to_atom(current_scope), [], __MODULE__}} - compile_param_application(rest, [var | acc]) + %{ + label: next_scope, + data_value: data_value + } = args + + next_scope = IdentifierSanitizer.to_variable(next_scope) + + var = + quote do + # {next_scope, current_scope} + unquote({next_scope, compile_ast(data_value)}) + end + + compile_params_to_function_args(rest, [var | acc]) end - def compile_param_application([], acc), do: acc + def compile_params_to_function_args([], acc), do: acc @doc """ - Compiles a function blocks params. + Compiles a function block's params. # Example A `sequence`s `locals` that look like @@ -455,7 +705,13 @@ defmodule Farmbot.CeleryScript.Compiler do "kind": "parameter_declaration", "args": { "label": "parent", - "data_type": "point" + "default_value": { + "kind": "coordinate", + "args": { + "x": 100.0, + "y": 200.0, + "z": 300.0 + } } } ] @@ -463,26 +719,67 @@ defmodule Farmbot.CeleryScript.Compiler do Would be compiled to - parent = Keyword.fetch!(params, :parent) + parent = Keyword.get(params, :parent, %{x: 100, y: 200, z: 300}) """ - def compile_param_declaration(%{args: %{label: var_name, data_type: _type}}) do - var_fetch = - quote do - Keyword.fetch!(params, unquote(String.to_atom(var_name))) - end + # Add parameter_declaration to the list of implemented kinds + @kinds "parameter_declaration" + def compile_param_declaration(%{args: %{label: var_name, default_value: default}}) do + var_name = IdentifierSanitizer.to_variable(var_name) - {:=, [], [{String.to_atom(var_name), [], __MODULE__}, var_fetch]} + quote do + unquote({var_name, [], __MODULE__}) = + Keyword.get(params, unquote(var_name), unquote(compile_ast(default))) + end + end + + @doc """ + Compiles a function block's assigned value. + + # Example + A `sequence`s `locals` that look like + { + "kind": "scope_declaration", + "args": {}, + "body": [ + { + "kind": "parameter_application", + "args": { + "label": "parent", + "data_value": { + "kind": "coordinate", + "args": { + "x": 100.0, + "y": 200.0, + "z": 300.0 + } + } + } + } + ] + } + """ + # Add parameter_application to the list of implemented kinds + @kinds "parameter_application" + def compile_param_application(%{args: %{label: var_name, data_value: value}}) do + var_name = IdentifierSanitizer.to_variable(var_name) + + quote do + unquote({var_name, [], __MODULE__}) = unquote(compile_ast(value)) + end end defp decompose_block_to_steps({:__block__, _, steps} = _orig) do Enum.map(steps, fn step -> - IO.inspect(step, label: "STEP") - quote do - fn -> - unquote(step) - end + fn -> unquote(step) end end end) end + + defp delete_me(compiled) do + compiled + |> Macro.to_string() + |> Code.format_string!() + |> IO.puts() + end end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/compiler/identifier_sanitizer.ex b/farmbot_celery_script/lib/farmbot_celery_script/compiler/identifier_sanitizer.ex new file mode 100644 index 00000000..8a9c6ff9 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/compiler/identifier_sanitizer.ex @@ -0,0 +1,19 @@ +defmodule Farmbot.CeleryScript.Compiler.IdentifierSanitizer do + @moduledoc """ + Responsible for ensuring variable names in Sequences are clean. + """ + + @token "unsafe_" + + @doc """ + Takes an unsafe string, and returns a safe variable name. + """ + def to_variable(string) when is_binary(string) do + String.to_atom(@token <> Base.url_encode64(string, padding: false)) + end + + @doc "Takes an encoded safe variable name and returns the original unsafe string." + def to_string(<<@token <> encoded>>) do + Base.url_decode64!(encoded, padding: false) + end +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/compiler/tools.ex b/farmbot_celery_script/lib/farmbot_celery_script/compiler/tools.ex index c419b8ca..da96e481 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/compiler/tools.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/compiler/tools.ex @@ -1,5 +1,6 @@ defmodule Farmbot.CeleryScript.Compiler.Tools do @moduledoc false + # This is an internal DSL tool. Please don't use it for anything else. alias Farmbot.CeleryScript.{AST, Compiler, Corpus} @@ -9,6 +10,12 @@ defmodule Farmbot.CeleryScript.Compiler.Tools do import Compiler.Tools @after_compile Compiler.Tools Module.register_attribute(__MODULE__, :kinds, accumulate: true) + + @doc "Takes CeleryScript AST and returns Elixir AST" + def compile_ast(celery_script_ast) + # Numbers and strings are treated as literals. + def compile_ast(lit) when is_number(lit), do: lit + def compile_ast(lit) when is_binary(lit), do: lit end end @@ -18,15 +25,44 @@ defmodule Farmbot.CeleryScript.Compiler.Tools do not_implemented = Corpus.all_node_names() -- kinds for kind <- not_implemented do + spec = Corpus.node(kind) + + args = + for %{name: name} <- spec.allowed_args do + "#{name}: #{name}" + end + + body = if spec.allowed_body_types == [], do: nil, else: ", _body" + + boilerplate = """ + compile :#{kind}, %{#{Enum.join(args, ", ")}}#{body} do + quote do + # Add code here + end + end + """ + IO.warn( """ - CeleryScript Node not yet implemented: #{inspect(Corpus.node(kind))} + CeleryScript Node not yet implemented: #{inspect(spec)} + Boilerplate: + #{boilerplate} """, [] ) end end + @doc false + defmacro compile(kind, do: block) when is_atom(kind) do + quote do + @kinds unquote(to_string(kind)) + def compile_ast(%AST{kind: unquote(kind)}) do + unquote(block) + end + end + end + @doc false defmacro compile(kind, args_pattern, do: block) when is_atom(kind) do quote do diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex deleted file mode 100644 index b3ca130c..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex +++ /dev/null @@ -1,386 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime do - @moduledoc """ - Manages many FarmProcs - """ - alias Farmbot.CeleryScript.RunTime - use GenServer - use Bitwise, only: [bsl: 2] - alias RunTime.{FarmProc, ProcStorage} - import Farmbot.CeleryScript.Utils - alias Farmbot.CeleryScript.AST - alias AST.Heap - require Logger - - # Frequency of vm ticks. - @tick_timeout 20 - - @kinds_that_need_fw [ - :config_update, - :_if, - :write_pin, - :read_pin, - :move_absolute, - :set_servo_angle, - :move_relative, - :home, - :find_home, - :toggle_pin, - :zero, - :calibrate - ] - - @kinds_allowed_while_locked [ - :rpc_request, - :sequence, - :check_updates, - :config_update, - :uninstall_farmware, - :update_farmware, - :rpc_request, - :rpc_ok, - :rpc_error, - :install, - :read_status, - :sync, - :power_off, - :reboot, - :factory_reset, - :set_user_env, - :install_first_party_farmware, - :change_ownership, - :dump_info, - :_if, - :send_message, - :sequence, - :wait, - :execute, - :execute_script, - :emergency_lock, - :emergency_unlock - ] - - defstruct [ - # Agent wrapper for round robin Circular List struct - :proc_storage, - :hyper_state, - # Reference to the FarmProc that is using the firmware - :fw_proc, - # Pid/Impl of IO bound asts - :process_io_layer, - # Pid/Impl of hyper io bound asts - :hyper_io_layer, - # Clock - :tick_timer, - # map of job_id => GenServer.from - :callers - ] - - @opaque job_id :: CircularList.index() - - @doc "Execute an rpc_request, this is sync." - def rpc_request(pid \\ __MODULE__, %{} = map, fun) - when is_function(fun) do - %AST{} = ast = AST.decode(map) - label = ast.args[:label] || raise(ArgumentError) - - case queue(pid, map, -1) do - {:error, :busy} -> - rpc_request(pid, map, fun) - - nil -> - # if no job is returned, this was a hyper function, which - # can never fail. - results = ast(:rpc_ok, %{label: label}, []) - apply_callback(fun, [results]) - - job -> - proc = await(pid, job) - - case FarmProc.get_status(proc) do - :done -> - results = ast(:rpc_ok, %{label: label}, []) - apply_callback(fun, [results]) - - :crashed -> - message = FarmProc.get_crash_reason(proc) - explanation = ast(:explanation, %{message: message}) - results = ast(:rpc_error, %{label: label}, [explanation]) - apply_callback(fun, [results]) - end - end - end - - @doc "Execute a sequence. This is async." - def sequence(pid \\ __MODULE__, %{} = map, id, fun) when is_function(fun) do - case queue(pid, map, id) do - {:error, :busy} -> - sequence(pid, map, id, fun) - - job -> - spawn_link(fn -> - proc = await(pid, job) - - case FarmProc.get_status(proc) do - :done -> - apply_callback(fun, [:ok]) - - :crashed -> - apply_callback(fun, [{:error, FarmProc.get_crash_reason(proc)}]) - end - end) - end - end - - # Queues some data for execution. - # If kind == :emergency_lock or :emergency_unlock - # (or this is an rpc request with the first item being one of those.) - # this ast will immediately execute the `hyper_io_layer` function. - @spec queue(GenServer.server(), map, integer) :: job_id | nil - defp queue(pid, %{} = map, page_id) when is_integer(page_id) do - case AST.decode(map) do - %AST{kind: :rpc_request, body: [%AST{kind: :emergency_lock}]} -> - :emergency_lock = GenServer.call(pid, :emergency_lock) - nil - - %AST{kind: :rpc_request, body: [%AST{kind: :emergency_unlock}]} -> - :emergency_unlock = GenServer.call(pid, :emergency_unlock) - nil - - # An rpc with an empty list doesn't need to be queued. - %AST{kind: :rpc_request, body: []} -> - nil - - %AST{} = ast -> - %Heap{} = heap = AST.slice(ast) - %Address{} = page = addr(page_id) - - case GenServer.call(pid, {:queue, heap, page}) do - {:error, :busy} -> queue(pid, map, page_id) - job -> job - end - end - end - - # TODO(Connor) - Make this subscribe to a currently unimplemented GC - # Polls the GenServer until it returns a FarmProc with a stopped status - @spec await(GenServer.server(), job_id) :: FarmProc.t() - defp await(pid, job_id, timeout \\ :infinity) do - case GenServer.call(pid, {:await, job_id}, timeout) do - {:error, :busy} -> await(pid, job_id, timeout) - result -> result - end - end - - @doc """ - Start a CSVM monitor. - - ## Required params: - * `process_io_layer` -> - function that takes an AST whenever a FarmProc needs IO operations. - * `hyper_io_layer` - function that takes one of the hyper calls - """ - @spec start_link(Keyword.t(), GenServer.name()) :: GenServer.server() - def start_link(args, name \\ __MODULE__) do - GenServer.start_link(__MODULE__, Keyword.put(args, :name, name), name: name) - end - - def init(args) do - tick_timer = start_tick(self()) - storage = ProcStorage.new(Keyword.fetch!(args, :name)) - io_fun = Keyword.fetch!(args, :process_io_layer) - hyper_fun = Keyword.fetch!(args, :hyper_io_layer) - unless is_function(io_fun), do: raise(ArgumentError) - unless is_function(hyper_fun), do: raise(ArgumentError) - - {:ok, - %RunTime{ - callers: %{}, - process_io_layer: io_fun, - hyper_io_layer: hyper_fun, - tick_timer: tick_timer, - proc_storage: storage - }} - end - - def handle_call(:emergency_lock, _from, %RunTime{} = state) do - apply_callback(state.hyper_io_layer, [:emergency_lock]) - {:reply, :emergency_lock, %{state | hyper_state: :emergency_lock}} - end - - def handle_call(:emergency_unlock, _from, %RunTime{} = state) do - apply_callback(state.hyper_io_layer, [:emergency_unlock]) - {:reply, :emergency_unlock, %{state | hyper_state: nil}} - end - - def handle_call(_, _from, {:busy, state}) do - {:reply, {:error, :busy}, {:busy, state}} - end - - def handle_call({:queue, %Heap{} = h, %Address{} = p}, _from, %RunTime{} = state) do - %FarmProc{} = new_proc = FarmProc.new(state.process_io_layer, p, h) - index = ProcStorage.insert(state.proc_storage, new_proc) - {:reply, index, %{state | callers: Map.put(state.callers, index, [])}} - end - - def handle_call({:await, id}, from, %RunTime{} = state) do - case state.callers[id] do - old when is_list(old) -> - {:noreply, %{state | callers: Map.put(state.callers, id, [from | old])}} - - nil -> - {:reply, {:error, "no job by that id"}, state} - end - end - - def handle_info(:tick, %RunTime{} = state) do - pid = self() - # Calls `do_tick/3` with either - # * a FarmProc that needs updating - # * a :noop atom - # state is set to {:busy, old_state} - # until `do_step` calls - # send(pid, %RunTime{}) - ProcStorage.update(state.proc_storage, &do_step(&1, pid, state)) - {:noreply, {:busy, state}} - end - - # make sure to update the timer _AFTER_ we tick. - # This message comes from the do_step/3 function that gets called - # When updating a FarmProc. - def handle_info(%RunTime{} = state, {:busy, _old}) do - # Enumerate over every caller and - # If the proc is stopped - # reply to the caller - state = - state.proc_storage - |> ProcStorage.all() - |> Enum.reduce(state, fn {id, proc}, state -> - case proc do - %FarmProc{status: status} = proc when status in [:done, :crashed] -> - ProcStorage.delete(state.proc_storage, id) - for caller <- state.callers[id] || [], do: GenServer.reply(caller, proc) - - if proc.ref == state.fw_proc do - %{state | callers: Map.delete(state.callers, id), fw_proc: nil} - else - %{state | callers: Map.delete(state.callers, id)} - end - - %FarmProc{} = _proc -> - state - end - end) - - new_tick_timer = start_tick(self()) - {:noreply, %RunTime{state | tick_timer: new_tick_timer}} - end - - defp start_tick(pid, timeout \\ @tick_timeout), - do: Process.send_after(pid, :tick, timeout) - - @doc false - # If there are no procs - def do_step(:noop, pid, state), do: send(pid, state) - - # If the proc is crashed or done, don't step. - def do_step(%FarmProc{status: :crashed} = farm_proc, pid, state) do - send(pid, state) - farm_proc - end - - def do_step(%FarmProc{status: :done} = farm_proc, pid, state) do - send(pid, state) - farm_proc - end - - # If nothing currently owns the firmware, - # Check kind needs fw, - # Check kind is aloud while the bot is locked, - # Check if bot is unlocked - # If kind needs fw, update state. - def do_step(%FarmProc{} = farm_proc, pid, %{fw_proc: nil} = state) do - pc_ptr = FarmProc.get_pc_ptr(farm_proc) - kind = FarmProc.get_kind(farm_proc, pc_ptr) - b0 = (kind in @kinds_allowed_while_locked) |> bit() - b1 = (kind in @kinds_that_need_fw) |> bit() - b2 = true |> bit() - b3 = (state.hyper_state == :emergency_lock) |> bit() - bits = bsl(b0, 3) + bsl(b1, 2) + bsl(b2, 1) + b3 - - if should_step(bits) do - # Update state if this kind needs fw. - if bool(b1), - do: send(pid, %{state | fw_proc: farm_proc.ref}), - else: send(pid, state) - - actual_step(farm_proc) - else - send(pid, state) - farm_proc - end - end - - def do_step(%FarmProc{} = farm_proc, pid, state) do - pc_ptr = FarmProc.get_pc_ptr(farm_proc) - kind = FarmProc.get_kind(farm_proc, pc_ptr) - b0 = (kind in @kinds_allowed_while_locked) |> bit() - b1 = (kind in @kinds_that_need_fw) |> bit() - b2 = (farm_proc.ref == state.fw_proc) |> bit() - b3 = (state.hyper_state == :emergency_lock) |> bit() - bits = bsl(b0, 3) + bsl(b1, 2) + bsl(b2, 1) + b3 - send(pid, state) - - if should_step(bits), - do: actual_step(farm_proc), - else: farm_proc - end - - defp should_step(0b0000), do: true - defp should_step(0b0001), do: false - defp should_step(0b0010), do: true - defp should_step(0b0011), do: false - defp should_step(0b0100), do: false - defp should_step(0b0101), do: false - defp should_step(0b0110), do: true - defp should_step(0b0111), do: false - defp should_step(0b1000), do: true - defp should_step(0b1001), do: true - defp should_step(0b1010), do: true - defp should_step(0b1011), do: true - defp should_step(0b1100), do: false - defp should_step(0b1101), do: false - defp should_step(0b1110), do: true - defp should_step(0b1111), do: true - - defp bit(true), do: 1 - defp bit(false), do: 0 - defp bool(1), do: true - defp bool(0), do: false - - @spec actual_step(FarmProc.t()) :: FarmProc.t() - defp actual_step(farm_proc) do - try do - FarmProc.step(farm_proc) - rescue - ex in FarmProc.Error -> - ex.farm_proc - - ex -> - IO.warn("Exception in step: #{ex}", __STACKTRACE__) - - farm_proc - |> FarmProc.set_status(:crashed) - |> FarmProc.set_crash_reason(Exception.message(ex)) - end - end - - defp apply_callback(fun, results) when is_function(fun) do - try do - _ = apply(fun, results) - rescue - ex -> - Logger.error("Error executing farmbot_celery_script callback: #{Exception.message(ex)}") - end - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex deleted file mode 100644 index 8f92b4b6..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.Error do - @moduledoc """ - CSVM runtime error - """ - - defexception [:message, :farm_proc] -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex deleted file mode 100644 index 6a1a73ec..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex +++ /dev/null @@ -1,273 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.FarmProc do - @moduledoc """ - FarmProc is a _single_ running unit of execution. It must be - `step`ed. It has access IO, but does no IO management. - """ - alias Farmbot.CeleryScript.RunTime.{ - AST, - FarmProc, - SysCallHandler, - InstructionSet - } - - import Farmbot.CeleryScript.Utils - alias Farmbot.CeleryScript.AST - alias AST.Heap - - @max_reduction_count 1000 - - defstruct sys_call_fun: nil, - zero_page: nil, - reduction_count: 0, - pc: nil, - rs: [], - io_latch: nil, - io_result: nil, - crash_reason: nil, - status: :ok, - heap: %{}, - ref: nil - - @typedoc "Program counter" - @type heap_address :: Address.t() - - @typedoc "Page address register" - @type page :: Address.t() - - @typedoc "Possible values of the status attribute." - @type status_enum :: :ok | :done | :crashed | :waiting - - @type t :: %FarmProc{ - ref: reference(), - crash_reason: nil | String.t(), - heap: %{Address.t() => Heap.t()}, - io_latch: nil | pid, - io_result: nil | any, - pc: Pointer.t(), - reduction_count: 0 | pos_integer(), - rs: [Pointer.t()], - status: status_enum(), - sys_call_fun: Farmbot.CeleryScript.RunTime.SysCallHandler.sys_call_fun(), - zero_page: Address.t() - } - - @typedoc false - @type new :: %Farmbot.CeleryScript.RunTime.FarmProc{ - ref: reference(), - crash_reason: nil, - heap: %{Address.t() => Heap.t()}, - io_latch: nil, - io_result: nil, - pc: Pointer.t(), - reduction_count: 0, - rs: [], - status: :ok, - sys_call_fun: Farmbot.CeleryScript.RunTime.SysCallHandler.sys_call_fun(), - zero_page: Address.t() - } - - @spec new(Farmbot.CeleryScript.RunTime.SysCallHandler.sys_call_fun(), page, Heap.t()) :: new() - def new(sys_call_fun, %Address{} = page, %Heap{} = heap) - when is_function(sys_call_fun) do - pc = Pointer.new(page, addr(1)) - - %FarmProc{ - ref: make_ref(), - status: :ok, - zero_page: page, - pc: pc, - sys_call_fun: sys_call_fun, - heap: %{page => heap} - } - end - - @spec new_page(FarmProc.t(), page, Heap.t()) :: FarmProc.t() - def new_page( - %FarmProc{} = farm_proc, - %Address{} = page_num, - %Heap{} = heap_contents - ) do - new_heap = Map.put(farm_proc.heap, page_num, heap_contents) - %FarmProc{farm_proc | heap: new_heap} - end - - @spec get_zero_page(FarmProc.t()) :: page - def get_zero_page(%FarmProc{} = farm_proc), - do: farm_proc.zero_page - - @spec has_page?(FarmProc.t(), page) :: boolean() - def has_page?(%FarmProc{} = farm_proc, %Address{} = page), - do: Map.has_key?(farm_proc.heap, page) - - @spec step(FarmProc.t()) :: FarmProc.t() | no_return - def step(%FarmProc{status: :crashed} = farm_proc), - do: exception(farm_proc, "Tried to step with crashed process!") - - def step(%FarmProc{status: :done} = farm_proc), do: farm_proc - - def step(%FarmProc{reduction_count: c} = proc) when c >= @max_reduction_count, - do: exception(proc, "Too many reductions!") - - def step(%FarmProc{status: :waiting} = farm_proc) do - case SysCallHandler.get_status(farm_proc.io_latch) do - :ok -> - farm_proc - - :complete -> - io_result = SysCallHandler.get_results(farm_proc.io_latch) - - set_status(farm_proc, :ok) - |> set_io_latch_result(io_result) - |> remove_io_latch() - |> step() - end - end - - def step(%FarmProc{} = farm_proc) do - pc_ptr = get_pc_ptr(farm_proc) - kind = get_kind(farm_proc, pc_ptr) - # TODO Connor 07-31-2018: why do i have to load the module here? - available? = - Code.ensure_loaded?(InstructionSet) and function_exported?(InstructionSet, kind, 1) - - unless available? do - exception(farm_proc, "No implementation for: #{kind}") - end - - farm_proc = %FarmProc{ - farm_proc - | reduction_count: farm_proc.reduction_count + 1 - } - - apply(InstructionSet, kind, [farm_proc]) - end - - @spec get_pc_ptr(FarmProc.t()) :: Pointer.t() - def get_pc_ptr(%FarmProc{pc: pc}), do: pc - - @spec set_pc_ptr(FarmProc.t(), Pointer.t()) :: FarmProc.t() - def set_pc_ptr(%FarmProc{} = farm_proc, %Pointer{} = pc), - do: %FarmProc{farm_proc | pc: pc} - - def set_io_latch(%FarmProc{} = farm_proc, pid) when is_pid(pid), - do: %FarmProc{farm_proc | io_latch: pid} - - def set_io_latch_result(%FarmProc{} = farm_proc, result), - do: %FarmProc{farm_proc | io_result: result} - - @spec clear_io_result(FarmProc.t()) :: FarmProc.t() - def clear_io_result(%FarmProc{} = farm_proc), - do: %FarmProc{farm_proc | io_result: nil} - - @spec remove_io_latch(FarmProc.t()) :: FarmProc.t() - def remove_io_latch(%FarmProc{} = farm_proc), - do: %FarmProc{farm_proc | io_latch: nil} - - @spec get_heap_by_page_index(FarmProc.t(), page) :: Heap.t() | no_return - def get_heap_by_page_index(%FarmProc{heap: heap} = proc, %Address{} = page) do - heap[page] || exception(proc, "no page: #{inspect(page)}") - end - - @spec get_return_stack(FarmProc.t()) :: [Pointer.t()] - def get_return_stack(%FarmProc{rs: rs}), do: rs - - @spec get_kind(FarmProc.t(), Pointer.t()) :: atom - def get_kind(%FarmProc{} = farm_proc, %Pointer{} = ptr) do - get_cell_attr(farm_proc, ptr, Heap.kind()) - end - - @spec get_parent(FarmProc.t(), Pointer.t()) :: Address.t() - def get_parent(%FarmProc{} = farm_proc, %Pointer{} = ptr) do - get_cell_attr(farm_proc, ptr, Heap.parent()) - end - - @spec get_status(FarmProc.t()) :: status_enum() - def get_status(%FarmProc{status: status}), do: status - - @spec set_status(FarmProc.t(), status_enum()) :: FarmProc.t() - def set_status(%FarmProc{} = farm_proc, status) do - %FarmProc{farm_proc | status: status} - end - - @spec get_body_address(FarmProc.t(), Pointer.t()) :: Pointer.t() - def get_body_address( - %FarmProc{} = farm_proc, - %Pointer{} = here_address - ) do - get_cell_attr_as_pointer(farm_proc, here_address, Heap.body()) - end - - @spec get_next_address(FarmProc.t(), Pointer.t()) :: Pointer.t() - def get_next_address( - %FarmProc{} = farm_proc, - %Pointer{} = here_address - ) do - get_cell_attr_as_pointer(farm_proc, here_address, Heap.next()) - end - - @spec get_cell_attr(FarmProc.t(), Pointer.t(), atom) :: - Address.t() | String.t() | number() | boolean() | atom() - def get_cell_attr( - %FarmProc{} = farm_proc, - %Pointer{} = location, - field - ) do - cell = get_cell_by_address(farm_proc, location) - - cell[field] || exception(farm_proc, "no field called: #{field} at #{inspect(location)}") - end - - @spec get_cell_attr_as_pointer(FarmProc.t(), Pointer.t(), atom) :: Pointer.t() - def get_cell_attr_as_pointer( - %FarmProc{} = farm_proc, - %Pointer{} = location, - field - ) do - %Address{} = data = get_cell_attr(farm_proc, location, field) - Pointer.new(location.page_address, data) - end - - @spec push_rs(FarmProc.t(), Pointer.t()) :: FarmProc.t() - def push_rs(%FarmProc{} = farm_proc, %Pointer{} = ptr) do - new_rs = [ptr | FarmProc.get_return_stack(farm_proc)] - %FarmProc{farm_proc | rs: new_rs} - end - - @spec pop_rs(FarmProc.t()) :: {Pointer.t(), FarmProc.t()} - def pop_rs(%FarmProc{rs: rs} = farm_proc) do - case rs do - [hd | new_rs] -> - {hd, %FarmProc{farm_proc | rs: new_rs}} - - [] -> - {Pointer.null(FarmProc.get_zero_page(farm_proc)), farm_proc} - end - end - - @spec get_crash_reason(FarmProc.t()) :: String.t() | nil - def get_crash_reason(%FarmProc{} = crashed), - do: crashed.crash_reason - - @spec set_crash_reason(FarmProc.t(), String.t()) :: FarmProc.t() - def set_crash_reason(%FarmProc{} = crashed, reason) - when is_binary(reason) do - %FarmProc{crashed | crash_reason: reason} - end - - @spec is_null_address?(Address.t() | Pointer.t()) :: boolean() - def is_null_address?(%Address{value: 0}), do: true - def is_null_address?(%Address{}), do: false - - def is_null_address?(%Pointer{heap_address: %Address{value: 0}}), - do: true - - def is_null_address?(%Pointer{}), do: false - - @spec get_cell_by_address(FarmProc.t(), Pointer.t()) :: map | no_return - def get_cell_by_address( - %FarmProc{} = farm_proc, - %Pointer{page_address: page, heap_address: %Address{} = ha} - ) do - get_heap_by_page_index(farm_proc, page)[ha] || exception(farm_proc, "bad address") - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex deleted file mode 100644 index cc0df139..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex +++ /dev/null @@ -1,7 +0,0 @@ -defimpl Inspect, for: Farmbot.CeleryScript.RunTime.FarmProc do - def inspect(data, _opts) do - "#FarmProc<[#{Farmbot.CeleryScript.RunTime.FarmProc.get_status(data)}] #{ - inspect(Farmbot.CeleryScript.RunTime.FarmProc.get_pc_ptr(data)) - }>" - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex deleted file mode 100644 index fcd61544..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex +++ /dev/null @@ -1,45 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.Instruction do - @moduledoc """ - Macros for quickly defining executionally similar instructions. - """ - - alias Farmbot.CeleryScript.RunTime.{FarmProc, SysCallHandler} - alias Farmbot.CeleryScript.AST - import SysCallHandler, only: [apply_sys_call_fun: 2] - - @doc """ - A simple IO based instruction that doesn't do any variable resolution or - special transformation before passing to the SysCallHandler. - """ - defmacro simple_io_instruction(instruction_name) do - quote do - @spec unquote(instruction_name)(FarmProc.t()) :: FarmProc.t() - def unquote(instruction_name)(%FarmProc{} = farm_proc) do - case farm_proc.io_result do - nil -> - pc = get_pc_ptr(farm_proc) - - heap = get_heap_by_page_index(farm_proc, pc.page_address) - - data = AST.unslice(heap, pc.heap_address) - latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - - :ok -> - farm_proc - |> clear_io_result() - |> next_or_return() - - {:error, reason} -> - crash(farm_proc, reason) - - other -> - exception(farm_proc, "Bad return value: #{inspect(other)}") - end - end - end - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex deleted file mode 100644 index f019a849..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex +++ /dev/null @@ -1,383 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.InstructionSet do - @moduledoc """ - Implementation for each and every executable CeleryScript AST node. - """ - - alias Farmbot.CeleryScript.RunTime.{ - FarmProc, - Instruction, - SysCallHandler, - Resolver - } - - alias Farmbot.CeleryScript.AST - import Farmbot.CeleryScript.Utils - import Instruction, only: [simple_io_instruction: 1] - import SysCallHandler, only: [apply_sys_call_fun: 2] - - import FarmProc, - only: [ - get_pc_ptr: 1, - get_next_address: 2, - get_body_address: 2, - get_cell_attr_as_pointer: 3, - pop_rs: 1, - push_rs: 2, - set_pc_ptr: 2, - set_status: 2, - set_crash_reason: 2, - clear_io_result: 1, - set_io_latch: 2, - get_heap_by_page_index: 2, - new_page: 3, - get_zero_page: 1, - is_null_address?: 1 - ] - - # Command Nodes - @doc "Write a pin." - simple_io_instruction(:write_pin) - - @doc "Read a pin." - simple_io_instruction(:read_pin) - - @doc "Write servo pin value." - simple_io_instruction(:set_servo_angle) - - @doc "Send a message." - simple_io_instruction(:send_message) - - @doc "move relative to the bot's current position." - simple_io_instruction(:move_relative) - - @doc "Move an axis home." - simple_io_instruction(:home) - - @doc "Find an axis home." - simple_io_instruction(:find_home) - - @doc "Wait (block) a number of milliseconds." - simple_io_instruction(:wait) - - @doc "Toggle a pin atomicly." - simple_io_instruction(:toggle_pin) - - @doc "Force axis position to become zero." - simple_io_instruction(:zero) - - @doc "Calibrate an axis." - simple_io_instruction(:calibrate) - - @doc "Execute `take_photo` Farmware if installed." - simple_io_instruction(:take_photo) - - # RPC Nodes - - @doc "Update bot or firmware configuration." - simple_io_instruction(:config_update) - - @doc "Set environment variables for a Farmware." - simple_io_instruction(:set_user_env) - - @doc "Force the bot's state to be dispatched." - simple_io_instruction(:read_status) - - @doc "Sync all resources with the Farmbot Web Application." - simple_io_instruction(:sync) - - @doc "Power the bot down." - simple_io_instruction(:power_off) - - @doc "Reboot the bot." - simple_io_instruction(:reboot) - - @doc "Factory reset the bot allowing for reconfiguration." - simple_io_instruction(:factory_reset) - - @doc "Factory reset the bot, but supply new credentials without reconfiguration." - simple_io_instruction(:change_ownership) - - @doc "Check for OS updates." - simple_io_instruction(:check_updates) - - @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) - heap = get_heap_by_page_index(farm_proc, pc.page_address) - data = AST.unslice(heap, pc.heap_address) - - data = - case data.args.location do - %AST{kind: :identifier} -> - location = Resolver.resolve(farm_proc, pc, data.args.location.args.label) - %{data | args: %{data.args | location: location}} - - _ -> - data - end - - data = - case data.args.offset do - %AST{kind: :identifier} -> - offset = Resolver.resolve(farm_proc, pc, data.args.offset.args.label) - %{data | args: %{data.args | offset: offset}} - - _ -> - data - end - - case farm_proc.io_result do - # If we need to lookup a coordinate, do that. - # We will come back to {:ok, ast} or {:error, reason} - # next iteration. - # Or if we didn't need to lookup a coordinate, just execute `move_absolute` - # and come back to `:ok` or `{:error, reason}` - nil -> - latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - - # Result of coordinate lookup. - # This starts the actual movement. - {:ok, %AST{} = result} -> - args = AST.new(:move_absolute, %{location: result, offset: data.args.offset}, []) - latch = apply_sys_call_fun(farm_proc.sys_call_fun, args) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - - # Result of _actual_ movement. - :ok -> - next_or_return(farm_proc) - - {:error, reason} -> - crash(farm_proc, reason) - - other -> - exception(farm_proc, "Bad return value handling move_absolute IO: #{inspect(other)}") - end - end - - def rpc_request(%FarmProc{} = farm_proc) do - sequence(farm_proc) - end - - @doc "Execute a sequeence." - @spec sequence(FarmProc.t()) :: FarmProc.t() - def sequence(%FarmProc{} = farm_proc) do - pc_ptr = get_pc_ptr(farm_proc) - body_addr = get_body_address(farm_proc, pc_ptr) - - if is_null_address?(body_addr), - do: return(farm_proc), - else: call(farm_proc, body_addr) - end - - @doc "Conditionally execute a sequence." - @spec _if(FarmProc.t()) :: FarmProc.t() - def _if(%FarmProc{io_result: nil} = farm_proc) do - pc = get_pc_ptr(farm_proc) - heap = get_heap_by_page_index(farm_proc, pc.page_address) - data = Farmbot.CeleryScript.AST.Unslicer.run(heap, pc.heap_address) - latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - end - - def _if(%FarmProc{io_result: result} = farm_proc) do - pc = get_pc_ptr(farm_proc) - - case result do - {:ok, true} -> - farm_proc - |> set_pc_ptr(get_cell_attr_as_pointer(farm_proc, pc, :___then)) - |> clear_io_result() - - {:ok, false} -> - farm_proc - |> set_pc_ptr(get_cell_attr_as_pointer(farm_proc, pc, :___else)) - |> clear_io_result() - - :ok -> - exception(farm_proc, "Bad _if implementation.") - - {:error, reason} -> - crash(farm_proc, reason) - end - end - - @doc "Do nothing. Triggers `status` to be set to `done`." - @spec nothing(FarmProc.t()) :: FarmProc.t() - def nothing(%FarmProc{} = farm_proc) do - next_or_return(farm_proc) - end - - @doc "Lookup and execute another sequence." - @spec execute(FarmProc.t()) :: FarmProc.t() - def execute(%FarmProc{io_result: nil} = farm_proc) do - pc = get_pc_ptr(farm_proc) - heap = get_heap_by_page_index(farm_proc, pc.page_address) - sequence_id = FarmProc.get_cell_attr(farm_proc, pc, :sequence_id) - next_ptr = get_next_address(farm_proc, pc) - - if FarmProc.has_page?(farm_proc, addr(sequence_id)) do - farm_proc - |> push_rs(next_ptr) - |> set_pc_ptr(ptr(sequence_id, 1)) - else - # Step 0: Unslice current address. - data = AST.unslice(heap, pc.heap_address) - latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - end - end - - def execute(%FarmProc{io_result: result} = farm_proc) do - pc = get_pc_ptr(farm_proc) - sequence_id = FarmProc.get_cell_attr(farm_proc, pc, :sequence_id) - next_ptr = get_next_address(farm_proc, pc) - # Step 1: Get a copy of the sequence. - case result do - {:ok, %AST{} = sequence} -> - # Step 2: Push PC -> RS - # Step 3: Slice it - new_heap = AST.slice(sequence) - seq_addr = addr(sequence_id) - seq_ptr = ptr(sequence_id, 1) - - push_rs(farm_proc, next_ptr) - # Step 4: Add the new page. - |> new_page(seq_addr, new_heap) - # Step 5: Set PC to Ptr(1, 1) - |> set_pc_ptr(seq_ptr) - |> clear_io_result() - - {:error, reason} -> - crash(farm_proc, reason) - - _ -> - exception(farm_proc, "Bad execute implementation.") - end - end - - def execute_script(%FarmProc{io_result: nil} = farm_proc) do - pc = get_pc_ptr(farm_proc) - heap = get_heap_by_page_index(farm_proc, pc.page_address) - - # Step 0: Unslice current address. - data = AST.unslice(heap, pc.heap_address) - latch = apply_sys_call_fun(farm_proc.sys_call_fun, data) - - farm_proc - |> set_status(:waiting) - |> set_io_latch(latch) - end - - def execute_script(%FarmProc{io_result: result} = farm_proc) do - pc = get_pc_ptr(farm_proc) - package = FarmProc.get_cell_attr(farm_proc, pc, :package) |> :erlang.crc32() - next_ptr = get_next_address(farm_proc, pc) - # Step 1: Get a copy of the ast. - case result do - {:ok, %AST{} = ast} -> - # Step 2: Push PC -> RS - # Step 3: Slice it - new_heap = AST.slice(ast) - farmware_addr = addr(package) - farmware_ptr = ptr(package, 1) - - push_rs(farm_proc, pc) - # Step 4: Add the new page. - |> new_page(farmware_addr, new_heap) - # Step 5: Set PC to Ptr(1, 1) - |> set_pc_ptr(farmware_ptr) - |> clear_io_result() - - {:error, reason} -> - crash(farm_proc, reason) - - :ok -> - farm_proc - |> clear_io_result() - |> set_pc_ptr(next_ptr) - - _data -> - exception(farm_proc, "Bad execute_script implementation.") - end - end - - ## Private - - @spec call(FarmProc.t(), Pointer.t()) :: FarmProc.t() - defp call(%FarmProc{} = farm_proc, %Pointer{} = address) do - current_pc = get_pc_ptr(farm_proc) - next_ptr = get_next_address(farm_proc, current_pc) - - farm_proc - |> push_rs(next_ptr) - |> set_pc_ptr(address) - end - - @spec return(FarmProc.t()) :: FarmProc.t() - defp return(%FarmProc{} = farm_proc) do - {value, farm_proc} = pop_rs(farm_proc) - - farm_proc - |> set_pc_ptr(value) - |> set_status(:ok) - end - - @spec next(FarmProc.t()) :: FarmProc.t() - defp next(%FarmProc{} = farm_proc) do - current_pc = get_pc_ptr(farm_proc) - next_ptr = get_next_address(farm_proc, current_pc) - - farm_proc - |> set_pc_ptr(next_ptr) - |> set_status(:ok) - end - - @spec next_or_return(FarmProc.t()) :: FarmProc.t() - defp next_or_return(farm_proc) do - pc_ptr = get_pc_ptr(farm_proc) - addr = get_next_address(farm_proc, pc_ptr) - farm_proc = clear_io_result(farm_proc) - - is_null_address? = is_null_address?(addr) - return_stack_is_empty? = farm_proc.rs == [] - - cond do - is_null_address? && return_stack_is_empty? -> set_status(farm_proc, :done) - is_null_address? -> return(farm_proc) - !is_null_address? -> next(farm_proc) - end - end - - @spec crash(FarmProc.t(), String.t()) :: FarmProc.t() - defp crash(farm_proc, reason) do - crash_address = get_pc_ptr(farm_proc) - zero_page_ptr = get_zero_page(farm_proc) |> Pointer.null() - # Push PC -> RS - farm_proc - |> push_rs(crash_address) - # set PC to 0,0 - |> set_pc_ptr(zero_page_ptr) - # Set status to crashed, return the farmproc - |> set_status(:crashed) - |> set_crash_reason(reason) - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex deleted file mode 100644 index dabcbc38..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex +++ /dev/null @@ -1,54 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.ProcStorage do - @moduledoc """ - Process wrapper around CircularList - """ - - alias Farmbot.CeleryScript.RunTime.FarmProc - - @opaque proc_storage :: pid - @opaque index :: pos_integer - - def new(_farmbot_celery_script_id) do - {:ok, agent} = Agent.start_link(&CircularList.new/0) - agent - end - - @spec insert(proc_storage, FarmProc.t()) :: index - def insert(this, %FarmProc{} = farm_proc) do - Agent.get_and_update(this, fn cl -> - new_cl = - cl - |> CircularList.push(farm_proc) - |> CircularList.rotate() - - {CircularList.get_index(new_cl), new_cl} - end) - end - - @spec current_index(proc_storage) :: index - def current_index(this) do - Agent.get(this, &CircularList.get_index(&1)) - end - - @spec lookup(proc_storage, index) :: FarmProc.t() - def lookup(this, index) do - Agent.get(this, &CircularList.at(&1, index)) - end - - @spec delete(proc_storage, index) :: :ok - def delete(this, index) do - Agent.update(this, &CircularList.remove(&1, index)) - end - - @spec update(proc_storage, (FarmProc.t() -> FarmProc.t())) :: :ok - def update(this, fun) when is_function(fun) do - Agent.update(this, &CircularList.update_current(&1, fun)) - Agent.update(this, &CircularList.rotate(&1)) - end - - def all(this) do - Agent.get(this, fn %{items: items} -> - items - end) - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex deleted file mode 100644 index fdaf659d..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex +++ /dev/null @@ -1,80 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.Resolver do - @moduledoc """ - Recursivly climbs a FarmProc stack until a variable is found, or - raises an error. - """ - - alias Farmbot.CeleryScript.RunTime.{FarmProc, Error} - alias Farmbot.CeleryScript.AST - - @nodes_with_declerations [:sequence] - - @spec resolve(FarmProc.t(), Pointer.t(), String.t()) :: AST.t() - def resolve(%FarmProc{} = farm_proc, %Pointer{} = pointer, label) - when is_binary(label) do - # step1 keep climbing (recursivly) __parent until kind in @nodes_with_declerations - # step2 execute rule for resolution per node - # step2.5 if no data, explode - # step3 unslice at address - # step4 profit?? - search_tree(farm_proc, pointer, label) - end - - def search_tree( - %FarmProc{} = farm_proc, - %Pointer{} = pointer, - label - ) - when is_binary(label) do - if FarmProc.is_null_address?(pointer) do - error_opts = [ - farm_proc: farm_proc, - message: "unbound identifier: #{label} from pc: #{inspect(pointer)}" - ] - - raise Error, error_opts - end - - kind = FarmProc.get_kind(farm_proc, pointer) - - if kind in @nodes_with_declerations do - result = do_resolve(kind, farm_proc, pointer, label) - %Address{} = page = pointer.page_address - - %Pointer{} = new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer)) - - if is_nil(result) do - search_tree(farm_proc, new_pointer, label) - else - result - end - else - %Address{} = page = pointer.page_address - - %Pointer{} = new_pointer = Pointer.new(page, FarmProc.get_parent(farm_proc, pointer)) - - search_tree(farm_proc, new_pointer, label) - end - end - - def do_resolve(:sequence, farm_proc, pointer, label) do - locals_ptr = FarmProc.get_cell_attr_as_pointer(farm_proc, pointer, :__locals) - - ast = - AST.unslice( - farm_proc.heap[locals_ptr.page_address], - locals_ptr.heap_address - ) - - Enum.find_value(ast.body, fn %{ - args: %{ - label: sub_label, - data_value: val - } - } -> - if sub_label == label do - val - end - end) - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex deleted file mode 100644 index cd6253e5..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex +++ /dev/null @@ -1,71 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.SysCallHandler do - @moduledoc """ - Simple GenServer that wraps a single function call. - """ - use GenServer - @type ast :: Farmbot.CeleryScript.AST.t() - @type return_value :: :ok | {:ok, any} | {:error, String.t()} - @type sys_call_fun :: (ast -> return_value) - @type sys_call :: pid - - @spec apply_sys_call_fun(sys_call_fun, ast) :: sys_call - def apply_sys_call_fun(fun, ast) do - {:ok, sys_call} = GenServer.start_link(__MODULE__, [fun, ast]) - sys_call - end - - @spec get_status(sys_call) :: :ok | :complete - def get_status(sys_call) do - GenServer.call(sys_call, :get_status) - end - - @spec get_results(sys_call) :: return_value | no_return() - def get_results(sys_call) do - case GenServer.call(sys_call, :get_results) do - nil -> raise("no results") - results -> results - end - end - - def init([fun, ast]) do - pid = spawn_link(__MODULE__, :do_apply, [self(), fun, ast]) - {:ok, %{status: :ok, results: nil, pid: pid}} - end - - def handle_info({_pid, info}, state) do - {:noreply, %{state | results: info, status: :complete}} - end - - def handle_call(:get_status, _from, state) do - {:reply, state.status, state} - end - - def handle_call(:get_results, _from, %{results: nil} = state) do - {:stop, :normal, nil, state} - end - - def handle_call(:get_results, _from, %{results: results} = state) do - {:stop, :normal, results, state} - end - - def do_apply(pid, fun, %Farmbot.CeleryScript.AST{} = ast) - when is_pid(pid) and is_function(fun) do - result = - try do - apply(fun, [ast]) - rescue - ex -> - IO.warn( - """ - error in io function: #{inspect(ex)} - #{inspect(fun)} #{inspect(ast, limit: :infinity)} - """, - __STACKTRACE__ - ) - - {:error, Exception.message(ex)} - end - - send(pid, {self(), result}) - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/runtime_error.ex b/farmbot_celery_script/lib/farmbot_celery_script/runtime_error.ex new file mode 100644 index 00000000..aae77c41 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/runtime_error.ex @@ -0,0 +1,7 @@ +defmodule Farmbot.CeleryScript.RuntimeError do + @moduledoc """ + CeleryScript error raised when a syscall fails. + Examples of this include a movement failed, a resource was unavailable, etc. + """ + defexception [:message] +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/scheduler.ex b/farmbot_celery_script/lib/farmbot_celery_script/scheduler.ex new file mode 100644 index 00000000..4a48a10d --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/scheduler.ex @@ -0,0 +1,149 @@ +defmodule Farmbot.CeleryScript.Scheduler do + @moduledoc """ + Handles execution of CeleryScript. + + CeleryScript can be `execute`d or `schedule`d. Both have the same API but + slightly different behaviour. + + A message will arrive in the callers inbox after either shaped like + + {Farmbot.CeleryScript.Scheduler, result} + + where result will be + + :ok | {:error, "some string error"} + + The Scheduler makes no effort to rescue bad syscall implementations. See + the docs foro SysCalls for more details. + """ + + use GenServer + alias __MODULE__, as: State + alias Farmbot.CeleryScript.{AST, RuntimeError, Compiler} + + defstruct steps: [], + execute: false + + @doc "Start an instance of a CeleryScript Scheduler" + def start_link(args, opts \\ [name: __MODULE__]) do + GenServer.start_link(__MODULE__, args, opts) + end + + @doc """ + Execute CeleryScript right now. Multiple calls will always + execute in the order they were called. This means that if two + processes call `execute/2` at the exact same time, there could be a + race condition. In practice this will not happen, because calls are + executed with a microsecond granularity. + + CeleryScript added via this call will also execute asyncronously to + that loaded by `schedule/2`. This means for example if there is a `schedule`d + node currently executing `move_absolute`, and one chooses to `execute` + `move_absolute` at the same time, the `execute`d call will have somewhat + undefined behaviour depending on the `move_absolute` implementation. + """ + @spec execute(GenServer.server(), AST.t() | [Compiler.compiled()]) :: {:ok, reference()} + def execute(scheduler_pid \\ __MODULE__, celery_script) + + def execute(sch, %AST{} = ast) do + execute(sch, Compiler.compile(ast)) + end + + def execute(sch, compiled) when is_list(compiled) do + GenServer.call(sch, {:execute, compiled}) + end + + @doc """ + Schedule CeleryScript to execute whenever there is time for it. + Calls are executed in a first in first out buffer, with things being added + by `execute/2` taking priority. + """ + @spec schedule(GenServer.server(), AST.t() | [Compiler.compiled()]) :: {:ok, reference()} + def schedule(scheduler_pid \\ __MODULE__, celery_script) + + def schedule(sch, %AST{} = ast) do + schedule(sch, Compiler.compile(ast)) + end + + def schedule(sch, compiled) when is_list(compiled) do + GenServer.call(sch, {:schedule, compiled}) + end + + @impl true + def init(args) do + {:ok, %State{}} + end + + @impl true + def handle_call({:execute, compiled}, {_pid, ref} = from, state) do + # Warning, timestamps may be unstable in offline situations. + # IO.inspect(ref, label: "execeuting") + send(self(), :timeout) + {:reply, {:ok, ref}, %{state | steps: [{from, :os.system_time(), compiled} | state.steps]}} + end + + def handle_call({:schedule, compiled}, {_pid, ref} = from, state) do + # IO.inspect(ref, label: "Scheduling") + send(self(), :timeout) + {:reply, {:ok, ref}, %{state | steps: state.steps ++ [{from, nil, compiled}]}} + end + + @impl true + def handle_info(:timeout, %{steps: steps} = state) when length(steps) >= 1 do + [{{_pid, ref} = from, timestamp, compiled} | rest] = + Enum.sort(steps, fn + {_, first_ts, _}, {_, second_ts, _} when first_ts <= second_ts -> true + {_, _, _}, {_, _, _} -> false + end) + + # IO.inspect(state, label: "timeout") + case state.execute do + true -> + # IO.inspect(ref, label: "already executing") + {:noreply, state} + + false -> + # IO.inspect(ref, label: "starting executing") + + {:noreply, %{state | execute: is_number(timestamp), steps: rest}, + {:continue, {from, compiled}}} + end + end + + def handle_info(:timeout, %{steps: []} = state) do + # IO.inspect(state, label: "timeout no steps") + {:noreply, state} + end + + @impl true + def handle_continue({{pid, ref} = from, [step | rest]}, state) do + case step(state, step) do + [fun | _] = more when is_function(fun, 0) -> + {:noreply, state, {:continue, {from, more ++ rest}}} + + {:error, reason} -> + send(pid, {__MODULE__, ref, {:error, reason}}) + send(self(), :timeout) + {:noreply, state} + + _ -> + {:noreply, state, {:continue, {from, rest}}} + end + end + + def handle_continue({{pid, ref}, []}, state) do + send(pid, {__MODULE__, ref, :ok}) + send(self(), :timeout) + # IO.inspect(ref, label: "complete") + {:noreply, %{state | execute: false}} + end + + def step(_state, fun) when is_function(fun, 0) do + try do + fun.() + rescue + e in RuntimeError -> {:error, Exception.message(e)} + exception -> reraise(exception, __STACKTRACE__) + end + end +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex new file mode 100644 index 00000000..be0d5132 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex @@ -0,0 +1,182 @@ +defmodule Farmbot.CeleryScript.SysCalls do + @moduledoc """ + Behaviour for abstracting CeleryScript functionality. + """ + alias Farmbot.CeleryScript.{AST, RuntimeError} + + @sys_call_implementation Application.get_env(:farmbot_celery_script, __MODULE__)[:sys_calls] + @sys_call_implementation || + Mix.raise(""" + config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, [ + sys_calls: SomeModuleThatImplementsTheBehaviour + ] + """) + + @type error :: {:error, String.t()} + + @type resource_id :: integer() + + @type point_type :: String.t() + @type named_pin_type :: String.t() + + @type axis_position :: float() + @type axis :: String.t() + @type axis_speed :: integer() + @type coordinate :: %{x: axis_position, y: axis_position, z: axis_position} + + @type(pin_number :: {:boxled, 3 | 4}, integer) + @type pin_mode :: 0 | 1 | nil + @type pin_value :: integer + + @type milliseconds :: integer + + @type message_level :: String.t() + @type message_channel :: String.t() + + @callback point(point_type, resource_id) :: coordinate | error + @callback move_absolute(x :: axis_position, y :: axis_position, z :: axis_position, axis_speed) :: + :ok | error + @callback find_home(axis, axis_speed) :: :ok | error + + @callback get_current_x() :: axis_position | error + @callback get_current_y() :: axis_position | error + @callback get_current_z() :: axis_position | error + + @callback write_pin(pin_number, pin_mode, pin_value) :: :ok | error + @callback read_pin(pin_number, pin_mode) :: :ok | error + @callback named_pin(named_pin_type, resource_id) :: pin_number | error + + @callback wait(milliseconds) :: any() + + @callback send_message(message_level, String.t(), [message_channel]) :: :ok | error + + @callback get_sequence(resource_id) :: map() | error + @callback execute_script(String.t(), map()) :: :ok | error + + @callback read_status() :: map() + + @callback set_user_env() :: :ok | error + + @callback sync() :: :ok | error + + def point(module \\ @sys_call_implementation, type, id) do + case module.point(type, id) do + %{x: x, y: y, z: z} -> coordinate(x, y, z) + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def move_absolute(module \\ @sys_call_implementation, x, y, z, speed) do + case module.move_absolute(x, y, z, speed) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def get_current_x(module \\ @sys_call_implementation) do + case module.get_current_x() do + position when is_number(position) -> position + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def get_current_y(module \\ @sys_call_implementation) do + case module.get_current_y() do + position when is_number(position) -> position + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def get_current_z(module \\ @sys_call_implementation) do + case module.get_current_z() do + position when is_number(position) -> position + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def write_pin(module \\ @sys_call_implementation, pin_number, pin_mode, pin_value) do + case module.write_pin(pin_number, pin_mode, pin_value) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def read_pin(module \\ @sys_call_implementation, pin_number, pin_mode) do + case module.read_pin(pin_number, pin_mode) do + value when is_number(value) -> value + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def named_pin(module \\ @sys_call_implementation, type, id) do + case module.named_pin(type, id) do + {:boxled, boxledid} when boxledid in [3, 4] -> {:boxled, boxledid} + number when is_integer(number) -> number + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def wait(module \\ @sys_call_implementation, milliseconds) do + _ = module.wait(milliseconds) + :ok + end + + def send_message(module \\ @sys_call_implementation, level, message, channels) do + case module.send_message(level, message, channels) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def find_home(module \\ @sys_call_implementation, axis, speed) do + case module.find_home(axis, speed) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def get_sequence(module \\ @sys_call_implementation, id) do + case module.get_sequence(id) do + %{kind: _, args: _} = probably_sequence -> + AST.decode(probably_sequence) + + {:error, reason} when is_binary(reason) -> + error(reason) + end + end + + def execute_script(module \\ @sys_call_implementation, name, args) do + case module.execute_script(name, args) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def read_status(module \\ @sys_call_implementation) do + _ = module.read_status + end + + def set_user_env(module \\ @sys_call_implementation, key, val) do + case module.set_user_env(key, val) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def sync(module \\ @sys_call_implementation) do + case module.sync() do + :ok -> :ok + {:error, reason} when is_binary(reason) -> error(reason) + end + end + + def nothing, do: nil + + def coordinate(x, y, z) do + %{x: x, y: y, z: z} + end + + def error(message) when is_binary(message) do + raise RuntimeError, message: message + end +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex new file mode 100644 index 00000000..cb83d270 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/sys_calls/stubs.ex @@ -0,0 +1,15 @@ +defmodule Farmbot.CeleryScript.SysCalls.Stubs do + @moduledoc """ + SysCall implementation that doesn't do anything. Useful for tests. + """ + # @behaviour Farmbot.CeleryScript.SysCalls + + require Logger + + @doc false + def unquote(:"$handle_undefined_function")(function, args) do + args = Enum.map(args, &inspect/1) |> Enum.join(", ") + Logger.error("CeleryScript syscall stubbed: \n\n\t #{function}(#{args})") + {:error, "SysCall stubbed by #{__MODULE__}"} + end +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/syscalls.ex b/farmbot_celery_script/lib/farmbot_celery_script/syscalls.ex deleted file mode 100644 index 6db53a87..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/syscalls.ex +++ /dev/null @@ -1,83 +0,0 @@ -defmodule Farmbot.CeleryScript.Syscalls do - alias Farmbot.CeleryScript.{AST, Compiler} - - def test(params \\ []) do - File.read!("fixture/sequence_pair.json") - |> Jason.decode!() - |> Enum.at(0) - |> AST.decode() - |> Compiler.compile() - |> Code.eval_quoted() - |> elem(0) - # generates the sequence - |> apply([params]) - |> Enum.map(fn step -> apply(step, []) end) - end - - def point(type, id) do - IO.puts("point(#{type}, #{id})") - %{x: 1, y: 122, z: 100} - end - - def coordinate(x, y, z) do - IO.puts("coodinate(#{x}, #{y}, #{z})") - %{x: x, y: y, z: z} - end - - def move_absolute(x, y, z, speed) do - IO.puts("move_absolute(#{x}, #{y}, #{z}, #{speed})") - end - - def get_current_x() do - 100 - end - - def get_current_y() do - 234 - end - - def get_current_z() do - 12 - end - - def write_pin(pin, mode, value) do - IO.puts("write_pin(#{pin}, #{mode}, #{value}") - end - - def pin(type, id) do - IO.puts("pin(#{type}, #{id})") - end - - def read_pin(pin, mode) do - IO.puts("read_pin(#{pin}, #{mode})") - end - - def find_home(axis, speed) do - IO.puts("find_home(#{axis}, #{speed})") - end - - def find_home(axis) do - IO.puts("find_home(#{axis})") - end - - def send_message(level, message, channels) do - IO.puts("send_message(#{level}, #{message}, #{inspect(channels)}") - end - - def nothing do - IO.puts("nothing()") - end - - def get_sequence(id) do - IO.puts("get_sequence(#{id})") - - File.read!("fixture/sequence_pair.json") - |> Jason.decode!() - |> Enum.at(1) - |> AST.decode() - end - - def execute_script(name, env) do - IO.puts("execute_script(#{name}, #{inspect(env)})") - end -end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/utils.ex b/farmbot_celery_script/lib/farmbot_celery_script/utils.ex deleted file mode 100644 index 4ba5e242..00000000 --- a/farmbot_celery_script/lib/farmbot_celery_script/utils.ex +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Farmbot.CeleryScript.Utils do - @moduledoc """ - Common Farmbot.CeleryScript.RunTime utilities - """ - alias Farmbot.CeleryScript.AST - - @doc "Build a new AST." - @spec ast(AST.kind(), AST.args(), AST.body()) :: AST.t() - def ast(kind, args, body \\ []), do: AST.new(kind, args, body) - - @doc "Build a new pointer." - @spec ptr(Address.value(), Address.value()) :: Pointer.t() - def ptr(page, addr), - do: Pointer.new(Address.new(page), Address.new(addr)) - - @doc "Build a new address." - @spec addr(Address.value()) :: Address.t() - def addr(val), do: Address.new(val) - - # @compile {:inline, exception: 2} - @spec exception(Farmbot.CeleryScript.RunTime.FarmProc.t(), String.t()) :: no_return - def exception(farm_proc, message) when is_binary(message) do - raise(Farmbot.CeleryScript.RunTime.Error, farm_proc: farm_proc, message: message) - end -end diff --git a/farmbot_celery_script/lib/pointer.ex b/farmbot_celery_script/lib/pointer.ex deleted file mode 100644 index 1de18f37..00000000 --- a/farmbot_celery_script/lib/pointer.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Pointer do - @moduledoc "Generic pointer that takes two values." - defstruct [:heap_address, :page_address] - - @type t :: %__MODULE__{ - heap_address: Address.t(), - page_address: Address.t() - } - - @type null_pointer :: %__MODULE__{ - heap_address: Address.null(), - page_address: Address.t() - } - - @doc """ - Returns a new Pointer. - """ - @spec new(Address.t(), Address.t()) :: Pointer.t() - def new(%Address{} = page_address, %Address{} = heap_address) do - %Pointer{ - heap_address: heap_address, - page_address: page_address - } - end - - @doc "Returns a null pointer based on a passed in zero page address." - @spec null(Address.t()) :: Pointer.null_pointer() - def null(%Address{} = zero_page) do - %Pointer{ - heap_address: Address.new(0), - page_address: zero_page - } - end - - defimpl Inspect, for: __MODULE__ do - def inspect(%Pointer{heap_address: ha, page_address: pa}, _), - do: "#Pointer<#{pa.value}, #{ha.value}>" - end -end diff --git a/farmbot_celery_script/test/address_test.exs b/farmbot_celery_script/test/address_test.exs deleted file mode 100644 index bc4d8eb5..00000000 --- a/farmbot_celery_script/test/address_test.exs +++ /dev/null @@ -1,17 +0,0 @@ -defmodule AddressTest do - use ExUnit.Case, async: true - - test "inspect gives nice stuff" do - assert inspect(Address.new(100)) == "#Address<100>" - end - - test "increments an address" do - base = Address.new(123) - assert Address.inc(base) == Address.new(124) - end - - test "decrements an address" do - base = Address.new(123) - assert Address.dec(base) == Address.new(122) - end -end diff --git a/farmbot_celery_script/test/circular_list_test.exs b/farmbot_celery_script/test/circular_list_test.exs deleted file mode 100644 index 7fd313b1..00000000 --- a/farmbot_celery_script/test/circular_list_test.exs +++ /dev/null @@ -1,71 +0,0 @@ -defmodule CircularListTest do - use ExUnit.Case - - test "rotates list of integers" do - cl0 = CircularList.new() - cl1 = CircularList.push(cl0, 1) - cl2 = CircularList.push(cl1, 2) - cl3 = CircularList.push(cl2, 3) - - assert CircularList.current(cl3) == 1 - cl3_a = CircularList.rotate(cl3) - - assert CircularList.current(cl3_a) == 2 - - cl3_b = CircularList.rotate(cl3_a) - assert CircularList.current(cl3_b) == 3 - - cl3_c = CircularList.rotate(cl3_b) - assert CircularList.current(cl3_c) == 1 - end - - test "removes an integer" do - cl0 = - CircularList.new() - |> CircularList.push(:a) - |> CircularList.push(:b) - |> CircularList.push(:c) - - index = CircularList.get_index(cl0) - cl1 = CircularList.remove(cl0, index) - assert CircularList.current(cl1) == :b - end - - test "remove doesn't break things if out of bounds" do - cl0 = - CircularList.new() - |> CircularList.push(:a) - |> CircularList.push(:b) - |> CircularList.push(:c) - - index = CircularList.get_index(cl0) - cl1 = CircularList.remove(cl0, index) - assert CircularList.remove(cl1, index) == cl1 - end - - test "update_at" do - cl0 = - CircularList.new() - |> CircularList.push(100) - - cl1 = CircularList.update_current(cl0, fn old -> old + old end) - assert CircularList.current(cl1) == 200 - end - - test "reduces over items" do - cl0 = - CircularList.new() - |> CircularList.push(:a) - |> CircularList.push(:b) - |> CircularList.push(:c) - - cl1 = - CircularList.reduce(cl0, fn {index, value}, acc -> - if value == :b, - do: Map.put(acc, index, :z), - else: Map.put(acc, index, value) - end) - - assert CircularList.current(cl1) == :z - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs deleted file mode 100644 index 4e9a8747..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs +++ /dev/null @@ -1,70 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.HeapTest do - use ExUnit.Case - alias Farmbot.CeleryScript.AST - alias AST.Heap - - test "Heap access with address" do - heap = - Heap.new() - |> Heap.alot("abc") - |> Heap.alot("def") - |> Heap.alot("ghi") - - assert is_null?(heap.entries[Address.new(0)]) - assert match?(%{:__kind => "abc"}, heap.entries[Address.new(1)]) - assert match?(%{:__kind => "def"}, heap.entries[Address.new(2)]) - assert match?(%{:__kind => "ghi"}, heap.entries[Address.new(3)]) - end - - test "puts a key value pair on an existing aloted slot" do - heap = - Heap.new() - |> Heap.alot("abc") - |> Heap.put("key", "value") - - assert match?( - %{:__kind => "abc", key: "value"}, - heap.entries[Address.new(1)] - ) - end - - test "Puts key/value pairs at arbitrary addresses" do - heap = - Heap.new() - |> Heap.alot("abc") - |> Heap.alot("def") - |> Heap.alot("ghi") - - mutated = Heap.put(heap, Address.new(2), "abc_key", "value") - - assert match?( - %{:__kind => "def", abc_key: "value"}, - mutated.entries[Address.new(2)] - ) - end - - test "Can't update on bad a address" do - heap = - Heap.new() - |> Heap.alot("abc") - |> Heap.alot("def") - |> Heap.alot("ghi") - - assert_raise RuntimeError, fn -> - Heap.put(heap, Address.new(200), "abc_key", "value") - end - end - - defp is_null?(%Address{value: 0}), do: true - defp is_null?(%Address{value: _}), do: false - - defp is_null?(%{ - __body: %Address{value: 0}, - __kind: :nothing, - __next: %Address{value: 0}, - __parent: %Address{value: 0} - }), - do: true - - defp is_null?(_), do: false -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs deleted file mode 100644 index a3124670..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs +++ /dev/null @@ -1,217 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.SlicerTest do - use ExUnit.Case - alias Farmbot.CeleryScript.AST - alias AST.{Heap, Slicer} - - @big_real_sequence %AST{ - kind: :sequence, - args: %{ - is_outdated: false, - locals: %{args: %{}, kind: :scope_declaration}, - version: 6 - }, - body: [ - %AST{ - args: %{ - location: %{args: %{x: 1, y: 2, z: 3}, kind: :coordinate}, - offset: %{args: %{x: 0, y: 0, z: 0}, kind: :coordinate}, - speed: 4 - }, - body: [], - kind: :move_absolute - }, - %AST{ - args: %{ - location: %{args: %{tool_id: 1}, kind: :tool}, - offset: %{args: %{x: 0, y: 0, z: 0}, kind: :coordinate}, - speed: 4 - }, - body: [], - kind: :move_absolute - }, - %AST{ - args: %{speed: 4, x: 1, y: 2, z: 3}, - body: [], - kind: :move_relative - }, - %AST{ - args: %{pin_mode: 1, pin_number: 1, pin_value: 128}, - body: [], - kind: :write_pin - }, - %AST{ - args: %{label: "my_pin", pin_mode: 1, pin_number: 1}, - body: [], - kind: :read_pin - }, - %AST{ - args: %{milliseconds: 500}, - body: [], - kind: :wait - }, - %AST{ - args: %{ - message: "Bot at coord {{ x }} {{ y }} {{ z }}.", - message_type: "info" - }, - body: [ - %AST{ - args: %{channel_name: "toast"}, - body: [], - kind: :channel - } - ], - kind: :send_message - }, - %AST{ - args: %{ - _else: %{args: %{}, kind: :nothing}, - _then: %{args: %{sequence_id: 1}, kind: :execute}, - lhs: "x", - op: "is", - rhs: 300 - }, - body: [], - kind: :_if - }, - %AST{ - args: %{sequence_id: 1}, - body: [], - kind: :execute - } - ] - } - - @unrealistic_but_valid_sequence %AST{ - kind: AST.Node.ROOT, - args: %{a: "b"}, - body: [ - %AST{ - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0]", - args: %{c: "d"}, - body: [ - %AST{ - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0][0]", - args: %{e: "f"}, - body: [] - } - ] - }, - %AST{ - args: %{c: "d"}, - body: [ - %AST{ - args: %{g: "H"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1][0]" - }, - %AST{ - args: %{i: "j"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1][1]" - }, - %AST{ - args: %{k: "l"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1][2]" - } - ], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1]" - }, - %AST{ - args: %{c: "d"}, - body: [ - %AST{ - args: %{m: "n"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][0]" - }, - %AST{ - args: %{o: "p"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][1]" - }, - %AST{ - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2]", - args: %{ - q: "1", - z: %AST{ - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][-1]", - args: %{}, - body: [] - } - }, - body: [ - %AST{ - args: %{g: "H"}, - body: [], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2][0]" - } - ] - } - ], - kind: :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2]" - } - ] - } - - @parent Heap.parent() - @kind Heap.kind() - @body Heap.body() - @next Heap.next() - - test "Slices a realistic sequence" do - Slicer.run(@big_real_sequence) - # TODO Actually check this? - end - - test "Slices an unrealistic_but_valid_sequence" do - heap = Slicer.run(@unrealistic_but_valid_sequence) - assert Enum.count(heap.entries) == 14 - assert heap.here == Address.new(13) - - assert heap[addr(1)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT" - - assert heap[heap[addr(1)][@body]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0]" - - assert heap[heap[addr(1)][@next]][@kind] == :nothing - assert heap[heap[addr(1)][@parent]][@kind] == :nothing - - assert heap[addr(2)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0]" - - assert heap[heap[addr(2)][@body]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[0][0]" - - assert heap[heap[addr(2)][@next]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[1]" - - assert heap[heap[addr(2)][@parent]][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT" - - # AST with more ast in the args and asts in the body - assert heap[addr(11)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2]" - - assert heap[heap[addr(11)][@body]][@kind] == - :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2][0]" - - assert heap[heap[addr(11)][@next]][@kind] == :nothing - - assert heap[heap[addr(11)][@parent]][@kind] == - :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][1]" - - assert heap[addr(12)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2][0]" - - assert heap[heap[addr(12)][@body]][@kind] == :nothing - assert heap[heap[addr(12)][@next]][@kind] == :nothing - - assert heap[heap[addr(12)][@parent]][@kind] == - :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2]" - - assert heap[addr(13)][@kind] == :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][-1]" - - assert heap[heap[addr(13)][@body]][@kind] == :nothing - assert heap[heap[addr(13)][@next]][@kind] == :nothing - - assert heap[heap[addr(13)][@parent]][@kind] == - :"Elixir.Farmbot.CeleryScript.AST.Node.ROOT[2][2]" - end - - defp addr(num), do: Address.new(num) -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs deleted file mode 100644 index a074f8d8..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs +++ /dev/null @@ -1,25 +0,0 @@ -defmodule Farmbot.CeleryScript.AST.UnslicerTest do - use ExUnit.Case, async: true - alias Farmbot.CeleryScript.AST.Unslicer - - test "unslices all the things" do - heap = Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap() - ast = Unslicer.run(heap, Address.new(1)) - assert ast.kind == :sequence - - assert ast.args == %{ - locals: %Farmbot.CeleryScript.AST{ - args: %{}, - body: [], - comment: nil, - kind: :scope_declaration - }, - version: 20_180_209 - } - - assert Enum.at(ast.body, 0).kind == :move_absolute - assert Enum.at(ast.body, 1).kind == :move_relative - assert Enum.at(ast.body, 2).kind == :write_pin - refute ast.comment - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs index 48968223..111a459e 100644 --- a/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs +++ b/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs @@ -8,6 +8,14 @@ defmodule Farmbot.CeleryScript.ASTTest do Jason.encode!(@nothing_json) }]}" |> Jason.decode!() + + @nothing_json_with_args "{\"kind\": \"nothing\", \"args\": {\"nothing\": \"hello world\"}, \"body\":[]}" + |> Jason.decode!() + + @nothing_json_with_cs_args "{\"kind\": \"nothing\", \"args\": {\"nothing\": #{ + Jason.encode!(@nothing_json) + }}, \"body\":[]}" + |> Jason.decode!() @bad_json "{\"whoops\": " test "decodes ast from json" do @@ -20,9 +28,29 @@ defmodule Farmbot.CeleryScript.ASTTest do assert match?(%AST{}, res) end + test "decodes ast with sub asts in the args" do + res = AST.decode(@nothing_json_with_cs_args) + assert match?(%AST{}, res) + end + + test "decodes ast with literals in the args" do + res = AST.decode(@nothing_json_with_args) + assert match?(%AST{}, res) + end + + test "decodes already decoded celeryscript" do + %AST{} = ast = AST.decode(@nothing_json) + assert ast == AST.decode(ast) + end + test "won't decode ast from bad json" do assert_raise RuntimeError, fn -> AST.decode(@bad_json) end end + + test "builds a new ast" do + res = AST.new(:nothing, %{nothing: @nothing_json}, [@nothing_json]) + assert match?(%AST{}, res) + end end diff --git a/farmbot_celery_script/test/farmbot_celery_script/compiler_test.exs b/farmbot_celery_script/test/farmbot_celery_script/compiler_test.exs new file mode 100644 index 00000000..4773ce04 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/compiler_test.exs @@ -0,0 +1,78 @@ +defmodule Farmbot.CeleryScript.CompilerTest do + use ExUnit.Case, async: true + alias Farmbot.CeleryScript.{AST, Compiler} + # Only required to compile + alias Farmbot.CeleryScript.SysCalls, warn: false + + test "compiles a sequence with no body" do + sequence = %AST{ + args: %{ + locals: %AST{ + args: %{}, + body: [], + comment: nil, + kind: :scope_declaration + }, + version: 20_180_209 + }, + body: [], + comment: "This is the root", + kind: :sequence + } + + body = Compiler.compile(sequence) + assert body == [] + end + + test "identifier sanitization" do + label = "System.cmd(\"rm\", [\"-rf /*\"])" + value_ast = AST.Factory.new("coordinate", x: 1, y: 1, z: 1) + identifier_ast = AST.Factory.new("identifier", label: label) + + parameter_application_ast = + AST.Factory.new("parameter_application", label: label, data_value: value_ast) + + celery_ast = %AST{ + kind: :sequence, + args: %{ + locals: %{ + kind: :scope_declaration, + args: %{}, + body: [ + parameter_application_ast + ] + } + }, + body: [ + identifier_ast + ] + } + + elixir_ast = Compiler.compile_ast(celery_ast) + + elixir_code = + elixir_ast + |> Macro.to_string() + |> Code.format_string!() + |> IO.iodata_to_binary() + + var_name = Compiler.IdentifierSanitizer.to_variable(label) + + assert elixir_code =~ """ + #{var_name} = coordinate(1, 1, 1) + [fn -> #{var_name} end] + """ + + refute String.contains?(elixir_code, label) + {fun, _} = Code.eval_string(elixir_code, [], __ENV__) + assert is_function(fun, 1) + end + + # defp fixture(filename) do + # filename + # |> Path.expand("fixture") + # |> File.read!() + # |> Jason.decode!() + # |> AST.decode() + # end +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/corpus/arg_test.exs b/farmbot_celery_script/test/farmbot_celery_script/corpus/arg_test.exs new file mode 100644 index 00000000..5cdbadb5 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/corpus/arg_test.exs @@ -0,0 +1,8 @@ +defmodule Farmbot.CeleryScript.Corpus.ArgTest do + use ExUnit.Case, async: true + alias Farmbot.CeleryScript.Corpus + + test "inspect" do + assert "#Arg<_then [execute, nothing]>" = inspect(Corpus.arg("_then")) + end +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/corpus/node_test.exs b/farmbot_celery_script/test/farmbot_celery_script/corpus/node_test.exs new file mode 100644 index 00000000..0c77ad04 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/corpus/node_test.exs @@ -0,0 +1,9 @@ +defmodule Farmbot.CeleryScript.Corpus.NodeTest do + use ExUnit.Case, async: true + alias Farmbot.CeleryScript.Corpus + + test "inspect" do + assert "Sequence(version, locals) [_if, execute, execute_script, find_home, move_absolute, move_relative, read_pin, send_message, take_photo, wait, write_pin, resource_update]" = + inspect(Corpus.sequence()) + end +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/corpus_test.exs b/farmbot_celery_script/test/farmbot_celery_script/corpus_test.exs new file mode 100644 index 00000000..16dbd3ae --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/corpus_test.exs @@ -0,0 +1,28 @@ +defmodule Farmbot.CeleryScript.CorpusTest do + use ExUnit.Case, async: true + alias Farmbot.CeleryScript.Corpus + + test "lists all node names" do + assert "sequence" in Corpus.all_node_names() + assert "move_absolute" in Corpus.all_node_names() + end + + test "gets a node spec by its name" do + assert match?(%{name: "sequence", allowed_args: _}, Corpus.node("sequence")) + assert match?(%{name: "sequence", allowed_args: _}, Corpus.node(:sequence)) + end + + test "gets a node by a defined function" do + assert match?(%{name: "sequence", allowed_args: _}, Corpus.sequence()) + end + + test "list all arg names" do + assert "_else" in Corpus.all_arg_names() + assert "location" in Corpus.all_arg_names() + end + + test "gets a arg spec by it's name" do + assert match?(%{name: "_else"}, Corpus.arg("_else")) + assert match?(%{name: "_else"}, Corpus.arg(:_else)) + end +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs deleted file mode 100644 index 18357779..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs +++ /dev/null @@ -1,591 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.FarmProcTest do - use ExUnit.Case - alias Farmbot.CeleryScript.RunTime.{FarmProc, Error} - alias Farmbot.CeleryScript.AST - import Farmbot.CeleryScript.Utils - - test "inspects farm_proc" do - heap = Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap() - farm_proc = FarmProc.new(fn _ -> :ok end, addr(0), heap) - assert inspect(farm_proc) == "#FarmProc<[ok] #Pointer<0, 1>>" - end - - test "init a new farm_proc" do - fun = fn _ast -> - :ok - end - - heap = Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap() - farm_proc = FarmProc.new(fun, addr(0), heap) - - assert FarmProc.get_pc_ptr(farm_proc) == Pointer.new(addr(0), addr(1)) - - assert FarmProc.get_heap_by_page_index(farm_proc, addr(0)) == heap - assert FarmProc.get_return_stack(farm_proc) == [] - - assert FarmProc.get_kind( - farm_proc, - FarmProc.get_pc_ptr(farm_proc) - ) == :sequence - end - - test "IO functions require 2 steps" do - fun = fn _ast -> :ok end - - heap = - AST.new(:move_relative, %{x: 1, y: 2, z: 3}, []) - |> AST.Slicer.run() - - step0 = FarmProc.new(fun, addr(1), heap) - assert FarmProc.get_status(step0) == :ok - - # Step into the `move_relative` block. - # step1: waiting for IO to complete async. - step1 = FarmProc.step(step0) - assert FarmProc.get_status(step1) == :waiting - - # step2: IO is _probably_ completed by now. Complete the step. - # This is _technically_ a race condition, but it shouldn't fail in this case - step2 = FarmProc.step(step1) - assert FarmProc.get_status(step2) == :done - end - - test "io functions crash the vm" do - fun = fn _ -> {:error, "movement error"} end - - heap = - AST.new(:move_relative, %{x: 100, y: 123, z: 0}, []) - |> Farmbot.CeleryScript.AST.Slicer.run() - - step0 = FarmProc.new(fun, addr(0), heap) - step1 = FarmProc.step(step0) - assert FarmProc.get_pc_ptr(step1).page_address == addr(0) - assert FarmProc.get_status(step1) == :waiting - step2 = FarmProc.step(step1) - assert FarmProc.get_status(step2) == :crashed - - assert FarmProc.get_pc_ptr(step2) == Pointer.null(FarmProc.get_zero_page(step1)) - end - - test "io functions bad return values raise Farmbot.CeleryScript.RunTime.Error exception" do - fun = fn _ -> {:eroror, 100} end - - heap = - AST.new(:move_relative, %{x: 100, y: 123, z: 0}, []) - |> Farmbot.CeleryScript.AST.Slicer.run() - - step0 = FarmProc.new(fun, addr(0), heap) - step1 = FarmProc.step(step0) - assert FarmProc.get_status(step1) == :waiting - assert Process.alive?(step1.io_latch) - - assert_raise Error, - "Bad return value: {:eroror, 100}", - fn -> - assert Process.alive?(step1.io_latch) - FarmProc.step(step1) - end - end - - test "get_body_address" do - farm_proc = - FarmProc.new( - fn _ -> :ok end, - addr(0), - Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap() - ) - - data = - FarmProc.get_body_address( - farm_proc, - Pointer.new(addr(0), addr(1)) - ) - - refute FarmProc.is_null_address?(data) - end - - test "null address" do - farm_proc = - FarmProc.new( - fn _ -> :ok end, - addr(0), - Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap() - ) - - assert FarmProc.is_null_address?(Pointer.null(FarmProc.get_zero_page(farm_proc))) - - assert FarmProc.is_null_address?(Address.null()) - assert FarmProc.is_null_address?(Pointer.new(addr(0), addr(0))) - assert FarmProc.is_null_address?(addr(0)) - assert FarmProc.is_null_address?(Pointer.new(addr(100), addr(0))) - assert FarmProc.is_null_address?(ptr(100, 0)) - refute FarmProc.is_null_address?(ptr(100, 99)) - refute FarmProc.is_null_address?(Pointer.new(addr(100), addr(50))) - refute FarmProc.is_null_address?(addr(99)) - end - - test "performs all the steps" do - this = self() - - fun = fn ast -> - send(this, ast) - :ok - end - - step0 = FarmProc.new(fun, addr(2), Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.heap()) - - assert FarmProc.get_kind(step0, FarmProc.get_pc_ptr(step0)) == :sequence - - %FarmProc{} = step1 = FarmProc.step(step0) - assert Enum.count(FarmProc.get_return_stack(step1)) == 1 - assert FarmProc.get_status(step1) == :ok - - pc_pointer = FarmProc.get_pc_ptr(step1) - actual_kind = FarmProc.get_kind(step1, pc_pointer) - step1_cell = FarmProc.get_cell_by_address(step1, pc_pointer) - assert actual_kind == :move_absolute - assert step1_cell[:speed] == 100 - - # Perform "move_abs" pt1 - %FarmProc{} = step2 = FarmProc.step(step1) - assert FarmProc.get_status(step2) == :waiting - - # Perform "move_abs" pt2 - %FarmProc{} = step3 = FarmProc.step(step2) - assert FarmProc.get_status(step3) == :ok - - # Make sure side effects are called - pc_pointer = FarmProc.get_pc_ptr(step3) - actual_kind = FarmProc.get_kind(step3, pc_pointer) - step3_cell = FarmProc.get_cell_by_address(step3, pc_pointer) - assert actual_kind == :move_relative - assert step3_cell[:x] == 10 - assert step3_cell[:y] == 20 - assert step3_cell[:z] == 30 - assert step3_cell[:speed] == 50 - # Test side effects. - - assert_receive %Farmbot.CeleryScript.AST{ - args: %{ - location: %Farmbot.CeleryScript.AST{ - args: %{pointer_id: 1, pointer_type: "Plant"}, - kind: :point - }, - offset: %Farmbot.CeleryScript.AST{ - args: %{x: 10, y: 20, z: -30}, - kind: :coordinate - }, - speed: 100 - }, - kind: :move_absolute - } - - # Perform "move_rel" pt1 - %FarmProc{} = step4 = FarmProc.step(step3) - assert FarmProc.get_status(step4) == :waiting - - # Perform "move_rel" pt2 - %FarmProc{} = step5 = FarmProc.step(step4) - assert FarmProc.get_status(step5) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :move_relative, - comment: nil, - args: %{ - x: 10, - y: 20, - z: 30, - speed: 50 - } - } - - # Perform "write_pin" pt1 - %FarmProc{} = step6 = FarmProc.step(step5) - assert FarmProc.get_status(step6) == :waiting - - # Perform "write_pin" pt2 - %FarmProc{} = step7 = FarmProc.step(step6) - assert FarmProc.get_status(step7) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :write_pin, - args: %{ - pin_number: 0, - pin_value: 0, - pin_mode: 0 - } - } - - # Perform "write_pin" pt1 - %FarmProc{} = step8 = FarmProc.step(step7) - assert FarmProc.get_status(step8) == :waiting - - # Perform "write_pin" pt2 - %FarmProc{} = step9 = FarmProc.step(step8) - assert FarmProc.get_status(step9) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :write_pin, - args: %{ - pin_mode: 0, - pin_value: 1, - pin_number: %Farmbot.CeleryScript.AST{ - kind: :named_pin, - args: %{ - pin_type: "Peripheral", - pin_id: 5 - } - } - } - } - - # Perform "read_pin" pt1 - %FarmProc{} = step10 = FarmProc.step(step9) - assert FarmProc.get_status(step10) == :waiting - - # Perform "read_pin" pt2 - %FarmProc{} = step11 = FarmProc.step(step10) - assert FarmProc.get_status(step11) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :read_pin, - args: %{ - pin_mode: 0, - label: "---", - pin_number: 0 - } - } - - # Perform "read_pin" pt1 - %FarmProc{} = step12 = FarmProc.step(step11) - assert FarmProc.get_status(step12) == :waiting - - # Perform "read_pin" pt2 - %FarmProc{} = step13 = FarmProc.step(step12) - assert FarmProc.get_status(step13) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :read_pin, - args: %{ - pin_mode: 1, - label: "---", - pin_number: %Farmbot.CeleryScript.AST{ - kind: :named_pin, - args: %{ - pin_type: "Sensor", - pin_id: 1 - } - } - } - } - - # Perform "read_pin" pt1 - %FarmProc{} = step14 = FarmProc.step(step13) - assert FarmProc.get_status(step14) == :waiting - - # Perform "read_pin" pt2 - %FarmProc{} = step15 = FarmProc.step(step14) - assert FarmProc.get_status(step15) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :wait, - args: %{ - milliseconds: 100 - } - } - - # Perform "send_message" pt1 - %FarmProc{} = step16 = FarmProc.step(step15) - assert FarmProc.get_status(step16) == :waiting - - # Perform "send_message" pt2 - %FarmProc{} = step17 = FarmProc.step(step16) - assert FarmProc.get_status(step17) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :send_message, - args: %{ - message: "FarmBot is at position {{ x }}, {{ y }}, {{ z }}.", - message_type: "success" - }, - body: [ - %Farmbot.CeleryScript.AST{kind: :channel, args: %{channel_name: "toast"}}, - %Farmbot.CeleryScript.AST{kind: :channel, args: %{channel_name: "email"}}, - %Farmbot.CeleryScript.AST{kind: :channel, args: %{channel_name: "espeak"}} - ] - } - - # Perform "find_home" pt1 - %FarmProc{} = step18 = FarmProc.step(step17) - assert FarmProc.get_status(step18) == :waiting - - # Perform "find_home" pt2 - %FarmProc{} = step19 = FarmProc.step(step18) - assert FarmProc.get_status(step19) == :ok - - assert_receive %Farmbot.CeleryScript.AST{ - kind: :find_home, - args: %{ - speed: 100, - axis: "all" - } - } - end - - test "nonrecursive execute" do - seq2 = - AST.new(:sequence, %{}, [ - AST.new(:wait, %{milliseconds: 100}, []) - ]) - - main_seq = - AST.new(:sequence, %{}, [ - AST.new(:execute, %{sequence_id: 2}, []) - ]) - - initial_heap = AST.Slicer.run(main_seq) - - fun = fn ast -> - if ast.kind == :execute do - {:ok, seq2} - else - :ok - end - end - - step0 = FarmProc.new(fun, addr(1), initial_heap) - assert FarmProc.get_heap_by_page_index(step0, addr(1)) - assert FarmProc.get_status(step0) == :ok - - assert_raise Error, ~r(page), fn -> - FarmProc.get_heap_by_page_index(step0, addr(2)) - end - - # enter sequence. - step1 = FarmProc.step(step0) - assert FarmProc.get_status(step1) == :ok - - # enter execute. - step2 = FarmProc.step(step1) - assert FarmProc.get_status(step2) == :waiting - - # Finish execute. - step2 = FarmProc.step(step2) - assert FarmProc.get_status(step2) == :ok - - assert FarmProc.get_heap_by_page_index(step2, addr(2)) - [ptr1, ptr2] = FarmProc.get_return_stack(step2) - assert ptr1 == Pointer.new(addr(1), addr(0)) - assert ptr2 == Pointer.new(addr(1), addr(0)) - - # start sequence - step3 = FarmProc.step(step2) - assert FarmProc.get_status(step3) == :ok - - [ptr3 | _] = FarmProc.get_return_stack(step3) - assert ptr3 == Pointer.new(addr(2), addr(0)) - - step4 = FarmProc.step(step3) - assert FarmProc.get_status(step4) == :waiting - - step5 = FarmProc.step(step4) - step6 = FarmProc.step(step5) - step7 = FarmProc.step(step6) - assert FarmProc.get_return_stack(step7) == [] - - assert FarmProc.get_pc_ptr(step7) == Pointer.null(FarmProc.get_zero_page(step7)) - end - - test "raises when trying to step thru a crashed proc" do - heap = AST.new(:execute, %{sequence_id: 100}, []) |> AST.Slicer.run() - - fun = fn _ -> {:error, "could not find sequence"} end - step0 = FarmProc.new(fun, addr(1), heap) - waiting = FarmProc.step(step0) - crashed = FarmProc.step(waiting) - assert FarmProc.get_status(crashed) == :crashed - - assert_raise Error, - "Tried to step with crashed process!", - fn -> - FarmProc.step(crashed) - end - end - - test "recursive sequence" do - sequence_5 = - AST.new(:sequence, %{}, [ - AST.new(:execute, %{sequence_id: 5}, []) - ]) - - fun = fn ast -> - if ast.kind == :execute do - {:error, "Should already be cached."} - else - :ok - end - end - - heap = AST.Slicer.run(sequence_5) - step0 = FarmProc.new(fun, addr(5), heap) - - step1 = FarmProc.step(step0) - assert Enum.count(FarmProc.get_return_stack(step1)) == 1 - - step2 = FarmProc.step(step1) - assert Enum.count(FarmProc.get_return_stack(step2)) == 2 - - step3 = FarmProc.step(step2) - assert Enum.count(FarmProc.get_return_stack(step3)) == 3 - - pc = FarmProc.get_pc_ptr(step3) - zero_page_num = FarmProc.get_zero_page(step3) - assert pc.page_address == zero_page_num - - step999 = - Enum.reduce(0..996, step3, fn _, acc -> - FarmProc.step(acc) - end) - - assert_raise Error, "Too many reductions!", fn -> - FarmProc.step(step999) - end - end - - test "raises an exception when no implementation is found for a `kind`" do - heap = - AST.new(:sequence, %{}, [AST.new(:fire_laser, %{}, [])]) - |> Farmbot.CeleryScript.AST.Slicer.run() - - assert_raise Error, - "No implementation for: fire_laser", - fn -> - step_0 = FarmProc.new(fn _ -> :ok end, addr(0), heap) - - step_1 = FarmProc.step(step_0) - _step_2 = FarmProc.step(step_1) - end - end - - test "sequence with no body halts" do - heap = AST.new(:sequence, %{}, []) |> Farmbot.CeleryScript.AST.Slicer.run() - farm_proc = FarmProc.new(fn _ -> :ok end, addr(0), heap) - assert FarmProc.get_status(farm_proc) == :ok - - # step into the sequence. - next = FarmProc.step(farm_proc) - - assert FarmProc.get_pc_ptr(next) == Pointer.null(FarmProc.get_zero_page(next)) - - assert FarmProc.get_return_stack(next) == [] - - # Each following step should still be stopped/paused. - next1 = FarmProc.step(next) - - assert FarmProc.get_pc_ptr(next1) == Pointer.null(FarmProc.get_zero_page(next1)) - - assert FarmProc.get_return_stack(next1) == [] - - next2 = FarmProc.step(next1) - - assert FarmProc.get_pc_ptr(next2) == Pointer.null(FarmProc.get_zero_page(next2)) - - assert FarmProc.get_return_stack(next2) == [] - - next3 = FarmProc.step(next2) - - assert FarmProc.get_pc_ptr(next3) == Pointer.null(FarmProc.get_zero_page(next3)) - - assert FarmProc.get_return_stack(next3) == [] - end - - test "_if" do - pid = self() - - fun_gen = fn bool -> - fn ast -> - if ast.kind == :_if do - send(pid, bool) - {:ok, bool} - else - :ok - end - end - end - - nothing_ast = AST.new(:nothing, %{}, []) - - heap = - AST.new( - :_if, - %{ - rhs: 0, - op: "is_undefined", - lhs: "x", - _then: nothing_ast, - _else: nothing_ast - }, - [] - ) - |> AST.Slicer.run() - - truthy_step0 = FarmProc.new(fun_gen.(true), addr(1), heap) - truthy_step1 = FarmProc.step(truthy_step0) - assert FarmProc.get_status(truthy_step1) == :waiting - truthy_step2 = FarmProc.step(truthy_step1) - assert FarmProc.get_status(truthy_step2) == :ok - assert_received true - - falsy_step0 = FarmProc.new(fun_gen.(false), addr(1), heap) - falsy_step1 = FarmProc.step(falsy_step0) - assert FarmProc.get_status(falsy_step1) == :waiting - falsy_step2 = FarmProc.step(falsy_step1) - assert FarmProc.get_status(falsy_step2) == :ok - assert_received false - end - - test "IO isn't instant" do - sleep_time = 100 - - fun = fn _move_relative_ast -> - Process.sleep(sleep_time) - :ok - end - - heap = - AST.new(:move_relative, %{x: 1, y: 2, z: 0}, []) - |> AST.Slicer.run() - - step0 = FarmProc.new(fun, addr(1), heap) - - step1 = FarmProc.step(step0) - step2 = FarmProc.step(step1) - assert FarmProc.get_status(step1) == :waiting - assert FarmProc.get_status(step2) == :waiting - Process.sleep(sleep_time) - final = FarmProc.step(step2) - assert FarmProc.get_status(final) == :done - end - - test "get_cell_attr missing attr raises" do - fun = fn _ -> :ok end - heap = ast(:wait, %{milliseconds: 123}) |> AST.slice() - farm_proc = FarmProc.new(fun, addr(1), heap) - pc_ptr = FarmProc.get_pc_ptr(farm_proc) - assert FarmProc.get_cell_attr(farm_proc, pc_ptr, :milliseconds) == 123 - - assert_raise Error, "no field called: macroseconds at #Pointer<1, 1>", fn -> - FarmProc.get_cell_attr(farm_proc, pc_ptr, :macroseconds) - end - end - - test "get_cell_by_address raises if no cell at address" do - fun = fn _ -> :ok end - heap = ast(:wait, %{milliseconds: 123}) |> AST.slice() - farm_proc = FarmProc.new(fun, addr(1), heap) - - assert_raise Error, "bad address", fn -> - FarmProc.get_cell_by_address(farm_proc, ptr(1, 200)) - end - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/farmware_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/farmware_test.exs deleted file mode 100644 index 37db0c3a..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/farmware_test.exs +++ /dev/null @@ -1,77 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.FarmwareTest do - use ExUnit.Case - alias Farmbot.CeleryScript.AST - alias Farmbot.CeleryScript.RunTime.FarmProc - import Farmbot.CeleryScript.Utils - - @execute_farmware_fixture %{ - kind: :execute_script, - args: %{package: "test-farmware"} - } - - @rpc_request %{ - kind: :rpc_request, - args: %{label: "test-farmware-rpc"}, - body: [ - @execute_farmware_fixture - ] - } - - test "farmware" do - pid = self() - {:ok, agent} = Agent.start_link(fn -> 0 end) - - fun = fn ast -> - case ast.kind do - :execute_script -> - case Agent.get_and_update(agent, fn state -> {state, state + 1} end) do - 0 -> {:ok, ast(:wait, %{milliseconds: 123})} - 1 -> {:ok, ast(:wait, %{milliseconds: 456})} - 2 -> :ok - end - - :wait -> - send(pid, ast) - :ok - end - end - - heap = - @rpc_request - |> AST.decode() - |> AST.slice() - - %FarmProc{} = step0 = FarmProc.new(fun, addr(-1), heap) - - complete = - Enum.reduce(0..200, step0, fn _, proc -> - %FarmProc{} = wait_for_io(proc) - end) - - assert complete.status == :done - - assert_receive %AST{kind: :wait, args: %{milliseconds: 123}} - assert_receive %AST{kind: :wait, args: %{milliseconds: 456}} - end - - def wait_for_io(%FarmProc{} = farm_proc, timeout \\ 1000) do - timer = Process.send_after(self(), :timeout, timeout) - results = do_step(FarmProc.step(farm_proc)) - Process.cancel_timer(timer) - results - end - - defp do_step(%{status: :ok} = farm_proc), do: farm_proc - defp do_step(%{status: :done} = farm_proc), do: farm_proc - - defp do_step(farm_proc) do - receive do - :timeout -> raise("timed out waiting for farm_proc io!") - after - 10 -> :notimeout - end - - FarmProc.step(farm_proc) - |> do_step() - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs deleted file mode 100644 index cca92e54..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs +++ /dev/null @@ -1,186 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.InstructionTest do - alias Farmbot.CeleryScript.RunTime.FarmProc - alias Farmbot.CeleryScript.AST - - defmacro io_test(kind) do - kind_atom = String.to_atom(kind) - - quote do - test unquote(kind) do - pid = self() - - fun = fn ast -> - send(pid, ast) - :ok - end - - heap = - AST.new(unquote(kind_atom), %{}, []) - |> AST.slice() - - step0 = FarmProc.new(fun, addr(0), heap) - step1 = FarmProc.step(step0) - assert FarmProc.get_status(step1) == :waiting - step2 = FarmProc.step(step1) - assert FarmProc.get_status(step2) == :done - assert_received %AST{kind: unquote(kind_atom), args: %{}} - end - end - end -end - -defmodule Farmbot.CeleryScript.RunTime.InstructionSetTest do - use ExUnit.Case - alias Farmbot.CeleryScript.RunTime.{FarmProc, Error} - import Farmbot.CeleryScript.RunTime.InstructionTest - import Farmbot.CeleryScript.Utils - alias Farmbot.CeleryScript.AST - - @fixture AST.decode(%{ - kind: :_if, - args: %{ - lhs: :x, - op: "is", - rhs: 10, - _then: %{kind: :nothing, args: %{}}, - _else: %{kind: :nothing, args: %{}} - } - }) - - io_test("write_pin") - io_test("read_pin") - io_test("set_servo_angle") - io_test("send_message") - io_test("move_relative") - io_test("home") - io_test("find_home") - io_test("wait") - io_test("toggle_pin") - io_test("zero") - io_test("calibrate") - io_test("take_photo") - io_test("config_update") - io_test("set_user_env") - io_test("read_status") - io_test("sync") - io_test("power_off") - io_test("reboot") - io_test("factory_reset") - io_test("change_ownership") - io_test("check_updates") - io_test("dump_info") - - test "nothing returns or sets status" do - seq_1 = - AST.new(:sequence, %{}, [ - AST.new(:execute, %{sequence_id: 2}, []), - AST.new(:wait, %{milliseconds: 10}, []) - ]) - - seq_2 = AST.new(:sequence, %{}, [AST.new(:execute, %{sequence_id: 3}, [])]) - seq_3 = AST.new(:sequence, %{}, [AST.new(:wait, %{milliseconds: 10}, [])]) - - pid = self() - - fun = fn ast -> - case ast do - %{kind: :execute, args: %{sequence_id: 2}} -> - send(pid, {:execute, 2}) - {:ok, seq_2} - - %{kind: :execute, args: %{sequence_id: 3}} -> - send(pid, {:execute, 3}) - {:ok, seq_3} - - %{kind: :wait} -> - send(pid, :wait) - :ok - end - end - - proc0 = FarmProc.new(fun, addr(1), AST.slice(seq_1)) - - complete = - Enum.reduce(0..100, proc0, fn _, proc -> - FarmProc.step(proc) - end) - - assert FarmProc.get_status(complete) == :done - - assert_received {:execute, 2} - assert_received {:execute, 3} - - # we only want to execute those sequences once. - refute_receive {:execute, 2} - refute_receive {:execute, 3} - - # Execute wait twice. - assert_received :wait - assert_received :wait - refute_receive :wait - end - - test "Sets the correct `crash_reason`" do - fun = fn _ -> {:error, "whatever"} end - heap = AST.slice(@fixture) - farm_proc = FarmProc.new(fun, Address.new(1), heap) - - waiting = FarmProc.step(farm_proc) - assert FarmProc.get_status(waiting) == :waiting - - crashed = FarmProc.step(waiting) - assert FarmProc.get_status(crashed) == :crashed - assert FarmProc.get_crash_reason(crashed) == "whatever" - end - - test "_if handles bad interaction layer implementations" do - fun = fn _ -> :ok end - heap = AST.slice(@fixture) - farm_proc = FarmProc.new(fun, Address.new(1), heap) - - assert_raise Error, "Bad _if implementation.", fn -> - %{status: :waiting} = farm_proc = FarmProc.step(farm_proc) - FarmProc.step(farm_proc) - end - end - - test "move absolute bad implementation" do - zero00 = AST.new(:location, %{x: 0, y: 0, z: 0}, []) - fun = fn _ -> :blah end - - heap = - AST.new(:move_absolute, %{location: zero00, offset: zero00}, []) - |> AST.slice() - - proc = FarmProc.new(fun, Address.new(0), heap) - - assert_raise(Error, "Bad return value handling move_absolute IO: :blah", fn -> - Enum.reduce(0..100, proc, fn _num, acc -> - FarmProc.step(acc) - end) - end) - - fun2 = fn _ -> {:error, "whatever"} end - proc2 = FarmProc.new(fun2, Address.new(0), heap) - - result = - Enum.reduce(0..1, proc2, fn _num, acc -> - FarmProc.step(acc) - end) - - assert(FarmProc.get_status(result) == :crashed) - assert(FarmProc.get_crash_reason(result) == "whatever") - end - - test "execute handles bad interaction layer implementation." do - fun = fn _ -> {:ok, :not_ast} end - ast = AST.new(:execute, %{sequence_id: 100}, []) - heap = AST.slice(ast) - farm_proc = FarmProc.new(fun, Address.new(1), heap) - - assert_raise Error, "Bad execute implementation.", fn -> - %{status: :waiting} = farm_proc = FarmProc.step(farm_proc) - FarmProc.step(farm_proc) - end - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs deleted file mode 100644 index f9fbd179..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.ProcStorageTest do - use ExUnit.Case - alias Farmbot.CeleryScript.RunTime.{ProcStorage, FarmProc} - - test "inserts farm_proc" do - storage = ProcStorage.new(self()) - data = %FarmProc{ref: make_ref()} - indx = ProcStorage.insert(storage, data) - assert ProcStorage.current_index(storage) == indx - assert ProcStorage.lookup(storage, indx) == data - end - - test "updates a farm_proc" do - storage = ProcStorage.new(self()) - data = %FarmProc{ref: make_ref()} - indx = ProcStorage.insert(storage, data) - ProcStorage.update(storage, fn ^data -> %{data | ref: make_ref()} end) - assert ProcStorage.lookup(storage, indx) != data - end - - test "deletes a farm_proc" do - storage = ProcStorage.new(self()) - data = %FarmProc{ref: make_ref()} - indx = ProcStorage.insert(storage, data) - ProcStorage.delete(storage, indx) - refute ProcStorage.lookup(storage, indx) - pid = self() - # When there is no farm_procs in the circle buffer, we get a noop. - ProcStorage.update(storage, fn data -> send(pid, data) end) - assert_received :noop - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs deleted file mode 100644 index 47aa4e4f..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs +++ /dev/null @@ -1,138 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.ResolverTest do - use ExUnit.Case, async: true - alias Farmbot.CeleryScript.RunTime.FarmProc - alias Farmbot.CeleryScript.AST - import Farmbot.CeleryScript.Utils - - def fetch_fixture(fname) do - File.read!(fname) |> Jason.decode!() |> AST.decode() - end - - defp io_fun(pid) do - fn ast -> - case ast.kind do - :wait -> - send(pid, ast) - :ok - - :move_absolute -> - send(pid, ast) - :ok - - :execute -> - {:ok, fetch_fixture("fixture/inner_sequence.json")} - end - end - end - - test "variable resolution" do - outer_json = fetch_fixture("fixture/outer_sequence.json") |> AST.Slicer.run() - - farm_proc0 = FarmProc.new(io_fun(self()), addr(0), outer_json) - - farm_proc1 = - Enum.reduce(0..120, farm_proc0, fn _num, acc -> - wait_for_io(acc) - end) - - assert FarmProc.get_status(farm_proc1) == :done - - assert_received %AST{ - kind: :move_absolute, - args: %{ - location: %AST{kind: :point, args: %{pointer_id: 456, pointer_type: "Plant"}}, - offset: %AST{kind: :coordinate, args: %{x: 0, y: 0, z: 0}}, - speed: 100 - } - } - - assert_received %AST{ - kind: :move_absolute, - args: %{ - location: %AST{kind: :point, args: %{pointer_id: 123, pointer_type: "GenericPointer"}}, - offset: %AST{kind: :coordinate, args: %{x: 0, y: 0, z: 0}}, - speed: 100 - } - } - - assert_received %AST{ - kind: :wait, - args: %{milliseconds: 1000} - } - - assert_received %AST{ - kind: :wait, - args: %{milliseconds: 1050} - } - end - - test "sequence with unbound variable" do - unbound_json = fetch_fixture("fixture/unbound.json") |> AST.Slicer.run() - - farm_proc0 = FarmProc.new(io_fun(self()), addr(0), unbound_json) - - assert_raise Farmbot.CeleryScript.RunTime.Error, - "unbound identifier: var20 from pc: #Pointer<0, 0>", - fn -> - Enum.reduce(0..120, farm_proc0, fn _num, acc -> - wait_for_io(acc) - end) - end - end - - test "won't traverse pages" do - fixture = File.read!("fixture/unbound_var_x.json") |> Jason.decode!() - - outter = fixture["outter"] |> AST.decode() |> AST.slice() - inner = fixture["inner"] |> AST.decode() - - syscall = fn ast -> - case ast.kind do - :point -> - {:ok, AST.new(:coordinate, %{x: 0, y: 1, z: 2}, [])} - - :move_absolute -> - :ok - - :execute -> - {:ok, inner} - end - end - - proc = FarmProc.new(syscall, addr(456), outter) - - assert_raise( - Farmbot.CeleryScript.RunTime.Error, - "unbound identifier: x from pc: #Pointer<123, 0>", - fn -> - result = - Enum.reduce(0..100, proc, fn _num, acc -> - wait_for_io(acc) - end) - - IO.inspect(result) - end - ) - end - - def wait_for_io(%FarmProc{} = farm_proc, timeout \\ 1000) do - timer = Process.send_after(self(), :timeout, timeout) - results = do_step(FarmProc.step(farm_proc)) - Process.cancel_timer(timer) - results - end - - defp do_step(%{status: :ok} = farm_proc), do: farm_proc - defp do_step(%{status: :done} = farm_proc), do: farm_proc - - defp do_step(farm_proc) do - receive do - :timeout -> raise("timed out waiting for farm_proc io!") - after - 10 -> :notimeout - end - - FarmProc.step(farm_proc) - |> do_step() - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs deleted file mode 100644 index 15d2d659..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs +++ /dev/null @@ -1,24 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.SysCallHandlerTest do - use ExUnit.Case, async: true - alias Farmbot.CeleryScript.RunTime.SysCallHandler - alias Farmbot.CeleryScript.AST - - test "trying to get results before they are ready crashes" do - Process.flag(:trap_exit, true) - - fun = fn _ -> - Process.sleep(500) - :ok - end - - ast = AST.new(:implode, %{}, []) - - pid = SysCallHandler.apply_sys_call_fun(fun, ast) - - assert_raise RuntimeError, "no results", fn -> - SysCallHandler.get_results(pid) - end - - refute Process.alive?(pid) - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs deleted file mode 100644 index c7efdc99..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs +++ /dev/null @@ -1,23 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.UtilsTest do - use ExUnit.Case, async: true - alias Farmbot.CeleryScript.Utils - - test "new pointer utility" do - assert match?(%Pointer{}, Utils.ptr(1, 100)) - assert Utils.ptr(100, 50).heap_address == Address.new(50) - assert Utils.ptr(99, 20).page_address == Address.new(99) - assert inspect(Utils.ptr(20, 20)) == "#Pointer<20, 20>" - end - - test "new ast utility" do - alias Farmbot.CeleryScript.AST - assert match?(%AST{}, Utils.ast(:action, %{a: 1}, [])) - assert match?(%AST{}, Utils.ast(:explode, %{a: 2})) - assert Utils.ast(:drink, %{}, []) == AST.new(:drink, %{}, []) - end - - test "new address utility" do - assert match?(%Address{}, Utils.addr(100)) - assert Utils.addr(4000) == Address.new(4000) - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs deleted file mode 100644 index 69907036..00000000 --- a/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs +++ /dev/null @@ -1,226 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTimeTest do - use ExUnit.Case - alias Farmbot.CeleryScript.RunTime - import Farmbot.CeleryScript.Utils - alias Farmbot.CeleryScript.AST - - test "simple rpc_request returns rpc_ok" do - pid = self() - - io_fun = fn ast -> - send(pid, ast) - :ok - end - - hyper_fun = fn _ -> :ok end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - label = to_string(name) - ast = ast(:rpc_request, %{label: label}, [ast(:wait, %{milliseconds: 100})]) - - RunTime.rpc_request(farmbot_celery_script, ast, fn result_ast -> - send(pid, result_ast) - end) - - assert_receive %AST{kind: :wait, args: %{milliseconds: 100}} - assert_receive %AST{kind: :rpc_ok, args: %{label: ^label}} - end - - test "simple rpc_request returns rpc_error" do - pid = self() - - io_fun = fn ast -> - send(pid, ast) - {:error, "reason"} - end - - hyper_fun = fn _ -> :ok end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - label = to_string(name) - ast = ast(:rpc_request, %{label: label}, [ast(:wait, %{milliseconds: 100})]) - - RunTime.rpc_request(farmbot_celery_script, ast, fn result_ast -> - send(pid, result_ast) - end) - - assert_receive %AST{kind: :wait, args: %{milliseconds: 100}} - - assert_receive %AST{ - kind: :rpc_error, - args: %{label: ^label}, - body: [%AST{kind: :explanation, args: %{message: "reason"}}] - } - end - - test "rpc_request requires `label` argument" do - assert_raise ArgumentError, fn -> - # don't need to start a vm here, since this shouldn't actual call the vm. - RunTime.rpc_request(ast(:rpc_request, %{}, []), fn _ -> :ok end) - end - end - - test "emergency_lock and emergency_unlock" do - pid = self() - io_fun = fn _ast -> :ok end - hyper_fun = fn hyper -> send(pid, hyper) end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - lock_ast = ast(:rpc_request, %{label: name}, [ast(:emergency_lock, %{})]) - RunTime.rpc_request(farmbot_celery_script, lock_ast, io_fun) - assert_receive :emergency_lock - - unlock_ast = ast(:rpc_request, %{label: name}, [ast(:emergency_unlock, %{})]) - - RunTime.rpc_request(farmbot_celery_script, unlock_ast, io_fun) - assert_receive :emergency_unlock - end - - test "rpc_requests get queued" do - pid = self() - - io_fun = fn %{kind: :wait, args: %{milliseconds: secs}} -> - Process.sleep(secs) - :ok - end - - hyper_fun = fn _ -> :ok end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - - to = 500 - label1 = "one" - label2 = "two" - - ast1 = ast(:rpc_request, %{label: label1}, [ast(:wait, %{milliseconds: to})]) - - ast2 = ast(:rpc_request, %{label: label2}, [ast(:wait, %{milliseconds: to})]) - - cb = fn %{kind: :rpc_ok} = rpc_ok -> send(pid, rpc_ok) end - spawn_link(RunTime, :rpc_request, [farmbot_celery_script, ast1, cb]) - spawn_link(RunTime, :rpc_request, [farmbot_celery_script, ast2, cb]) - - rpc_ok1 = ast(:rpc_ok, %{label: label1}) - rpc_ok2 = ast(:rpc_ok, %{label: label2}) - refute_received ^rpc_ok1 - refute_received ^rpc_ok2 - - assert_receive ^rpc_ok2, to * 2 - assert_receive ^rpc_ok1, to * 2 - end - - test "farm_proc step doesn't crash farmbot_celery_script" do - pid = self() - - io_fun = fn _ast -> - raise("oh noes!!") - end - - hyper_fun = fn _ -> :ok end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - ast = ast(:rpc_request, %{label: name}, [ast(:wait, %{})]) - RunTime.rpc_request(farmbot_celery_script, ast, fn rpc_err -> send(pid, rpc_err) end) - - assert_receive %AST{ - kind: :rpc_error, - args: %{label: ^name}, - body: [%AST{kind: :explanation, args: %{message: "oh noes!!"}}] - } - end - - test "farmbot_celery_script callbacks with exception won't crash farmbot_celery_script" do - pid = self() - io_fun = fn _ast -> :ok end - hyper_fun = fn _ -> :ok end - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - ast = ast(:rpc_request, %{label: name}, []) - - RunTime.rpc_request(farmbot_celery_script, ast, fn rpc_ok -> - send(pid, rpc_ok) - raise("bye!") - end) - - assert_receive %AST{ - kind: :rpc_ok, - args: %{label: ^name} - } - end - - test "farmbot_celery_script sequence executes callback async" do - pid = self() - - io_fun = fn ast -> - send(pid, ast) - - case ast.kind do - :wait -> :ok - :send_message -> {:error, "whoops!"} - end - end - - hyper_fun = fn _ -> :ok end - - name = __ENV__.function |> elem(0) - - opts = [ - process_io_layer: io_fun, - hyper_io_layer: hyper_fun - ] - - {:ok, farmbot_celery_script} = RunTime.start_link(opts, name) - ok_ast = ast(:sequence, %{id: 100}, [ast(:wait, %{milliseconds: 100})]) - - err_ast = ast(:sequence, %{id: 101}, [ast(:send_message, %{message: "???"})]) - - cb = fn results -> send(pid, results) end - vm_pid = RunTime.sequence(farmbot_celery_script, ok_ast, 100, cb) - assert Process.alive?(vm_pid) - - assert_receive %AST{kind: :wait, args: %{milliseconds: 100}} - assert_receive :ok - - vm_pid = RunTime.sequence(farmbot_celery_script, err_ast, 101, cb) - assert Process.alive?(vm_pid) - - assert_receive %AST{kind: :send_message, args: %{message: "???"}} - assert_receive {:error, "whoops!"} - end -end diff --git a/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs b/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs new file mode 100644 index 00000000..90443f6b --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/scheduler_test.exs @@ -0,0 +1,301 @@ +defmodule Farmbot.CeleryScript.SchedulerTest do + use ExUnit.Case, async: false + alias Farmbot.{CeleryScript.Scheduler, CeleryScript.Compiler, CeleryScript.AST} + alias Farmbot.CeleryScript.TestSupport.TestSysCalls + + setup do + {:ok, shim} = TestSysCalls.checkout() + {:ok, sch} = Scheduler.start_link([], []) + [shim: shim, sch: sch] + end + + test "syscall errors", %{sch: sch} do + execute_ast = + %{ + kind: :rpc_request, + args: %{label: "hello world"}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + executed = Compiler.compile(execute_ast) + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :read_pin, _ -> {:error, "failed to read pin!"} + end) + + {:ok, execute_ref} = Scheduler.schedule(sch, executed) + assert_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}} + end + + @tag :annoying + test "regular exceptions still occur", %{sch: sch} do + Process.flag(:trap_exit, true) + + execute_ast = + %{ + kind: :rpc_request, + args: %{label: "hello world"}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + # {:ok, execute_ref} = Scheduler.schedule(sch, executed) + # refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}} + # assert_receive {:EXIT, ^sch, _} + + executed = Compiler.compile(execute_ast) + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :read_pin, _ -> raise("failed to read pin!") + end) + + {:ok, execute_ref} = Scheduler.execute(sch, executed) + refute_receive {Scheduler, ^execute_ref, {:error, "failed to read pin!"}} + assert_receive {:EXIT, ^sch, _}, 1000 + end + + test "executing a sequence on top of a scheduled sequence", %{sch: sch} do + scheduled_ast = + %{ + kind: :sequence, + args: %{locals: %{kind: :variable_declaration, args: %{}}}, + body: [ + %{kind: :wait, args: %{milliseconds: 2000}}, + %{kind: :write_pin, args: %{pin_number: 1, pin_mode: 0, pin_value: 1}} + ] + } + |> AST.decode() + + scheduled = Compiler.compile(scheduled_ast) + + execute_ast = + %{ + kind: :rpc_request, + args: %{label: "hello world"}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + executed = Compiler.compile(execute_ast) + + pid = self() + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :wait, [millis] -> + send(pid, {:wait, :os.system_time()}) + Process.sleep(millis) + + :write_pin, _ -> + send(pid, {:write_pin, :os.system_time()}) + :ok + + :read_pin, _ -> + send(pid, {:read_pin, :os.system_time()}) + 1 + end) + + {:ok, scheduled_ref} = Scheduler.schedule(sch, scheduled) + {:ok, execute_ref} = Scheduler.schedule(sch, executed) + + assert_receive {Scheduler, ^scheduled_ref, :ok}, 5_000 + assert_receive {Scheduler, ^execute_ref, :ok}, 5_000 + + assert_receive {:wait, time_1} + assert_receive {:read_pin, time_2} + assert_receive {:write_pin, time_3} + + assert [^time_1, ^time_3, ^time_2] = Enum.sort([time_1, time_2, time_3], &(&1 <= &2)) + end + + test "execute twice", %{sch: sch} do + execute_ast_1 = + %{ + kind: :rpc_request, + args: %{label: "hello world 1"}, + body: [ + %{kind: :wait, args: %{milliseconds: 1000}} + ] + } + |> AST.decode() + + execute_ast_2 = + %{ + kind: :rpc_request, + args: %{label: "hello world 2"}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + execute_1 = Compiler.compile(execute_ast_1) + execute_2 = Compiler.compile(execute_ast_2) + + pid = self() + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :wait, [millis] -> + send(pid, {:wait, :os.system_time()}) + Process.sleep(millis) + + :read_pin, _ -> + send(pid, {:read_pin, :os.system_time()}) + 1 + end) + + task_1 = + Task.async(fn -> + {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1) + IO.inspect(execute_ref_1, label: "task_1") + assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000 + end) + + task_2 = + Task.async(fn -> + {:ok, execute_ref_2} = Scheduler.execute(sch, execute_2) + IO.inspect(execute_ref_2, label: "task_2") + assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000 + end) + + _ = Task.await(task_1) + _ = Task.await(task_2) + + assert_receive {:wait, time_1} + assert_receive {:read_pin, time_2} + + assert time_2 >= time_1 + 1000 + end + + test "execute then schedule", %{sch: sch} do + execute_ast_1 = + %{ + kind: :rpc_request, + args: %{label: "hello world 1"}, + body: [ + %{kind: :wait, args: %{milliseconds: 1000}} + ] + } + |> AST.decode() + + schedule_ast_1 = + %{ + kind: :sequence, + args: %{locals: %{kind: :variable_declaration, args: %{}}}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + execute_1 = Compiler.compile(execute_ast_1) + schedule_1 = Compiler.compile(schedule_ast_1) + + pid = self() + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :wait, [millis] -> + send(pid, {:wait, :os.system_time()}) + Process.sleep(millis) + + :read_pin, _ -> + send(pid, {:read_pin, :os.system_time()}) + 1 + end) + + task_1 = + Task.async(fn -> + {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1) + IO.inspect(execute_ref_1, label: "task_1") + assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000 + end) + + task_2 = + Task.async(fn -> + {:ok, execute_ref_2} = Scheduler.execute(sch, schedule_1) + IO.inspect(execute_ref_2, label: "task_2") + assert_receive {Scheduler, ^execute_ref_2, :ok}, 3000 + end) + + _ = Task.await(task_1) + _ = Task.await(task_2) + + assert_receive {:wait, time_1} + assert_receive {:read_pin, time_2} + + # Assert that the read pin didn't execute until the wait is complete + assert time_2 >= time_1 + 1000 + end + + test "schedule and execute simotaniously", %{sch: sch} do + schedule_ast_1 = + %{ + kind: :sequence, + args: %{locals: %{kind: :variable_declaration, args: %{}}}, + body: [ + %{kind: :wait, args: %{milliseconds: 2500}} + ] + } + |> AST.decode() + + execute_ast_1 = + %{ + kind: :rpc_request, + args: %{label: "hello world 1"}, + body: [ + %{kind: :read_pin, args: %{pin_number: 1, pin_mode: 0}} + ] + } + |> AST.decode() + + schedule_1 = Compiler.compile(schedule_ast_1) + execute_1 = Compiler.compile(execute_ast_1) + + pid = self() + + :ok = + TestSysCalls.handle(TestSysCalls, fn + :wait, [millis] -> + send(pid, {:wait, :os.system_time()}) + Process.sleep(millis) + + :read_pin, _ -> + send(pid, {:read_pin, :os.system_time()}) + 1 + end) + + task_1 = + Task.async(fn -> + {:ok, schedule_ref_1} = Scheduler.schedule(sch, schedule_1) + IO.inspect(schedule_ref_1, label: "task_1") + assert_receive {Scheduler, ^schedule_ref_1, :ok}, 3000 + end) + + task_2 = + Task.async(fn -> + {:ok, execute_ref_1} = Scheduler.execute(sch, execute_1) + IO.inspect(execute_ref_1, label: "task_2") + assert_receive {Scheduler, ^execute_ref_1, :ok}, 3000 + end) + + _ = Task.await(task_1) + _ = Task.await(task_2) + + assert_receive {:wait, time_1} + assert_receive {:read_pin, time_2} + + # Assert that the read pin executed and finished before the wait. + assert time_2 <= time_1 + 2500 + end +end diff --git a/farmbot_celery_script/test/farmbot_celery_script/sys_calls_test.exs b/farmbot_celery_script/test/farmbot_celery_script/sys_calls_test.exs new file mode 100644 index 00000000..204fe0f7 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/sys_calls_test.exs @@ -0,0 +1,191 @@ +defmodule Farmbot.CeleryScript.SysCallsTest do + use ExUnit.Case, async: false + alias Farmbot.CeleryScript.TestSupport.TestSysCalls + alias Farmbot.CeleryScript.{SysCalls, RuntimeError} + + setup do + {:ok, shim} = TestSysCalls.checkout() + [shim: shim] + end + + test "point", %{shim: shim} do + :ok = shim_fun_ok(shim, %{x: 100, y: 200, z: 300}) + assert %{x: 100, y: 200, z: 300} = SysCalls.point(TestSysCalls, "Peripheral", 1) + assert_receive {:point, ["Peripheral", 1]} + + :ok = shim_fun_error(shim, "point error") + + assert_raise RuntimeError, "point error", fn -> + SysCalls.point(TestSysCalls, "Peripheral", 1) + end + end + + test "move_absolute", %{shim: shim} do + :ok = shim_fun_ok(shim) + assert :ok = SysCalls.move_absolute(TestSysCalls, 1, 2, 3, 4) + assert_receive {:move_absolute, [1, 2, 3, 4]} + + :ok = shim_fun_error(shim, "move failed!") + + assert_raise RuntimeError, "move failed!", fn -> + SysCalls.move_absolute(TestSysCalls, 1, 2, 3, 4) + end + end + + test "get current positions", %{shim: shim} do + :ok = shim_fun_ok(shim, 100.00) + assert 100.00 = SysCalls.get_current_x(TestSysCalls) + assert 100.00 = SysCalls.get_current_y(TestSysCalls) + assert 100.00 = SysCalls.get_current_z(TestSysCalls) + + assert_receive {:get_current_x, []} + assert_receive {:get_current_y, []} + assert_receive {:get_current_z, []} + + :ok = shim_fun_error(shim, "firmware error") + + assert_raise RuntimeError, "firmware error", fn -> + SysCalls.get_current_x(TestSysCalls) + end + + assert_raise RuntimeError, "firmware error", fn -> + SysCalls.get_current_y(TestSysCalls) + end + + assert_raise RuntimeError, "firmware error", fn -> + SysCalls.get_current_z(TestSysCalls) + end + end + + test "write_pin", %{shim: shim} do + :ok = shim_fun_ok(shim) + assert :ok = SysCalls.write_pin(TestSysCalls, 1, 0, 1) + assert :ok = SysCalls.write_pin(TestSysCalls, {:boxled, 4}, 0, 1) + assert :ok = SysCalls.write_pin(TestSysCalls, {:boxled, 3}, 1, 123) + + assert_receive {:write_pin, [1, 0, 1]} + assert_receive {:write_pin, [{:boxled, 4}, 0, 1]} + assert_receive {:write_pin, [{:boxled, 3}, 1, 123]} + + :ok = shim_fun_error(shim, "firmware error") + + assert_raise RuntimeError, "firmware error", fn -> + SysCalls.write_pin(TestSysCalls, 1, 0, 1) + end + end + + test "read_pin", %{shim: shim} do + :ok = shim_fun_ok(shim, 1) + assert 1 == SysCalls.read_pin(TestSysCalls, 10, 0) + assert 1 == SysCalls.read_pin(TestSysCalls, 77, nil) + assert_receive {:read_pin, [10, 0]} + assert_receive {:read_pin, [77, nil]} + + :ok = shim_fun_error(shim, "firmware error") + + assert_raise RuntimeError, "firmware error", fn -> + SysCalls.read_pin(TestSysCalls, 1, 0) + end + end + + test "wait", %{shim: shim} do + :ok = shim_fun_ok(shim, "this doesn't matter!") + assert :ok = SysCalls.wait(TestSysCalls, 1000) + assert_receive {:wait, [1000]} + end + + test "named_pin", %{shim: shim} do + # Peripheral and Sensor are on the Arduino + :ok = shim_fun_ok(shim, 44) + assert 44 == SysCalls.named_pin(TestSysCalls, "Peripheral", 5) + assert 44 == SysCalls.named_pin(TestSysCalls, "Sensor", 1999) + + # BoxLed is on the GPIO + :ok = shim_fun_ok(shim, {:boxled, 3}) + assert {:boxled, 3} == SysCalls.named_pin(TestSysCalls, "BoxLed", 3) + + :ok = shim_fun_ok(shim, {:boxled, 4}) + assert {:boxled, 4} == SysCalls.named_pin(TestSysCalls, "BoxLed", 4) + + assert_receive {:named_pin, ["Peripheral", 5]} + assert_receive {:named_pin, ["Sensor", 1999]} + assert_receive {:named_pin, ["BoxLed", 3]} + assert_receive {:named_pin, ["BoxLed", 4]} + + :ok = shim_fun_error(shim, "error finding resource") + + assert_raise RuntimeError, "error finding resource", fn -> + SysCalls.named_pin(TestSysCalls, "Peripheral", 888) + end + end + + test "send_message", %{shim: shim} do + :ok = shim_fun_ok(shim) + assert :ok = SysCalls.send_message(TestSysCalls, "success", "hello world", ["email"]) + assert_receive {:send_message, ["success", "hello world", ["email"]]} + + :ok = shim_fun_error(shim, "email machine broke") + + assert_raise RuntimeError, "email machine broke", fn -> + SysCalls.send_message(TestSysCalls, "error", "goodbye world", ["email"]) + end + end + + test "find_home", %{shim: shim} do + :ok = shim_fun_ok(shim) + assert :ok = SysCalls.find_home(TestSysCalls, "x", 100) + assert_receive {:find_home, ["x", 100]} + + :ok = shim_fun_error(shim, "home lost") + + assert_raise RuntimeError, "home lost", fn -> + SysCalls.find_home(TestSysCalls, "x", 100) + end + end + + test "execute_script", %{shim: shim} do + :ok = shim_fun_ok(shim) + assert :ok = SysCalls.execute_script(TestSysCalls, "take-photo", %{}) + assert_receive {:execute_script, ["take-photo", %{}]} + + :ok = shim_fun_error(shim, "not installed") + + assert_raise RuntimeError, "not installed", fn -> + SysCalls.execute_script(TestSysCalls, "take-photo", %{}) + end + end + + test "get_sequence", %{shim: shim} do + :ok = + shim_fun_ok(shim, %{ + kind: :sequence, + args: %{locals: %{kind: :scope_declaration, args: %{}}} + }) + + assert %{} = SysCalls.get_sequence(TestSysCalls, 123) + assert_receive {:get_sequence, [123]} + + :ok = shim_fun_error(shim, "sequence not found") + + assert_raise RuntimeError, "sequence not found", fn -> + SysCalls.get_sequence(TestSysCalls, 123) + end + end + + def shim_fun_ok(shim, val \\ :ok) do + pid = self() + + :ok = + TestSysCalls.handle(shim, fn kind, args -> + send(pid, {kind, args}) + val + end) + end + + def shim_fun_error(shim, val) when is_binary(val) do + :ok = + TestSysCalls.handle(shim, fn _kind, _args -> + {:error, val} + end) + end +end diff --git a/farmbot_celery_script/test/pointer_test.exs b/farmbot_celery_script/test/pointer_test.exs deleted file mode 100644 index 13e9ef15..00000000 --- a/farmbot_celery_script/test/pointer_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -defmodule PointerTest do - use ExUnit.Case, async: true - - test "inspects a pointer" do - ptr = Pointer.new(Address.new(1), Address.new(2)) - assert inspect(ptr) == "#Pointer<1, 2>" - end -end diff --git a/farmbot_celery_script/test/support/fixtures.ex b/farmbot_celery_script/test/support/fixtures.ex deleted file mode 100644 index acf9f55a..00000000 --- a/farmbot_celery_script/test/support/fixtures.ex +++ /dev/null @@ -1,13 +0,0 @@ -defmodule Farmbot.CeleryScript.RunTime.TestSupport.Fixtures do - @moduledoc false - def master_sequence do - File.read!("fixture/master_sequence.term") - |> :erlang.binary_to_term() - end - - def heap do - {:ok, map} = Farmbot.CeleryScript.RunTime.TestSupport.Fixtures.master_sequence() - ast = Farmbot.CeleryScript.AST.decode(map) - Farmbot.CeleryScript.AST.Slicer.run(ast) - end -end diff --git a/farmbot_celery_script/test/support/test_sys_calls.ex b/farmbot_celery_script/test/support/test_sys_calls.ex new file mode 100644 index 00000000..42052f04 --- /dev/null +++ b/farmbot_celery_script/test/support/test_sys_calls.ex @@ -0,0 +1,126 @@ +defmodule Farmbot.CeleryScript.TestSupport.TestSysCalls do + @moduledoc """ + Stub implementation of CeleryScript SysCalls + """ + + @behaviour Farmbot.CeleryScript.SysCalls + use GenServer + + def checkout do + case GenServer.start_link(__MODULE__, [], name: __MODULE__) do + {:error, {:already_started, pid}} -> + :ok = GenServer.call(pid, :checkout) + {:ok, pid} + + {:ok, pid} -> + :ok = GenServer.call(pid, :checkout) + {:ok, pid} + end + end + + def handle(pid, fun) when is_function(fun, 2) do + GenServer.call(pid, {:handle, fun}) + end + + @impl true + def init([]) do + {:ok, %{checked_out: nil, handler: nil}} + end + + @impl true + def handle_call(:checkout, {pid, _}, state) do + {:reply, :ok, %{state | checked_out: pid}} + end + + def handle_call({:handle, fun}, {pid, _}, %{checked_out: pid} = state) do + {:reply, :ok, %{state | handler: fun}} + end + + def handle_call({kind, args}, _from, %{handler: fun} = state) when is_function(fun, 2) do + result = state.handler.(kind, args) + {:reply, result, state} + end + + @impl true + def point(type, id) do + call({:point, [type, id]}) + end + + @impl true + def move_absolute(x, y, z, speed) do + call({:move_absolute, [x, y, z, speed]}) + end + + @impl true + def get_current_x do + call({:get_current_x, []}) + end + + @impl true + def get_current_y do + call({:get_current_y, []}) + end + + @impl true + def get_current_z do + call({:get_current_z, []}) + end + + @impl true + def write_pin(pin_number, mode, value) do + call({:write_pin, [pin_number, mode, value]}) + end + + @impl true + def named_pin(type, id) do + call({:named_pin, [type, id]}) + end + + @impl true + def read_pin(number, mode) do + call({:read_pin, [number, mode]}) + end + + @impl true + def wait(millis) do + call({:wait, [millis]}) + end + + @impl true + def send_message(level, message, channels) do + call({:send_message, [level, message, channels]}) + end + + @impl true + def find_home(axis, speed) do + call({:find_home, [axis, speed]}) + end + + @impl true + def get_sequence(id) do + call({:get_sequence, [id]}) + end + + @impl true + def execute_script(name, args) do + call({:execute_script, [name, args]}) + end + + @impl true + def read_status do + call({:read_status, []}) + end + + def set_user_env(key, val) do + call({:set_user_env, [key, val]}) + end + + @impl true + def sync do + call({:sync, []}) + end + + defp call(data) do + GenServer.call(__MODULE__, data, :infinity) + end +end diff --git a/farmbot_core/config/config.exs b/farmbot_core/config/config.exs index f443351c..8c10081f 100644 --- a/farmbot_core/config/config.exs +++ b/farmbot_core/config/config.exs @@ -23,8 +23,8 @@ config :farmbot_core, Farmbot.BotState.FileSystem, root_dir: "/tmp/farmbot", sleep_time: 200 -config :farmbot_core, Farmbot.Core.CeleryScript.RunTimeWrapper, - celery_script_io_layer: Farmbot.Core.CeleryScript.StubIOLayer +config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, + sys_calls: Farmbot.CeleryScript.SysCalls.Stubs config :farmbot_core, Farmbot.EctoMigrator, expected_fw_versions: ["6.4.2.F", "6.4.2.R", "6.4.2.G"], diff --git a/farmbot_core/lib/asset_workers/peripheral_worker.ex b/farmbot_core/lib/asset_workers/peripheral_worker.ex deleted file mode 100644 index 81d060a7..00000000 --- a/farmbot_core/lib/asset_workers/peripheral_worker.ex +++ /dev/null @@ -1,54 +0,0 @@ -defimpl Farmbot.AssetWorker, for: Farmbot.Asset.Peripheral do - use GenServer - require Farmbot.Logger - - alias Farmbot.Asset.Peripheral - alias Farmbot.Core.CeleryScript - import Farmbot.CeleryScript.Utils - @retry_ms 5_000 - - def preload(%Peripheral{}), do: [] - - def start_link(peripheral, _args) do - GenServer.start_link(__MODULE__, peripheral) - end - - def init(peripheral) do - {:ok, peripheral, 0} - end - - def handle_info(:timeout, peripheral) do - Farmbot.Logger.busy(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 - [%{args: %{message: reason}}] = rpc.body - Farmbot.Logger.error(1, "Read peripheral: #{peripheral.label} error: #{reason}") - {:noreply, peripheral, @retry_ms} - end - - def handle_ast(ast, pid) do - :ok = GenServer.cast(pid, ast) - end - - def peripheral_to_rpc(peripheral) do - ast(:rpc_request, %{label: peripheral.local_id}, [ - ast( - :read_pin, - %{ - pin_num: peripheral.pin, - label: peripheral.label, - pin_mode: peripheral.mode - }, - [] - ) - ]) - end -end diff --git a/farmbot_core/lib/asset_workers/pin_binding_worker.ex b/farmbot_core/lib/asset_workers/pin_binding_worker.ex deleted file mode 100644 index 69bcf85e..00000000 --- a/farmbot_core/lib/asset_workers/pin_binding_worker.ex +++ /dev/null @@ -1,160 +0,0 @@ -defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PinBinding do - use GenServer - require Logger - require Farmbot.Logger - - import Farmbot.CeleryScript.Utils - - alias Farmbot.{ - Core.CeleryScript, - Asset.PinBinding, - Asset.Sequence, - Asset - } - - @error_retry_time_ms Application.get_env(:farmbot_core, __MODULE__)[:error_retry_time_ms] - - @gpio_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] - @gpio_handler || - Mix.raise(""" - config :farmbot_core, #{__MODULE__}, gpio_handler: MyModule - """) - - @error_retry_time_ms || - Mix.raise(""" - config :farmbot_core, #{__MODULE__}, error_retry_time_ms: 30_000 - """) - - @typedoc "Opaque function that should be called upon a trigger" - @type trigger_fun :: (pid -> any) - - @typedoc "Integer representing a GPIO on the target platform." - @type pin_number :: integer - - @doc """ - Start a GPIO Handler. Returns the same values as a GenServer start. - - Should call `#{__MODULE__}.trigger/1` when a pin has been triggered. - """ - @callback start_link(pin_number, trigger_fun) :: GenServer.on_start() - - def preload(%PinBinding{}), do: [] - - def start_link(%PinBinding{} = pin_binding, _args) do - GenServer.start_link(__MODULE__, %PinBinding{} = pin_binding) - end - - # This function is opaque and should be considered private. - @doc false - def trigger(pid) do - GenServer.cast(pid, :trigger) - end - - def init(%PinBinding{} = pin_binding) do - {:ok, pin_binding, 0} - end - - def handle_info(:timeout, %PinBinding{} = pin_binding) do - worker_pid = self() - - case gpio_handler().start_link(pin_binding.pin_num, fn -> trigger(worker_pid) end) do - {:ok, pid} when is_pid(pid) -> - Process.link(pid) - {:noreply, pin_binding} - - {:error, {:already_started, pid}} -> - Process.link(pid) - {:noreply, pin_binding, :hibernate} - - {:error, reason} -> - Logger.error("Failed to start PinBinding GPIO Handler: #{inspect(reason)}") - {:noreply, pin_binding, @error_retry_time_ms} - - :ignore -> - Logger.info("Failed to start PinBinding GPIO Handler. Not retrying.") - {:noreply, pin_binding, :hibernate} - end - end - - def handle_cast(:trigger, %PinBinding{special_action: nil} = pin_binding) do - case Asset.get_sequence(id: pin_binding.sequence_id) do - %Sequence{} = seq -> - pid = CeleryScript.sequence(seq, &handle_sequence_results(&1, pin_binding)) - Process.link(pid) - {:noreply, pin_binding, :hibernate} - - nil -> - Farmbot.Logger.error(1, "Failed to find assosiated Sequence for: #{pin_binding}") - {:noreply, pin_binding, :hibernate} - end - end - - def handle_cast(:trigger, %PinBinding{special_action: "dump_info"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:dump_info, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "emergency_lock"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ - ast(:emergency_lock, %{}, []) - ]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "emergency_unlock"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ - ast(:emergency_unlock, %{}, []) - ]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "power_off"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:power_off, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "read_status"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:read_status, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "reboot"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:reboot, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "sync"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:sync, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{special_action: "take_photo"} = pin_binding) do - ast(:rpc_request, %{label: "pin_binding.#{pin_binding.pin_num}"}, [ast(:take_photo, %{}, [])]) - |> CeleryScript.rpc_request(&handle_rpc_request(&1, pin_binding)) - end - - def handle_cast(:trigger, %PinBinding{} = pin_binding) do - Farmbot.Logger.error(1, "Unknown PinBinding: #{pin_binding}") - {:noreply, pin_binding, :hibernate} - end - - def handle_sequence_results({:error, reason}, %PinBinding{} = pin_binding) do - Farmbot.Logger.error(1, "PinBinding #{pin_binding} failed to execute sequence: #{reason}") - end - - def handle_sequence_results(_, _), do: :ok - - def handle_rpc_request( - %{kind: :rpc_error, body: [%{args: %{message: m}}]}, - %PinBinding{} = pin_binding - ) do - Farmbot.Logger.error(1, "PinBinding: #{pin_binding} failed to execute special action: #{m}") - {:noreply, pin_binding, :hibernate} - end - - def handle_rpc_request(_, %PinBinding{} = pin_binding), - do: {:noreply, pin_binding, :hibernate} - - defp gpio_handler, - do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] -end diff --git a/farmbot_core/lib/celery_script/celery_script.ex b/farmbot_core/lib/celery_script/celery_script.ex deleted file mode 100644 index 22383505..00000000 --- a/farmbot_core/lib/celery_script/celery_script.ex +++ /dev/null @@ -1,17 +0,0 @@ -defmodule Farmbot.Core.CeleryScript do - @moduledoc """ - Helpers for executing CeleryScript. - """ - - alias Farmbot.CeleryScript.{RunTime, AST} - - @doc "Execute an RPC request" - def rpc_request(ast, fun) do - RunTime.rpc_request(RunTime, ast, fun) - end - - @doc "Execute a Sequence" - def sequence(%Farmbot.Asset.Sequence{} = seq, fun) do - RunTime.sequence(RunTime, AST.decode(seq), seq.id, fun) - end -end diff --git a/farmbot_core/lib/celery_script/io_layer.ex b/farmbot_core/lib/celery_script/io_layer.ex deleted file mode 100644 index cfa6ab6b..00000000 --- a/farmbot_core/lib/celery_script/io_layer.ex +++ /dev/null @@ -1,43 +0,0 @@ -defmodule Farmbot.Core.CeleryScript.IOLayer do - @moduledoc """ - Behaviour for all functions a CeleryScript Runtime IO layer needs to - implement. - """ - alias Farmbot.CeleryScript.AST - @type args :: AST.args() - @type body :: AST.body() - - # Simple IO - @callback write_pin(args, body) :: :ok | {:error, String.t()} - @callback read_pin(args, body) :: :ok | {:error, String.t()} - @callback set_servo_angle(args, body) :: :ok | {:error, String.t()} - @callback send_message(args, body) :: :ok | {:error, String.t()} - @callback move_relative(args, body) :: :ok | {:error, String.t()} - @callback home(args, body) :: :ok | {:error, String.t()} - @callback find_home(args, body) :: :ok | {:error, String.t()} - @callback wait(args, body) :: :ok | {:error, String.t()} - @callback toggle_pin(args, body) :: :ok | {:error, String.t()} - @callback execute_script(args, body) :: :ok | {:error, String.t()} - @callback zero(args, body) :: :ok | {:error, String.t()} - @callback calibrate(args, body) :: :ok | {:error, String.t()} - @callback take_photo(args, body) :: :ok | {:error, String.t()} - @callback config_update(args, body) :: :ok | {:error, String.t()} - @callback set_user_env(args, body) :: :ok | {:error, String.t()} - @callback read_status(args, body) :: :ok | {:error, String.t()} - @callback sync(args, body) :: :ok | {:error, String.t()} - @callback power_off(args, body) :: :ok | {:error, String.t()} - @callback reboot(args, body) :: :ok | {:error, String.t()} - @callback factory_reset(args, body) :: :ok | {:error, String.t()} - @callback change_ownership(args, body) :: :ok | {:error, String.t()} - @callback check_updates(args, body) :: :ok | {:error, String.t()} - @callback dump_info(args, body) :: :ok | {:error, String.t()} - @callback move_absolute(args, body) :: :ok | {:error, String.t()} - - # Complex IO. - # @callbcak _if(args, body) :: {:ok, AST.t()} | {:error, String.t()} - @callback execute(args, body) :: {:ok, AST.t()} | {:error, String.t()} - - # Special IO. - @callback emergency_lock(args, body) :: any - @callback emergency_unlock(args, body) :: any -end diff --git a/farmbot_core/lib/celery_script/run_time_wrapper.ex b/farmbot_core/lib/celery_script/run_time_wrapper.ex deleted file mode 100644 index 37fca164..00000000 --- a/farmbot_core/lib/celery_script/run_time_wrapper.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Farmbot.Core.CeleryScript.RunTimeWrapper do - @moduledoc false - alias Farmbot.CeleryScript.AST - alias Farmbot.CeleryScript.RunTime - @io_layer Application.get_env(:farmbot_core, __MODULE__)[:celery_script_io_layer] - @io_layer || Mix.raise("No celery_script IO layer!") - - @doc false - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, opts}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - - @doc false - def start_link do - opts = [ - process_io_layer: &handle_io/1, - hyper_io_layer: &handle_hyper/1 - ] - - RunTime.start_link(opts) - end - - @doc false - def handle_io(%AST{kind: :execute_script, args: args}) do - %{package: package} = args - Farmbot.FarmwareRuntime.execute_script(package) - end - - def handle_io(%AST{kind: kind, args: args, body: body}) do - apply(@io_layer, kind, [args, body]) - end - - @doc false - def handle_hyper(:emergency_lock) do - apply(@io_layer, :emergency_lock, [%{}, []]) - end - - def handle_hyper(:emergency_unlock) do - apply(@io_layer, :emergency_unlock, [%{}, []]) - end -end diff --git a/farmbot_core/lib/celery_script/stub_io_layer.ex b/farmbot_core/lib/celery_script/stub_io_layer.ex deleted file mode 100644 index 41bc09d1..00000000 --- a/farmbot_core/lib/celery_script/stub_io_layer.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.Core.CeleryScript.StubIOLayer do - @behaviour Farmbot.Core.CeleryScript.IOLayer - def calibrate(_args, _body), do: {:error, "Stubbed"} - def change_ownership(_args, _body), do: {:error, "Stubbed"} - def check_updates(_args, _body), do: {:error, "Stubbed"} - def config_update(_args, _body), do: {:error, "Stubbed"} - def dump_info(_args, _body), do: {:error, "Stubbed"} - def emergency_lock(_args, _body), do: {:error, "Stubbed"} - def emergency_unlock(_args, _body), do: {:error, "Stubbed"} - def execute(_args, _body), do: {:error, "Stubbed"} - def execute_script(_args, _body), do: {:error, "Stubbed"} - def factory_reset(_args, _body), do: {:error, "Stubbed"} - def find_home(_args, _body), do: {:error, "Stubbed"} - def home(_args, _body), do: {:error, "Stubbed"} - def move_absolute(_args, _body), do: {:error, "Stubbed"} - def move_relative(_args, _body), do: {:error, "Stubbed"} - def power_off(_args, _body), do: {:error, "Stubbed"} - def read_pin(_args, _body), do: {:error, "Stubbed"} - def read_status(_args, _body), do: {:error, "Stubbed"} - def reboot(_args, _body), do: {:error, "Stubbed"} - def send_message(_args, _body), do: {:error, "Stubbed"} - def set_servo_angle(_args, _body), do: {:error, "Stubbed"} - def set_user_env(_args, _body), do: {:error, "Stubbed"} - def sync(_args, _body), do: {:error, "Stubbed"} - def take_photo(_args, _body), do: {:error, "Stubbed"} - def toggle_pin(_args, _body), do: {:error, "Stubbed"} - def wait(_args, _body), do: {:error, "Stubbed"} - def write_pin(_args, _body), do: {:error, "Stubbed"} - def zero(_args, _body), do: {:error, "Stubbed"} - def _if(_args, _body), do: {:error, "Stubbed"} -end diff --git a/farmbot_core/lib/celery_script/supervisor.ex b/farmbot_core/lib/celery_script/supervisor.ex deleted file mode 100644 index 873df84a..00000000 --- a/farmbot_core/lib/celery_script/supervisor.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Farmbot.Core.CeleryScript.Supervisor do - @moduledoc false - use Supervisor - - def start_link(args) do - Supervisor.start_link(__MODULE__, args, name: __MODULE__) - end - - def init([]) do - children = [ - {Farmbot.Core.CeleryScript.RunTimeWrapper, []} - ] - - Supervisor.init(children, strategy: :one_for_one) - end -end diff --git a/farmbot_core/lib/celery_script/utils.ex b/farmbot_core/lib/celery_script/utils.ex deleted file mode 100644 index 91351505..00000000 --- a/farmbot_core/lib/celery_script/utils.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule Farmbot.Core.CeleryScript.Utils do - def new_vec3(x, y, z) do - %{x: x, y: y, z: z} - end -end diff --git a/farmbot_core/lib/farmbot_core.ex b/farmbot_core/lib/farmbot_core.ex index 96d2652c..696299db 100644 --- a/farmbot_core/lib/farmbot_core.ex +++ b/farmbot_core/lib/farmbot_core.ex @@ -19,7 +19,7 @@ defmodule Farmbot.Core do Farmbot.Config.Supervisor, Farmbot.Asset.Supervisor, Farmbot.Core.FirmwareSupervisor, - Farmbot.Core.CeleryScript.Supervisor, + Farmbot.CeleryScript.Scheduler, ] Supervisor.init(children, [strategy: :one_for_all]) end diff --git a/farmbot_core/lib/asset.ex b/farmbot_core/lib/farmbot_core/asset.ex similarity index 100% rename from farmbot_core/lib/asset.ex rename to farmbot_core/lib/farmbot_core/asset.ex diff --git a/farmbot_core/lib/asset/device.ex b/farmbot_core/lib/farmbot_core/asset/device.ex similarity index 100% rename from farmbot_core/lib/asset/device.ex rename to farmbot_core/lib/farmbot_core/asset/device.ex diff --git a/farmbot_core/lib/asset/device_cert.ex b/farmbot_core/lib/farmbot_core/asset/device_cert.ex similarity index 100% rename from farmbot_core/lib/asset/device_cert.ex rename to farmbot_core/lib/farmbot_core/asset/device_cert.ex diff --git a/farmbot_core/lib/asset/diagnostic_dump.ex b/farmbot_core/lib/farmbot_core/asset/diagnostic_dump.ex similarity index 100% rename from farmbot_core/lib/asset/diagnostic_dump.ex rename to farmbot_core/lib/farmbot_core/asset/diagnostic_dump.ex diff --git a/farmbot_core/lib/asset/farm_event.ex b/farmbot_core/lib/farmbot_core/asset/farm_event.ex similarity index 100% rename from farmbot_core/lib/asset/farm_event.ex rename to farmbot_core/lib/farmbot_core/asset/farm_event.ex diff --git a/farmbot_core/lib/asset/farmware_env.ex b/farmbot_core/lib/farmbot_core/asset/farmware_env.ex similarity index 100% rename from farmbot_core/lib/asset/farmware_env.ex rename to farmbot_core/lib/farmbot_core/asset/farmware_env.ex diff --git a/farmbot_core/lib/asset/farmware_installation.ex b/farmbot_core/lib/farmbot_core/asset/farmware_installation.ex similarity index 100% rename from farmbot_core/lib/asset/farmware_installation.ex rename to farmbot_core/lib/farmbot_core/asset/farmware_installation.ex diff --git a/farmbot_core/lib/asset/farmware_installation/manifest.ex b/farmbot_core/lib/farmbot_core/asset/farmware_installation/manifest.ex similarity index 100% rename from farmbot_core/lib/asset/farmware_installation/manifest.ex rename to farmbot_core/lib/farmbot_core/asset/farmware_installation/manifest.ex diff --git a/farmbot_core/lib/asset/fbos_config.ex b/farmbot_core/lib/farmbot_core/asset/fbos_config.ex similarity index 100% rename from farmbot_core/lib/asset/fbos_config.ex rename to farmbot_core/lib/farmbot_core/asset/fbos_config.ex diff --git a/farmbot_core/lib/asset/firmware_config.ex b/farmbot_core/lib/farmbot_core/asset/firmware_config.ex similarity index 100% rename from farmbot_core/lib/asset/firmware_config.ex rename to farmbot_core/lib/farmbot_core/asset/firmware_config.ex diff --git a/farmbot_core/lib/asset/peripheral.ex b/farmbot_core/lib/farmbot_core/asset/peripheral.ex similarity index 100% rename from farmbot_core/lib/asset/peripheral.ex rename to farmbot_core/lib/farmbot_core/asset/peripheral.ex diff --git a/farmbot_core/lib/asset/persistent_regimen.ex b/farmbot_core/lib/farmbot_core/asset/persistent_regimen.ex similarity index 100% rename from farmbot_core/lib/asset/persistent_regimen.ex rename to farmbot_core/lib/farmbot_core/asset/persistent_regimen.ex diff --git a/farmbot_core/lib/asset/pin_binding.ex b/farmbot_core/lib/farmbot_core/asset/pin_binding.ex similarity index 100% rename from farmbot_core/lib/asset/pin_binding.ex rename to farmbot_core/lib/farmbot_core/asset/pin_binding.ex diff --git a/farmbot_core/lib/asset/point.ex b/farmbot_core/lib/farmbot_core/asset/point.ex similarity index 100% rename from farmbot_core/lib/asset/point.ex rename to farmbot_core/lib/farmbot_core/asset/point.ex diff --git a/farmbot_core/lib/asset/private.ex b/farmbot_core/lib/farmbot_core/asset/private.ex similarity index 100% rename from farmbot_core/lib/asset/private.ex rename to farmbot_core/lib/farmbot_core/asset/private.ex diff --git a/farmbot_core/lib/asset/private/local_meta.ex b/farmbot_core/lib/farmbot_core/asset/private/local_meta.ex similarity index 100% rename from farmbot_core/lib/asset/private/local_meta.ex rename to farmbot_core/lib/farmbot_core/asset/private/local_meta.ex diff --git a/farmbot_core/lib/asset/regimen.ex b/farmbot_core/lib/farmbot_core/asset/regimen.ex similarity index 100% rename from farmbot_core/lib/asset/regimen.ex rename to farmbot_core/lib/farmbot_core/asset/regimen.ex diff --git a/farmbot_core/lib/asset/repo.ex b/farmbot_core/lib/farmbot_core/asset/repo.ex similarity index 100% rename from farmbot_core/lib/asset/repo.ex rename to farmbot_core/lib/farmbot_core/asset/repo.ex diff --git a/farmbot_core/lib/asset/schema.ex b/farmbot_core/lib/farmbot_core/asset/schema.ex similarity index 100% rename from farmbot_core/lib/asset/schema.ex rename to farmbot_core/lib/farmbot_core/asset/schema.ex diff --git a/farmbot_core/lib/asset/sensor.ex b/farmbot_core/lib/farmbot_core/asset/sensor.ex similarity index 100% rename from farmbot_core/lib/asset/sensor.ex rename to farmbot_core/lib/farmbot_core/asset/sensor.ex diff --git a/farmbot_core/lib/asset/sensor_reading.ex b/farmbot_core/lib/farmbot_core/asset/sensor_reading.ex similarity index 100% rename from farmbot_core/lib/asset/sensor_reading.ex rename to farmbot_core/lib/farmbot_core/asset/sensor_reading.ex diff --git a/farmbot_core/lib/asset/sequence.ex b/farmbot_core/lib/farmbot_core/asset/sequence.ex similarity index 100% rename from farmbot_core/lib/asset/sequence.ex rename to farmbot_core/lib/farmbot_core/asset/sequence.ex diff --git a/farmbot_core/lib/asset/storage_auth.ex b/farmbot_core/lib/farmbot_core/asset/storage_auth.ex similarity index 100% rename from farmbot_core/lib/asset/storage_auth.ex rename to farmbot_core/lib/farmbot_core/asset/storage_auth.ex diff --git a/farmbot_core/lib/asset/supervisor.ex b/farmbot_core/lib/farmbot_core/asset/supervisor.ex similarity index 100% rename from farmbot_core/lib/asset/supervisor.ex rename to farmbot_core/lib/farmbot_core/asset/supervisor.ex diff --git a/farmbot_core/lib/asset/sync.ex b/farmbot_core/lib/farmbot_core/asset/sync.ex similarity index 100% rename from farmbot_core/lib/asset/sync.ex rename to farmbot_core/lib/farmbot_core/asset/sync.ex diff --git a/farmbot_core/lib/asset/tool.ex b/farmbot_core/lib/farmbot_core/asset/tool.ex similarity index 100% rename from farmbot_core/lib/asset/tool.ex rename to farmbot_core/lib/farmbot_core/asset/tool.ex diff --git a/farmbot_core/lib/asset/view.ex b/farmbot_core/lib/farmbot_core/asset/view.ex similarity index 100% rename from farmbot_core/lib/asset/view.ex rename to farmbot_core/lib/farmbot_core/asset/view.ex diff --git a/farmbot_core/lib/asset_monitor.ex b/farmbot_core/lib/farmbot_core/asset_monitor.ex similarity index 100% rename from farmbot_core/lib/asset_monitor.ex rename to farmbot_core/lib/farmbot_core/asset_monitor.ex diff --git a/farmbot_core/lib/asset_supervisor.ex b/farmbot_core/lib/farmbot_core/asset_supervisor.ex similarity index 100% rename from farmbot_core/lib/asset_supervisor.ex rename to farmbot_core/lib/farmbot_core/asset_supervisor.ex diff --git a/farmbot_core/lib/asset_worker.ex b/farmbot_core/lib/farmbot_core/asset_worker.ex similarity index 100% rename from farmbot_core/lib/asset_worker.ex rename to farmbot_core/lib/farmbot_core/asset_worker.ex diff --git a/farmbot_core/lib/asset_workers/device_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/device_worker.ex similarity index 100% rename from farmbot_core/lib/asset_workers/device_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/device_worker.ex diff --git a/farmbot_core/lib/asset_workers/farm_event_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/farm_event_worker.ex similarity index 88% rename from farmbot_core/lib/asset_workers/farm_event_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/farm_event_worker.ex index c5afdb66..bb9aa7b8 100644 --- a/farmbot_core/lib/asset_workers/farm_event_worker.ex +++ b/farmbot_core/lib/farmbot_core/asset_workers/farm_event_worker.ex @@ -9,7 +9,7 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do Asset.Sequence } - alias Farmbot.Core.CeleryScript + alias Farmbot.CeleryScript.Scheduler defstruct [:farm_event, :datetime, :handle_sequence, :handle_regimen] alias __MODULE__, as: State @@ -30,9 +30,17 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do # Logger.disable(self()) ensure_executable!(farm_event) now = DateTime.utc_now() - handle_sequence = Keyword.get(args, :handle_sequence, &CeleryScript.sequence/2) + handle_sequence = Keyword.get(args, :handle_sequence, &Scheduler.schedule/1) handle_regimen = Keyword.get(args, :handle_regimen, &handle_regimen/3) + unless is_function(handle_sequence, 1) do + raise "FarmEvent Sequence handler should be a 1 arity function" + end + + unless is_function(handle_regimen, 3) do + raise "FarmEvent Regimen handler should be a 3 arity function" + end + state = %State{ farm_event: farm_event, handle_regimen: handle_regimen, @@ -115,7 +123,7 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do true -> Logger.warn("Sequence: #{inspect(exe)} has not run before: #{comp} minutes difference.") - apply(handle_sequence, [exe, fn _ -> :ok end]) + apply(handle_sequence, [wrap_sequence(event, exe)]) Asset.update_farm_event!(event, %{last_executed: next_dt}) end end @@ -127,7 +135,7 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do cond do comp > 2 -> Logger.warn("Sequence: #{inspect(exe)} needs executing") - apply(handle_sequence, [exe, fn _ -> :ok end]) + apply(handle_sequence, [wrap_sequence(event, exe)]) Asset.update_farm_event!(event, %{last_executed: next_dt}) 0 -> @@ -161,6 +169,12 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.FarmEvent do Asset.get_regimen!(id: id) end + # Should wrap a sequence with the `body` of the event. + # TODO + defp wrap_sequence(%FarmEvent{}, %Sequence{} = sequence) do + sequence + end + @doc false def handle_regimen(exe, event, params) do Asset.upsert_persistent_regimen!(exe, event, params) diff --git a/farmbot_core/lib/asset_workers/farmware_env_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/farmware_env_worker.ex similarity index 100% rename from farmbot_core/lib/asset_workers/farmware_env_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/farmware_env_worker.ex diff --git a/farmbot_core/lib/asset_workers/farmware_installation_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/farmware_installation_worker.ex similarity index 100% rename from farmbot_core/lib/asset_workers/farmware_installation_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/farmware_installation_worker.ex diff --git a/farmbot_core/lib/asset_workers/fbos_config_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/fbos_config_worker.ex similarity index 100% rename from farmbot_core/lib/asset_workers/fbos_config_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/fbos_config_worker.ex diff --git a/farmbot_core/lib/farmbot_core/asset_workers/peripheral_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/peripheral_worker.ex new file mode 100644 index 00000000..062f9f12 --- /dev/null +++ b/farmbot_core/lib/farmbot_core/asset_workers/peripheral_worker.ex @@ -0,0 +1,49 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.Peripheral do + use GenServer + require Farmbot.Logger + + alias Farmbot.{Asset.Peripheral, CeleryScript.Scheduler, CeleryScript.AST} + @retry_ms 1_000 + + @impl true + def preload(%Peripheral{}), do: [] + + @impl true + def start_link(peripheral, _args) do + GenServer.start_link(__MODULE__, peripheral) + end + + @impl true + def init(peripheral) do + {:ok, %{peripheral: peripheral, scheduled_ref: nil, errors: 0}, 0} + end + + @impl true + def handle_info(:timeout, %{peripheral: peripheral} = state) do + Farmbot.Logger.busy(2, "Read peripheral: #{peripheral.label}") + rpc = peripheral_to_rpc(peripheral) + {:ok, ref} = Scheduler.schedule(rpc) + {:noreply, %{state | peripheral: peripheral, scheduled_ref: ref}} + end + + def handle_info({Scheduler, ref, :ok}, %{peripheral: peripheral, scheduled_ref: ref} = state) do + Farmbot.Logger.success(2, "Read peripheral: #{peripheral.label} ok") + {:noreply, state, :hibernate} + end + + def handle_info({Scheduler, ref, {:error, reason}}, %{peripheral: peripheral, scheduled_ref: ref, errors: 5} = state) do + Farmbot.Logger.error(1, "Read peripheral: #{peripheral.label} error: #{reason} errors=5 not trying again.") + {:noreply, state, :hibernate} + end + + def handle_info({Scheduler, ref, {:error, reason}}, %{peripheral: peripheral, scheduled_ref: ref} = state) do + Farmbot.Logger.error(1, "Read peripheral: #{peripheral.label} error: #{reason} errors=#{state.errors}") + {:noreply, %{state | scheduled_ref: nil, errors: state.errors + 1}, @retry_ms} + end + + def peripheral_to_rpc(peripheral) do + AST.Factory.new() + |> AST.Factory.rpc_request(peripheral.local_id) + |> AST.Factory.read_pin(peripheral.pin, peripheral.mode) + end +end diff --git a/farmbot_core/lib/asset_workers/persistent_regimen_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/persistent_regimen_worker.ex similarity index 91% rename from farmbot_core/lib/asset_workers/persistent_regimen_worker.ex rename to farmbot_core/lib/farmbot_core/asset_workers/persistent_regimen_worker.ex index e5311af1..8c7b0e4c 100644 --- a/farmbot_core/lib/asset_workers/persistent_regimen_worker.ex +++ b/farmbot_core/lib/farmbot_core/asset_workers/persistent_regimen_worker.ex @@ -5,7 +5,7 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PersistentRegimen do alias Farmbot.Asset alias Farmbot.Asset.{PersistentRegimen, FarmEvent, Regimen} - alias Farmbot.Core.CeleryScript + alias Farmbot.CeleryScript.Scheduler @checkup_time_ms Application.get_env(:farmbot_core, __MODULE__)[:checkup_time_ms] @checkup_time_ms || @@ -20,7 +20,10 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PersistentRegimen do end def init([persistent_regimen, args]) do - apply_sequence = Keyword.get(args, :apply_sequence, &CeleryScript.sequence/2) + apply_sequence = Keyword.get(args, :apply_sequence, &Scheduler.schedule/1) + unless is_function(apply_sequence, 1) do + raise "PersistentRegimen Sequence handler should be a 1 arity function" + end Process.put(:apply_sequence, apply_sequence) with %Regimen{} <- persistent_regimen.regimen, @@ -57,7 +60,7 @@ defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PersistentRegimen do exe = Asset.get_sequence!(id: pr.next_sequence_id) fun = Process.get(:apply_sequence) - apply(fun, [exe, fn _ -> :ok end]) + apply(fun, [exe]) calculate_next(pr) end end diff --git a/farmbot_core/lib/farmbot_core/asset_workers/pin_binding_worker.ex b/farmbot_core/lib/farmbot_core/asset_workers/pin_binding_worker.ex new file mode 100644 index 00000000..0052d3be --- /dev/null +++ b/farmbot_core/lib/farmbot_core/asset_workers/pin_binding_worker.ex @@ -0,0 +1,185 @@ +defimpl Farmbot.AssetWorker, for: Farmbot.Asset.PinBinding do + use GenServer + require Logger + require Farmbot.Logger + + alias Farmbot.{ + CeleryScript.AST, + CeleryScript.Scheduler, + Asset.PinBinding, + Asset.Sequence, + Asset + } + + @error_retry_time_ms Application.get_env(:farmbot_core, __MODULE__)[:error_retry_time_ms] + + @gpio_handler Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] + @gpio_handler || + Mix.raise(""" + config :farmbot_core, #{__MODULE__}, gpio_handler: MyModule + """) + + @error_retry_time_ms || + Mix.raise(""" + config :farmbot_core, #{__MODULE__}, error_retry_time_ms: 30_000 + """) + + @typedoc "Opaque function that should be called upon a trigger" + @type trigger_fun :: (pid -> any) + + @typedoc "Integer representing a GPIO on the target platform." + @type pin_number :: integer + + @doc """ + Start a GPIO Handler. Returns the same values as a GenServer start. + + Should call `#{__MODULE__}.trigger/1` when a pin has been triggered. + """ + @callback start_link(pin_number, trigger_fun) :: GenServer.on_start() + + @impl true + def preload(_), do: [] + + @impl true + def start_link(%PinBinding{} = pin_binding, _args) do + GenServer.start_link(__MODULE__, %PinBinding{} = pin_binding) + end + + # This function is opaque and should be considered private. + @doc false + def trigger(pid) do + GenServer.cast(pid, :trigger) + end + + @impl true + def init(%PinBinding{} = pin_binding) do + {:ok, %{pin_binding: pin_binding, scheduled_ref: nil}, 0} + end + + @impl true + def handle_cast(:trigger, %{pin_binding: %{special_action: nil} = pin_binding} = state) do + case Asset.get_sequence(id: pin_binding.sequence_id) do + %Sequence{} = seq -> + ref = Scheduler.schedule(seq) + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + + nil -> + Farmbot.Logger.error(1, "Failed to find assosiated Sequence for: #{pin_binding}") + {:noreply, state, :hibernate} + end + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "dump_info"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.dump_info() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_lock"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.emergency_lock() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "emergency_unlock"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.emergency_unlock() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "power_off"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.power_off() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "read_status"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.read_status() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "reboot"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.reboot() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "sync"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.sync() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: %{special_action: "take_photo"} = pin_binding} = state) do + ref = + AST.Factory.new() + |> AST.Factory.rpc_request("pin_binding.#{pin_binding.pin_num}") + |> AST.Factory.take_photo() + |> Scheduler.schedule() + {:noreply, %{state | scheduled_ref: ref}, :hibernate} + end + + def handle_cast(:trigger, %{pin_binding: pin_binding} = state) do + Farmbot.Logger.error(1, "Unknown PinBinding: #{pin_binding}") + {:noreply, state, :hibernate} + end + + @impl true + def handle_info(:timeout, %{pin_binding: pin_binding} = state) do + worker_pid = self() + + case gpio_handler().start_link(pin_binding.pin_num, fn -> trigger(worker_pid) end) do + {:ok, pid} when is_pid(pid) -> + Process.link(pid) + {:noreply, state} + + {:error, {:already_started, pid}} -> + Process.link(pid) + {:noreply, state, :hibernate} + + {:error, reason} -> + Logger.error("Failed to start PinBinding GPIO Handler: #{inspect(reason)}") + {:noreply, state, @error_retry_time_ms} + + :ignore -> + Logger.info("Failed to start PinBinding GPIO Handler. Not retrying.") + {:noreply, state, :hibernate} + end + end + + @impl true + def handle_info({Scheduler, ref, :ok}, %{scheduled_ref: ref} = state) do + {:noreply, state, :hibernate} + end + + def handle_info({Scheduler, ref, {:error, reason}}, %{scheduled_ref: ref} = state) do + pin_binding = state.pin_binding + Farmbot.Logger.error(1, "PinBinding: #{pin_binding} failed to execute: #{reason}") + {:noreply, state, :hibernate} + end + + defp gpio_handler, + do: Application.get_env(:farmbot_core, __MODULE__)[:gpio_handler] +end diff --git a/farmbot_core/lib/asset_workers/pin_binding_worker/stub_gpio_handler.ex b/farmbot_core/lib/farmbot_core/asset_workers/pin_binding_worker/stub_gpio_handler.ex similarity index 100% rename from farmbot_core/lib/asset_workers/pin_binding_worker/stub_gpio_handler.ex rename to farmbot_core/lib/farmbot_core/asset_workers/pin_binding_worker/stub_gpio_handler.ex diff --git a/farmbot_core/lib/bot_state/bot_state.ex b/farmbot_core/lib/farmbot_core/bot_state.ex similarity index 100% rename from farmbot_core/lib/bot_state/bot_state.ex rename to farmbot_core/lib/farmbot_core/bot_state.ex diff --git a/farmbot_core/lib/bot_state/filesystem.ex b/farmbot_core/lib/farmbot_core/bot_state/filesystem.ex similarity index 100% rename from farmbot_core/lib/bot_state/filesystem.ex rename to farmbot_core/lib/farmbot_core/bot_state/filesystem.ex diff --git a/farmbot_core/lib/bot_state/job_progress.ex b/farmbot_core/lib/farmbot_core/bot_state/job_progress.ex similarity index 100% rename from farmbot_core/lib/bot_state/job_progress.ex rename to farmbot_core/lib/farmbot_core/bot_state/job_progress.ex diff --git a/farmbot_core/lib/bot_state_ng.ex b/farmbot_core/lib/farmbot_core/bot_state_ng.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng.ex diff --git a/farmbot_core/lib/bot_state_ng/change_generator.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/change_generator.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/change_generator.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/change_generator.ex diff --git a/farmbot_core/lib/bot_state_ng/configuration.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/configuration.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/configuration.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/configuration.ex diff --git a/farmbot_core/lib/bot_state_ng/informational_settings.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/informational_settings.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/informational_settings.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/informational_settings.ex diff --git a/farmbot_core/lib/bot_state_ng/location_data.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/location_data.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/location_data.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/location_data.ex diff --git a/farmbot_core/lib/bot_state_ng/mcu_params.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/mcu_params.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/mcu_params.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/mcu_params.ex diff --git a/farmbot_core/lib/bot_state_ng/process_info.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/process_info.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/process_info.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/process_info.ex diff --git a/farmbot_core/lib/bot_state_ng/schema_to_docs.ex b/farmbot_core/lib/farmbot_core/bot_state_ng/schema_to_docs.ex similarity index 100% rename from farmbot_core/lib/bot_state_ng/schema_to_docs.ex rename to farmbot_core/lib/farmbot_core/bot_state_ng/schema_to_docs.ex diff --git a/farmbot_core/lib/config_storage/bool_value.ex b/farmbot_core/lib/farmbot_core/config_storage/bool_value.ex similarity index 100% rename from farmbot_core/lib/config_storage/bool_value.ex rename to farmbot_core/lib/farmbot_core/config_storage/bool_value.ex diff --git a/farmbot_core/lib/config_storage/config.ex b/farmbot_core/lib/farmbot_core/config_storage/config.ex similarity index 100% rename from farmbot_core/lib/config_storage/config.ex rename to farmbot_core/lib/farmbot_core/config_storage/config.ex diff --git a/farmbot_core/lib/config_storage/config_storage.ex b/farmbot_core/lib/farmbot_core/config_storage/config_storage.ex similarity index 100% rename from farmbot_core/lib/config_storage/config_storage.ex rename to farmbot_core/lib/farmbot_core/config_storage/config_storage.ex diff --git a/farmbot_core/lib/config_storage/float_value.ex b/farmbot_core/lib/farmbot_core/config_storage/float_value.ex similarity index 100% rename from farmbot_core/lib/config_storage/float_value.ex rename to farmbot_core/lib/farmbot_core/config_storage/float_value.ex diff --git a/farmbot_core/lib/config_storage/group.ex b/farmbot_core/lib/farmbot_core/config_storage/group.ex similarity index 100% rename from farmbot_core/lib/config_storage/group.ex rename to farmbot_core/lib/farmbot_core/config_storage/group.ex diff --git a/farmbot_core/lib/config_storage/migration_helpers.ex b/farmbot_core/lib/farmbot_core/config_storage/migration_helpers.ex similarity index 100% rename from farmbot_core/lib/config_storage/migration_helpers.ex rename to farmbot_core/lib/farmbot_core/config_storage/migration_helpers.ex diff --git a/farmbot_core/lib/config_storage/network_interface.ex b/farmbot_core/lib/farmbot_core/config_storage/network_interface.ex similarity index 100% rename from farmbot_core/lib/config_storage/network_interface.ex rename to farmbot_core/lib/farmbot_core/config_storage/network_interface.ex diff --git a/farmbot_core/lib/config_storage/repo.ex b/farmbot_core/lib/farmbot_core/config_storage/repo.ex similarity index 100% rename from farmbot_core/lib/config_storage/repo.ex rename to farmbot_core/lib/farmbot_core/config_storage/repo.ex diff --git a/farmbot_core/lib/config_storage/string_value.ex b/farmbot_core/lib/farmbot_core/config_storage/string_value.ex similarity index 100% rename from farmbot_core/lib/config_storage/string_value.ex rename to farmbot_core/lib/farmbot_core/config_storage/string_value.ex diff --git a/farmbot_core/lib/config_storage/supervisor.ex b/farmbot_core/lib/farmbot_core/config_storage/supervisor.ex similarity index 100% rename from farmbot_core/lib/config_storage/supervisor.ex rename to farmbot_core/lib/farmbot_core/config_storage/supervisor.ex diff --git a/farmbot_core/lib/ecto_migrator.ex b/farmbot_core/lib/farmbot_core/ecto_migrator.ex similarity index 100% rename from farmbot_core/lib/ecto_migrator.ex rename to farmbot_core/lib/farmbot_core/ecto_migrator.ex diff --git a/farmbot_core/lib/farmware_runtime.ex b/farmbot_core/lib/farmbot_core/farmware_runtime.ex similarity index 100% rename from farmbot_core/lib/farmware_runtime.ex rename to farmbot_core/lib/farmbot_core/farmware_runtime.ex diff --git a/farmbot_core/lib/farmware_runtime/pipe_worker.ex b/farmbot_core/lib/farmbot_core/farmware_runtime/pipe_worker.ex similarity index 100% rename from farmbot_core/lib/farmware_runtime/pipe_worker.ex rename to farmbot_core/lib/farmbot_core/farmware_runtime/pipe_worker.ex diff --git a/farmbot_core/lib/firmware/estop_timer.ex b/farmbot_core/lib/farmbot_core/firmware/estop_timer.ex similarity index 100% rename from farmbot_core/lib/firmware/estop_timer.ex rename to farmbot_core/lib/farmbot_core/firmware/estop_timer.ex diff --git a/farmbot_core/lib/firmware/firmware_side_effects.ex b/farmbot_core/lib/farmbot_core/firmware/firmware_side_effects.ex similarity index 100% rename from farmbot_core/lib/firmware/firmware_side_effects.ex rename to farmbot_core/lib/farmbot_core/firmware/firmware_side_effects.ex diff --git a/farmbot_core/lib/firmware/firmware_supervisor.ex b/farmbot_core/lib/farmbot_core/firmware/firmware_supervisor.ex similarity index 100% rename from farmbot_core/lib/firmware/firmware_supervisor.ex rename to farmbot_core/lib/farmbot_core/firmware/firmware_supervisor.ex diff --git a/farmbot_core/lib/json/jason_parser.ex b/farmbot_core/lib/farmbot_core/json/jason_parser.ex similarity index 100% rename from farmbot_core/lib/json/jason_parser.ex rename to farmbot_core/lib/farmbot_core/json/jason_parser.ex diff --git a/farmbot_core/lib/json/json.ex b/farmbot_core/lib/farmbot_core/json/json.ex similarity index 100% rename from farmbot_core/lib/json/json.ex rename to farmbot_core/lib/farmbot_core/json/json.ex diff --git a/farmbot_core/lib/json/parser.ex b/farmbot_core/lib/farmbot_core/json/parser.ex similarity index 100% rename from farmbot_core/lib/json/parser.ex rename to farmbot_core/lib/farmbot_core/json/parser.ex diff --git a/farmbot_core/lib/leds/led_handler.ex b/farmbot_core/lib/farmbot_core/leds/led_handler.ex similarity index 100% rename from farmbot_core/lib/leds/led_handler.ex rename to farmbot_core/lib/farmbot_core/leds/led_handler.ex diff --git a/farmbot_core/lib/leds/leds.ex b/farmbot_core/lib/farmbot_core/leds/leds.ex similarity index 100% rename from farmbot_core/lib/leds/leds.ex rename to farmbot_core/lib/farmbot_core/leds/leds.ex diff --git a/farmbot_core/lib/leds/stub_handler.ex b/farmbot_core/lib/farmbot_core/leds/stub_handler.ex similarity index 100% rename from farmbot_core/lib/leds/stub_handler.ex rename to farmbot_core/lib/farmbot_core/leds/stub_handler.ex diff --git a/farmbot_core/lib/log_storage/log.ex b/farmbot_core/lib/farmbot_core/log_storage/log.ex similarity index 100% rename from farmbot_core/lib/log_storage/log.ex rename to farmbot_core/lib/farmbot_core/log_storage/log.ex diff --git a/farmbot_core/lib/log_storage/logger.ex b/farmbot_core/lib/farmbot_core/log_storage/logger.ex similarity index 100% rename from farmbot_core/lib/log_storage/logger.ex rename to farmbot_core/lib/farmbot_core/log_storage/logger.ex diff --git a/farmbot_core/lib/log_storage/repo.ex b/farmbot_core/lib/farmbot_core/log_storage/repo.ex similarity index 100% rename from farmbot_core/lib/log_storage/repo.ex rename to farmbot_core/lib/farmbot_core/log_storage/repo.ex diff --git a/farmbot_core/lib/log_storage/supervisor.ex b/farmbot_core/lib/farmbot_core/log_storage/supervisor.ex similarity index 100% rename from farmbot_core/lib/log_storage/supervisor.ex rename to farmbot_core/lib/farmbot_core/log_storage/supervisor.ex diff --git a/farmbot_core/lib/project.ex b/farmbot_core/lib/farmbot_core/project.ex similarity index 100% rename from farmbot_core/lib/project.ex rename to farmbot_core/lib/farmbot_core/project.ex diff --git a/farmbot_core/lib/time_utils.ex b/farmbot_core/lib/farmbot_core/time_utils.ex similarity index 100% rename from farmbot_core/lib/time_utils.ex rename to farmbot_core/lib/farmbot_core/time_utils.ex diff --git a/farmbot_core/priv/config/migrations/20190208211728_add_update_channel_field.exs b/farmbot_core/priv/config/migrations/20190208211728_add_update_channel_field.exs index 94ae2cba..91944561 100644 --- a/farmbot_core/priv/config/migrations/20190208211728_add_update_channel_field.exs +++ b/farmbot_core/priv/config/migrations/20190208211728_add_update_channel_field.exs @@ -1,6 +1,6 @@ defmodule Farmbot.System.ConfigStorage.Migrations.AddUpdateChannelField do use Ecto.Migration - import Farmbot.System.ConfigStorage.MigrationHelpers + import Farmbot.Config.MigrationHelpers def change do create_settings_config("update_channel", :string, nil) diff --git a/farmbot_core/test/asset_workers/farm_event_worker_test.exs b/farmbot_core/test/asset_workers/farm_event_worker_test.exs index e9b0e974..318a1117 100644 --- a/farmbot_core/test/asset_workers/farm_event_worker_test.exs +++ b/farmbot_core/test/asset_workers/farm_event_worker_test.exs @@ -24,7 +24,7 @@ defmodule Farmbot.FarmEventWorkerTest do test_pid = self() args = [ - handle_sequence: fn _sequence, _function -> + handle_sequence: fn _sequence -> send(test_pid, {:executed, test_pid}) end ] @@ -55,7 +55,7 @@ defmodule Farmbot.FarmEventWorkerTest do test_pid = self() args = [ - handle_sequence: fn _sequence, _fun -> + handle_sequence: fn _sequence -> send(test_pid, {:executed, test_pid}) end ] @@ -85,7 +85,7 @@ defmodule Farmbot.FarmEventWorkerTest do test_pid = self() args = [ - handle_sequence: fn _sequence, _fun -> + handle_sequence: fn _sequence -> send(test_pid, {:executed, test_pid}) end ] diff --git a/farmbot_core/test/asset_workers/persistent_regimen_worker_test.exs b/farmbot_core/test/asset_workers/persistent_regimen_worker_test.exs index 3303305f..06d18e15 100644 --- a/farmbot_core/test/asset_workers/persistent_regimen_worker_test.exs +++ b/farmbot_core/test/asset_workers/persistent_regimen_worker_test.exs @@ -27,7 +27,7 @@ defmodule Farmbot.PersistentRegimenWorkerTest do test_pid = self() args = [ - apply_sequence: fn _seq, _fun -> + apply_sequence: fn _seq -> send(test_pid, :executed) end ] diff --git a/farmbot_core/test/celery_script_test.exs b/farmbot_core/test/celery_script_test.exs deleted file mode 100644 index ee3c9629..00000000 --- a/farmbot_core/test/celery_script_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -defmodule Farmbot.Core.CeleryScriptTest do - use ExUnit.Case - import Farmbot.TestSupport.AssetFixtures - alias Farmbot.Core.CeleryScript - alias Farmbot.TestSupport.CeleryScript.TestIOLayer - - test "rpc_request" do - TestIOLayer.subscribe() - debug_ast = TestIOLayer.debug_ast() - CeleryScript.rpc_request(debug_ast, &TestIOLayer.debug_fun/1) - assert_receive ^debug_ast - end - - test "sequence" do - TestIOLayer.subscribe() - debug_ast = TestIOLayer.debug_ast() - seq = sequence(%{args: %{}, body: [debug_ast]}) - CeleryScript.sequence(seq, &TestIOLayer.debug_fun/1) - assert_receive ^debug_ast - end -end diff --git a/farmbot_ext/config/config.exs b/farmbot_ext/config/config.exs index 2a1266a7..b68a1ed5 100644 --- a/farmbot_ext/config/config.exs +++ b/farmbot_ext/config/config.exs @@ -4,6 +4,9 @@ config :logger, handle_otp_reports: true, handle_sasl_reports: true config :farmbot_ext, Farmbot.AMQP.NervesHubTransport, handle_nerves_hub_msg: Farmbot.Ext.HandleNervesHubMsg +config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, + sys_calls: Farmbot.CeleryScript.SysCalls.Stubs + import_config "ecto.exs" import_config "farmbot_core.exs" import_config "lagger.exs" diff --git a/farmbot_ext/lib/amqp/bot_state_ng_transport.ex b/farmbot_ext/lib/amqp/bot_state_ng_transport.ex index 6dfc5df2..d945ce5c 100644 --- a/farmbot_ext/lib/amqp/bot_state_ng_transport.ex +++ b/farmbot_ext/lib/amqp/bot_state_ng_transport.ex @@ -13,7 +13,7 @@ defmodule Farmbot.AMQP.BotStateNGTransport do * `bot//status_v8/location_data_.position.z` => `"4.0"` One could subscribe to `bot//status_v8/location_data/#` - and recieve all of those notifications. + and recieve all of those notifications. """ use GenServer @@ -33,6 +33,7 @@ defmodule Farmbot.AMQP.BotStateNGTransport do defstruct [:conn, :chan, :jwt, :changes] alias __MODULE__, as: State + @doc "Forces pushing the most current state tree" def force do GenServer.cast(__MODULE__, :force) end @@ -98,7 +99,7 @@ defmodule Farmbot.AMQP.BotStateNGTransport do error -> msg = """ - Failed to send state value: #{path}, #{inspect(value)} + Failed to send state value: #{path}, #{inspect(value)} error: #{inspect(error)} """ diff --git a/farmbot_ext/lib/amqp/celery_script_transport.ex b/farmbot_ext/lib/amqp/celery_script_transport.ex index d25f5c54..601155b8 100644 --- a/farmbot_ext/lib/amqp/celery_script_transport.ex +++ b/farmbot_ext/lib/amqp/celery_script_transport.ex @@ -11,10 +11,11 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do require Logger alias Farmbot.AMQP.ConnectionWorker + alias Farmbot.{CeleryScript.AST, CeleryScript.Scheduler} @exchange "amq.topic" - defstruct [:conn, :chan, :jwt] + defstruct [:conn, :chan, :jwt, :rpc_requests] alias __MODULE__, as: State @doc false @@ -25,7 +26,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do def init(args) do jwt = Keyword.fetch!(args, :jwt) Process.flag(:sensitive, true) - {:ok, %State{conn: nil, chan: nil, jwt: jwt}, 0} + {:ok, %State{conn: nil, chan: nil, jwt: jwt, rpc_requests: %{}}, 0} end def terminate(reason, state) do @@ -77,30 +78,75 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do device = state.jwt.bot ["bot", ^device, "from_clients"] = String.split(key, ".") + ast = Farmbot.JSON.decode!(payload) |> AST.decode() - spawn_link(fn -> - {_us, _results} = :timer.tc(__MODULE__, :handle_celery_script, [payload, state]) - # IO.puts("#{results.args.label} took: #{us}µs") - end) - - {:noreply, state} - end - - @doc false - def handle_celery_script(payload, state) do - json = Farmbot.JSON.decode!(payload) - # IO.inspect(json, label: "RPC_REQUEST") - Farmbot.Core.CeleryScript.rpc_request(json, fn results_ast -> - reply = Farmbot.JSON.encode!(results_ast) - - if results_ast.kind == :rpc_error do - [%{args: %{message: message}}] = results_ast.body - msg = ["CeleryScript Error\n", message, "\n", inspect(json)] - Logger.error(msg) + {:ok, ref} = + if ast.args[:priority] == 1 do + Scheduler.execute(ast) + else + Scheduler.schedule(ast) end - AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.jwt.bot}.from_device", reply) - results_ast - end) + timer = + if ast.args[:timeout] && ast.args[:timeout] > 0 do + msg = {Scheduler, ref, {:error, "timeout"}} + Process.send_after(self(), msg, ast.args[:timeout]) + end + + req = %{ + started_at: :os.system_time(), + label: ast.args.label, + timer: timer + } + + {:noreply, %{state | rpc_requests: Map.put(state.rpc_requests, ref, req)}} + end + + def handle_info({Scheduler, ref, :ok}, state) do + case state.rpc_requests[ref] do + %{label: label, timer: timer} -> + label != "ping" && Logger.error("CeleryScript success: #{label}") + timer && Process.cancel_timer(timer) + + result_ast = %{ + kind: :rpc_ok, + args: %{ + label: label + } + } + + reply = Farmbot.JSON.encode!(result_ast) + AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.jwt.bot}.from_device", reply) + {:noreply, %{state | rpc_requests: Map.delete(state.rpc_requests, ref)}} + + nil -> + {:noreply, state} + end + end + + def handle_info({Scheduler, ref, {:error, reason}}, state) do + case state.rpc_requests[ref] do + %{label: label, timer: timer} -> + timer && Process.cancel_timer(timer) + + result_ast = %{ + kind: :rpc_error, + args: %{ + label: label + }, + body: [ + %{kind: :explanation, args: %{message: reason}} + ] + } + + reply = Farmbot.JSON.encode!(result_ast) + AMQP.Basic.publish(state.chan, @exchange, "bot.#{state.jwt.bot}.from_device", reply) + msg = ["CeleryScript Error\n", reason] + Logger.error(msg) + {:noreply, %{state | rpc_requests: Map.delete(state.rpc_requests, ref)}} + + nil -> + {:noreply, state} + end end end diff --git a/farmbot_os/config/config.exs b/farmbot_os/config/config.exs index 72db3c1b..b8847a34 100644 --- a/farmbot_os/config/config.exs +++ b/farmbot_os/config/config.exs @@ -29,8 +29,7 @@ config :farmbot_core, Farmbot.EctoMigrator, default_currently_on_beta: String.contains?(to_string(:os.cmd('git rev-parse --abbrev-ref HEAD')), "beta") -config :farmbot_core, Farmbot.Core.CeleryScript.RunTimeWrapper, - celery_script_io_layer: Farmbot.OS.IOLayer +config :farmbot_celery_script, Farmbot.CeleryScript.SysCalls, sys_calls: Farmbot.System.SysCalls config :farmbot_core, Farmbot.BotState.FileSystem, root_dir: "/tmp/farmbot_state", diff --git a/farmbot_os/lib/celery_script/_if.ex b/farmbot_os/lib/celery_script/_if.ex deleted file mode 100644 index f7deea54..00000000 --- a/farmbot_os/lib/celery_script/_if.ex +++ /dev/null @@ -1,76 +0,0 @@ -defmodule Farmbot.OS.IOLayer.If do - @moduledoc false - - alias Farmbot.{Asset, Firmware} - require Farmbot.Logger - - def execute(%{lhs: lhs, op: op, rhs: rhs}, _body) do - case eval_lhs(lhs) do - {:ok, left} -> - left - |> eval_if(op, rhs) - - {:error, _} = err -> - Farmbot.Logger.error(1, "Error evaluating IF statement. #{inspect(err)}") - err - end - end - - def eval_lhs(axis) when axis in ["x", "y", "z"] do - case Firmware.request({:position_read, []}) do - {:ok, {_, {:report_position, pos}}} -> - {:ok, Keyword.fetch!(pos, String.to_existing_atom(axis))} - - _ -> - {:error, "Firmware Error reading position"} - end - end - - def eval_lhs(%{kind: :named_pin} = named_pin) do - id = named_pin.args.pin_id - type = named_pin.args.pin_type - - case fetch_resource(type, id) do - {:ok, number} -> eval_lhs("pin#{number}") - {:error, reason} -> {:error, reason} - end - end - - def eval_lhs("pin" <> p) do - p = String.to_integer(p) - - case Firmware.request({:pin_read, [p: p]}) do - {:ok, {_, {:report_pin_value, [p: ^p, v: v]}}} -> {:ok, v} - _ -> {:error, "Firmware error reading pin: #{p}"} - end - end - - defp fetch_resource("Peripheral", id) do - case Asset.get_peripheral(id: id) do - nil -> {:error, "could not find Peripheral #{id}"} - %{pin: p} -> {:ok, p} - end - end - - defp fetch_resource(unknown_type, _) do - {:error, "Unknown resource type: #{unknown_type}"} - end - - defp eval_if(nil, "is_undefined", _), do: {:ok, true} - defp eval_if(_, "is_undefined", _), do: {:ok, false} - - defp eval_if(nil, _, _), - do: {:error, "Could not eval IF because left hand side of if statement is undefined."} - - defp eval_if(lhs, ">", rhs) when lhs > rhs, do: {:ok, true} - defp eval_if(_lhs, ">", _rhs), do: {:ok, false} - - defp eval_if(lhs, "<", rhs) when lhs < rhs, do: {:ok, true} - defp eval_if(_lhs, "<", _rhs), do: {:ok, false} - - defp eval_if(lhs, "is", rhs) when lhs == rhs, do: {:ok, true} - defp eval_if(_lhs, "is", _rhs), do: {:ok, false} - - defp eval_if(lhs, "not", rhs) when lhs != rhs, do: {:ok, true} - defp eval_if(_lhs, "not", _rhs), do: {:ok, false} -end diff --git a/farmbot_os/lib/celery_script/calibrate.ex b/farmbot_os/lib/celery_script/calibrate.ex deleted file mode 100644 index 12ab2a24..00000000 --- a/farmbot_os/lib/celery_script/calibrate.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Farmbot.OS.IOLayer.Calibrate do - @moduledoc false - - alias Farmbot.Firmware - - def execute(%{axis: "all"} = args, body) do - with :ok <- execute(%{args | axis: "z"}, body), - :ok <- execute(%{args | axis: "y"}, body), - :ok <- execute(%{args | axis: "x"}, body) do - :ok - end - end - - def execute(%{axis: axis}, _body) do - command = {:command_movement_calibrate, [String.to_existing_atom(axis)]} - - case Firmware.command(command) do - :ok -> :ok - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/dump_info.ex b/farmbot_os/lib/celery_script/dump_info.ex deleted file mode 100644 index 8e032c5f..00000000 --- a/farmbot_os/lib/celery_script/dump_info.ex +++ /dev/null @@ -1,46 +0,0 @@ -defmodule Farmbot.OS.IOLayer.DumpInfo do - @moduledoc false - - alias Farmbot.{Firmware, Asset, Config, Project} - - def execute(_args, _body) do - conf = Asset.fbos_config() - fw_state = get_fw_state(Process.whereis(Firmware), conf) - network_iface = Config.get_all_network_configs() |> Enum.at(0) - - params = %{ - firmware_state: fw_state, - network_interface: network_iface[:name], - firmware_hardware: fw_state[:firmware_hardware], - fbos_commit: Project.commit(), - fbos_version: Project.version(), - fbos_dmesg_dump: System.cmd("dmesg", []) |> elem(0), - fbos_target: Project.target() - } - - case Farmbot.Asset.new_diagnostic_dump(params) do - {:ok, _} -> :ok - {:error, _} -> {:error, "Failed to create diagnostic dump"} - end - end - - def get_fw_state(nil, _), do: nil - - def get_fw_state(fw, conf) do - %{ - firmware_hardware: conf.firmware_hardware, - firmware_version: firmware_version(fw), - busy: false, - serial_port: conf.firmware_path, - locked: false, - current_command: nil - } - end - - defp firmware_version(fw) do - case Firmware.request(fw, {:software_version_read, []}) do - {:ok, {_, {:report_software_version, [ver]}}} -> ver - _error -> nil - end - end -end diff --git a/farmbot_os/lib/celery_script/find_home.ex b/farmbot_os/lib/celery_script/find_home.ex deleted file mode 100644 index bd243ccc..00000000 --- a/farmbot_os/lib/celery_script/find_home.ex +++ /dev/null @@ -1,39 +0,0 @@ -defmodule Farmbot.OS.IOLayer.FindHome do - @moduledoc false - - alias Farmbot.Firmware - - def execute(%{axis: "all"} = args, body) do - with :ok <- execute(%{args | axis: "z"}, body), - :ok <- execute(%{args | axis: "y"}, body), - :ok <- execute(%{args | axis: "x"}, body) do - :ok - end - end - - def execute(%{axis: axis}, _body) do - ep_param = :"movement_enable_endpoints_#{axis}" - enc_param = :"encoder_enabled_#{axis}" - - # I'm sorry about these long lines - with {:ok, {_, {:report_paramater_value, [{^ep_param, ep_val}]}}} <- - Firmware.request({:paramater_read, [ep_param]}), - {:ok, {_, {:report_paramater_value, [{^enc_param, enc_val}]}}} <- - Firmware.request({:paramater_read, [enc_param]}) do - command([String.to_existing_atom(axis)], ep_val, enc_val) - else - _ -> {:error, "Firmware Error"} - end - end - - defp command([axis], 0.0, 0.0) do - {:error, "Could not find home on #{axis} axis because endpoints and encoders are disabled."} - end - - defp command(args, _, _) do - case Firmware.command({:command_movement_find_home, args}) do - :ok -> :ok - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/home.ex b/farmbot_os/lib/celery_script/home.ex deleted file mode 100644 index f81a8268..00000000 --- a/farmbot_os/lib/celery_script/home.ex +++ /dev/null @@ -1,28 +0,0 @@ -defmodule Farmbot.OS.IOLayer.Home do - @moduledoc false - - alias Farmbot.Firmware - - def execute(%{axis: "all"}, _body) do - command([:x, :y, :z]) - end - - def execute(%{axis: "x"}, _body) do - command([:x]) - end - - def execute(%{axis: "y"}, _body) do - command([:y]) - end - - def execute(%{axis: "z"}, _body) do - command([:z]) - end - - defp command(args) do - case Firmware.command({:command_movement_home, args}) do - :ok -> :ok - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/io_layer.ex b/farmbot_os/lib/celery_script/io_layer.ex deleted file mode 100644 index 61ca2cd6..00000000 --- a/farmbot_os/lib/celery_script/io_layer.ex +++ /dev/null @@ -1,108 +0,0 @@ -defmodule Farmbot.OS.IOLayer do - @moduledoc false - @behaviour Farmbot.Core.CeleryScript.IOLayer - alias Farmbot.OS.IOLayer.{ - Calibrate, - DumpInfo, - FindHome, - Home, - If, - MoveAbsolute, - MoveRelative, - ReadPin, - SendMessage, - SetServoAngle, - Sync, - TogglePin, - WritePin, - Zero - } - - # Reporting commands - def read_status(_args, _body) do - Farmbot.AMQP.BotStateNGTransport.force() - Farmbot.AMQP.BotStateTransport.force() - :ok - end - - # send_message needs the firmware to serialize - # position and pins - def send_message(args, body), do: require_firmware(SendMessage, args, body) - - def dump_info(args, body), do: DumpInfo.execute(args, body) - - # Flow Control - def _if(args, body), do: require_firmware(If, args, body) - - def execute(%{sequence_id: id}, _body) do - case Farmbot.Asset.get_sequence(id: id) do - nil -> {:error, "Sequence #{id} not found. Try syncing first."} - %{} = seq -> {:ok, Farmbot.CeleryScript.AST.decode(seq)} - end - end - - def wait(%{milliseconds: ms}, _body), do: Process.sleep(ms) - - # Emergency control - def emergency_lock(_args, _body) do - if Process.whereis(Farmbot.Firmware) do - _ = Farmbot.Firmware.command({:command_emergency_lock, []}) - end - - :ok - end - - def emergency_unlock(_args, _body) do - if Process.whereis(Farmbot.Firmware) do - _ = Farmbot.Firmware.command({:command_emergency_unlock, []}) - end - - :ok - end - - # Firmware commands - def calibrate(args, body), do: require_firmware(Calibrate, args, body) - def find_home(args, body), do: require_firmware(FindHome, args, body) - def home(args, body), do: require_firmware(Home, args, body) - def move_absolute(args, body), do: require_firmware(MoveAbsolute, args, body) - def move_relative(args, body), do: require_firmware(MoveRelative, args, body) - def read_pin(args, body), do: require_firmware(ReadPin, args, body) - def set_servo_angle(args, body), do: require_firmware(SetServoAngle, args, body) - def toggle_pin(args, body), do: require_firmware(TogglePin, args, body) - def write_pin(args, body), do: require_firmware(WritePin, args, body) - def zero(args, body), do: require_firmware(Zero, args, body) - - # Farmware - def set_user_env(_args, body) do - for %{args: %{label: key, value: value}} <- body do - Farmbot.Asset.new_farmware_env(%{key: key, value: value}) - end - - :ok - end - - def take_photo(_args, _body), do: {:error, "take_photo Stubbed"} - - def execute_script(_args, _body), do: {:error, "execute_script Stubbed"} - - # Sync/Data - def check_updates(_args, _body), do: {:error, "check_updates Stubbed"} - def sync(args, body), do: Sync.execute(args, body) - - # Power/System - def change_ownership(_args, _body), do: {:error, "change_ownership Stubbed"} - def factory_reset(_args, _body), do: Farmbot.System.factory_reset("CeleryScript") - def power_off(_args, _body), do: Farmbot.System.shutdown("CeleryScript") - def reboot(_args, _body), do: Farmbot.System.reboot("CeleryScript") - - # deprecated commands - def config_update(_args, _body), do: {:error, "config_update deprecated"} - - defp require_firmware(module, args, body) do - if Process.whereis(Farmbot.Firmware) do - module.execute(args, body) - else - {:error, "Firmware not initialized"} - end - end -end diff --git a/farmbot_os/lib/celery_script/move_absolute.ex b/farmbot_os/lib/celery_script/move_absolute.ex deleted file mode 100644 index 600612e9..00000000 --- a/farmbot_os/lib/celery_script/move_absolute.ex +++ /dev/null @@ -1,62 +0,0 @@ -defmodule Farmbot.OS.IOLayer.MoveAbsolute do - @moduledoc false - - alias Farmbot.Firmware - require Farmbot.Logger - - @generic_pointer_types [ - "Plant", - "GenericPointer" - ] - - def execute(%{location: %{args: %{pointer_type: type, pointer_id: id}}} = args, body) - when type in @generic_pointer_types do - Farmbot.Logger.debug(1, "Finding plant before movement") - - case Farmbot.Asset.get_point(id: id) do - %{x: pos_x, y: pos_y, z: pos_z} -> - new_args = %{ - location: %{args: %{x: pos_x, y: pos_y, z: pos_z}}, - offset: args[:offset] || %{args: %{x: 0.0, y: 0.0, z: 0.0}} - } - - execute(new_args, body) - - nil -> - {:error, "Could not find plant by id: #{id}"} - end - end - - def execute(%{location: %{args: %{tool_id: id}}} = args, body) do - Farmbot.Logger.debug(1, "Finding Tool before movement") - - case Farmbot.Asset.get_point(tool_id: id) do - %{x: pos_x, y: pos_y, z: pos_z} -> - new_args = %{ - location: %{args: %{x: pos_x, y: pos_y, z: pos_z}}, - offset: args[:offset] || %{args: %{x: 0.0, y: 0.0, z: 0.0}} - } - - execute(new_args, body) - - nil -> - {:error, "Could not find Tool by id: #{id}"} - end - end - - def execute(args, _body) do - with %{args: %{x: pos_x, y: pos_y, z: pos_z}} <- args[:location], - %{args: %{x: offset_x, y: offset_y, z: offset_z}} <- args[:offset], - x <- offset_x + pos_x / 1.0, - y <- offset_y + pos_y / 1.0, - z <- offset_z + pos_z / 1.0, - :ok <- Firmware.command({:command_movement, [x: x, y: y, z: z]}) do - Farmbot.Logger.success(1, "Movement complete.") - :ok - else - reason -> - Farmbot.Logger.error(1, "Movement failed: #{inspect(reason)}") - {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/move_relative.ex b/farmbot_os/lib/celery_script/move_relative.ex deleted file mode 100644 index f9d01a29..00000000 --- a/farmbot_os/lib/celery_script/move_relative.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule Farmbot.OS.IOLayer.MoveRelative do - @moduledoc false - - alias Farmbot.Firmware - require Farmbot.Logger - - def execute(%{x: x, y: y, z: z, speed: s}, _body) do - Farmbot.Logger.info(1, "starting relative movement: #{x}, #{y}, #{z}") - - with {:ok, {_, {:report_paramater_value, [movement_max_spd_x: max_spd_x]}}} <- - Firmware.request({:paramater_read, [:movement_max_spd_x]}), - {:ok, {_, {:report_paramater_value, [movement_max_spd_y: max_spd_y]}}} <- - Firmware.request({:paramater_read, [:movement_max_spd_y]}), - {:ok, {_, {:report_paramater_value, [movement_max_spd_z: max_spd_z]}}} <- - Firmware.request({:paramater_read, [:movement_max_spd_z]}), - {:ok, {_, {:report_position, [x: cur_x, y: cur_y, z: cur_z]}}} <- - Firmware.request({:position_read, []}) do - x_pos = x / 1.0 + cur_x - x_spd = s / 100 * max_spd_x - - y_pos = y / 1.0 + cur_y - y_spd = s / 100 * max_spd_y - - z_pos = z / 1.0 + cur_z - z_spd = s / 100 * max_spd_z - do_move(x_pos, x_spd, y_pos, y_spd, z_pos, z_spd) - else - error -> - Farmbot.Logger.error(1, "Relative movement failed: #{inspect(error)}") - {:error, "Firmware Error"} - end - end - - def do_move(x_pos, x_spd, y_pos, y_spd, z_pos, z_spd) do - command_args = [x: x_pos, y: y_pos, z: z_pos, a: x_spd, b: y_spd, c: z_spd] - - case Firmware.command({:command_movement, command_args}) do - :ok -> - Farmbot.Logger.success(1, "Relative movement complete") - :ok - - error -> - Farmbot.Logger.error(1, "Relative movement failed: #{inspect(error)}") - {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/read_pin.ex b/farmbot_os/lib/celery_script/read_pin.ex deleted file mode 100644 index d267a04a..00000000 --- a/farmbot_os/lib/celery_script/read_pin.ex +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Farmbot.OS.IOLayer.ReadPin do - @moduledoc false - - alias Farmbot.Firmware - - def execute(%{pin_number: %{args: %{pin_id: id, pin_type: "Peripheral"}}, pin_mode: m}, body) - when is_integer(m) do - case Farmbot.Asset.get_peripheral(id: id) do - %{pin: p} -> execute(%{pin_number: p, pin_mode: m}, body) - nil -> {:error, "Could not find peripheral"} - end - end - - def execute(%{pin_num: p, pin_mode: m}, body) - when is_integer(p) - when is_integer(m) do - execute(%{pin_number: p, pin_mode: m}, body) - end - - def execute(%{pin_number: p, pin_mode: m}, _body) - when is_integer(p) - when is_integer(m) do - with {:ok, {_, {:report_pin_value, [p: ^p, v: _]}}} <- - Firmware.request({:pin_read, p: p, m: m}) do - :ok - else - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/set_servo_angle.ex b/farmbot_os/lib/celery_script/set_servo_angle.ex deleted file mode 100644 index 86336428..00000000 --- a/farmbot_os/lib/celery_script/set_servo_angle.ex +++ /dev/null @@ -1,7 +0,0 @@ -defmodule Farmbot.OS.IOLayer.SetServoAngle do - @moduledoc false - - def execute(args, _body) do - {:error, "SetServoAngle Stubbed: #{inspect(args)}"} - end -end diff --git a/farmbot_os/lib/celery_script/sync.ex b/farmbot_os/lib/celery_script/sync.ex deleted file mode 100644 index 311837ae..00000000 --- a/farmbot_os/lib/celery_script/sync.ex +++ /dev/null @@ -1,32 +0,0 @@ -defmodule Farmbot.OS.IOLayer.Sync do - @moduledoc false - require Farmbot.Logger - alias Farmbot.{Asset.Repo, Asset.Sync, API} - alias API.{Reconciler, SyncGroup} - alias Ecto.{Changeset, Multi} - - def execute(_args, _body) do - Farmbot.Logger.busy(3, "Syncing") - sync_changeset = API.get_changeset(Sync) - sync = Changeset.apply_changes(sync_changeset) - multi = Multi.new() - - :ok = Farmbot.BotState.set_sync_status("syncing") - - with {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_1()), - {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_2()), - {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_3()), - {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_4()) do - Multi.insert(multi, :syncs, sync_changeset) - |> Repo.transaction() - - Farmbot.Logger.success(3, "Synced") - :ok = Farmbot.BotState.set_sync_status("synced") - :ok - else - error -> - :ok = Farmbot.BotState.set_sync_status("sync_error") - {:error, inspect(error)} - end - end -end diff --git a/farmbot_os/lib/celery_script/toggle_pin.ex b/farmbot_os/lib/celery_script/toggle_pin.ex deleted file mode 100644 index 4571d39b..00000000 --- a/farmbot_os/lib/celery_script/toggle_pin.ex +++ /dev/null @@ -1,31 +0,0 @@ -defmodule Farmbot.OS.IOLayer.TogglePin do - @moduledoc false - - alias Farmbot.Firmware - @mode_digital 0 - - def execute(%{pin_number: num}, _body) when is_integer(num) do - case Firmware.request({:pin_read, p: num, m: @mode_digital}) do - {:ok, {_, {:report_pin_value, [p: ^num, v: v]}}} -> do_toggle(num, v) - {:error, _} -> {:error, "Firmware Error"} - end - end - - def do_toggle(num, 0) when is_integer(num) do - command(num, 1) - end - - def do_toggle(num, _) when is_integer(num) do - command(num, 0) - end - - def command(num, val) when is_integer(num) do - with :ok <- Firmware.command({:pin_write, [p: num, m: @mode_digital, v: val]}), - {:ok, {_, {:report_pin_value, [p: ^num, v: ^val]}}} <- - Firmware.request({:pin_read, p: num, m: @mode_digital}) do - :ok - else - _ -> {:error, "Ffirmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/write_pin.ex b/farmbot_os/lib/celery_script/write_pin.ex deleted file mode 100644 index 298d5725..00000000 --- a/farmbot_os/lib/celery_script/write_pin.ex +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Farmbot.OS.IOLayer.WritePin do - @moduledoc false - - alias Farmbot.Firmware - - def execute( - %{pin_number: %{args: %{pin_id: id, pin_type: "Peripheral"}}, pin_mode: m, pin_value: v}, - body - ) - when is_integer(m) - when is_integer(v) do - case Farmbot.Asset.get_peripheral(id: id) do - %{pin: p} -> execute(%{pin_number: p, pin_mode: m, pin_value: v}, body) - nil -> {:error, "Could not find peripheral"} - end - end - - def execute(%{pin_number: p, pin_mode: m, pin_value: v}, _body) when is_integer(p) do - with :ok <- Firmware.command({:pin_write, [p: p, m: m, v: v]}), - {:ok, {_, {:report_pin_value, [p: ^p, v: ^v]}}} <- - Firmware.request({:pin_read, p: p, m: m}) do - :ok - else - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/celery_script/zero.ex b/farmbot_os/lib/celery_script/zero.ex deleted file mode 100644 index ce67738d..00000000 --- a/farmbot_os/lib/celery_script/zero.ex +++ /dev/null @@ -1,22 +0,0 @@ -defmodule Farmbot.OS.IOLayer.Zero do - @moduledoc false - - alias Farmbot.Firmware - - def execute(%{axis: "all"} = args, body) do - with :ok <- execute(%{args | axis: "z"}, body), - :ok <- execute(%{args | axis: "y"}, body), - :ok <- execute(%{args | axis: "x"}, body) do - :ok - end - end - - def execute(%{axis: axis}, _body) do - command = {:position_write_zero, [String.to_existing_atom(axis)]} - - case Firmware.command(command) do - :ok -> :ok - _ -> {:error, "Firmware Error"} - end - end -end diff --git a/farmbot_os/lib/firmware/auto_detector.ex b/farmbot_os/lib/firmware/auto_detector.ex deleted file mode 100644 index 48a308a5..00000000 --- a/farmbot_os/lib/firmware/auto_detector.ex +++ /dev/null @@ -1,2 +0,0 @@ -defmodule Farmbot.Firmware.AutoDetector do -end diff --git a/farmbot_os/lib/system_sys_calls.ex b/farmbot_os/lib/system_sys_calls.ex new file mode 100644 index 00000000..3c247c2b --- /dev/null +++ b/farmbot_os/lib/system_sys_calls.ex @@ -0,0 +1,94 @@ +defmodule Farmbot.System.SysCalls do + alias Farmbot.CeleryScript.AST + alias Farmbot.System.SysCalls.SendMessage + @behaviour Farmbot.CeleryScript.SysCalls + + defdelegate send_message(level, message, channels), to: SendMessage + + def read_status do + :ok = Farmbot.AMQP.BotStateNGTransport.force() + end + + def set_user_env(key, value) do + Farmbot.BotState.set_user_env(key, value) + end + + def get_current_x do + get_position(:x) + end + + def get_current_y do + get_position(:y) + end + + def get_current_z do + get_position(:z) + end + + def read_pin(pin_number, mode) do + {:error, "not implemented"} + end + + def point(kind, id) do + case Farmbot.Asset.get_point(id: id) do + nil -> {:error, "#{kind} not found"} + %{x: x, y: y, z: z} -> %{x: x, y: y, z: z} + end + end + + defp get_position(axis) do + case Farmbot.Firmware.request({nil, {:position_read, []}}) do + {:ok, {nil, {:report_position, params}}} -> + Keyword.fetch!(params, axis) + + _ -> + {:error, "firmware error"} + end + end + + def move_absolute(x, y, z, speed) do + params = [x: x / 1.0, y: y / 1.0, z: z / 1.0, s: speed / 1.0] + + case Farmbot.Firmware.command({nil, {:command_movement, params}}) do + :ok -> :ok + {:error, reason} -> {:error, to_string(reason)} + end + end + + def get_sequence(id) do + case Farmbot.Asset.get_sequence(id: id) do + nil -> {:error, "sequence not found"} + %{} = sequence -> AST.decode(sequence) + end + end + + require Farmbot.Logger + alias Farmbot.{Asset.Repo, Asset.Sync, API} + alias API.{Reconciler, SyncGroup} + alias Ecto.{Changeset, Multi} + + def sync() do + Farmbot.Logger.busy(3, "Syncing") + sync_changeset = API.get_changeset(Sync) + sync = Changeset.apply_changes(sync_changeset) + multi = Multi.new() + + :ok = Farmbot.BotState.set_sync_status("syncing") + + with {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_1()), + {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_2()), + {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_3()), + {:ok, multi} <- Reconciler.sync_group(multi, sync, SyncGroup.group_4()) do + Multi.insert(multi, :syncs, sync_changeset) + |> Repo.transaction() + + Farmbot.Logger.success(3, "Synced") + :ok = Farmbot.BotState.set_sync_status("synced") + :ok + else + error -> + :ok = Farmbot.BotState.set_sync_status("sync_error") + {:error, inspect(error)} + end + end +end diff --git a/farmbot_os/lib/celery_script/send_message.ex b/farmbot_os/lib/system_sys_calls/send_message.ex similarity index 83% rename from farmbot_os/lib/celery_script/send_message.ex rename to farmbot_os/lib/system_sys_calls/send_message.ex index 15e69a9a..f6db3413 100644 --- a/farmbot_os/lib/celery_script/send_message.ex +++ b/farmbot_os/lib/system_sys_calls/send_message.ex @@ -1,18 +1,13 @@ -defmodule Farmbot.OS.IOLayer.SendMessage do - @moduledoc false - +defmodule Farmbot.System.SysCalls.SendMessage do alias Farmbot.Firmware @root_regex ~r/{{\s*[\w\.]+\s*}}/ @extract_reg ~r/[\w\.]+/ - def execute(%{message: templ, message_type: type}, channels) do + def send_message(type, templ, channels) do type = String.to_existing_atom(type) meta = [ - channels: - Enum.map(channels, fn %{kind: :channel, args: %{channel_name: nm}} -> - nm - end) + channels: channels ] case render(templ) do diff --git a/test/support/test_io_layer.ex b/test/support/test_io_layer.ex deleted file mode 100644 index 4decfe21..00000000 --- a/test/support/test_io_layer.ex +++ /dev/null @@ -1,87 +0,0 @@ -defmodule Farmbot.TestSupport.CeleryScript.TestIOLayer do - @behaviour Farmbot.Core.CeleryScript.IOLayer - def calibrate(args, body), do: dispatch(:calibrate, args, body) - def change_ownership(args, body), do: dispatch(:change_ownership, args, body) - def check_updates(args, body), do: dispatch(:check_updates, args, body) - def config_update(args, body), do: dispatch(:config_update, args, body) - def dump_info(args, body), do: dispatch(:dump_info, args, body) - def emergency_lock(args, body), do: dispatch(:emergency_lock, args, body) - def emergency_unlock(args, body), do: dispatch(:emergency_unlock, args, body) - def execute(args, body), do: dispatch(:execute, args, body) - def execute_script(args, body), do: dispatch(:execute_script, args, body) - def factory_reset(args, body), do: dispatch(:factory_reset, args, body) - def find_home(args, body), do: dispatch(:find_home, args, body) - def home(args, body), do: dispatch(:home, args, body) - def move_absolute(args, body), do: dispatch(:move_absolute, args, body) - def move_relative(args, body), do: dispatch(:move_relative, args, body) - def power_off(args, body), do: dispatch(:power_off, args, body) - def read_pin(args, body), do: dispatch(:read_pin, args, body) - def read_status(args, body), do: dispatch(:read_status, args, body) - def reboot(args, body), do: dispatch(:reboot, args, body) - def send_message(args, body), do: dispatch(:send_message, args, body) - def set_servo_angle(args, body), do: dispatch(:set_servo_angle, args, body) - def set_user_env(args, body), do: dispatch(:set_user_env, args, body) - def sync(args, body), do: dispatch(:sync, args, body) - def take_photo(args, body), do: dispatch(:take_photo, args, body) - def toggle_pin(args, body), do: dispatch(:toggle_pin, args, body) - def wait(args, body), do: dispatch(:wait, args, body) - def write_pin(args, body), do: dispatch(:write_pin, args, body) - def zero(args, body), do: dispatch(:zero, args, body) - def _if(args, body), do: dispatch(:_if, args, body) - def debug(args, body), do: dispatch(:debug, args, body) - - defmodule Tracker do - use GenServer - - def dispatch(msg) do - GenServer.cast(__MODULE__, {:dispatch, msg}) - end - - def subscribe do - GenServer.cast(__MODULE__, {:subscribe, self()}) - end - - def start_link do - GenServer.start_link(__MODULE__, [], name: __MODULE__) - end - - def init(subs) do - {:ok, subs} - end - - def handle_cast({:dispatch, msg}, subs) do - for pid <- subs do - Process.alive?(pid) && send(pid, msg) - end - - {:noreply, subs} - end - - def handle_cast({:subscribe, pid}, subs), do: {:noreply, [pid | subs]} - end - - def dispatch(kind, args, body) do - Tracker.start_link() - ast = %{kind: kind, args: args, body: body} - Tracker.dispatch(ast) - {:error, to_string(kind)} - end - - def subscribe do - Tracker.start_link() - Tracker.subscribe() - end - - def debug_ast(params \\ %{}) do - %{ - kind: :debug, - args: %{label: uuid()}, - body: [] - } - |> Map.merge(params) - end - - def debug_fun(_), do: :ok - - def uuid, do: Ecto.UUID.generate() -end