Update AMQP workers to reconnect on a 4 second timer

Pull in new csvm implementation

Update circleci config

Implement syncing and write_pin

add migration for syncing

add saftey to write_pin

Implement read_pin

Implement set_servo_angle

Implement more ast nodes

Implement e-stop and e-unlock

Fix a bunch of stuf

Fix missing assets on boot/init

It actually works

Rename csvm -> farmbot_celery_script; fix initial sync/dispatch

Fix a bunch of small bugs

Identify problem

Fix Sqlite bug and increase performance by 10 times!!

Fix sequences inside of sequences
pull/974/head
connor rigby 2018-08-01 12:51:08 -07:00 committed by Connor Rigby
parent cf1ef23b17
commit 358a1e209e
No known key found for this signature in database
GPG Key ID: 29A88B24B70456E0
118 changed files with 5720 additions and 464 deletions

1
ELIXIR_VERSION 100644
View File

@ -0,0 +1 @@
~> 1.6

View File

@ -18,6 +18,10 @@ all: help
help:
@echo "no"
farmbot_celery_script_clean:
cd farmbot_celery_script && \
rm -rf _build deps
farmbot_core_clean:
cd farmbot_core && \
make clean && \
@ -35,7 +39,7 @@ farmbot_os_clean:
cd farmbot_os && \
rm -rf _build deps
clean: farmbot_core_clean farmbot_ext_clean farmbot_os_clean
clean: farmbot_celery_script_clean farmbot_core_clean farmbot_ext_clean farmbot_os_clean
farmbot_core_test:
cd farmbot_core && \

View File

@ -0,0 +1,73 @@
# All CeleryScript Nodes
This list is split into three categories.
* RPC Nodes - Nodes that control Farmbot's state, but don't don't command
a real world side effect. This includes:
* updating configuration data.
* syncing.
* starting/stopping a process of some sort.
* rebooting
* Command Nodes - Nodes that physically do something. This includes:
* moving the gantry.
* writing or reading a GPIO.
* Data Nodes - Nodes that simply contain data. They are not to be executed.
This includes:
* explanation
* location data
## RPC Nodes
| Name | Args | Body |
|:-------------------------------|:-------------------------------------:|:-------------------------:|
| `check_updates` | `package` | --- |
| `config_update` | `package` | `pair` |
| `uninstall_farmware` | `package` | --- |
| `update_farmware` | `package` | --- |
| `rpc_request` | `label` | more command or rpc nodes |
| `rpc_ok` | `label` | --- |
| `rpc_error` | `label` | `explanation` |
| `install farmware` | `url` | --- |
| `read_status` | --- | --- |
| `sync` | --- | --- |
| `power_off` | --- | --- |
| `reboot` | --- | --- |
| `factory_reset` | --- | --- |
| `set_usr_env` | --- | `pair` |
| `install_first_party_farmware` | --- | --- |
| `change_ownership` | --- | `pair` |
| `dump_info` | --- | --- |
## Command Nodes
| Name | Args | Body |
|:-------------------------------|:-------------------------------------:|:-------------------------:|
| `_if` | `lhs`, `op`, `rhs`, `_then`, `_else` | `pair` |
| `write_pin` | `pin_number`, `pin_value`, `pin_mode` | --- |
| `read_pin` | `pin_number`, `pin_value`, `pin_mode` | --- |
| `move_absolute` | `location`, `speed`, `offset` | --- |
| `set_servo_angle` | `pin_number`, `pin_value` | --- |
| `send_message` | `message`, `message_type` | `channel` |
| `move_relative` | `speed`, `x`, `y`, `z` | --- |
| `sequence` | `version`, `locals` | more command nodes |
| `home` | `speed`, `axis` | --- |
| `find_home` | `speed`, `axis` | --- |
| `wait` | `milliseconds` | --- |
| `execute` | `sequence_id` | --- |
| `toggle_pin` | `pin_number` | --- |
| `execute_script` | `package` | `pair` |
| `zero` | `axis` | --- |
| `calibrate` | `axis` | --- |
| `emergency_lock` | --- | --- |
| `emergency_unlock` | --- | --- |
| `take_photo` | --- | --- |
## Data Nodes
| Name | Args | Body |
|:-------------------------------|:-------------------------------------:|:-------------------------:|
| `point` | `pointer_type`, `pointer_id` | --- |
| `named_pin` | `pin_type`, `pin_id` | --- |
| `pair` | `label`, `value` | --- |
| `channel` | `channel_name` | --- |
| `coordinate` | `x`, `y`, `z` | --- |
| `tool` | `tool_id` | --- |
| `explanation` | `message` | --- |
| `identifier` | `label` | --- |
| `nothing` | --- | --- |
| `scope_declaration` | --- | `paramater_decleration`, `variable_decleration` |

View File

@ -0,0 +1,70 @@
# CeleryScript
CeleryScript is an AST definition of commands, rpcs, and functions that
can all be executed by Farmbot. The basic syntax is as follows:
```elixir
%{
kind: :some_command,
args: %{non_order_arg1: 1, non_order_arg2: "data"},
body: []
}
```
Note the three main fields: `kind`, `args` and `body`.
There is also another field `comment` that is optional. While technically
optional, `body` should be supplied when working with any and all modules
in this project.
## kind
`kind` is the identifier for a command. Examples include:
* `move_absolute`
* `sync`
* `read_status`
* `wait`
Each `kind` will have it's own set of rules for execution. These rules will
define what is required inside of both `args` and `body`.
## args
`args` is arguments to be passed to `kind`. Each `kind` defines it's own
set of optional and required `args`. Args can any of the following types:
* `number`
* `string` (with possible enum types)
* `boolean`
* another AST.
in the case of another AST, that AST will likely need to be evaluated before
executing the parent AST. Examples of `args` include:
* `x`
* `y`
* `z`
* `location`
* `milliseconds`
## body
`body` is the only way a `list` or `array` type is aloud in CeleryScript.
It may only contain _more CeleryScript nodes_. This is useful for
enumeration, scripting looping etc. Here's a syntacticly correct example:
```elixir
%{
kind: :script,
args: %{},
body: [
%{kind: :command, args: %{x: 1}, body: []}
%{kind: :command, args: %{x: 2}, body: []}
%{kind: :command, args: %{x: 3}, body: []}
]
}
```
Note there is nesting limit for CeleryScript body nodes, and nodes can
even be self referential. Example:
```elixir
%{
kind: :self_referencing_script,
args: %{id: 1},
body: [
%{kind: :execute_self_referencing_script, args: %{id: 1}, body: []}
]
}
```

View File

@ -0,0 +1,27 @@
# The directory Mix will write compiled artifacts to.
/_build/
# If you run "mix test --cover", coverage assets end up here.
/cover/
# The directory Mix downloads your dependencies sources to.
/deps/
# Where 3rd-party dependencies like ExDoc output generated docs.
/doc/
# Ignore .fetch files in case you like to edit your project deps locally.
/.fetch
# If the VM crashes, it generates a dump, let's ignore it too.
erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez
# Ignore package tarball (built via "mix hex.build").
farmbot_ng-*.tar
*.sqlite3
*.so
*.hex

View File

@ -0,0 +1 @@
../.tool-versions

View File

@ -0,0 +1,51 @@
{
"id": 123,
"kind": "sequence",
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {},
"body": [{
"kind": "variable_declaration",
"args": {
"label": "var1",
"data_value": {
"kind": "point",
"args": {
"pointer_type": "GenericPointer",
"pointer_id": 123
}
}
}
}]
}
},
"body": [{
"kind": "wait",
"args": {
"milliseconds": 1050
}
},
{
"kind": "move_absolute",
"args": {
"speed": 100,
"location": {
"kind": "identifier",
"args": {
"label": "var1"
}
},
"offset": {
"kind": "coordinate",
"args": {
"x": 0,
"y": 0,
"z": 0
}
}
}
}
]
}

View File

@ -0,0 +1,194 @@
{
"kind": "sequence",
"name": "Test Sequence (TM)",
"color": "red",
"id": 2,
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {}
}
},
"body": [{
"kind": "move_absolute",
"args": {
"speed": 100,
"offset": {
"kind": "coordinate",
"args": {
"y": 20,
"x": 10,
"z": -30
}
},
"location": {
"kind": "point",
"args": {
"pointer_type": "Plant",
"pointer_id": 1
}
}
}
},
{
"kind": "move_relative",
"args": {
"x": 10,
"y": 20,
"z": 30,
"speed": 50
},
"comment": "Slow move"
},
{
"kind": "write_pin",
"args": {
"pin_number": 0,
"pin_value": 0,
"pin_mode": 0
}
},
{
"kind": "write_pin",
"args": {
"pin_mode": 0,
"pin_value": 1,
"pin_number": {
"kind": "named_pin",
"args": {
"pin_type": "Peripheral",
"pin_id": 5
}
}
}
},
{
"kind": "read_pin",
"args": {
"pin_mode": 0,
"label": "---",
"pin_number": 0
}
},
{
"kind": "read_pin",
"args": {
"pin_mode": 1,
"label": "---",
"pin_number": {
"kind": "named_pin",
"args": {
"pin_type": "Sensor",
"pin_id": 1
}
}
}
},
{
"kind": "wait",
"args": {
"milliseconds": 100
}
},
{
"kind": "send_message",
"args": {
"message": "FarmBot is at position {{ x }}, {{ y }}, {{ z }}.",
"message_type": "success"
},
"body": [{
"kind": "channel",
"args": {
"channel_name": "toast"
}
},
{
"kind": "channel",
"args": {
"channel_name": "email"
}
},
{
"kind": "channel",
"args": {
"channel_name": "espeak"
}
}
]
},
{
"kind": "find_home",
"args": {
"speed": 100,
"axis": "all"
}
},
{
"kind": "_if",
"args": {
"rhs": 0,
"op": "is_undefined",
"lhs": "x",
"_then": {
"kind": "execute",
"args": {
"sequence_id": 1
}
},
"_else": {
"kind": "nothing",
"args": {}
}
}
},
{
"kind": "_if",
"args": {
"rhs": 500,
"op": ">",
"_then": {
"kind": "nothing",
"args": {}
},
"_else": {
"kind": "execute",
"args": {
"sequence_id": 1
}
},
"lhs": {
"kind": "named_pin",
"args": {
"pin_type": "Sensor",
"pin_id": 2
}
}
}
},
{
"kind": "execute",
"args": {
"sequence_id": 1
}
},
{
"kind": "execute_script",
"args": {
"label": "plant-detection"
},
"body": [{
"kind": "pair",
"args": {
"value": 0,
"label": "plant_detection_input"
},
"comment": "Input"
}]
},
{
"kind": "take_photo",
"args": {}
}
]
}

View File

@ -0,0 +1,57 @@
{
"id": 456,
"kind": "sequence",
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {},
"body": [{
"kind": "variable_declaration",
"args": {
"label": "var1",
"data_value": {
"kind": "point",
"args": {
"pointer_type": "Plant",
"pointer_id": 456
}
}
}
}]
}
},
"body": [{
"kind": "wait",
"args": {
"milliseconds": 1000
}
},
{
"kind": "move_absolute",
"args": {
"speed": 100,
"location": {
"kind": "identifier",
"args": {
"label": "var1"
}
},
"offset": {
"kind": "coordinate",
"args": {
"x": 0,
"y": 0,
"z": 0
}
}
}
},
{
"kind": "execute",
"args": {
"sequence_id": 123
}
}
]
}

View File

@ -0,0 +1,51 @@
{
"id": 123,
"kind": "sequence",
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {},
"body": [{
"kind": "variable_declaration",
"args": {
"label": "var1",
"data_value": {
"kind": "point",
"args": {
"pointer_type": "GenericPointer",
"pointer_id": 123
}
}
}
}]
}
},
"body": [{
"kind": "wait",
"args": {
"milliseconds": 1050
}
},
{
"kind": "move_absolute",
"args": {
"speed": 100,
"location": {
"kind": "identifier",
"args": {
"label": "var20"
}
},
"offset": {
"kind": "coordinate",
"args": {
"x": 0,
"y": 0,
"z": 0
}
}
}
}
]
}

View File

@ -0,0 +1,90 @@
{
"outter": {
"id": 456,
"kind": "sequence",
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {},
"body": [
{
"kind": "variable_declaration",
"args": {
"label": "x",
"data_value": {
"kind": "point",
"args": {
"pointer_type": "Plant",
"pointer_id": 456
}
}
}
}
]
}
},
"body": [
{
"kind": "move_absolute",
"args": {
"speed": 100,
"location": {
"kind": "identifier",
"args": {
"label": "x"
}
},
"offset": {
"kind": "coordinate",
"args": {
"x": 0,
"y": 0,
"z": 0
}
}
}
},
{
"kind": "execute",
"args": {
"sequence_id": 123
}
}
]
},
"inner": {
"id": 123,
"kind": "sequence",
"args": {
"version": 20180209,
"locals": {
"kind": "scope_declaration",
"args": {},
"body": []
}
},
"body": [
{
"kind": "move_absolute",
"args": {
"speed": 100,
"location": {
"kind": "identifier",
"args": {
"label": "x"
}
},
"offset": {
"kind": "coordinate",
"args": {
"x": 0,
"y": 0,
"z": 0
}
}
}
}
]
}
}

View File

@ -0,0 +1,31 @@
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

@ -0,0 +1,93 @@
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,89 @@
defmodule Farmbot.CeleryScript.AST do
@moduledoc """
Handy functions for turning various data types into Farbot Celery Script
Ast nodes.
"""
alias Farmbot.CeleryScript.AST
alias AST.{Heap, Slicer, Unslicer}
@typedoc "Arguments to a ast node."
@type args :: map
@typedoc "Body of a ast node."
@type body :: [t]
@typedoc "Kind of a ast node."
@type kind :: module
@typedoc "AST node."
@type t :: %__MODULE__{
kind: kind,
args: args,
body: body,
comment: binary
}
defstruct [:args, :body, :kind, :comment]
@doc "Decode a base map into CeleryScript AST."
@spec decode(t() | map) :: t()
def decode(map_or_list_of_maps)
def decode(%{__struct__: _} = thing) do
thing |> Map.from_struct() |> decode
end
def decode(%{} = thing) do
kind = thing["kind"] || thing[:kind] || raise("Bad ast: #{inspect(thing)}")
args = thing["args"] || thing[:args] || raise("Bad ast: #{inspect(thing)}")
body = thing["body"] || thing[:body] || []
comment = thing["comment"] || thing[:comment] || nil
%__MODULE__{
kind: String.to_atom(to_string(kind)),
args: decode_args(args),
body: decode_body(body),
comment: comment
}
end
def decode(bad_ast), do: raise("Bad ast: #{inspect(bad_ast)}")
# You can give a list of nodes.
@spec decode_body([map]) :: [t()]
def decode_body(body) when is_list(body) do
Enum.map(body, fn itm ->
decode(itm)
end)
end
@spec decode_args(map) :: args
def decode_args(map) when is_map(map) do
Enum.reduce(map, %{}, fn {key, val}, acc ->
if is_map(val) do
# if it is a map, it could be another node so decode it too.
real_val = decode(val)
Map.put(acc, String.to_atom(to_string(key)), real_val)
else
Map.put(acc, String.to_atom(to_string(key)), val)
end
end)
end
@spec new(atom, map, [map]) :: t()
def new(kind, args, body) when is_map(args) and is_list(body) do
%__MODULE__{
kind: String.to_atom(to_string(kind)),
args: args,
body: body
}
|> 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)
end

View File

@ -0,0 +1,98 @@
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
}
def link, do: @link
def parent, do: @parent
def body, do: @body
def next, do: @next
def kind, do: @kind
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

@ -0,0 +1,115 @@
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 "Sice 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

@ -0,0 +1,78 @@
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

