Implement new CeleryScript Runtime environment.
This is obviously a rather large change warranting an essay describing it. A Brief overview Basically the old implementation had quite a few down sides preventing it from really working as intended, especially with the addition of the variables feature. Here is the shortlist of things that needed addressing: * No scoping between sequences. What this essentially means is that a sequence that executes another sequence is unable to add data to the calle. This is important for using Variables. * Error recovery certain nodes have a high likelyhood of failing such as anything that interfaces the firmware. Much focus was spent ensuring that errors would be recoverable when desired. * Complexity of control flow asts versus action asts. Nodes such as `if` will always work in the same way regardless of the state of the rest of the system meaning there is no reason for it to have a special implementation per environment. on the other hand `move_absolute` is bound to a specific part of the system. Seperating these concerns allows for better testing of each piece independently. A More In Depth overview The core of this change resolves around 1 really big change resulting in many more small changes. This change is the CeleryScript `compiler`. The TLDR of this system is that now CeleryScript ASTs are deterministicly compiled to Elixir's AST and executed. Doing this has some big benifits as described below. 1) CeleryScript "runtime" environment is now much simpiler in favor of a somewhat complex "compile time" environment. Basically instead of EVERY single CeleryScript AST having a custom runtime implementation, only a subset of ASTs that require external services such as the Firmware, Database, HTTP, etc require having a runtime implementation. This subset of ASTs are called `SysCalls`. Also the runtime implementations are compiled to a single function call that can be implemented instead of needing to have a contextual environment and making decisions at runtime to evaluate variables and the like. 2) Static analysis is now possible. This means an incorrectly crafted sequence can be validated at compile time rather than getting half way through a sequence before finding the error. 3) Having the "external services" separated leads to better plugability. There is now a behaviour to be implemented for the subset of syscalls that are system specific.pull/974/head
parent
519791e99e
commit
4114e26804
|
@ -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
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"skip_files": [
|
||||
"lib/farmbot_celery_script/compiler/tools.ex"
|
||||
]
|
||||
}
|
Binary file not shown.
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
defmodule Farmbot.CeleryScript do
|
||||
@moduledoc """
|
||||
"""
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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,51 +367,66 @@ 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
|
||||
# Expands home(all) into three home/1 calls
|
||||
compile :home, %{axis: "all", speed: speed} do
|
||||
quote do
|
||||
home("x", unquote(compile_ast(speed)))
|
||||
home("y", unquote(compile_ast(speed)))
|
||||
home("z", unquote(compile_ast(speed)))
|
||||
end
|
||||
end
|
||||
|
||||
# compiles home
|
||||
compile :home, %{axis: axis, speed: speed} do
|
||||
quote do
|
||||
home(unquote(compile_ast(axis)), unquote(compile_ast(speed)))
|
||||
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
|
||||
find_home(unquote(compile_ast(millis)))
|
||||
wait(unquote(compile_ast(millis)))
|
||||
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)
|
||||
|
||||
# quote do
|
||||
# # send_message("success", "Hello world!", [:email, :toast])
|
||||
# send_message(
|
||||
# unquote(compile_ast(type)),
|
||||
# unquote(compile_ast(msg)),
|
||||
# unquote(channels)
|
||||
# )
|
||||
# end
|
||||
# end
|
||||
|
||||
compile :send_message, %{message: msg, message_type: type}, channels do
|
||||
# body gets turned into a list of atoms.
|
||||
# Example:
|
||||
|
@ -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
|
||||
|
||||
def compile_param_application([], acc), do: acc
|
||||
compile_params_to_function_args(rest, [var | acc])
|
||||
end
|
||||
|
||||
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 =
|
||||
# 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)
|
||||
|
||||
quote do
|
||||
Keyword.fetch!(params, unquote(String.to_atom(var_name)))
|
||||
unquote({var_name, [], __MODULE__}) =
|
||||
Keyword.get(params, unquote(var_name), unquote(compile_ast(default)))
|
||||
end
|
||||
end
|
||||
|
||||
{:=, [], [{String.to_atom(var_name), [], __MODULE__}, var_fetch]}
|
||||
@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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -1,7 +0,0 @@
|
|||
defmodule Farmbot.CeleryScript.RunTime.Error do
|
||||
@moduledoc """
|
||||
CSVM runtime error
|
||||
"""
|
||||
|
||||
defexception [:message, :farm_proc]
|
||||
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue