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 sequencespull/974/head
parent
cf1ef23b17
commit
358a1e209e
|
@ -0,0 +1 @@
|
||||||
|
~> 1.6
|
6
Makefile
6
Makefile
|
@ -18,6 +18,10 @@ all: help
|
||||||
help:
|
help:
|
||||||
@echo "no"
|
@echo "no"
|
||||||
|
|
||||||
|
farmbot_celery_script_clean:
|
||||||
|
cd farmbot_celery_script && \
|
||||||
|
rm -rf _build deps
|
||||||
|
|
||||||
farmbot_core_clean:
|
farmbot_core_clean:
|
||||||
cd farmbot_core && \
|
cd farmbot_core && \
|
||||||
make clean && \
|
make clean && \
|
||||||
|
@ -35,7 +39,7 @@ farmbot_os_clean:
|
||||||
cd farmbot_os && \
|
cd farmbot_os && \
|
||||||
rm -rf _build deps
|
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:
|
farmbot_core_test:
|
||||||
cd farmbot_core && \
|
cd farmbot_core && \
|
||||||
|
|
|
@ -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` |
|
|
@ -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: []}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
../.tool-versions
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
Binary file not shown.
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
||||||
|
defmodule Farmbot.CeleryScript.RunTime.Error do
|
||||||
|
@moduledoc """
|
||||||
|
CSVM runtime error
|
||||||
|
"""
|
||||||
|
|
||||||
|
defexception [:message, :farm_proc]
|
||||||
|
end
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"},
|
||||||
|
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
ExUnit.start()
|
|
@ -1,4 +0,0 @@
|
||||||
# Used by "mix format"
|
|
||||||
[
|
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
|
||||||
]
|
|
|
@ -1 +0,0 @@
|
||||||
../.tool-versions
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
erlang 21.0.4
|
||||||
|
elixir 1.6.6-otp-21
|
|
@ -1,3 +0,0 @@
|
||||||
# FarmbotCore
|
|
||||||
Core Farmbot Services.
|
|
||||||
This includes Logging, Configuration, Asset management and Firmware.
|
|
|
@ -25,19 +25,16 @@ config :farmbot_core, Farmbot.Config.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_configs.sqlite3",
|
database: ".#{Mix.env}_configs.sqlite3",
|
||||||
priv: "priv/config",
|
priv: "priv/config"
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Logger.Repo,
|
config :farmbot_core, Farmbot.Logger.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_logs.sqlite3",
|
database: ".#{Mix.env}_logs.sqlite3",
|
||||||
priv: "priv/logger",
|
priv: "priv/logger"
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Asset.Repo,
|
config :farmbot_core, Farmbot.Asset.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_assets.sqlite3",
|
database: ".#{Mix.env}_assets.sqlite3",
|
||||||
priv: "priv/asset",
|
priv: "priv/asset"
|
||||||
pool_size: 1
|
|
||||||
|
|
|
@ -25,6 +25,46 @@ defmodule Farmbot.Asset do
|
||||||
alias Repo.Snapshot
|
alias Repo.Snapshot
|
||||||
require Farmbot.Logger
|
require Farmbot.Logger
|
||||||
import Ecto.Query
|
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
|
def fragment_sync(verbosity \\ 1) do
|
||||||
Farmbot.Logger.busy verbosity, "Syncing"
|
Farmbot.Logger.busy verbosity, "Syncing"
|
||||||
|
@ -43,6 +83,43 @@ defmodule Farmbot.Asset do
|
||||||
:ok
|
:ok
|
||||||
end
|
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
|
def apply_sync_cmd(cmd) do
|
||||||
mod = Module.concat(["Farmbot", "Asset", cmd.kind])
|
mod = Module.concat(["Farmbot", "Asset", cmd.kind])
|
||||||
if Code.ensure_loaded?(mod) do
|
if Code.ensure_loaded?(mod) do
|
||||||
|
@ -64,7 +141,8 @@ defmodule Farmbot.Asset do
|
||||||
destroy_sync_cmd(cmd)
|
destroy_sync_cmd(cmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp dispatch_sync(diff) do
|
@doc false
|
||||||
|
def dispatch_sync(diff) do
|
||||||
for deletion <- diff.deletions do
|
for deletion <- diff.deletions do
|
||||||
Farmbot.Registry.dispatch(__MODULE__, {:deletion, deletion})
|
Farmbot.Registry.dispatch(__MODULE__, {:deletion, deletion})
|
||||||
end
|
end
|
||||||
|
@ -127,10 +205,17 @@ defmodule Farmbot.Asset do
|
||||||
Use the `Farmbot.Asset.Registry` for these types of events.
|
Use the `Farmbot.Asset.Registry` for these types of events.
|
||||||
"""
|
"""
|
||||||
def register_sync_cmd(remote_id, kind, body) when is_binary(kind) do
|
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!()
|
|> Repo.insert!()
|
||||||
end
|
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."
|
@doc "Destroy all sync cmds locally."
|
||||||
def destroy_all_sync_cmds do
|
def destroy_all_sync_cmds do
|
||||||
Repo.delete_all(SyncCmd)
|
Repo.delete_all(SyncCmd)
|
||||||
|
@ -140,6 +225,7 @@ defmodule Farmbot.Asset do
|
||||||
Repo.all(SyncCmd)
|
Repo.all(SyncCmd)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def destroy_sync_cmd(%SyncCmd{id: nil} = cmd), do: {:ok, cmd}
|
||||||
def destroy_sync_cmd(%SyncCmd{} = cmd) do
|
def destroy_sync_cmd(%SyncCmd{} = cmd) do
|
||||||
Repo.delete(cmd)
|
Repo.delete(cmd)
|
||||||
end
|
end
|
||||||
|
@ -217,11 +303,21 @@ defmodule Farmbot.Asset do
|
||||||
Repo.one(from(p in Peripheral, where: p.id == ^peripheral_id))
|
Repo.one(from(p in Peripheral, where: p.id == ^peripheral_id))
|
||||||
end
|
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."
|
@doc "Get a Sensor by it's id."
|
||||||
def get_sensor_by_id(sensor_id) do
|
def get_sensor_by_id(sensor_id) do
|
||||||
Repo.one(from(s in Sensor, where: s.id == ^sensor_id))
|
Repo.one(from(s in Sensor, where: s.id == ^sensor_id))
|
||||||
end
|
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."
|
@doc "Get a Sequence by it's id."
|
||||||
def get_sequence_by_id(sequence_id) do
|
def get_sequence_by_id(sequence_id) do
|
||||||
Repo.one(from(s in Sequence, where: s.id == ^sequence_id))
|
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."
|
@doc "Fetches all regimens that use a particular sequence."
|
||||||
def get_regimens_using_sequence(sequence_id) do
|
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)
|
Repo.all(Regimen)
|
||||||
|> Enum.filter(&Enum.find(Map.fetch!(&1, :regimen_items), uses_seq))
|
|> Enum.filter(&Enum.find(Map.fetch!(&1, :regimen_items), uses_seq))
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -21,7 +21,7 @@ defmodule Farmbot.Asset.Point do
|
||||||
@optional_fields [:tool_id]
|
@optional_fields [:tool_id]
|
||||||
|
|
||||||
def changeset(%Point{} = point, params \\ %{}) do
|
def changeset(%Point{} = point, params \\ %{}) do
|
||||||
%Point{} = point
|
point
|
||||||
|> cast(params, @required_fields ++ @optional_fields)
|
|> cast(params, @required_fields ++ @optional_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,7 @@ defmodule Farmbot.Asset.Sensor do
|
||||||
@required_fields [:id, :pin, :mode, :label]
|
@required_fields [:id, :pin, :mode, :label]
|
||||||
|
|
||||||
def changeset(%Sensor{} = sensor, params \\ %{}) do
|
def changeset(%Sensor{} = sensor, params \\ %{}) do
|
||||||
%Sensor{} = sensor
|
sensor
|
||||||
|> cast(params, @required_fields)
|
|> cast(params, @required_fields)
|
||||||
|> validate_required(@required_fields)
|
|> validate_required(@required_fields)
|
||||||
|> unique_constraint(:id)
|
|> unique_constraint(:id)
|
||||||
|
|
|
@ -7,6 +7,7 @@ defmodule Farmbot.Asset.Sequence do
|
||||||
alias Farmbot.EctoTypes.TermType
|
alias Farmbot.EctoTypes.TermType
|
||||||
use Ecto.Schema
|
use Ecto.Schema
|
||||||
import Ecto.Changeset
|
import Ecto.Changeset
|
||||||
|
require Farmbot.Logger
|
||||||
|
|
||||||
schema "sequences" do
|
schema "sequences" do
|
||||||
field(:name, :string)
|
field(:name, :string)
|
||||||
|
@ -26,9 +27,14 @@ defmodule Farmbot.Asset.Sequence do
|
||||||
|
|
||||||
@behaviour Farmbot.Asset.FarmEvent
|
@behaviour Farmbot.Asset.FarmEvent
|
||||||
def schedule_event(%Sequence{} = sequence, _now) do
|
def schedule_event(%Sequence{} = sequence, _now) do
|
||||||
case Farmbot.CeleryScript.schedule_sequence(sequence) do
|
Farmbot.Logger.busy 1, "[#{sequence.name}] Sequence init."
|
||||||
%{status: :crashed} = proc -> {:error, Csvm.FarmProc.get_crash_reason(proc)}
|
Farmbot.Core.CeleryScript.sequence(sequence, fn(result) ->
|
||||||
_ -> :ok
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,11 +8,14 @@ defmodule Farmbot.Asset.Supervisor do
|
||||||
|
|
||||||
def init([]) do
|
def init([]) do
|
||||||
children = [
|
children = [
|
||||||
{Farmbot.Asset.Repo, [] },
|
{Farmbot.Asset.Logger, []},
|
||||||
{Farmbot.Regimen.NameProvider, [] },
|
{Farmbot.Asset.Repo, []},
|
||||||
{Farmbot.FarmEvent.Supervisor, [] },
|
{Farmbot.Regimen.NameProvider, []},
|
||||||
{Farmbot.Regimen.Supervisor, [] },
|
{Farmbot.FarmEvent.Supervisor, []},
|
||||||
{Farmbot.PinBinding.Supervisor, [] },
|
{Farmbot.Regimen.Supervisor, []},
|
||||||
|
{Farmbot.PinBinding.Supervisor, []},
|
||||||
|
{Farmbot.Peripheral.Supervisor, []},
|
||||||
|
{Farmbot.Asset.OnStartTask, []},
|
||||||
]
|
]
|
||||||
Supervisor.init(children, [strategy: :one_for_one])
|
Supervisor.init(children, [strategy: :one_for_one])
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,7 +13,7 @@ defmodule Farmbot.Asset.SyncCmd do
|
||||||
timestamps()
|
timestamps()
|
||||||
end
|
end
|
||||||
|
|
||||||
@required_fields [:remote_id, :kind]
|
@required_fields [:kind, :remote_id]
|
||||||
|
|
||||||
def changeset(%SyncCmd{} = cmd, params \\ %{}) do
|
def changeset(%SyncCmd{} = cmd, params \\ %{}) do
|
||||||
cmd
|
cmd
|
||||||
|
|
|
@ -111,8 +111,9 @@ defmodule Farmbot.BotState do
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def handle_call(:fetch, _from, state) do
|
def handle_call(:fetch, _from, state) do
|
||||||
Farmbot.Registry.dispatch(__MODULE__, state)
|
new_state = handle_event({:informational_settings, %{cache_bust: :rand.uniform(1000)}}, state)
|
||||||
{:reply, state, [], state}
|
Farmbot.Registry.dispatch(__MODULE__, new_state)
|
||||||
|
{:reply, state, [], new_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
# TODO(Connor) - Fix this to use event system.
|
# TODO(Connor) - Fix this to use event system.
|
||||||
|
@ -181,7 +182,7 @@ defmodule Farmbot.BotState do
|
||||||
{:noreply, [], new_state}
|
{:noreply, [], new_state}
|
||||||
end
|
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}}
|
event = {:informational_settings, %{sync_status: status}}
|
||||||
new_state = handle_event(event, state)
|
new_state = handle_event(event, state)
|
||||||
Farmbot.Registry.dispatch(__MODULE__, new_state)
|
Farmbot.Registry.dispatch(__MODULE__, new_state)
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
defmodule Farmbot.BotState.LocationData do
|
defmodule Farmbot.BotState.LocationData do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
defstruct [
|
defstruct [
|
||||||
scaled_encoders: nil,
|
scaled_encoders: %{x: -1, y: -1, z: -1},
|
||||||
raw_encoders: nil,
|
raw_encoders: %{x: -1, y: -1, z: -1},
|
||||||
position: nil
|
position: %{x: -1, y: -1, z: -1}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,18 +1,12 @@
|
||||||
defmodule Farmbot.CeleryScript do
|
defmodule Farmbot.Core.CeleryScript do
|
||||||
def to_ast(data) do
|
@moduledoc """
|
||||||
Csvm.AST.decode(data)
|
Helpers for executing CeleryScript.
|
||||||
|
"""
|
||||||
|
def rpc_request(data, fun) do
|
||||||
|
Farmbot.CeleryScript.RunTime.rpc_request(Farmbot.CeleryScript.RunTime, data, fun)
|
||||||
end
|
end
|
||||||
|
|
||||||
def execute_sequence(%Farmbot.Asset.Sequence{} = seq) do
|
def sequence(%Farmbot.Asset.Sequence{} = seq, fun) do
|
||||||
schedule_sequence(seq)
|
Farmbot.CeleryScript.RunTime.sequence(Csvm, seq, seq.id, fun)
|
||||||
|> 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)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
|
@ -1,3 +1,47 @@
|
||||||
defmodule Farmbot.CeleryScript.IOLayer do
|
defmodule Farmbot.Core.CeleryScript.IOLayer do
|
||||||
@callback handle_io(Csvm.AST.t()) :: {:ok, Csvm.AST.t()} | :ok | {:error, String.t()}
|
@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
|
end
|
||||||
|
|
|
@ -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
|
|
@ -1,9 +1,35 @@
|
||||||
defmodule Farmbot.CeleryScript.StubIOLayer do
|
defmodule Farmbot.Core.CeleryScript.StubIOLayer do
|
||||||
@behaviour Farmbot.CeleryScript.IOLayer
|
@behaviour Farmbot.Core.CeleryScript.IOLayer
|
||||||
|
def calibrate(_args, _body), do: {:error, "Stubbed"}
|
||||||
def handle_io(ast) do
|
def change_ownership(_args, _body), do: {:error, "Stubbed"}
|
||||||
IO.puts "#{ast.kind} not implemented."
|
def check_updates(_args, _body), do: {:error, "Stubbed"}
|
||||||
# {:error, "#{ast.kind} not implemented."}
|
def config_update(_args, _body), do: {:error, "Stubbed"}
|
||||||
:ok
|
def dump_info(_args, _body), do: {:error, "Stubbed"}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
defmodule Farmbot.CeleryScript.Supervisor do
|
defmodule Farmbot.Core.CeleryScript.Supervisor do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Supervisor
|
use Supervisor
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ defmodule Farmbot.CeleryScript.Supervisor do
|
||||||
|
|
||||||
def init([]) do
|
def init([]) do
|
||||||
children = [
|
children = [
|
||||||
{Farmbot.CeleryScript.CsvmWrapper, []}
|
{Farmbot.Core.CeleryScript.RunTimeWrapper, []}
|
||||||
]
|
]
|
||||||
Supervisor.init(children, [strategy: :one_for_one])
|
Supervisor.init(children, [strategy: :one_for_one])
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -5,30 +5,18 @@ defmodule Farmbot.Core do
|
||||||
"""
|
"""
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
def child_spec(opts) do
|
|
||||||
%{
|
|
||||||
id: __MODULE__,
|
|
||||||
start: {__MODULE__, :start_link, [opts]},
|
|
||||||
type: :worker,
|
|
||||||
restart: :permanent,
|
|
||||||
shutdown: 500
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__)
|
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
|
def init([]) do
|
||||||
children = [
|
children = [
|
||||||
{Farmbot.Registry, [] },
|
{Farmbot.Registry, []},
|
||||||
{Farmbot.Logger.Supervisor, [] },
|
{Farmbot.Logger.Supervisor, []},
|
||||||
{Farmbot.Config.Supervisor, [] },
|
{Farmbot.Config.Supervisor, []},
|
||||||
{Farmbot.Asset.Supervisor, [] },
|
{Farmbot.Firmware.Supervisor, []},
|
||||||
{Farmbot.Firmware.Supervisor, [] },
|
{Farmbot.Asset.Supervisor, []},
|
||||||
{Farmbot.BotState, [] },
|
{Farmbot.BotState, []},
|
||||||
{Farmbot.CeleryScript.Supervisor, [] },
|
{Farmbot.Core.CeleryScript.Supervisor, []},
|
||||||
]
|
]
|
||||||
Supervisor.init(children, [strategy: :one_for_one])
|
Supervisor.init(children, [strategy: :one_for_one])
|
||||||
end
|
end
|
||||||
|
|
|
@ -22,12 +22,14 @@ defmodule Farmbot.Firmware do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Calibrate an axis."
|
@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)
|
GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Find home on an axis."
|
@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)
|
GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -37,12 +39,14 @@ defmodule Farmbot.Firmware do
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Home an axis."
|
@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)
|
GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc "Manually set an axis's current position to zero."
|
@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)
|
GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -107,6 +111,14 @@ defmodule Farmbot.Firmware do
|
||||||
GenStage.call(__MODULE__, :params_reported)
|
GenStage.call(__MODULE__, :params_reported)
|
||||||
end
|
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."
|
@doc "Start the firmware services."
|
||||||
def start_link(args) do
|
def start_link(args) do
|
||||||
GenStage.start_link(__MODULE__, args, name: __MODULE__)
|
GenStage.start_link(__MODULE__, args, name: __MODULE__)
|
||||||
|
@ -121,6 +133,11 @@ defmodule Farmbot.Firmware do
|
||||||
handler_mod: nil,
|
handler_mod: nil,
|
||||||
idle: false,
|
idle: false,
|
||||||
timer: nil,
|
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: %{},
|
pins: %{},
|
||||||
params: %{},
|
params: %{},
|
||||||
params_reported: false,
|
params_reported: false,
|
||||||
|
@ -155,6 +172,7 @@ defmodule Farmbot.Firmware do
|
||||||
def init([]) do
|
def init([]) do
|
||||||
handler_mod =
|
handler_mod =
|
||||||
Application.get_env(:farmbot_core, :behaviour)[:firmware_handler] || raise("No fw handler.")
|
Application.get_env(:farmbot_core, :behaviour)[:firmware_handler] || raise("No fw handler.")
|
||||||
|
|> IO.inspect(label: "FW Handler")
|
||||||
|
|
||||||
case handler_mod.start_link() do
|
case handler_mod.start_link() do
|
||||||
{:ok, handler} ->
|
{:ok, handler} ->
|
||||||
|
@ -165,9 +183,9 @@ defmodule Farmbot.Firmware do
|
||||||
struct(State, initial),
|
struct(State, initial),
|
||||||
subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher
|
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)
|
replace_firmware_handler(Farmbot.Firmware.StubHandler)
|
||||||
Farmbot.Logger.error 1, "Failed to initialize firmware: #{inspect reason} Falling back to stub implementation."
|
|
||||||
init([])
|
init([])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -181,7 +199,7 @@ defmodule Farmbot.Firmware do
|
||||||
unless :queue.is_empty(state.queue) do
|
unless :queue.is_empty(state.queue) do
|
||||||
list = :queue.to_list(state.queue)
|
list = :queue.to_list(state.queue)
|
||||||
for cmd <- list do
|
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
|
end
|
||||||
end
|
end
|
||||||
|
@ -212,7 +230,7 @@ defmodule Farmbot.Firmware do
|
||||||
:ok ->
|
:ok ->
|
||||||
timer = start_timer(current, state.timeout_ms)
|
timer = start_timer(current, state.timeout_ms)
|
||||||
{:noreply, [], %{state | current: current, timer: timer}}
|
{:noreply, [], %{state | current: current, timer: timer}}
|
||||||
{:error, _} = res ->
|
{:error, reason} = res when is_binary(reason) ->
|
||||||
do_reply(state, res)
|
do_reply(state, res)
|
||||||
{:noreply, [], %{state | current: nil, queue: :queue.new()}}
|
{:noreply, [], %{state | current: nil, queue: :queue.new()}}
|
||||||
end
|
end
|
||||||
|
@ -229,13 +247,23 @@ defmodule Farmbot.Firmware do
|
||||||
end
|
end
|
||||||
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
|
def handle_call(:params_reported, _, state) do
|
||||||
{:reply, state.params_reported, [], state}
|
{:reply, state.params_reported, [], state}
|
||||||
end
|
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
|
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
|
end
|
||||||
|
|
||||||
def handle_call({fun, args}, from, state) do
|
def handle_call({fun, args}, from, state) do
|
||||||
|
@ -244,7 +272,7 @@ defmodule Farmbot.Firmware do
|
||||||
cond do
|
cond do
|
||||||
fun == :emergency_lock ->
|
fun == :emergency_lock ->
|
||||||
if current_current do
|
if current_current do
|
||||||
do_reply(state, {:error, :emergency_lock})
|
do_reply(state, {:error, "emergency_lock"})
|
||||||
end
|
end
|
||||||
do_begin_cmd(next_current, state, [])
|
do_begin_cmd(next_current, state, [])
|
||||||
match?(%Command{}, current_current) ->
|
match?(%Command{}, current_current) ->
|
||||||
|
@ -264,7 +292,7 @@ defmodule Farmbot.Firmware do
|
||||||
else
|
else
|
||||||
{:noreply, dispatch, %{state | current: current, timer: timer}}
|
{:noreply, dispatch, %{state | current: current, timer: timer}}
|
||||||
end
|
end
|
||||||
{:error, _} = res ->
|
{:error, reason} = res when is_binary(reason) ->
|
||||||
do_reply(%{state | current: current}, res)
|
do_reply(%{state | current: current}, res)
|
||||||
{:noreply, dispatch, %{state | current: nil}}
|
{:noreply, dispatch, %{state | current: nil}}
|
||||||
end
|
end
|
||||||
|
@ -281,12 +309,17 @@ defmodule Farmbot.Firmware do
|
||||||
# if after handling the current buffer of gcodes,
|
# if after handling the current buffer of gcodes,
|
||||||
# Try to start the next command in the queue if it exists.
|
# Try to start the next command in the queue if it exists.
|
||||||
if List.last(gcodes) == :idle && state.current == nil do
|
if List.last(gcodes) == :idle && state.current == nil do
|
||||||
|
if state.initialized do
|
||||||
case :queue.out(state.queue) do
|
case :queue.out(state.queue) do
|
||||||
{{:value, next_current}, new_queue} ->
|
{{:value, next_current}, new_queue} ->
|
||||||
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
|
do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs)
|
||||||
{:empty, queue} -> # nothing to do if the queue is empty.
|
{:empty, queue} -> # nothing to do if the queue is empty.
|
||||||
{:noreply, diffs, %{state | queue: queue}}
|
{:noreply, diffs, %{state | queue: queue}}
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
Farmbot.Logger.warn 1, "Fw not initialized yet"
|
||||||
|
{:noreply, diffs, state}
|
||||||
|
end
|
||||||
else
|
else
|
||||||
{:noreply, diffs, state}
|
{:noreply, diffs, state}
|
||||||
end
|
end
|
||||||
|
@ -314,7 +347,7 @@ defmodule Farmbot.Firmware do
|
||||||
maybe_cancel_timer(state.timer, state.current)
|
maybe_cancel_timer(state.timer, state.current)
|
||||||
if state.current do
|
if state.current do
|
||||||
Farmbot.Logger.error 1, "Got #{code} while executing `#{inspect state.current}`."
|
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}}
|
{nil, %{state | current: nil}}
|
||||||
else
|
else
|
||||||
{nil, state}
|
{nil, state}
|
||||||
|
@ -368,15 +401,21 @@ defmodule Farmbot.Firmware do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_gcode({:report_current_position, x, y, z}, state) do
|
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
|
end
|
||||||
|
|
||||||
defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do
|
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
|
end
|
||||||
|
|
||||||
defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do
|
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
|
end
|
||||||
|
|
||||||
defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
|
defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do
|
||||||
|
@ -444,17 +483,17 @@ defmodule Farmbot.Firmware do
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_gcode(:report_axis_timeout_x, state) do
|
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}}
|
{nil, %{state | timer: nil}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_gcode(:report_axis_timeout_y, state) do
|
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}}
|
{nil, %{state | timer: nil}}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp handle_gcode(:report_axis_timeout_z, state) do
|
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}}
|
{nil, %{state | timer: nil}}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -487,9 +526,9 @@ defmodule Farmbot.Firmware do
|
||||||
maybe_cancel_timer(state.timer, state.current)
|
maybe_cancel_timer(state.timer, state.current)
|
||||||
if state.current do
|
if state.current do
|
||||||
do_reply(state, :ok)
|
do_reply(state, :ok)
|
||||||
{:informational_settings, %{busy: true}, %{state | current: nil}}
|
{:informational_settings, %{busy: false}, %{state | current: nil}}
|
||||||
else
|
else
|
||||||
{:informational_settings, %{busy: true}, state}
|
{:informational_settings, %{busy: false}, state}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -582,7 +621,7 @@ defmodule Farmbot.Firmware do
|
||||||
:ok
|
:ok
|
||||||
_ -> report_calibration_callback(tries - 1, param, val)
|
_ -> report_calibration_callback(tries - 1, param, val)
|
||||||
end
|
end
|
||||||
{:error, reason} ->
|
{:error, reason} when is_binary(reason) ->
|
||||||
Farmbot.Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
|
Farmbot.Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})"
|
||||||
report_calibration_callback(tries - 1, param, val)
|
report_calibration_callback(tries - 1, param, val)
|
||||||
end
|
end
|
||||||
|
@ -597,7 +636,7 @@ defmodule Farmbot.Firmware do
|
||||||
EstopTimer.cancel_timer()
|
EstopTimer.cancel_timer()
|
||||||
:ok = GenServer.reply from, reply
|
:ok = GenServer.reply from, reply
|
||||||
%Command{fun: :emergency_lock, from: from} ->
|
%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} ->
|
%Command{fun: _fun, from: from} ->
|
||||||
# Farmbot.Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
|
# Farmbot.Logger.success 3, "FW Replying: #{fun}: #{inspect from}"
|
||||||
:ok = GenServer.reply from, reply
|
:ok = GenServer.reply from, reply
|
||||||
|
|
|
@ -5,7 +5,7 @@ defmodule Farmbot.Firmware.Supervisor do
|
||||||
@doc "Reinitializes the Firmware stack. Warning has MANY SIDE EFFECTS."
|
@doc "Reinitializes the Firmware stack. Warning has MANY SIDE EFFECTS."
|
||||||
def reinitialize do
|
def reinitialize do
|
||||||
Farmbot.Firmware.UartHandler.AutoDetector.start_link([])
|
Farmbot.Firmware.UartHandler.AutoDetector.start_link([])
|
||||||
Supervisor.terminate_child(Farmbot.Bootstrap.Supervisor, Farmbot.Firmware.Supervisor)
|
Supervisor.terminate_child(Farmbot.Core, Farmbot.Firmware.Supervisor)
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
|
|
|
@ -14,7 +14,7 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do
|
||||||
alias Circuits.UART
|
alias Circuits.UART
|
||||||
alias Farmbot.Firmware.{UartHandler, StubHandler, Utils}
|
alias Farmbot.Firmware.{UartHandler, StubHandler, Utils}
|
||||||
import Utils
|
import Utils
|
||||||
require Farmbot.Logger
|
require Logger
|
||||||
use GenServer
|
use GenServer
|
||||||
|
|
||||||
#TODO(Connor) - Maybe make this configurable?
|
#TODO(Connor) - Maybe make this configurable?
|
||||||
|
@ -48,12 +48,12 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do
|
||||||
case auto_detect() do
|
case auto_detect() do
|
||||||
[dev] ->
|
[dev] ->
|
||||||
dev = "/dev/#{dev}"
|
dev = "/dev/#{dev}"
|
||||||
Farmbot.Logger.success 3, "detected target UART: #{dev}"
|
Logger.debug "detected target UART: #{dev}"
|
||||||
replace_firmware_handler(UartHandler)
|
replace_firmware_handler(UartHandler)
|
||||||
Application.put_env(:farmbot_core, :uart_handler, tty: dev)
|
Application.put_env(:farmbot_core, :uart_handler, tty: dev)
|
||||||
dev
|
dev
|
||||||
_ ->
|
_ ->
|
||||||
Farmbot.Logger.error 1, "Could not detect a UART device."
|
Logger.debug "Could not detect a UART device."
|
||||||
replace_firmware_handler(StubHandler)
|
replace_firmware_handler(StubHandler)
|
||||||
:error
|
:error
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,13 +109,13 @@ defmodule Farmbot.Firmware.UartHandler do
|
||||||
hw = get_config_value(:string, "settings", "firmware_hardware")
|
hw = get_config_value(:string, "settings", "firmware_hardware")
|
||||||
gen_stage_opts = [
|
gen_stage_opts = [
|
||||||
dispatcher: GenStage.BroadcastDispatcher,
|
dispatcher: GenStage.BroadcastDispatcher,
|
||||||
subscribe_to: [ConfigStorage.Dispatcher]
|
|
||||||
]
|
]
|
||||||
case open_tty(tty) do
|
case open_tty(tty) do
|
||||||
{:ok, nerves} ->
|
{:ok, nerves} ->
|
||||||
{:producer_consumer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts}
|
{:producer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts}
|
||||||
err ->
|
{:error, reason} ->
|
||||||
{:stop, err}
|
Farmbot.Logger.error 1, "Uart handler failed to initialize: #{inspect reason}"
|
||||||
|
:ignore
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@ defmodule Farmbot.Firmware.Utils do
|
||||||
@doc "Changes `:digital` => 0, and `:analog` => 1"
|
@doc "Changes `:digital` => 0, and `:analog` => 1"
|
||||||
def extract_pin_mode(:digital), do: 0
|
def extract_pin_mode(:digital), do: 0
|
||||||
def extract_pin_mode(:analog), do: 1
|
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
|
# https://github.com/arduino/Arduino/blob/2bfe164b9a5835e8cb6e194b928538a9093be333/hardware/arduino/avr/cores/arduino/Arduino.h#L43-L45
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -154,20 +154,29 @@ defmodule Farmbot.PinBinding.Manager do
|
||||||
%{state | registered: Map.delete(state.registered, pin_num), signal: Map.delete(state.signal, pin_num)}
|
%{state | registered: Map.delete(state.registered, pin_num), signal: Map.delete(state.signal, pin_num)}
|
||||||
end
|
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
|
sequence_id
|
||||||
|> Farmbot.Asset.get_sequence_by_id!()
|
|> Farmbot.Asset.get_sequence_by_id!()
|
||||||
|> Farmbot.CeleryScript.schedule_sequence()
|
|> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding))
|
||||||
end
|
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{
|
%Sequence{
|
||||||
id: 0,
|
id: 0,
|
||||||
name: action,
|
name: action,
|
||||||
kind: action,
|
kind: action,
|
||||||
args: %{},
|
args: %{},
|
||||||
body: [] }
|
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
|
end
|
||||||
|
|
||||||
defp debounce_timer(pin) do
|
defp debounce_timer(pin) do
|
||||||
|
|
|
@ -3,7 +3,7 @@ defmodule Farmbot.Regimen.Manager do
|
||||||
|
|
||||||
require Farmbot.Logger
|
require Farmbot.Logger
|
||||||
use GenServer
|
use GenServer
|
||||||
alias Farmbot.CeleryScript
|
alias Farmbot.Core.CeleryScript
|
||||||
alias Farmbot.Asset
|
alias Farmbot.Asset
|
||||||
alias Asset.Regimen
|
alias Asset.Regimen
|
||||||
import Farmbot.Regimen.NameProvider
|
import Farmbot.Regimen.NameProvider
|
||||||
|
@ -134,7 +134,14 @@ defmodule Farmbot.Regimen.Manager do
|
||||||
defp do_item(item, regimen, state) do
|
defp do_item(item, regimen, state) do
|
||||||
if item do
|
if item do
|
||||||
sequence = Farmbot.Asset.get_sequence_by_id!(item.sequence_id)
|
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
|
end
|
||||||
|
|
||||||
next_item = List.first(regimen.regimen_items)
|
next_item = List.first(regimen.regimen_items)
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
defmodule FarmbotCore.MixProject do
|
defmodule FarmbotCore.MixProject do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
|
|
||||||
@target System.get_env("MIX_TARGET") || "host"
|
@target System.get_env("MIX_TARGET") || "host"
|
||||||
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
|
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
|
||||||
@branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> 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
|
defp commit do
|
||||||
System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
|
System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim()
|
||||||
|
@ -20,7 +20,7 @@ defmodule FarmbotCore.MixProject do
|
||||||
[
|
[
|
||||||
app: :farmbot_core,
|
app: :farmbot_core,
|
||||||
description: "The Brains of the Farmbot Project",
|
description: "The Brains of the Farmbot Project",
|
||||||
elixir: "~> 1.7",
|
elixir: @elixir_version,
|
||||||
make_clean: ["clean"],
|
make_clean: ["clean"],
|
||||||
make_env: make_env(),
|
make_env: make_env(),
|
||||||
make_cwd: __DIR__,
|
make_cwd: __DIR__,
|
||||||
|
@ -56,6 +56,7 @@ defmodule FarmbotCore.MixProject do
|
||||||
# Run "mix help deps" to learn about dependencies.
|
# Run "mix help deps" to learn about dependencies.
|
||||||
defp deps do
|
defp deps do
|
||||||
[
|
[
|
||||||
|
{:farmbot_celery_script, path: "../farmbot_celery_script"},
|
||||||
# Arduino Firmware stuff.
|
# Arduino Firmware stuff.
|
||||||
{:elixir_make, "~> 0.4", runtime: false},
|
{:elixir_make, "~> 0.4", runtime: false},
|
||||||
{:nerves_uart, "~> 1.2"},
|
{:nerves_uart, "~> 1.2"},
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
"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"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [: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"},
|
"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"},
|
"decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"},
|
||||||
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [: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"},
|
"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"},
|
"elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"},
|
||||||
"esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "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"},
|
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
|
||||||
"gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [: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"},
|
"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"},
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -1,4 +0,0 @@
|
||||||
# Used by "mix format"
|
|
||||||
[
|
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
|
||||||
]
|
|
|
@ -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).
|
|
||||||
|
|
|
@ -34,22 +34,19 @@ config :farmbot_core, Farmbot.Config.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_configs.sqlite3",
|
database: ".#{Mix.env}_configs.sqlite3",
|
||||||
priv: "../farmbot_core/priv/config",
|
priv: "../farmbot_core/priv/config"
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Logger.Repo,
|
config :farmbot_core, Farmbot.Logger.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_logs.sqlite3",
|
database: ".#{Mix.env}_logs.sqlite3",
|
||||||
priv: "../farmbot_core/priv/logger",
|
priv: "../farmbot_core/priv/logger"
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Asset.Repo,
|
config :farmbot_core, Farmbot.Asset.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: ".#{Mix.env}_assets.sqlite3",
|
database: ".#{Mix.env}_assets.sqlite3",
|
||||||
priv: "../farmbot_core/priv/asset",
|
priv: "../farmbot_core/priv/asset"
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_ext, :behaviour,
|
config :farmbot_ext, :behaviour,
|
||||||
authorization: Farmbot.Bootstrap.Authorization,
|
authorization: Farmbot.Bootstrap.Authorization,
|
||||||
|
|
|
@ -2,7 +2,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
|
||||||
use GenServer
|
use GenServer
|
||||||
use AMQP
|
use AMQP
|
||||||
require Farmbot.Logger
|
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"
|
@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.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 = 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])
|
{: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])}
|
{:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])}
|
||||||
end
|
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
|
# Confirmation sent by the broker after registering this process as a consumer
|
||||||
def handle_info({:basic_consume_ok, _}, state) do
|
def handle_info({:basic_consume_ok, _}, state) do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
|
@ -43,7 +47,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
|
||||||
|
|
||||||
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
|
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
|
||||||
device = state.bot
|
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)
|
data = Farmbot.JSON.decode!(payload)
|
||||||
body = data["body"]
|
body = data["body"]
|
||||||
case asset_kind do
|
case asset_kind do
|
||||||
|
@ -57,10 +61,16 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
|
||||||
"FirmwareConfig" ->
|
"FirmwareConfig" ->
|
||||||
Farmbot.SettingsSync.apply_fw_map(Farmbot.Config.get_config_as_map()["hardware_params"], body)
|
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", "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
|
if get_config_value(:bool, "settings", "auto_sync") do
|
||||||
Farmbot.Asset.fragment_sync()
|
Farmbot.Asset.fragment_sync()
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
IO.puts "not accepting sync_cmd from amqp because bot needs http sync first."
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
json = Farmbot.JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"})
|
json = Farmbot.JSON.encode!(%{args: %{label: data["args"]["label"]}, kind: "rpc_ok"})
|
||||||
|
@ -68,4 +78,10 @@ defmodule Farmbot.AMQP.AutoSyncTransport do
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -8,6 +8,10 @@ defmodule Farmbot.AMQP.BotStateTransport do
|
||||||
defstruct [:conn, :chan, :bot, :state_cache]
|
defstruct [:conn, :chan, :bot, :state_cache]
|
||||||
alias __MODULE__, as: State
|
alias __MODULE__, as: State
|
||||||
|
|
||||||
|
def force do
|
||||||
|
GenServer.cast(__MODULE__, :force)
|
||||||
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def start_link(args) do
|
def start_link(args) do
|
||||||
GenServer.start_link(__MODULE__, args, [name: __MODULE__])
|
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])}
|
{:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])}
|
||||||
end
|
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
|
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, %{state_cache: bot_state} = state) do
|
||||||
|
# IO.puts "no state change"
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do
|
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)
|
cache = push_bot_state(state.chan, state.bot, bot_state)
|
||||||
{:noreply, %{state | state_cache: cache}}
|
{:noreply, %{state | state_cache: cache}}
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,6 +2,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
|
||||||
use GenServer
|
use GenServer
|
||||||
use AMQP
|
use AMQP
|
||||||
require Farmbot.Logger
|
require Farmbot.Logger
|
||||||
|
require Logger
|
||||||
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
|
import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4]
|
||||||
|
|
||||||
@exchange "amq.topic"
|
@exchange "amq.topic"
|
||||||
|
@ -48,28 +49,25 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do
|
||||||
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
|
def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do
|
||||||
device = state.bot
|
device = state.bot
|
||||||
["bot", ^device, "from_clients"] = String.split(key, ".")
|
["bot", ^device, "from_clients"] = String.split(key, ".")
|
||||||
reply = handle_celery_script(payload, state)
|
spawn_link fn() ->
|
||||||
:ok = AMQP.Basic.publish state.chan, @exchange, "bot.#{device}.from_device", reply
|
{_us, _results} = :timer.tc __MODULE__, :handle_celery_script, [payload, state]
|
||||||
|
# IO.puts "#{results.args.label} took: #{us}µs"
|
||||||
|
end
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc false
|
@doc false
|
||||||
def handle_celery_script(payload, _state) do
|
def handle_celery_script(payload, state) do
|
||||||
json = Farmbot.JSON.decode!(payload)
|
json = Farmbot.JSON.decode!(payload)
|
||||||
%Farmbot.Asset.Sequence{
|
# IO.inspect(json, label: "RPC_REQUEST")
|
||||||
name: json["args"]["label"],
|
Farmbot.Core.CeleryScript.rpc_request(json, fn(results_ast) ->
|
||||||
args: json["args"],
|
reply = Farmbot.JSON.encode!(results_ast)
|
||||||
body: json["body"],
|
if results_ast.kind == :rpc_error do
|
||||||
kind: "sequence",
|
[%{args: %{message: message}}] = results_ast.body
|
||||||
id: -1}
|
Logger.error(message)
|
||||||
|> 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
|
end
|
||||||
|> Farmbot.JSON.encode!()
|
AMQP.Basic.publish state.chan, @exchange, "bot.#{state.bot}.from_device", reply
|
||||||
|
results_ast
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Farmbot.AMQP.ConnectionWorker do
|
defmodule Farmbot.AMQP.ConnectionWorker do
|
||||||
use GenServer
|
use GenServer
|
||||||
require Farmbot.Logger
|
require Farmbot.Logger
|
||||||
|
require Logger
|
||||||
import Farmbot.Config, only: [update_config_value: 4]
|
import Farmbot.Config, only: [update_config_value: 4]
|
||||||
|
|
||||||
def start_link(args) do
|
def start_link(args) do
|
||||||
|
@ -38,6 +39,12 @@ defmodule Farmbot.AMQP.ConnectionWorker do
|
||||||
username: bot,
|
username: bot,
|
||||||
password: token,
|
password: token,
|
||||||
virtual_host: vhost]
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -79,7 +79,7 @@ defmodule Farmbot.AMQP.LogTransport do
|
||||||
:ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json
|
:ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json
|
||||||
end
|
end
|
||||||
|
|
||||||
defp add_position_to_log(%{} = log, %{position: pos}) do
|
defp add_position_to_log(%{} = log, %{position: %{} = pos}) do
|
||||||
Map.merge(log, pos)
|
Map.merge(log, pos)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -99,13 +99,15 @@ defmodule Farmbot.Bootstrap.Supervisor do
|
||||||
success_msg = "Successful Bootstrap authorization: #{email} - #{server}"
|
success_msg = "Successful Bootstrap authorization: #{email} - #{server}"
|
||||||
Farmbot.Logger.success(2, success_msg)
|
Farmbot.Logger.success(2, success_msg)
|
||||||
update_config_value(:bool, "settings", "first_boot", false)
|
update_config_value(:bool, "settings", "first_boot", false)
|
||||||
|
update_config_value(:bool, "settings", "needs_http_sync", true)
|
||||||
update_config_value(:string, "authorization", "token", token)
|
update_config_value(:string, "authorization", "token", token)
|
||||||
|
|
||||||
children = [
|
children = [
|
||||||
{Farmbot.HTTP.Supervisor, []},
|
{Farmbot.HTTP.Supervisor, []},
|
||||||
{Farmbot.SettingsSync, []},
|
{Farmbot.SettingsSync, []},
|
||||||
{Farmbot.AMQP.Supervisor , []},
|
{Farmbot.AMQP.Supervisor , []},
|
||||||
{Farmbot.Bootstrap.AuthTask, []}
|
{Farmbot.Bootstrap.AuthTask, []},
|
||||||
|
{Farmbot.AutoSyncTask, []},
|
||||||
]
|
]
|
||||||
|
|
||||||
opts = [strategy: :one_for_one]
|
opts = [strategy: :one_for_one]
|
||||||
|
|
|
@ -5,6 +5,7 @@ defmodule Farmbot.HTTP do
|
||||||
|
|
||||||
use GenServer
|
use GenServer
|
||||||
alias Farmbot.HTTP.{Adapter, Error, Response}
|
alias Farmbot.HTTP.{Adapter, Error, Response}
|
||||||
|
alias Farmbot.JSON
|
||||||
|
|
||||||
@adapter Application.get_env(:farmbot_ext, :behaviour)[:http_adapter]
|
@adapter Application.get_env(:farmbot_ext, :behaviour)[:http_adapter]
|
||||||
@adapter || raise("No http adapter.")
|
@adapter || raise("No http adapter.")
|
||||||
|
@ -15,6 +16,36 @@ defmodule Farmbot.HTTP do
|
||||||
@typep headers :: Adapter.headers
|
@typep headers :: Adapter.headers
|
||||||
@typep opts :: Adapter.opts
|
@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 """
|
@doc """
|
||||||
Make an http request. Will not raise.
|
Make an http request. Will not raise.
|
||||||
* `method` - can be any http verb
|
* `method` - can be any http verb
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
defmodule Farmbot.Ext.MixProject do
|
defmodule Farmbot.Ext.MixProject do
|
||||||
use Mix.Project
|
use Mix.Project
|
||||||
@version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim()
|
@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
|
def project do
|
||||||
[
|
[
|
||||||
app: :farmbot_ext,
|
app: :farmbot_ext,
|
||||||
version: @version,
|
version: @version,
|
||||||
branch: @branch,
|
elixir: @elixir_version,
|
||||||
elixir: "~> 1.6",
|
|
||||||
start_permanent: Mix.env() == :prod,
|
start_permanent: Mix.env() == :prod,
|
||||||
elixirc_paths: ["lib", "vendor"],
|
elixirc_paths: ["lib", "vendor"],
|
||||||
deps: deps()
|
deps: deps()
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
"certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"},
|
"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"},
|
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"},
|
||||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [: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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"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"},
|
"fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"},
|
||||||
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
|
"gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"},
|
||||||
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
|
"gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"},
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
# Used by "mix format"
|
|
||||||
[
|
|
||||||
inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
|
||||||
]
|
|
|
@ -1 +0,0 @@
|
||||||
../.tool-versions
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
erlang 21.0.4
|
||||||
|
elixir 1.6.6-otp-21
|
|
@ -12,20 +12,17 @@ config :farmbot_ext,
|
||||||
config :farmbot_core, Farmbot.Config.Repo,
|
config :farmbot_core, Farmbot.Config.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"),
|
database: Path.join(data_path, "config-#{Mix.env()}.sqlite3")
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Logger.Repo,
|
config :farmbot_core, Farmbot.Logger.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"),
|
database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3")
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_core, Farmbot.Asset.Repo,
|
config :farmbot_core, Farmbot.Asset.Repo,
|
||||||
adapter: Sqlite.Ecto2,
|
adapter: Sqlite.Ecto2,
|
||||||
loggers: [],
|
loggers: [],
|
||||||
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"),
|
database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3")
|
||||||
pool_size: 1
|
|
||||||
|
|
||||||
config :farmbot_os,
|
config :farmbot_os,
|
||||||
ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo],
|
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
Loading…
Reference in New Issue