@ -0,0 +1,421 @@
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_aloud_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 [
:proc_storage,
:hyper_state,
:fw_proc,
:process_io_layer,
:hyper_io_layer,
:tick_timer,
:monitors
]
defmodule Monitor do
defstruct [:pid, :ref, :index]
def new(pid, index) do
ref = Process.monitor(pid)
%Monitor{ref: ref, pid: pid, index: index}
end
end
@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)
job = queue(pid, map, -1)
if job do
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
else
# if no job is returned, this was a hyper function, which
# can never fail.
results = ast(:rpc_ok, %{label: label}, [])
apply_callback(fun, [results])
end
end
@doc "Execute a sequence. This is async."
def sequence(pid \\ __MODULE__, %{} = map, id, fun) when is_function(fun) do
job = queue(pid, map, id)
spawn(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
# 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
# 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) do
case GenServer.call(pid, {:lookup, job_id}) do
{:error, :busy} ->
await(pid, job_id)
%FarmProc{} = proc ->
case FarmProc.get_status(proc) do
status when status in [:ok, :waiting] ->
Process.sleep(@tick_timeout * 2)
await(pid, job_id)
_ ->
proc
end
_ ->
raise(ArgumentError, "no job by that identifier")
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
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{
monitors: %{},
process_io_layer: io_fun,
hyper_io_layer: hyper_fun,
tick_timer: 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}, {caller, _ref}, %RunTime{} = state) do
%FarmProc{} = new_proc = FarmProc.new(state.process_io_layer, p, h)
index = ProcStorage.insert(state.proc_storage, new_proc)
mon = Monitor.new(caller, index)
{:reply, index, %{state | monitors: Map.put(state.monitors, caller, mon)}}
end
def handle_call({:lookup, id}, _from, %RunTime{} = state) do
cleanup = fn proc, state ->
ProcStorage.delete(state.proc_storage, id)
state = case Enum.find(state.monitors, fn({_, %{index: index}}) -> index == id end) do
{pid, mon} ->
Process.demonitor(mon.ref)
%{state | monitors: Map.delete(state.monitors, pid)}
_ -> state
end
state = if proc.ref == state.fw_proc,
do: %{state | fw_proc: nil},
else: state
{:reply, proc, state}
end
# Looks up a FarmProc, causes a few different side affects.
# if the status is :done or :crashed, delete it from ProcStorage.
# if deleted, and this proc owns the firmware,
# delete it from there also.
case ProcStorage.lookup(state.proc_storage, id) do
%FarmProc{status: :crashed} = proc ->
cleanup.(proc, state)
%FarmProc{status: :done} = proc ->
cleanup.(proc, state)
reply ->
{:reply, reply, state}
end
end
def handle_info({:DOWN, _ref, :process, pid, _}, %RunTime{} = state) do
cleanup = fn proc, id, state ->
ProcStorage.delete(state.proc_storage, id)
if proc.ref == state.fw_proc,
do: %{state | fw_proc: nil},
else: state
end
mon = state.monitors[pid]
state = if mon do
case ProcStorage.lookup(state.proc_storage, mon.index) do
%FarmProc{status: :crashed} = proc -> cleanup.(proc, mon.index, state)
%FarmProc{status: :done} = proc -> cleanup.(proc, mon.index, state)
_ -> state
end
else
state
end
{:noreply, %{state | monitors: Map.delete(state.monitors, pid)}}
end
def handle_info({:DOWN, _, :process, _, _} = down, {:busy, _} = busy) do
Process.send_after(self(), down, @tick_timeout)
{:noreply, busy}
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
new_timer = start_tick(self())
{:noreply, %RunTime{state | tick_timer: new_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_aloud_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_aloud_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 ->
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

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

View File

@ -0,0 +1,278 @@
defmodule Farmbot.CeleryScript.RunTime.FarmProc do
@moduledoc """
FarmProc is a _single_ running unit of execution. It must be
`step`ed. It manages IO, but does no sort of 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
}
# IO.puts "executing: [#{pc_ptr.page_address}, #{inspect pc_ptr.heap_address}] #{kind}"
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

@ -0,0 +1,7 @@
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

@ -0,0 +1,43 @@
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 ->
next_or_return(farm_proc)
{:error, reason} ->
crash(farm_proc, reason)
other ->
exception(farm_proc, "Bad return value: #{inspect(other)}")
end
end
end
end
end

View File

@ -0,0 +1,340 @@
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 "Execute a Farmware."
simple_io_instruction(:execute_script)
@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 "(Re)Install Farmware written and developed by Farmbot, Inc."
simple_io_instruction(:install_first_party_farmware)
@doc "Install a Farmware from the web."
simple_io_instruction(:install_farmware)
@doc "Remove a Farmware."
simple_io_instruction(:uninstall_farmware)
@doc "Update a Farmware."
simple_io_instruction(:update_farmware)
@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 "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
## 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

@ -0,0 +1,48 @@
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
end

View File

@ -0,0 +1,83 @@
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

@ -0,0 +1,63 @@
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 ->
{:error, Exception.message(ex)}
end
send(pid, {self(), result})
end
end

View File

@ -0,0 +1,25 @@
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

@ -0,0 +1,39 @@
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

@ -0,0 +1,53 @@
defmodule Farmbot.CeleryScript.RunTime.MixProject do
use Mix.Project
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim()
def project do
[
app: :farmbot_celery_script,
version: @version,
elixir: @elixir_version,
start_permanent: Mix.env() == :prod,
elixirc_paths: elixirc_paths(Mix.env()),
deps: deps(),
dialyzer: [
flags: [
"-Wunmatched_returns",
:error_handling,
:race_conditions,
:underspecs
]
],
test_coverage: [tool: ExCoveralls],
preferred_cli_env: [
test: :test,
coveralls: :test,
"coveralls.circle": :test,
"coveralls.detail": :test,
"coveralls.post": :test,
"coveralls.html": :test
]
]
end
def elixirc_paths(:test), do: ["lib", "./test/support"]
def elixirc_paths(_), do: ["lib"]
# Run "mix help compile.app" to learn about applications.
def application do
[
extra_applications: [:logger]
]
end
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:excoveralls, "~> 0.9", only: [:test]},
{:dialyxir, "~> 1.0.0-rc.3", only: [:dev], runtime: false},
{:ex_doc, "~> 0.19", only: [:dev], runtime: false},
{:jason, "~> 1.1", only: [:test, :dev]}
]
end
end

View File

@ -0,0 +1,18 @@
%{
"certifi": {:hex, :certifi, "2.3.1", "d0f424232390bf47d82da8478022301c561cf6445b5b5fb6a84d49a9e76d2639", [:rebar3], [{:parse_trans, "3.2.0", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.2.5", "4d21980d5d2862a2e13ec3c49ad9ad783ffc7ca5769cf6ff891a4553fbaae761", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.0", "e22b6434373b4870ea77b24df069dbac7002c1f483615e9ebfc0c37497e1c75c", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"hackney": {:hex, :hackney, "1.13.0", "24edc8cd2b28e1c652593833862435c80661834f6c9344e84b6a2255e7aeef03", [:rebar3], [{:certifi, "2.3.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "5.1.2", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"idna": {:hex, :idna, "5.1.2", "e21cb58a09f0228a9e0b95eaa1217f1bcfc31a1aaa6e1fdf2f53a33f7dbd9494", [:rebar3], [{:unicode_util_compat, "0.3.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"},
"jason": {:hex, :jason, "1.1.1", "d3ccb840dfb06f2f90a6d335b536dd074db748b3e7f5b11ab61d239506585eb2", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
"makeup": {:hex, :makeup, "0.5.1", "966c5c2296da272d42f1de178c1d135e432662eca795d6dc12e5e8787514edf7", [:mix], [{:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"makeup_elixir": {:hex, :makeup_elixir, "0.8.0", "1204a2f5b4f181775a0e456154830524cf2207cf4f9112215c05e0b76e4eca8b", [:mix], [{:makeup, "~> 0.5.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 0.2.2", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"},
"mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.2.2", "d526b23bdceb04c7ad15b33c57c4526bf5f50aaa70c7c141b4b4624555c68259", [:mix], [], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.2.0", "2adfa4daf80c14dc36f522cf190eb5c4ee3e28008fc6394397c16f62a26258c2", [:rebar3], [], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], [], "hexpm"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.3.1", "a1f612a7b512638634a603c8f401892afbf99b8ce93a45041f8aaca99cadb85e", [:rebar3], [], "hexpm"},
}

View File

@ -0,0 +1,17 @@
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

@ -0,0 +1,71 @@
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

@ -0,0 +1,70 @@
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

@ -0,0 +1,218 @@
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

@ -0,0 +1,25 @@
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

@ -0,0 +1,28 @@
defmodule Farmbot.CeleryScript.ASTTest do
use ExUnit.Case, async: true
alias Farmbot.CeleryScript.AST
@nothing_json "{\"kind\": \"nothing\", \"args\": {}}"
|> Jason.decode!()
@nothing_json_with_body "{\"kind\": \"nothing\", \"args\": {}, \"body\":[#{
Jason.encode!(@nothing_json)
}]}"
|> Jason.decode!()
@bad_json "{\"whoops\": "
test "decodes ast from json" do
res = AST.decode(@nothing_json)
assert match?(%AST{}, res)
end
test "decodes ast with sub asts in the body" do
res = AST.decode(@nothing_json_with_body)
assert match?(%AST{}, res)
end
test "won't decode ast from bad json" do
assert_raise RuntimeError, fn ->
AST.decode(@bad_json)
end
end
end

View File

@ -0,0 +1,599 @@
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

@ -0,0 +1,191 @@
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("execute_script")
io_test("zero")
io_test("calibrate")
io_test("take_photo")
io_test("config_update")
io_test("set_user_env")
io_test("install_first_party_farmware")
io_test("install_farmware")
io_test("uninstall_farmware")
io_test("update_farmware")
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

@ -0,0 +1,32 @@
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

@ -0,0 +1,139 @@
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

@ -0,0 +1,24 @@
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

@ -0,0 +1,23 @@
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

@ -0,0 +1,230 @@
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,8 @@
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

@ -0,0 +1,13 @@
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 @@
ExUnit.start()

View File

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -1 +0,0 @@
../.tool-versions

View File

@ -0,0 +1,2 @@
erlang 21.0.4
elixir 1.6.6-otp-21

View File

@ -1,3 +0,0 @@
# FarmbotCore
Core Farmbot Services.
This includes Logging, Configuration, Asset management and Firmware.

View File

@ -25,19 +25,16 @@ config :farmbot_core, Farmbot.Config.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_configs.sqlite3",
priv: "priv/config",
pool_size: 1
priv: "priv/config"
config :farmbot_core, Farmbot.Logger.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_logs.sqlite3",
priv: "priv/logger",
pool_size: 1
priv: "priv/logger"
config :farmbot_core, Farmbot.Asset.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_assets.sqlite3",
priv: "priv/asset",
pool_size: 1
priv: "priv/asset"

View File

@ -25,6 +25,46 @@ defmodule Farmbot.Asset do
alias Repo.Snapshot
require Farmbot.Logger
import Ecto.Query
import Farmbot.Config, only: [update_config_value: 4]
require Logger
@device_fields ~W(id name timezone)
@farm_events_fields ~W(calendar end_time executable_id executable_type id repeat start_time time_unit)
@peripherals_fields ~W(id label mode pin)
@pin_bindings_fields ~W(id pin_num sequence_id special_action)
@points_fields ~W(id meta name pointer_type tool_id x y z)
@regimens_fields ~W(farm_event_id id name regimen_items)
@sensors_fields ~W(id label mode pin)
@sequences_fields ~W(args body id kind name)
@tools_fields ~W(id name)
def to_asset(body, kind) when is_binary(kind) do
camel_kind = Module.concat(["Farmbot", "Asset", Macro.camelize(kind)])
to_asset(body, camel_kind)
end
def to_asset(body, Device), do: resource_decode(body, @device_fields, Device)
def to_asset(body, FarmEvent), do: resource_decode(body, @farm_events_fields, FarmEvent)
def to_asset(body, Peripheral), do: resource_decode(body, @peripherals_fields, Peripheral)
def to_asset(body, PinBinding), do: resource_decode(body, @pin_bindings_fields, PinBinding)
def to_asset(body, Point), do: resource_decode(body, @points_fields, Point)
def to_asset(body, Regimen), do: resource_decode(body, @regimens_fields, Regimen)
def to_asset(body, Sensor), do: resource_decode(body, @sensors_fields, Sensor)
def to_asset(body, Sequence), do: resource_decode(body, @sequences_fields, Sequence)
def to_asset(body, Tool), do: resource_decode(body, @tools_fields, Tool)
def resource_decode(data, fields, kind) when is_list(data),
do: Enum.map(data, &resource_decode(&1, fields, kind))
def resource_decode(data, fields, kind) do
data
|> Map.take(fields)
|> Enum.map(&string_to_atom/1)
|> into_struct(kind)
end
def string_to_atom({k, v}), do: {String.to_atom(k), v}
def into_struct(data, kind), do: struct(kind, data)
def fragment_sync(verbosity \\ 1) do
Farmbot.Logger.busy verbosity, "Syncing"
@ -43,6 +83,43 @@ defmodule Farmbot.Asset do
:ok
end
def full_sync(verbosity \\ 1, fetch_fun) do
Farmbot.Logger.busy verbosity, "Syncing"
Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :syncing})
results = try do
fetch_fun.()
rescue
ex ->
Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :sync_error})
message = Exception.message(ex)
Logger.error "Fetching resources failed: #{message}"
update_config_value(:bool, "settings", "needs_http_sync", true)
{:error, message}
end
case results do
{:ok, all_sync_cmds} when is_list(all_sync_cmds) ->
Repo.transaction fn() ->
:ok = Farmbot.Asset.clear_all_data()
for cmd <- all_sync_cmds do
apply_sync_cmd(cmd)
end
end
destroy_all_sync_cmds()
Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :synced})
Farmbot.Logger.success verbosity, "Synced"
update_config_value(:bool, "settings", "needs_http_sync", false)
:ok
{:error, reason} when is_binary(reason) ->
destroy_all_sync_cmds()
Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :sync_error})
Farmbot.Logger.error verbosity, "Sync error: #{reason}"
update_config_value(:bool, "settings", "needs_http_sync", true)
:ok
end
end
def apply_sync_cmd(cmd) do
mod = Module.concat(["Farmbot", "Asset", cmd.kind])
if Code.ensure_loaded?(mod) do
@ -64,7 +141,8 @@ defmodule Farmbot.Asset do
destroy_sync_cmd(cmd)
end
defp dispatch_sync(diff) do
@doc false
def dispatch_sync(diff) do
for deletion <- diff.deletions do
Farmbot.Registry.dispatch(__MODULE__, {:deletion, deletion})
end
@ -127,10 +205,17 @@ defmodule Farmbot.Asset do
Use the `Farmbot.Asset.Registry` for these types of events.
"""
def register_sync_cmd(remote_id, kind, body) when is_binary(kind) do
SyncCmd.changeset(struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body}))
new_sync_cmd(remote_id, kind, body)
|> SyncCmd.changeset()
|> Repo.insert!()
end
def new_sync_cmd(remote_id, kind, body)
when is_integer(remote_id) when is_binary(kind)
do
struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body})
end
@doc "Destroy all sync cmds locally."
def destroy_all_sync_cmds do
Repo.delete_all(SyncCmd)
@ -140,6 +225,7 @@ defmodule Farmbot.Asset do
Repo.all(SyncCmd)
end
def destroy_sync_cmd(%SyncCmd{id: nil} = cmd), do: {:ok, cmd}
def destroy_sync_cmd(%SyncCmd{} = cmd) do
Repo.delete(cmd)
end
@ -217,11 +303,21 @@ defmodule Farmbot.Asset do
Repo.one(from(p in Peripheral, where: p.id == ^peripheral_id))
end
@doc "Get a peripheral by it's pin."
def get_peripheral_by_number(number) do
Repo.one(from(p in Peripheral, where: p.pin == ^number))
end
@doc "Get a Sensor by it's id."
def get_sensor_by_id(sensor_id) do
Repo.one(from(s in Sensor, where: s.id == ^sensor_id))
end
@doc "Get a peripheral by it's pin."
def get_sensor_by_number(number) do
Repo.one(from(s in Sensor, where: s.pin == ^number))
end
@doc "Get a Sequence by it's id."
def get_sequence_by_id(sequence_id) do
Repo.one(from(s in Sequence, where: s.id == ^sequence_id))
@ -271,7 +367,7 @@ defmodule Farmbot.Asset do
@doc "Fetches all regimens that use a particular sequence."
def get_regimens_using_sequence(sequence_id) do
uses_seq = &match?(^sequence_id, Map.fetch!(&1, :sequence_id))
uses_seq = &match?(^sequence_id, Map.fetch!(&1, "sequence_id"))
Repo.all(Regimen)
|> Enum.filter(&Enum.find(Map.fetch!(&1, :regimen_items), uses_seq))

