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
Connor Rigby 2019-02-20 11:57:45 -08:00
parent 519791e99e
commit 4114e26804
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
168 changed files with 2248 additions and 4814 deletions

View File

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

View File

@ -0,0 +1,5 @@
{
"skip_files": [
"lib/farmbot_celery_script/compiler/tools.ex"
]
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,4 @@
defmodule Farmbot.CeleryScript do
@moduledoc """
"""
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,16 +4,51 @@ defmodule Farmbot.CeleryScript.Compiler do
Elixir AST.
"""
alias Farmbot.CeleryScript.{AST, Compiler}
alias Farmbot.CeleryScript.{AST, Compiler, Compiler.IdentifierSanitizer, SysCalls}
use Compiler.Tools
@valid_entry_points [:sequence, :rpc_request]
# TODO(Connor) - Delete this when the new corpus is published
@kinds "regisiter_gpio"
@kinds "unregisiter_gpio"
@kinds "config_update"
@typedoc """
Compiled CeleryScript node should compile to an anon function.
Entrypoint nodes such as
* `rpc_request`
* `sequence`
will compile to a function that takes a Keyword list of variables. This function
needs to be executed before scheduling/executing.
Non entrypoint nodes compile to a function that symbolizes one individual step.
## Examples
`rpc_request` will be compiled to something like:
```
fn params ->
[
# Body of the `rpc_request` compiled in here.
]
end
```
as compared to a "simple" node like `wait` will compile to something like:
```
fn() -> wait(200) end
```
"""
@type compiled :: (Keyword.t() -> [(() -> any())]) | (() -> any())
@doc """
Recursive function that will emit Elixir AST from CeleryScript AST.
"""
@spec compile(AST.t(), Keyword.t()) :: [compiled()]
def compile(%AST{kind: kind} = ast, env \\ []) when kind in @valid_entry_points do
# compile the ast
{_, _, _} = compiled = compile_ast(ast)
delete_me(compiled)
# entry points must be evaluated once more with the calling `env`
# to return a list of compiled `steps`
case Code.eval_quoted(compiled, []) do
@ -22,42 +57,66 @@ defmodule Farmbot.CeleryScript.Compiler do
end
end
# The compile macro right here is generated by the Compiler.Tools module.
# The goal of the macro is to do two things:
# 1) take out all the common code between each node impl.
# Example:
# compile :fire_laser, %{after: 100}, targets, do: quote, do: fire_at(targets)
# will compile down to:
# def compile_ast(%AST{kind: :fire_laser, args: %{after: 100}, body: targets})
#
# 2) Accumulate implemented nodes behind the scenes.
# This allows for the Corpus to throw warnings when a new node
# is added.
# Compiles a `sequence` into an Elixir `fn`.
def compile_ast(%AST{kind: :sequence, args: %{locals: %{body: params}}, body: block}) do
compile :sequence, %{locals: %{body: params}}, block do
# Sort the args.body into two arrays.
# The `params` side gets turned into
# a keyword list. These `params` are passed in from a previous sequence.
# The `body` side declares variables in _this_ scope.
{params, body} =
{params_fetch, body} =
Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc ->
case ast do
# declares usage of a paramater as defined by variable_declaration
%{kind: :parameter_declaration} -> {params ++ [compile_param_declaration(ast)], body}
# declares usage of a variable as defined inside the body of itself
%{kind: :parameter_application} -> {params ++ [compile_param_application(ast)], body}
# defines a variable exists
%{kind: :variable_declaration} -> {params, body ++ [ast]}
end
end)
assignments = compile_block(body)
{:__block__, [], assignments} = compile_block(body)
steps = compile_block(block) |> decompose_block_to_steps()
quote do
fn params ->
import Farmbot.CeleryScript.Syscalls
import SysCalls
# Fetches variables from the previous execute()
# example:
# parent = Keyword.fetch!(params, :parent)
unquote_splicing(params)
unquote_splicing(params_fetch)
unquote_splicing(assignments)
# Unquote the remaining sequence steps.
unquote(assignments)
unquote(steps)
end
end
end
compile :rpc_request, %{label: _label}, block do
steps = compile_block(block) |> decompose_block_to_steps()
quote do
fn params ->
import SysCalls
unquote(steps)
end
end
end
# Compiles a variable asignment.
def compile_ast(%AST{
kind: :variable_declaration,
args: %{label: var_name, data_value: data_value_ast}
}) do
compile :variable_declaration, %{label: var_name, data_value: data_value_ast} do
# Compiles the `data_value`
# and assigns the result to a variable named `label`
# Example:
@ -78,14 +137,15 @@ defmodule Farmbot.CeleryScript.Compiler do
# parent = point("Plant", 456)
# NOTE: This needs to be Elixir AST syntax, not quoted
# because var! doesn't do what what we need.
{:=, [], [{String.to_atom(var_name), [], __MODULE__}, compile_ast(data_value_ast)]}
var_name = IdentifierSanitizer.to_variable(var_name)
quote do
unquote({var_name, [], nil}) = unquote(compile_ast(data_value_ast))
end
end
# Compiles an if statement.
def compile_ast(%AST{
kind: :_if,
args: %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs}
}) do
compile :_if, %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs} do
# Turns the left hand side arg into
# a number. x, y, z, and pin{number} are special that need to be
# evaluated before evaluating the if statement.
@ -157,42 +217,76 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# Compiles an `execute` block.
def compile_ast(%AST{kind: :execute, args: %{sequence_id: id}, body: variable_declarations}) do
compile :execute, %{sequence_id: id}, variable_declarations do
quote do
# We have to lookup the sequence by it's id.
%Farmbot.CeleryScript.AST{} = ast = get_sequence(unquote(id))
# compile the ast
compiled_fun = unquote(__MODULE__).compile(ast)
# And call it, serializing all the variables it expects.
# see the `compile_param_application/1` docs for more info.
compiled_fun.(unquote(compile_param_application(variable_declarations)))
env = unquote(compile_params_to_function_args(variable_declarations))
unquote(__MODULE__).compile(ast, env)
end
end
# Compiles `execute_script`
# TODO(Connor) - make this actually usable
def compile_ast(%AST{kind: :execute_script, args: %{label: package}, body: params}) do
compile :execute_script, %{label: package}, params do
env =
Enum.map(params, fn %{args: %{label: key, value: value}} ->
{to_string(key), value}
end)
quote do
execute_script(unquote(package), unquote(Macro.escape(Map.new(env))))
execute_script(unquote(compile_ast(package)), unquote(Macro.escape(Map.new(env))))
end
end
# TODO(Connor) - see above TODO
def compile_ast(%AST{kind: :take_photo}) do
compile :take_photo do
# {:execute_script, [], ["take_photo", {:%{}, [], []}]}
quote do
execute_script("take_photo", %{})
end
end
compile :set_user_env, _args, pairs do
kvs =
Enum.map(pairs, fn %{kind: :pair, args: %{label: key, value: value}} ->
quote do
set_user_env(unquote(key), unquote(value))
end
end)
quote do
(unquote_splicing(kvs))
end
end
compile :install_farmware, %{url: url} do
quote do
install_farmware(unquote(compile_ast(url)))
end
end
compile :update_farmware, %{package: package} do
quote do
update_farmware(unquote(compile_ast(package)))
end
end
compile :remove_farmware, %{package: package} do
quote do
remove_farmware(unquote(compile_ast(package)))
end
end
compile :install_first_party_farmware, _ do
quote do
install_first_party_farmware()
end
end
# Compiles a nothing block.
def compile_ast(%AST{kind: :nothing}) do
compile :nothing do
# AST looks like: {:nothing, [], []}
quote do
nothing()
@ -200,10 +294,7 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# Compiles move_absolute
def compile_ast(%AST{
kind: :move_absolute,
args: %{location: location, offset: offset, speed: speed}
}) do
compile :move_absolute, %{location: location, offset: offset, speed: speed} do
quote do
# Extract the location arg
%{x: locx, y: locy, z: locz} = unquote(compile_ast(location))
@ -218,7 +309,7 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# compiles move_relative into move absolute
def compile_ast(%AST{kind: :move_relative, args: %{x: x, y: y, z: z, speed: speed}}) do
compile :move_relative, %{x: x, y: y, z: z, speed: speed} do
quote do
# build a vec3 of passed in args
%{x: locx, y: locy, z: locz} = %{
@ -242,7 +333,7 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# compiles write_pin
def compile_ast(%AST{kind: :write_pin, args: %{pin_number: num, pin_mode: mode, pin_value: val}}) do
compile :write_pin, %{pin_number: num, pin_mode: mode, pin_value: val} do
quote do
write_pin(
unquote(compile_ast(num)),
@ -253,14 +344,21 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# compiles read_pin
def compile_ast(%AST{kind: :read_pin, args: %{pin_number: num, pin_mode: mode}}) do
compile :read_pin, %{pin_number: num, pin_mode: mode} do
quote do
read_pin(unquote(compile_ast(num)), unquote(compile_ast(mode)))
end
end
# compiles set_servo_angle
compile :set_servo_angle, %{pin_number: pin_number, pin_value: pin_value} do
quote do
set_servo_angle(unquote(compile_ast(pin_number)), unquote(compile_ast(pin_value)))
end
end
# Expands find_home(all) into three find_home/1 calls
def compile_ast(%AST{kind: :find_home, args: %{axis: "all", speed: speed}}) do
compile :find_home, %{axis: "all", speed: speed} do
quote do
find_home("x", unquote(compile_ast(speed)))
find_home("y", unquote(compile_ast(speed)))
@ -269,50 +367,65 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# compiles find_home
def compile_ast(%AST{kind: :find_home, args: %{axis: axis, speed: speed}}) do
compile :find_home, %{axis: axis, speed: speed} do
quote do
find_home(unquote(compile_ast(axis)), unquote(compile_ast(speed)))
end
end
# compiles wait
# def compile_ast(%AST{kind: :wait, args: %{milliseconds: millis}}) do
# quote do
# find_home(unquote(compile_ast(millis)))
# end
# end
compile :wait, %{milliseconds: millis} do
# Expands home(all) into three home/1 calls
compile :home, %{axis: "all", speed: speed} do
quote do
find_home(unquote(compile_ast(millis)))
home("x", unquote(compile_ast(speed)))
home("y", unquote(compile_ast(speed)))
home("z", unquote(compile_ast(speed)))
end
end
# # compiles send_message
# def compile_ast(%AST{
# kind: :send_message,
# args: %{message: msg, message_type: type},
# body: channels
# }) do
# # body gets turned into a list of atoms.
# # Example:
# # [{kind: "channel", args: {channel_name: "email"}}]
# # is turned into:
# # [:email]
# channels =
# Enum.map(channels, fn %{kind: :channel, args: %{channel_name: channel_name}} ->
# String.to_atom(channel_name)
# end)
# compiles home
compile :home, %{axis: axis, speed: speed} do
quote do
home(unquote(compile_ast(axis)), unquote(compile_ast(speed)))
end
end
# quote do
# # send_message("success", "Hello world!", [:email, :toast])
# send_message(
# unquote(compile_ast(type)),
# unquote(compile_ast(msg)),
# unquote(channels)
# )
# end
# end
# Expands zero(all) into three zero/1 calls
compile :zero, %{axis: "all", speed: speed} do
quote do
zero("x", unquote(compile_ast(speed)))
zero("y", unquote(compile_ast(speed)))
zero("z", unquote(compile_ast(speed)))
end
end
# compiles zero
compile :zero, %{axis: axis, speed: speed} do
quote do
zero(unquote(compile_ast(axis)), unquote(compile_ast(speed)))
end
end
# Expands calibrate(all) into three calibrate/1 calls
compile :calibrate, %{axis: "all", speed: speed} do
quote do
calibrate("x", unquote(compile_ast(speed)))
calibrate("y", unquote(compile_ast(speed)))
calibrate("z", unquote(compile_ast(speed)))
end
end
# compiles calibrate
compile :calibrate, %{axis: axis, speed: speed} do
quote do
calibrate(unquote(compile_ast(axis)), unquote(compile_ast(speed)))
end
end
compile :wait, %{milliseconds: millis} do
quote do
wait(unquote(compile_ast(millis)))
end
end
compile :send_message, %{message: msg, message_type: type}, channels do
# body gets turned into a list of atoms.
@ -337,7 +450,7 @@ defmodule Farmbot.CeleryScript.Compiler do
# compiles coordinate
# Coordinate should return a vec3
def compile_ast(%AST{kind: :coordinate, args: %{x: x, y: y, z: z}}) do
compile :coordinate, %{x: x, y: y, z: z} do
quote do
coordinate(
unquote(compile_ast(x)),
@ -348,29 +461,154 @@ defmodule Farmbot.CeleryScript.Compiler do
end
# compiles point
def compile_ast(%AST{kind: :point, args: %{pointer_type: type, pointer_id: id}}) do
compile :point, %{pointer_type: type, pointer_id: id} do
quote do
point(unquote(compile_ast(type)), unquote(compile_ast(id)))
end
end
# compile a named pin
def compile_ast(%AST{kind: :named_pin, args: %{pin_id: id, pin_type: type}}) do
compile :named_pin, %{pin_id: id, pin_type: type} do
quote do
pin(unquote(compile_ast(type)), unquote(compile_ast(id)))
named_pin(unquote(compile_ast(type)), unquote(compile_ast(id)))
end
end
# compiles identifier into a variable.
# We have to use Elixir ast syntax here because
# var! doesn't work quite the way we want.
def compile_ast(%AST{kind: :identifier, args: %{label: var_name}}) do
{String.to_atom(var_name), [], __MODULE__}
compile :identifier, %{label: var_name} do
var_name = IdentifierSanitizer.to_variable(var_name)
quote do
unquote({var_name, [], nil})
end
end
# Numbers and strings are treated as literals.
def compile_ast(lit) when is_number(lit), do: lit
def compile_ast(lit) when is_binary(lit), do: lit
compile :tool, %{tool_id: tool_id} do
quote do
get_tool(unquote(compile_ast(tool_id)))
end
end
compile :emergency_lock do
quote do
emergency_lock()
end
end
compile :emergency_unlock do
quote do
emergency_unlock()
end
end
compile :read_status do
quote do
read_status()
end
end
compile :sync do
quote do
sync()
end
end
compile :check_updates, %{package: "farmbot_os"} do
quote do
check_update()
end
end
compile :power_off do
quote do
power_off()
end
end
compile :reboot, %{package: "farmbot_os"} do
quote do
reboot()
end
end
compile :reboot, %{package: "arduino_firmware"} do
quote do
firmware_reboot()
end
end
compile :factory_reset, %{package: "farmbot_os"} do
quote do
factory_reset()
end
end
compile :change_ownership, %{}, _body do
quote do
# Add code here
end
end
compile :dump_info do
quote do
dump_info()
end
end
compile :toggle_pin, %{pin_number: pin_number} do
quote do
# mode 0 = digital
case read_pin(unquote(compile_ast(pin_number)), 0) do
0 -> write_pin(unquote(compile_ast(pin_number)), 0, 1)
_ -> write_pin(unquote(compile_ast(pin_number)), 0, 0)
end
end
end
# not actually used
compile :channel, %{channel_name: _channel_name} do
quote do
nothing()
end
end
# not actually used
compile :explanation, %{message: _message} do
quote do
nothing()
end
end
# not actually used
compile :rpc_ok, %{label: _label} do
quote do
nothing()
end
end
# not actually used
compile :rpc_error, %{label: _label}, _body do
quote do
nothing()
# Add code here
end
end
# not actually used
compile :pair, %{label: _label, value: _value} do
quote do
nothing()
end
end
# not actually used
compile :scope_declaration, _args, _body do
quote do
nothing()
end
end
@doc """
Recursively compiles a list or single Celery AST into an Elixir `__block__`
@ -430,20 +668,32 @@ defmodule Farmbot.CeleryScript.Compiler do
[variable_in_next_scope: variable_in_this_scope]
"""
def compile_param_application(list, acc \\ [])
def compile_params_to_function_args(list, acc \\ [])
def compile_param_application(
[%{args: %{label: next_scope, data_value: %{args: %{label: current_scope}}}} | rest],
def compile_params_to_function_args(
[%{kind: :variable_declaration, args: args} | rest],
acc
) do
var = {String.to_atom(next_scope), {String.to_atom(current_scope), [], __MODULE__}}
compile_param_application(rest, [var | acc])
%{
label: next_scope,
data_value: data_value
} = args
next_scope = IdentifierSanitizer.to_variable(next_scope)
var =
quote do
# {next_scope, current_scope}
unquote({next_scope, compile_ast(data_value)})
end
compile_params_to_function_args(rest, [var | acc])
end
def compile_param_application([], acc), do: acc
def compile_params_to_function_args([], acc), do: acc
@doc """
Compiles a function blocks params.
Compiles a function block's params.
# Example
A `sequence`s `locals` that look like
@ -455,7 +705,13 @@ defmodule Farmbot.CeleryScript.Compiler do
"kind": "parameter_declaration",
"args": {
"label": "parent",
"data_type": "point"
"default_value": {
"kind": "coordinate",
"args": {
"x": 100.0,
"y": 200.0,
"z": 300.0
}
}
}
]
@ -463,26 +719,67 @@ defmodule Farmbot.CeleryScript.Compiler do
Would be compiled to
parent = Keyword.fetch!(params, :parent)
parent = Keyword.get(params, :parent, %{x: 100, y: 200, z: 300})
"""
def compile_param_declaration(%{args: %{label: var_name, data_type: _type}}) do
var_fetch =
quote do
Keyword.fetch!(params, unquote(String.to_atom(var_name)))
end
# Add parameter_declaration to the list of implemented kinds
@kinds "parameter_declaration"
def compile_param_declaration(%{args: %{label: var_name, default_value: default}}) do
var_name = IdentifierSanitizer.to_variable(var_name)
{:=, [], [{String.to_atom(var_name), [], __MODULE__}, var_fetch]}
quote do
unquote({var_name, [], __MODULE__}) =
Keyword.get(params, unquote(var_name), unquote(compile_ast(default)))
end
end
@doc """
Compiles a function block's assigned value.
# Example
A `sequence`s `locals` that look like
{
"kind": "scope_declaration",
"args": {},
"body": [
{
"kind": "parameter_application",
"args": {
"label": "parent",
"data_value": {
"kind": "coordinate",
"args": {
"x": 100.0,
"y": 200.0,
"z": 300.0
}
}
}
}
]
}
"""
# Add parameter_application to the list of implemented kinds
@kinds "parameter_application"
def compile_param_application(%{args: %{label: var_name, data_value: value}}) do
var_name = IdentifierSanitizer.to_variable(var_name)
quote do
unquote({var_name, [], __MODULE__}) = unquote(compile_ast(value))
end
end
defp decompose_block_to_steps({:__block__, _, steps} = _orig) do
Enum.map(steps, fn step ->
IO.inspect(step, label: "STEP")
quote do
fn ->
unquote(step)
end
fn -> unquote(step) end
end
end)
end
defp delete_me(compiled) do
compiled
|> Macro.to_string()
|> Code.format_string!()
|> IO.puts()
end
end

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
defmodule Farmbot.CeleryScript.RunTime.Error do
@moduledoc """
CSVM runtime error
"""
defexception [:message, :farm_proc]
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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