View File

@ -0,0 +1,31 @@
defmodule Farmbot.Asset.Logger do
use GenServer
require Logger
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
Farmbot.Registry.subscribe()
{:ok, %{status: :undefined}}
end
def handle_info({Farmbot.Registry, {Farmbot.Asset, {:sync_status, status}}}, %{status: status} = state) do
{:noreply, state}
end
def handle_info({Farmbot.Registry, {Farmbot.Asset, {:sync_status, status}}}, state) do
Logger.debug "Asset sync_status #{state.status} => #{status}"
{:noreply, %{state | status: status}}
end
def handle_info({Farmbot.Registry, {Farmbot.Asset, {action, data}}}, state) do
Logger.debug "Asset #{action} #{inspect data}"
{:noreply, state}
end
def handle_info({Farmbot.Registry, {_ns, _data}}, state) do
{:noreply, state}
end
end

View File

@ -0,0 +1,25 @@
defmodule Farmbot.Asset.OnStartTask do
alias Farmbot.Asset.Repo
alias Repo.Snapshot
require Logger
@doc false
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :dispatch, opts},
type: :worker,
restart: :transient,
shutdown: 500
}
end
def dispatch do
old = %Snapshot{}
new = Repo.snapshot()
diff = Snapshot.diff(old, new)
Farmbot.Asset.dispatch_sync(diff)
:ignore
end
end

View File

@ -21,7 +21,7 @@ defmodule Farmbot.Asset.Point do
@optional_fields [:tool_id]
def changeset(%Point{} = point, params \\ %{}) do
%Point{} = point
point
|> cast(params, @required_fields ++ @optional_fields)
|> validate_required(@required_fields)
end

View File

@ -3,7 +3,7 @@ defmodule Farmbot.Asset.Sensor do
Sensors are descriptors for pins/modes.
"""
alias Farmbot.Asset.Sensor
alias Farmbot.Asset.Sensor
use Ecto.Schema
import Ecto.Changeset
@ -16,7 +16,7 @@ defmodule Farmbot.Asset.Sensor do
@required_fields [:id, :pin, :mode, :label]
def changeset(%Sensor{} = sensor, params \\ %{}) do
%Sensor{} = sensor
sensor
|> cast(params, @required_fields)
|> validate_required(@required_fields)
|> unique_constraint(:id)

View File

@ -7,6 +7,7 @@ defmodule Farmbot.Asset.Sequence do
alias Farmbot.EctoTypes.TermType
use Ecto.Schema
import Ecto.Changeset
require Farmbot.Logger
schema "sequences" do
field(:name, :string)
@ -26,9 +27,14 @@ defmodule Farmbot.Asset.Sequence do
@behaviour Farmbot.Asset.FarmEvent
def schedule_event(%Sequence{} = sequence, _now) do
case Farmbot.CeleryScript.schedule_sequence(sequence) do
%{status: :crashed} = proc -> {:error, Csvm.FarmProc.get_crash_reason(proc)}
_ -> :ok
end
Farmbot.Logger.busy 1, "[#{sequence.name}] Sequence init."
Farmbot.Core.CeleryScript.sequence(sequence, fn(result) ->
case result do
:ok ->
Farmbot.Logger.success 1, "[#{sequence.name}] Sequence complete."
{:error, _} ->
Farmbot.Logger.error 1, "[#{sequence.nam}] Sequece failed!"
end
end)
end
end

View File

@ -8,11 +8,14 @@ defmodule Farmbot.Asset.Supervisor do
def init([]) do
children = [
{Farmbot.Asset.Repo, [] },
{Farmbot.Regimen.NameProvider, [] },
{Farmbot.FarmEvent.Supervisor, [] },
{Farmbot.Regimen.Supervisor, [] },
{Farmbot.PinBinding.Supervisor, [] },
{Farmbot.Asset.Logger, []},
{Farmbot.Asset.Repo, []},
{Farmbot.Regimen.NameProvider, []},
{Farmbot.FarmEvent.Supervisor, []},
{Farmbot.Regimen.Supervisor, []},
{Farmbot.PinBinding.Supervisor, []},
{Farmbot.Peripheral.Supervisor, []},
{Farmbot.Asset.OnStartTask, []},
]
Supervisor.init(children, [strategy: :one_for_one])
end

View File

@ -13,7 +13,7 @@ defmodule Farmbot.Asset.SyncCmd do
timestamps()
end
@required_fields [:remote_id, :kind]
@required_fields [:kind, :remote_id]
def changeset(%SyncCmd{} = cmd, params \\ %{}) do
cmd

View File

@ -111,8 +111,9 @@ defmodule Farmbot.BotState do
@doc false
def handle_call(:fetch, _from, state) do
Farmbot.Registry.dispatch(__MODULE__, state)
{:reply, state, [], state}
new_state = handle_event({:informational_settings, %{cache_bust: :rand.uniform(1000)}}, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)
{:reply, state, [], new_state}
end
# TODO(Connor) - Fix this to use event system.
@ -181,7 +182,7 @@ defmodule Farmbot.BotState do
{:noreply, [], new_state}
end
def handle_info({Farmbot.Registry, {Farmbot.Asset.Repo, {:sync_status, status}}}, state) do
def handle_info({Farmbot.Registry, {_, {:sync_status, status}}}, state) do
event = {:informational_settings, %{sync_status: status}}
new_state = handle_event(event, state)
Farmbot.Registry.dispatch(__MODULE__, new_state)

View File

@ -1,8 +1,8 @@
defmodule Farmbot.BotState.LocationData do
@moduledoc false
defstruct [
scaled_encoders: nil,
raw_encoders: nil,
position: nil
scaled_encoders: %{x: -1, y: -1, z: -1},
raw_encoders: %{x: -1, y: -1, z: -1},
position: %{x: -1, y: -1, z: -1}
]
end

View File

@ -1,18 +1,12 @@
defmodule Farmbot.CeleryScript do
def to_ast(data) do
Csvm.AST.decode(data)
defmodule Farmbot.Core.CeleryScript do
@moduledoc """
Helpers for executing CeleryScript.
"""
def rpc_request(data, fun) do
Farmbot.CeleryScript.RunTime.rpc_request(Farmbot.CeleryScript.RunTime, data, fun)
end
def execute_sequence(%Farmbot.Asset.Sequence{} = seq) do
schedule_sequence(seq)
|> await_sequence()
end
def schedule_sequence(%Farmbot.Asset.Sequence{} = seq) do
Csvm.queue(seq, seq.id)
end
def await_sequence(ref) do
Csvm.await(ref)
def sequence(%Farmbot.Asset.Sequence{} = seq, fun) do
Farmbot.CeleryScript.RunTime.sequence(Csvm, seq, seq.id, fun)
end
end

View File

@ -1,20 +0,0 @@
defmodule Farmbot.CeleryScript.CsvmWrapper do
@moduledoc false
@io_layer Application.get_env(:farmbot_core, :behaviour)[:celery_script_io_layer]
@io_layer || Mix.raise("No celery_script IO layer!")
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
def start_link(_args) do
Csvm.start_link([io_layer: &@io_layer.handle_io/1], name: Csvm)
end
end

View File

@ -1,3 +1,47 @@
defmodule Farmbot.CeleryScript.IOLayer do
@callback handle_io(Csvm.AST.t()) :: {:ok, Csvm.AST.t()} | :ok | {:error, String.t()}
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 install_first_party_farmware(args, body) :: :ok | {:error, String.t}
@callback install_farmware(args, body) :: :ok | {:error, String.t}
@callback uninstall_farmware(args, body) :: :ok | {:error, String.t}
@callback update_farmware(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

@ -0,0 +1,42 @@
defmodule Farmbot.Core.CeleryScript.RunTimeWrapper do
@moduledoc false
alias Farmbot.CeleryScript.AST
alias Farmbot.CeleryScript.RunTime
@io_layer Application.get_env(:farmbot_core, :behaviour)[: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: 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,9 +1,35 @@
defmodule Farmbot.CeleryScript.StubIOLayer do
@behaviour Farmbot.CeleryScript.IOLayer
def handle_io(ast) do
IO.puts "#{ast.kind} not implemented."
# {:error, "#{ast.kind} not implemented."}
:ok
end
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 install_farmware(_args, _body), do: {:error, "Stubbed"}
def install_first_party_farmware(_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 uninstall_farmware(_args, _body), do: {:error, "Stubbed"}
def update_farmware(_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,4 +1,4 @@
defmodule Farmbot.CeleryScript.Supervisor do
defmodule Farmbot.Core.CeleryScript.Supervisor do
@moduledoc false
use Supervisor
@ -8,7 +8,7 @@ defmodule Farmbot.CeleryScript.Supervisor do
def init([]) do
children = [
{Farmbot.CeleryScript.CsvmWrapper, []}
{Farmbot.Core.CeleryScript.RunTimeWrapper, []}
]
Supervisor.init(children, [strategy: :one_for_one])
end

View File

@ -0,0 +1,5 @@
defmodule Farmbot.Core.CeleryScript.Utils do
def new_vec3(x, y, z) do
%{x: x, y: y, z: z}
end
end

View File

@ -5,30 +5,18 @@ defmodule Farmbot.Core do
"""
use Application
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]},
type: :worker,
restart: :permanent,
shutdown: 500
}
end
@doc false
def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
def start_link(args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
def init([]) do
children = [
{Farmbot.Registry, [] },
{Farmbot.Logger.Supervisor, [] },
{Farmbot.Config.Supervisor, [] },
{Farmbot.Asset.Supervisor, [] },
{Farmbot.Firmware.Supervisor, [] },
{Farmbot.BotState, [] },
{Farmbot.CeleryScript.Supervisor, [] },
{Farmbot.Registry, []},
{Farmbot.Logger.Supervisor, []},
{Farmbot.Config.Supervisor, []},
{Farmbot.Firmware.Supervisor, []},
{Farmbot.Asset.Supervisor, []},
{Farmbot.BotState, []},
{Farmbot.Core.CeleryScript.Supervisor, []},
]
Supervisor.init(children, [strategy: :one_for_one])
end

View File

@ -22,12 +22,14 @@ defmodule Farmbot.Firmware do
end
@doc "Calibrate an axis."
def calibrate(axis) do
def calibrate(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
end
@doc "Find home on an axis."
def find_home(axis) do
def find_home(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
end
@ -37,12 +39,14 @@ defmodule Farmbot.Firmware do
end
@doc "Home an axis."
def home(axis) do
def home(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
end
@doc "Manually set an axis's current position to zero."
def zero(axis) do
def zero(axis) when is_binary(axis) do
axis = String.to_atom(axis)
GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
end
@ -107,6 +111,14 @@ defmodule Farmbot.Firmware do
GenStage.call(__MODULE__, :params_reported)
end
def get_pin_value(pin_num) do
GenStage.call(__MODULE__, {:call, {:get_pin_value, pin_num}})
end
def get_current_position do
GenStage.call(__MODULE__, {:call, :get_current_position})
end
@doc "Start the firmware services."
def start_link(args) do
GenStage.start_link(__MODULE__, args, name: __MODULE__)
@ -121,6 +133,11 @@ defmodule Farmbot.Firmware do
handler_mod: nil,
idle: false,
timer: nil,
location_data: %{
position: %{x: -1, y: -1, z: -1},
scaled_encoders: %{x: -1, y: -1, z: -1},
raw_encoders: %{x: -1, y: -1, z: -1},
},
pins: %{},
params: %{},
params_reported: false,
@ -155,6 +172,7 @@ defmodule Farmbot.Firmware do
def init([]) do
handler_mod =
Application.get_env(:farmbot_core, :behaviour)[:firmware_handler] || raise("No fw handler.")
|> IO.inspect(label: "FW Handler")
case handler_mod.start_link() do
{:ok, handler} ->
@ -165,9 +183,9 @@ defmodule Farmbot.Firmware do
struct(State, initial),
subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
}
{:error, reason} ->
:ignore ->
Farmbot.Logger.error 1, "Failed to initialize firmware. Falling back to stub implementation."
replace_firmware_handler(Farmbot.Firmware.StubHandler)
Farmbot.Logger.error 1, "Failed to initialize firmware: #{inspect reason} Falling back to stub implementation."
init([])
end
@ -181,7 +199,7 @@ defmodule Farmbot.Firmware do
unless :queue.is_empty(state.queue) do
list = :queue.to_list(state.queue)
for cmd <- list do
:ok = do_reply(%{state | current: cmd}, {:error, reason})
:ok = do_reply(%{state | current: cmd}, {:error, "Firmware handler crash"})
end
end
end
@ -212,7 +230,7 @@ defmodule Farmbot.Firmware do
:ok ->
timer = start_timer(current, state.timeout_ms)
{:noreply, [], %{state | current: current, timer: timer}}
{:error, _} = res ->
{:error, reason} = res when is_binary(reason) ->
do_reply(state, res)
{:noreply, [], %{state | current: nil, queue: :queue.new()}}
end
@ -229,13 +247,23 @@ defmodule Farmbot.Firmware do
end
end
def handle_call({:call, {:get_pin_value, pin_num}}, _from, state) do
{:reply, state.pins[pin_num], [], state}
end
def handle_call({:call, :get_current_position}, _from, state) do
{:reply, state.location_data.position, [], state}
end
def handle_call(:params_reported, _, state) do
{:reply, state.params_reported, [], state}
end
def handle_call({fun, _}, _from, state = %{initialized: false})
def handle_call({fun, args}, from, state = %{initialized: false})
when fun not in [:read_all_params, :update_param, :emergency_unlock, :emergency_lock, :request_software_version] do
{:reply, {:error, :uninitialized}, [], state}
next_current = struct(Command, from: from, fun: fun, args: args)
do_queue_cmd(next_current, state)
# {:reply, {:error, "uninitialized"}, [], state}
end
def handle_call({fun, args}, from, state) do
@ -244,7 +272,7 @@ defmodule Farmbot.Firmware do
cond do
fun == :emergency_lock ->
if current_current do
do_reply(state, {:error, :emergency_lock})
do_reply(state, {:error, "emergency_lock"})
end
do_begin_cmd(next_current, state, [])
match?(%Command{}, current_current) ->
@ -264,7 +292,7 @@ defmodule Farmbot.Firmware do
else
{:noreply, dispatch, %{state | current: current, timer: timer}}
end
{:error, _} = res ->
{:error, reason} = res when is_binary(reason) ->
do_reply(%{state | current: current}, res)
{:noreply, dispatch, %{state | current: nil}}
end
@ -281,11 +309,16 @@ defmodule Farmbot.Firmware do
# if after handling the current buffer of gcodes,
# Try to start the next command in the queue if it exists.
if List.last(gcodes) == :idle && state.current == nil do
case :queue.out(state.queue) do
{{:value, next_current}, new_queue} ->
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
{:empty, queue} -> # nothing to do if the queue is empty.
{:noreply, diffs, %{state | queue: queue}}
if state.initialized do
case :queue.out(state.queue) do
{{:value, next_current}, new_queue} ->
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
{:empty, queue} -> # nothing to do if the queue is empty.
{:noreply, diffs, %{state | queue: queue}}
end
else
Farmbot.Logger.warn 1, "Fw not initialized yet"
{:noreply, diffs, state}
end
else
{:noreply, diffs, state}
@ -314,7 +347,7 @@ defmodule Farmbot.Firmware do
maybe_cancel_timer(state.timer, state.current)
if state.current do
Farmbot.Logger.error 1, "Got #{code} while executing `#{inspect state.current}`."
do_reply(state, {:error, :firmware_error})
do_reply(state, {:error, "Firmware error. See log."})
{nil, %{state | current: nil}}
else
{nil, state}
@ -368,15 +401,21 @@ defmodule Farmbot.Firmware do
end
defp handle_gcode({:report_current_position, x, y, z}, state) do
{:location_data, %{position: %{x: x, y: y, z: z}}, state}
position = %{position: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, position)}
{:location_data, position, new_state}
end
defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
{:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state}
scaled_encoders = %{scaled_encoders: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, scaled_encoders)}
{:location_data, scaled_encoders, new_state}
end
defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
{:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state}
raw_encoders = %{raw_encoders: %{x: x, y: y, z: z}}
new_state = %{state | location_data: Map.merge(state.location_data, raw_encoders)}
{:location_data, raw_encoders, new_state}
end
defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
@ -444,17 +483,17 @@ defmodule Farmbot.Firmware do
end
defp handle_gcode(:report_axis_timeout_x, state) do
do_reply(state, {:error, :axis_timeout_x})
do_reply(state, {:error, "Axis X timeout"})
{nil, %{state | timer: nil}}
end
defp handle_gcode(:report_axis_timeout_y, state) do
do_reply(state, {:error, :axis_timeout_y})
do_reply(state, {:error, "Axis Y timeout"})
{nil, %{state | timer: nil}}
end
defp handle_gcode(:report_axis_timeout_z, state) do
do_reply(state, {:error, :axis_timeout_z})
do_reply(state, {:error, "Axis Z timeout"})
{nil, %{state | timer: nil}}
end
@ -487,9 +526,9 @@ defmodule Farmbot.Firmware do
maybe_cancel_timer(state.timer, state.current)
if state.current do
do_reply(state, :ok)
{:informational_settings, %{busy: true}, %{state | current: nil}}
{:informational_settings, %{busy: false}, %{state | current: nil}}
else
{:informational_settings, %{busy: true}, state}
{:informational_settings, %{busy: false}, state}
end
end
@ -582,7 +621,7 @@ defmodule Farmbot.Firmware do
:ok
_ -> report_calibration_callback(tries - 1, param, val)
end
{:error, reason} ->
{:error, reason} when is_binary(reason) ->
Farmbot.Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
report_calibration_callback(tries - 1, param, val)
end
@ -597,7 +636,7 @@ defmodule Farmbot.Firmware do
EstopTimer.cancel_timer()
:ok = GenServer.reply from, reply
%Command{fun: :emergency_lock, from: from} ->
:ok = GenServer.reply from, {:error, :emergency_lock}
:ok = GenServer.reply from, {:error, "Emergency Lock"}
%Command{fun: _fun, from: from} ->
# Farmbot.Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
:ok = GenServer.reply from, reply

View File

@ -5,7 +5,7 @@ defmodule Farmbot.Firmware.Supervisor do
@doc "Reinitializes the Firmware stack. Warning has MANY SIDE EFFECTS."
def reinitialize do
Farmbot.Firmware.UartHandler.AutoDetector.start_link([])
Supervisor.terminate_child(Farmbot.Bootstrap.Supervisor, Farmbot.Firmware.Supervisor)
Supervisor.terminate_child(Farmbot.Core, Farmbot.Firmware.Supervisor)
end
@doc false

View File

@ -14,7 +14,7 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do
alias Circuits.UART
alias Farmbot.Firmware.{UartHandler, StubHandler, Utils}
import Utils
require Farmbot.Logger
require Logger
use GenServer
#TODO(Connor) - Maybe make this configurable?
@ -48,12 +48,12 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do
case auto_detect() do
[dev] ->
dev = "/dev/#{dev}"
Farmbot.Logger.success 3, "detected target UART: #{dev}"
Logger.debug "detected target UART: #{dev}"
replace_firmware_handler(UartHandler)
Application.put_env(:farmbot_core, :uart_handler, tty: dev)
dev
_ ->
Farmbot.Logger.error 1, "Could not detect a UART device."
Logger.debug "Could not detect a UART device."
replace_firmware_handler(StubHandler)
:error
end

View File

@ -109,13 +109,13 @@ defmodule Farmbot.Firmware.UartHandler do
hw = get_config_value(:string, "settings", "firmware_hardware")
gen_stage_opts = [
dispatcher: GenStage.BroadcastDispatcher,
subscribe_to: [ConfigStorage.Dispatcher]
]
case open_tty(tty) do
{:ok, nerves} ->
{:producer_consumer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts}
err ->
{:stop, err}
{:producer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts}
{:error, reason} ->
Farmbot.Logger.error 1, "Uart handler failed to initialize: #{inspect reason}"
:ignore
end
end

View File

@ -19,6 +19,8 @@ defmodule Farmbot.Firmware.Utils do
@doc "Changes `:digital` => 0, and `:analog` => 1"
def extract_pin_mode(:digital), do: 0
def extract_pin_mode(:analog), do: 1
def extract_pin_mode(0), do: 0
def extract_pin_mode(1), do: 1
# https://github.com/arduino/Arduino/blob/2bfe164b9a5835e8cb6e194b928538a9093be333/hardware/arduino/avr/cores/arduino/Arduino.h#L43-L45

View File

@ -0,0 +1,14 @@
defmodule Farmbot.Peripheral.Supervisor do
use Supervisor
def start_link(args) do
Supervisor.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
children = [
{Farmbot.Peripheral.Worker, []}
]
Supervisor.init(children, [strategy: :one_for_one])
end
end

View File

@ -0,0 +1,38 @@
defmodule Farmbot.Peripheral.Worker do
use GenServer
alias Farmbot.{Asset, Registry}
import Farmbot.CeleryScript.Utils
alias Asset.Peripheral
require Farmbot.Logger
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
end
def init([]) do
Registry.subscribe()
{:ok, %{}}
end
def handle_info({Registry, {Asset, {:deletion, %Peripheral{}}}}, state) do
{:noreply, state}
end
def handle_info({Registry, {Asset, {_action, %Peripheral{label: label, id: id, mode: mode}}}}, state) do
named_pin = ast(:named_pin, %{pin_type: "Peripheral", pin_id: id})
read_pin = ast(:read_pin, %{pin_number: named_pin, label: label, pin_mode: mode})
request = ast(:rpc_request, %{label: label}, [read_pin])
Farmbot.Core.CeleryScript.rpc_request(request, fn(results) ->
case results do
%{kind: :rpc_ok} -> :ok
%{kind: :rpc_error, body: [%{args: %{message: message}}]} ->
Farmbot.Logger.error(1, "Error reading peripheral #{label} => #{message}")
end
end)
{:noreply, state}
end
def handle_info({Registry, _}, state) do
{:noreply, state}
end
end

View File

@ -154,20 +154,29 @@ defmodule Farmbot.PinBinding.Manager do
%{state | registered: Map.delete(state.registered, pin_num), signal: Map.delete(state.signal, pin_num)}
end
defp do_execute(%PinBinding{sequence_id: sequence_id}) when is_number(sequence_id) do
defp do_execute(%PinBinding{sequence_id: sequence_id} = binding) when is_number(sequence_id) do
sequence_id
|> Farmbot.Asset.get_sequence_by_id!()
|> Farmbot.CeleryScript.schedule_sequence()
|> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding))
end
defp do_execute(%PinBinding{special_action: action}) when is_binary(action) do
defp do_execute(%PinBinding{special_action: action} = binding) when is_binary(action) do
%Sequence{
id: 0,
name: action,
kind: action,
args: %{},
body: [] }
|> Farmbot.CeleryScript.schedule_sequence()
|> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding))
end
@doc false
def execute_results(:ok, binding) do
Farmbot.Logger.success(1, "Pin Binding #{binding} execution complete.")
end
def execute_results({:error, _}, binding) do
Farmbot.Logger.error(1, "Pin Binding #{binding} execution failed.")
end
defp debounce_timer(pin) do

View File

@ -3,7 +3,7 @@ defmodule Farmbot.Regimen.Manager do
require Farmbot.Logger
use GenServer
alias Farmbot.CeleryScript
alias Farmbot.Core.CeleryScript
alias Farmbot.Asset
alias Asset.Regimen
import Farmbot.Regimen.NameProvider
@ -134,7 +134,14 @@ defmodule Farmbot.Regimen.Manager do
defp do_item(item, regimen, state) do
if item do
sequence = Farmbot.Asset.get_sequence_by_id!(item.sequence_id)
CeleryScript.schedule_sequence(sequence)
CeleryScript.sequence(sequence, fn(results) ->
case results do
:ok ->
Farmbot.Logger.success(1, "[#{sequence.name}] executed by [#{regimen.name}] complete.")
{:error, _} ->
Farmbot.Logger.error(1, "[#{sequence.name}] executed by [#{regimen.name}] failed.")
end
end)
end
next_item = List.first(regimen.regimen_items)

View File

@ -1,9 +1,9 @@
defmodule FarmbotCore.MixProject do
use Mix.Project
@target System.get_env("MIX_TARGET") || "host"
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
@branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim()
defp commit do
System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
@ -20,7 +20,7 @@ defmodule FarmbotCore.MixProject do
[
app: :farmbot_core,
description: "The Brains of the Farmbot Project",
elixir: "~> 1.7",
elixir: @elixir_version,
make_clean: ["clean"],
make_env: make_env(),
make_cwd: __DIR__,
@ -56,6 +56,7 @@ defmodule FarmbotCore.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:farmbot_celery_script, path: "../farmbot_celery_script"},
# Arduino Firmware stuff.
{:elixir_make, "~> 0.4", runtime: false},
{:nerves_uart, "~> 1.2"},

View File

@ -2,7 +2,6 @@
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"csvm": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "f1543e8047934026747bd6dc57e9a20ce130d698", [branch: "integrate_csvm"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"},
@ -10,8 +9,6 @@
"ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.10.1", "407d50ac8fc63dfee9175ccb4548e6c5512b5052afa63eedb9cd452a32a91495", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"},
"hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},

View File

@ -0,0 +1,17 @@
defmodule Farmbot.Config.Repo.Migrations.AddNtpAndDnsConfigs do
use Ecto.Migration
import Farmbot.Config.MigrationHelpers
@default_ntp_server_1 Application.get_env(:farmbot_core, :default_ntp_server_1, "0.pool.ntp.org")
@default_ntp_server_2 Application.get_env(:farmbot_core, :default_ntp_server_2, "1.pool.ntp.org")
@default_dns_name Application.get_env(:farmbot_core, :default_dns_name, "nerves-project.org")
if is_nil(@default_ntp_server_1), do: raise("Missing application env config: `:default_ntp_server_1`")
if is_nil(@default_ntp_server_2), do: raise("Missing application env config: `:default_ntp_server_2`")
if is_nil(@default_dns_name), do: raise("Missing application env config: `:default_dns_name`")
def change do
create_settings_config("default_ntp_server_1", :string, @default_ntp_server_1)
create_settings_config("default_ntp_server_2", :string, @default_ntp_server_2)
create_settings_config("default_dns_name", :string, @default_dns_name)
end
end

View File

@ -0,0 +1,8 @@
defmodule Farmbot.Config.Repo.Migrations.NeetsHttpSync do
use Ecto.Migration
import Farmbot.Config.MigrationHelpers
def change do
create_settings_config("needs_http_sync", :bool, true)
end
end

View File

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -1,21 +0,0 @@
# FarmbotExt
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `farmbot_ext` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:farmbot_ext, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at [https://hexdocs.pm/farmbot_ext](https://hexdocs.pm/farmbot_ext).

View File

@ -34,22 +34,19 @@ config :farmbot_core, Farmbot.Config.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_configs.sqlite3",
priv: "../farmbot_core/priv/config",
pool_size: 1
priv: "../farmbot_core/priv/config"
config :farmbot_core, Farmbot.Logger.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_logs.sqlite3",
priv: "../farmbot_core/priv/logger",
pool_size: 1
priv: "../farmbot_core/priv/logger"
config :farmbot_core, Farmbot.Asset.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: ".#{Mix.env}_assets.sqlite3",
priv: "../farmbot_core/priv/asset",
pool_size: 1
priv: "../farmbot_core/priv/asset"
config :farmbot_ext, :behaviour,
authorization: Farmbot.Bootstrap.Authorization,

View File

@ -2,7 +2,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
use GenServer
use AMQP
require Farmbot.Logger
import Farmbot.Config, only: [get_config_value: 3]
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
@exchange "amq.topic"
@ -21,10 +21,14 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
{:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_auto_sync", [auto_delete: false])
:ok = AMQP.Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, [routing_key: "bot.#{jwt.bot}.sync.#"])
{:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), [no_ack: true])
Farmbot.Registry.subscribe()
{:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])}
end
def terminate(_reason, _state) do
update_config_value(:bool, "settings", "needs_http_sync", true)
end
# Confirmation sent by the broker after registering this process as a consumer
def handle_info({:basic_consume_ok, _}, state) do
{:noreply, state}
@ -43,7 +47,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
device = state.bot
["bot", ^device, "sync", asset_kind, _id_str] = String.split(key, ".")
["bot", ^device, "sync", asset_kind, id_str] = String.split(key, ".")
data = Farmbot.JSON.decode!(payload)
body = data["body"]
case asset_kind do
@ -57,9 +61,15 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
"FirmwareConfig" ->
Farmbot.SettingsSync.apply_fw_map(Farmbot.Config.get_config_as_map()["hardware_params"], body)
_ ->
_cmd = Farmbot.Asset.register_sync_cmd(body["id"], asset_kind, body)
if get_config_value(:bool, "settings", "auto_sync") do
Farmbot.Asset.fragment_sync()
if !get_config_value(:bool, "settings", "needs_http_sync") do
id = String.to_integer(id_str)
body = if body, do: Farmbot.Asset.to_asset(body, asset_kind), else: nil
_cmd = Farmbot.Asset.register_sync_cmd(id, asset_kind, body)
if get_config_value(:bool, "settings", "auto_sync") do
Farmbot.Asset.fragment_sync()
end
else
IO.puts "not accepting sync_cmd from amqp because bot needs http sync first."
end
end
@ -68,4 +78,10 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
{:noreply, state}
end
def handle_info({Farmbot.Registry, {Farmbot.Config, {"settings", "auto_sync", true}}}, state) do
Farmbot.AutoSyncTask.maybe_auto_sync()
{:noreply, state}
end
def handle_info({Farmbot.Registry, _}, state), do: {:noreply, state}
end

View File

@ -8,6 +8,10 @@ defmodule Farmbot.AMQP.BotStateTransport do
defstruct [:conn, :chan, :bot, :state_cache]
alias __MODULE__, as: State
def force do
GenServer.cast(__MODULE__, :force)
end
@doc false
def start_link(args) do
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
@ -21,11 +25,19 @@ defmodule Farmbot.AMQP.BotStateTransport do
{:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])}
end
def handle_cast(:force, %{state_cache: bot_state} = state) do
push_bot_state(state.chan, state.bot, bot_state)
{:noreply, state}
end
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, %{state_cache: bot_state} = state) do
# IO.puts "no state change"
{:noreply, state}
end
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do
# IO.puts "pushing state"
state.state_cache
cache = push_bot_state(state.chan, state.bot, bot_state)
{:noreply, %{state | state_cache: cache}}
end

View File

@ -2,6 +2,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
use GenServer
use AMQP
require Farmbot.Logger
require Logger
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
@exchange "amq.topic"
@ -48,28 +49,25 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
device = state.bot
["bot", ^device, "from_clients"] = String.split(key, ".")
reply = handle_celery_script(payload, state)
:ok = AMQP.Basic.publish state.chan, @exchange, "bot.#{device}.from_device", reply
spawn_link fn() ->
{_us, _results} = :timer.tc __MODULE__, :handle_celery_script, [payload, state]
# IO.puts "#{results.args.label} took: #{us}µs"
end
{:noreply, state}
end
@doc false
def handle_celery_script(payload, _state) do
def handle_celery_script(payload, state) do
json = Farmbot.JSON.decode!(payload)
%Farmbot.Asset.Sequence{
name: json["args"]["label"],
args: json["args"],
body: json["body"],
kind: "sequence",
id: -1}
|> Farmbot.CeleryScript.execute_sequence()
|> case do
%{status: :crashed} = proc ->
expl = %{args: %{message: Csvm.FarmProc.get_crash_reason(proc)}}
%{args: %{label: json["args"]["label"]}, kind: "rpc_error", body: [expl]}
_ ->
%{args: %{label: json["args"]["label"]}, kind: "rpc_ok"}
end
|> Farmbot.JSON.encode!()
# IO.inspect(json, label: "RPC_REQUEST")
Farmbot.Core.CeleryScript.rpc_request(json, fn(results_ast) ->
reply = Farmbot.JSON.encode!(results_ast)
if results_ast.kind == :rpc_error do
[%{args: %{message: message}}] = results_ast.body
Logger.error(message)
end
AMQP.Basic.publish state.chan, @exchange, "bot.#{state.bot}.from_device", reply
results_ast
end)
end
end

View File

@ -1,6 +1,7 @@
defmodule Farmbot.AMQP.ConnectionWorker do
use GenServer
require Farmbot.Logger
require Logger
import Farmbot.Config, only: [update_config_value: 4]
def start_link(args) do
@ -38,6 +39,12 @@ defmodule Farmbot.AMQP.ConnectionWorker do
username: bot,
password: token,
virtual_host: vhost]
AMQP.Connection.open(opts)
case AMQP.Connection.open(opts) do
{:ok, conn} -> {:ok, conn}
{:error, reason} ->
Logger.error "Error connecting to AMPQ: #{inspect reason}"
Process.sleep(5000)
open_connection(token, bot, mqtt_server, vhost)
end
end
end

View File

@ -79,7 +79,7 @@ defmodule Farmbot.AMQP.LogTransport do
:ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json
end
defp add_position_to_log(%{} = log, %{position: pos}) do
defp add_position_to_log(%{} = log, %{position: %{} = pos}) do
Map.merge(log, pos)
end
end

View File

@ -0,0 +1,36 @@
defmodule Farmbot.AutoSyncTask do
@moduledoc false
require Farmbot.Logger
@rpc %{
kind: :rpc_request,
args: %{label: "auto_sync_task"},
body: [
%{kind: :sync, args: %{}}
]
}
@doc false
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :maybe_auto_sync, opts},
type: :worker,
restart: :transient,
shutdown: 500
}
end
def maybe_auto_sync() do
if Farmbot.Config.get_config_value(:bool, "settings", "auto_sync") do
Farmbot.Core.CeleryScript.rpc_request(@rpc, &handle_rpc/1)
end
:ignore
end
@doc false
def handle_rpc(%{kind: :rpc_ok}), do: :ok
def handle_rpc(%{kind: :rpc_error, body: [%{args: %{message: msg}}]}) do
Farmbot.Logger.error 1, "AutoSyncTask failed: #{msg}"
end
end

View File

@ -99,13 +99,15 @@ defmodule Farmbot.Bootstrap.Supervisor do
success_msg = "Successful Bootstrap authorization: #{email} - #{server}"
Farmbot.Logger.success(2, success_msg)
update_config_value(:bool, "settings", "first_boot", false)
update_config_value(:bool, "settings", "needs_http_sync", true)
update_config_value(:string, "authorization", "token", token)
children = [
{Farmbot.HTTP.Supervisor, []},
{Farmbot.SettingsSync, []},
{Farmbot.AMQP.Supervisor , []},
{Farmbot.Bootstrap.AuthTask, []}
{Farmbot.Bootstrap.AuthTask, []},
{Farmbot.AutoSyncTask, []},
]
opts = [strategy: :one_for_one]

View File

@ -5,6 +5,7 @@ defmodule Farmbot.HTTP do
use GenServer
alias Farmbot.HTTP.{Adapter, Error, Response}
alias Farmbot.JSON
@adapter Application.get_env(:farmbot_ext, :behaviour)[:http_adapter]
@adapter || raise("No http adapter.")
@ -15,6 +16,36 @@ defmodule Farmbot.HTTP do
@typep headers :: Adapter.headers
@typep opts :: Adapter.opts
alias Farmbot.Asset.{
Device,
FarmEvent,
Peripheral,
PinBinding,
Point,
Regimen,
Sensor,
Sequence,
Tool,
}
def device, do: fetch_and_decode("/api/device.json", Device)
def farm_events, do: fetch_and_decode("/api/farm_events.json", FarmEvent)
def peripherals, do: fetch_and_decode("/api/peripherals.json", Peripheral)
def pin_bindings, do: fetch_and_decode("/api/pin_bindings.json", PinBinding)
def points, do: fetch_and_decode("/api/points.json", Point)
def regimens, do: fetch_and_decode("/api/regimens.json", Regimen)
def sensors, do: fetch_and_decode("/api/sensors.json", Sensor)
def sequences, do: fetch_and_decode("/api/sequences.json", Sequence)
def tools, do: fetch_and_decode("/api/tools.json", Tool)
def fetch_and_decode(url, kind) do
url
|> get!()
|> Map.fetch!(:body)
|> JSON.decode!()
|> Farmbot.Asset.to_asset(kind)
end
@doc """
Make an http request. Will not raise.
* `method` - can be any http verb

View File

@ -1,14 +1,13 @@
defmodule Farmbot.Ext.MixProject do
use Mix.Project
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
@branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim()
@elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim()
def project do
[
app: :farmbot_ext,
version: @version,
branch: @branch,
elixir: "~> 1.6",
elixir: @elixir_version,
start_permanent: Mix.env() == :prod,
elixirc_paths: ["lib", "vendor"],
deps: deps()

View File

@ -4,7 +4,6 @@
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"},
"csvm": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "fcc87c97ad73e7d3cdd17fb9bfbbe4f6f19b2882", [branch: "integrate_csvm"]},
"db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"},
"decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"},
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"},
@ -15,6 +14,7 @@
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"},
"farmbot_celery_script": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "391cc58962abf1f39936202afcb079ce1114024d", [branch: "integrate_farmbot_celery_script"]},
"fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"},
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},

View File

@ -1,4 +0,0 @@
# Used by "mix format"
[
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
]

View File

@ -1 +0,0 @@
../.tool-versions

View File

@ -0,0 +1,2 @@
erlang 21.0.4
elixir 1.6.6-otp-21

View File

@ -12,20 +12,17 @@ config :farmbot_ext,
config :farmbot_core, Farmbot.Config.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"),
pool_size: 1
database: Path.join(data_path, "config-#{Mix.env()}.sqlite3")
config :farmbot_core, Farmbot.Logger.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"),
pool_size: 1
database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3")
config :farmbot_core, Farmbot.Asset.Repo,
adapter: Sqlite.Ecto2,
loggers: [],
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"),
pool_size: 1
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3")
config :farmbot_os,
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],

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