From 358a1e209eeecb393ca5f09022e9015464ceeda8 Mon Sep 17 00:00:00 2001 From: connor rigby Date: Wed, 1 Aug 2018 12:51:08 -0700 Subject: [PATCH] Update AMQP workers to reconnect on a 4 second timer Pull in new csvm implementation Update circleci config Implement syncing and write_pin add migration for syncing add saftey to write_pin Implement read_pin Implement set_servo_angle Implement more ast nodes Implement e-stop and e-unlock Fix a bunch of stuf Fix missing assets on boot/init It actually works Rename csvm -> farmbot_celery_script; fix initial sync/dispatch Fix a bunch of small bugs Identify problem Fix Sqlite bug and increase performance by 10 times!! Fix sequences inside of sequences --- ELIXIR_VERSION | 1 + Makefile | 6 +- docs/celery_script/all_nodes.md | 73 +++ docs/celery_script/celery_script.md | 70 ++ farmbot_celery_script/.gitignore | 27 + farmbot_celery_script/.tool-versions | 1 + .../fixture/inner_sequence.json | 51 ++ .../fixture/master_sequence.json | 194 ++++++ .../fixture/master_sequence.term | Bin 0 -> 2316 bytes .../fixture/outer_sequence.json | 57 ++ farmbot_celery_script/fixture/unbound.json | 51 ++ .../fixture/unbound_var_x.json | 90 +++ farmbot_celery_script/lib/address.ex | 31 + farmbot_celery_script/lib/circular_list.ex | 93 +++ .../lib/farmbot_celery_script/ast.ex | 89 +++ .../lib/farmbot_celery_script/ast/heap.ex | 98 +++ .../lib/farmbot_celery_script/ast/slicer.ex | 115 ++++ .../lib/farmbot_celery_script/ast/unslicer.ex | 78 +++ .../lib/farmbot_celery_script/run_time.ex | 421 ++++++++++++ .../farmbot_celery_script/run_time/error.ex | 7 + .../run_time/farm_proc.ex | 278 ++++++++ .../farmbot_celery_script/run_time/inspect.ex | 7 + .../run_time/instruction.ex | 43 ++ .../run_time/instruction_set.ex | 340 ++++++++++ .../run_time/proc_storage.ex | 48 ++ .../run_time/resolver.ex | 83 +++ .../run_time/sys_call_handler.ex | 63 ++ .../lib/farmbot_celery_script/utils.ex | 25 + farmbot_celery_script/lib/pointer.ex | 39 ++ farmbot_celery_script/mix.exs | 53 ++ farmbot_celery_script/mix.lock | 18 + farmbot_celery_script/test/address_test.exs | 17 + .../test/circular_list_test.exs | 71 +++ .../farmbot_celery_script/ast/heap_test.exs | 70 ++ .../farmbot_celery_script/ast/slicer_test.exs | 218 +++++++ .../ast/unslicer_test.exs | 25 + .../test/farmbot_celery_script/ast_test.exs | 28 + .../run_time/farm_proc_test.exs | 599 ++++++++++++++++++ .../run_time/instruction_set_test.exs | 191 ++++++ .../run_time/proc_storage_test.exs | 32 + .../run_time/resolver_test.exs | 139 ++++ .../run_time/sys_call_handler_test.exs | 24 + .../run_time/utils_test.exs | 23 + .../farmbot_celery_script/run_time_test.exs | 230 +++++++ farmbot_celery_script/test/pointer_test.exs | 8 + .../test/support/fixtures.ex | 13 + farmbot_celery_script/test/test_helper.exs | 1 + farmbot_core/.formatter.exs | 4 - farmbot_core/.tool-versions | 3 +- farmbot_core/README.md | 3 - farmbot_core/config/config.exs | 9 +- farmbot_core/lib/asset_storage/asset.ex | 102 ++- .../lib/asset_storage/asset_logger.ex | 31 + .../lib/asset_storage/on_start_task.ex | 25 + farmbot_core/lib/asset_storage/point.ex | 2 +- farmbot_core/lib/asset_storage/sensor.ex | 4 +- farmbot_core/lib/asset_storage/sequence.ex | 14 +- farmbot_core/lib/asset_storage/supervisor.ex | 13 +- farmbot_core/lib/asset_storage/sync_cmd.ex | 2 +- farmbot_core/lib/bot_state/bot_state.ex | 7 +- farmbot_core/lib/bot_state/location_data.ex | 6 +- .../lib/celery_script/celery_script.ex | 22 +- .../lib/celery_script/csvm_wrapper.ex | 20 - farmbot_core/lib/celery_script/io_layer.ex | 48 +- .../lib/celery_script/run_time_wrapper.ex | 42 ++ .../lib/celery_script/stub_io_layer.ex | 42 +- farmbot_core/lib/celery_script/supervisor.ex | 4 +- farmbot_core/lib/celery_script/utils.ex | 5 + farmbot_core/lib/farmbot_core.ex | 26 +- farmbot_core/lib/firmware/firmware.ex | 95 ++- farmbot_core/lib/firmware/supervisor.ex | 2 +- .../firmware/uart_handler/auto_detector.ex | 6 +- .../lib/firmware/uart_handler/uart_handler.ex | 8 +- farmbot_core/lib/firmware/utils.ex | 2 + farmbot_core/lib/peripheral/supervisor.ex | 14 + farmbot_core/lib/peripheral/worker.ex | 38 ++ farmbot_core/lib/pin_binding/manager.ex | 17 +- farmbot_core/lib/regimen/manager.ex | 11 +- farmbot_core/mix.exs | 5 +- farmbot_core/mix.lock | 3 - ...20180702143518_add_ntp_and_dns_configs.exs | 17 + .../20180802155935_neets_http_sync.exs | 8 + farmbot_ext/.formatter.exs | 4 - farmbot_ext/README.md | 21 - farmbot_ext/config/config.exs | 9 +- farmbot_ext/lib/amqp/auto_sync_transport.ex | 28 +- farmbot_ext/lib/amqp/bot_state_transport.ex | 12 + .../lib/amqp/celery_script_transport.ex | 34 +- farmbot_ext/lib/amqp/connection_worker.ex | 9 +- farmbot_ext/lib/amqp/log_transport.ex | 2 +- farmbot_ext/lib/auto_sync_task.ex | 36 ++ farmbot_ext/lib/bootstrap/supervisor.ex | 4 +- farmbot_ext/lib/http/http.ex | 31 + farmbot_ext/mix.exs | 5 +- farmbot_ext/mix.lock | 2 +- farmbot_os/.formatter.exs | 4 - farmbot_os/.tool-versions | 3 +- farmbot_os/config/host/auth_secret_ci.exs | 9 +- farmbot_os/config/host/dev.exs | 17 +- farmbot_os/config/target/dev.exs | 19 +- farmbot_os/config/target/prod.exs | 16 +- farmbot_os/lib/celery_script/ast.ex | 2 + farmbot_os/lib/celery_script/io_layer.ex | 352 +++++----- farmbot_os/lib/celery_script/io_layer/_if.ex | 69 ++ .../lib/celery_script/io_layer/find_home.ex | 41 ++ .../celery_script/io_layer/move_absolute.ex | 55 ++ .../lib/celery_script/io_layer/read_pin.ex | 84 +++ farmbot_os/lib/celery_script/io_layer/sync.ex | 60 ++ .../lib/celery_script/io_layer/toggle_pin.ex | 33 + .../lib/celery_script/io_layer/write_pin.ex | 78 +++ farmbot_os/lib/core_start.ex | 19 +- farmbot_os/lib/test.ex | 32 + farmbot_os/mix.exs | 4 +- farmbot_os/mix.lock.host | 66 ++ farmbot_os/mix.lock.rpi3 | 92 +++ .../{captive_portal => }/captive_portal.ex | 24 +- farmbot_os/platform/target/network/network.ex | 4 + .../config_wireless_step_2_PSK.html.eex | 5 +- 118 files changed, 5720 insertions(+), 464 deletions(-) create mode 100644 ELIXIR_VERSION create mode 100644 docs/celery_script/all_nodes.md create mode 100644 docs/celery_script/celery_script.md create mode 100644 farmbot_celery_script/.gitignore create mode 120000 farmbot_celery_script/.tool-versions create mode 100644 farmbot_celery_script/fixture/inner_sequence.json create mode 100644 farmbot_celery_script/fixture/master_sequence.json create mode 100644 farmbot_celery_script/fixture/master_sequence.term create mode 100644 farmbot_celery_script/fixture/outer_sequence.json create mode 100644 farmbot_celery_script/fixture/unbound.json create mode 100644 farmbot_celery_script/fixture/unbound_var_x.json create mode 100644 farmbot_celery_script/lib/address.ex create mode 100644 farmbot_celery_script/lib/circular_list.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/ast.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex create mode 100644 farmbot_celery_script/lib/farmbot_celery_script/utils.ex create mode 100644 farmbot_celery_script/lib/pointer.ex create mode 100644 farmbot_celery_script/mix.exs create mode 100644 farmbot_celery_script/mix.lock create mode 100644 farmbot_celery_script/test/address_test.exs create mode 100644 farmbot_celery_script/test/circular_list_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/ast_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs create mode 100644 farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs create mode 100644 farmbot_celery_script/test/pointer_test.exs create mode 100644 farmbot_celery_script/test/support/fixtures.ex create mode 100644 farmbot_celery_script/test/test_helper.exs delete mode 100644 farmbot_core/.formatter.exs mode change 120000 => 100644 farmbot_core/.tool-versions delete mode 100644 farmbot_core/README.md create mode 100644 farmbot_core/lib/asset_storage/asset_logger.ex create mode 100644 farmbot_core/lib/asset_storage/on_start_task.ex delete mode 100644 farmbot_core/lib/celery_script/csvm_wrapper.ex create mode 100644 farmbot_core/lib/celery_script/run_time_wrapper.ex create mode 100644 farmbot_core/lib/celery_script/utils.ex create mode 100644 farmbot_core/lib/peripheral/supervisor.ex create mode 100644 farmbot_core/lib/peripheral/worker.ex create mode 100644 farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs create mode 100644 farmbot_core/priv/config/migrations/20180802155935_neets_http_sync.exs delete mode 100644 farmbot_ext/.formatter.exs delete mode 100644 farmbot_ext/README.md create mode 100644 farmbot_ext/lib/auto_sync_task.ex delete mode 100644 farmbot_os/.formatter.exs mode change 120000 => 100644 farmbot_os/.tool-versions create mode 100644 farmbot_os/lib/celery_script/ast.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/_if.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/find_home.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/move_absolute.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/read_pin.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/sync.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/toggle_pin.ex create mode 100644 farmbot_os/lib/celery_script/io_layer/write_pin.ex create mode 100644 farmbot_os/lib/test.ex create mode 100644 farmbot_os/mix.lock.host create mode 100644 farmbot_os/mix.lock.rpi3 rename farmbot_os/platform/target/configurator/{captive_portal => }/captive_portal.ex (88%) diff --git a/ELIXIR_VERSION b/ELIXIR_VERSION new file mode 100644 index 00000000..7f85cf31 --- /dev/null +++ b/ELIXIR_VERSION @@ -0,0 +1 @@ +~> 1.6 diff --git a/Makefile b/Makefile index 447ead6b..afe580e7 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,10 @@ all: help help: @echo "no" +farmbot_celery_script_clean: + cd farmbot_celery_script && \ + rm -rf _build deps + farmbot_core_clean: cd farmbot_core && \ make clean && \ @@ -35,7 +39,7 @@ farmbot_os_clean: cd farmbot_os && \ rm -rf _build deps -clean: farmbot_core_clean farmbot_ext_clean farmbot_os_clean +clean: farmbot_celery_script_clean farmbot_core_clean farmbot_ext_clean farmbot_os_clean farmbot_core_test: cd farmbot_core && \ diff --git a/docs/celery_script/all_nodes.md b/docs/celery_script/all_nodes.md new file mode 100644 index 00000000..ae297a44 --- /dev/null +++ b/docs/celery_script/all_nodes.md @@ -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` | diff --git a/docs/celery_script/celery_script.md b/docs/celery_script/celery_script.md new file mode 100644 index 00000000..541a2d8a --- /dev/null +++ b/docs/celery_script/celery_script.md @@ -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: []} + ] +} +``` diff --git a/farmbot_celery_script/.gitignore b/farmbot_celery_script/.gitignore new file mode 100644 index 00000000..e4563e5a --- /dev/null +++ b/farmbot_celery_script/.gitignore @@ -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 diff --git a/farmbot_celery_script/.tool-versions b/farmbot_celery_script/.tool-versions new file mode 120000 index 00000000..d54b92fc --- /dev/null +++ b/farmbot_celery_script/.tool-versions @@ -0,0 +1 @@ +../.tool-versions \ No newline at end of file diff --git a/farmbot_celery_script/fixture/inner_sequence.json b/farmbot_celery_script/fixture/inner_sequence.json new file mode 100644 index 00000000..c5c0072a --- /dev/null +++ b/farmbot_celery_script/fixture/inner_sequence.json @@ -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 + } + } + } + } + ] +} \ No newline at end of file diff --git a/farmbot_celery_script/fixture/master_sequence.json b/farmbot_celery_script/fixture/master_sequence.json new file mode 100644 index 00000000..0e473787 --- /dev/null +++ b/farmbot_celery_script/fixture/master_sequence.json @@ -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": {} + } + ] +} \ No newline at end of file diff --git a/farmbot_celery_script/fixture/master_sequence.term b/farmbot_celery_script/fixture/master_sequence.term new file mode 100644 index 0000000000000000000000000000000000000000..44a7643e24b29e99aab5c8d1434a117f73ac855d GIT binary patch literal 2316 zcmb7GO>Yx15KXe#d=ZL3a7B9ri68-Y4jhm;AXRGbma{wQnt1JH?W9Sop7^Jnxgc>b zp4pGaY!lK;W6yftym_7(_ea`GIz7F#ola+vxj%hgTo~N-a5s=T=JIaeeC^QLrBDej zo|;(az!HchFSr$2p>?>1!npOk`{CDbIzQLRR=O`IO@Ib4zyl);hwkm~L}!v~p)3?k zB)p3b)7oISIiMjtm0Vd0R+9_q44MXdu`poU;1s;O;S)U7-SX%3do{nkz5VH7O=7K! zL?~{{XeI|pIBC$Y<2}yw8W^7&EmsyW7{NE0_7O2GJ>OHHR$%@T?Za4S8I;+K&!oPL z5ZU8X#wtKM)z%b?bZTOis^&tmOeetU)+V@Bt85NMX&9qn&E={(&}1^cEQAH-KJSri zv=_PUP}UPVxXTMM3-YcnZgL8R$WzBjdKr!t&mch#Xx^bp_kTRqQ1UqtbTFIEn(zl? zX|P8HaAMF-iu64MO)Pg5Z$o&x&~ZtDXu$Tbz=2oN}c=GAsr32(9MEZ}MSv=4c4WxbQ3Op%4RbJ~yl@kjr8>K(*QuvM2GWf=)<50$p5P!}X7 zv#A?0Ef13Zk%8|kP%$(Ux2d)vY#??64lx3m68G)~cRiis&r}$;QVBT9Kv@JuYRZYz zfCNk|+My3-@(^C(p4|4Uftj=kc|A&-2li+sN%teE<#d?4cjgan{v*Y)*&=7Zb;" + end +end diff --git a/farmbot_celery_script/lib/circular_list.ex b/farmbot_celery_script/lib/circular_list.ex new file mode 100644 index 00000000..c7c4ae8f --- /dev/null +++ b/farmbot_celery_script/lib/circular_list.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex new file mode 100644 index 00000000..113bdc8f --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex new file mode 100644 index 00000000..bb6448a5 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/heap.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex new file mode 100644 index 00000000..e8300517 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/slicer.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex new file mode 100644 index 00000000..f564ff79 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/unslicer.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex new file mode 100644 index 00000000..41b97032 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex new file mode 100644 index 00000000..8f92b4b6 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/error.ex @@ -0,0 +1,7 @@ +defmodule Farmbot.CeleryScript.RunTime.Error do + @moduledoc """ + CSVM runtime error + """ + + defexception [:message, :farm_proc] +end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex new file mode 100644 index 00000000..5481db29 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/farm_proc.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex new file mode 100644 index 00000000..cc0df139 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/inspect.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex new file mode 100644 index 00000000..57ebf1ea --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex new file mode 100644 index 00000000..0fc9bafb --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/instruction_set.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex new file mode 100644 index 00000000..6666129e --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/proc_storage.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex new file mode 100644 index 00000000..5317c6b8 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/resolver.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex b/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex new file mode 100644 index 00000000..ee4650a4 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/run_time/sys_call_handler.ex @@ -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 diff --git a/farmbot_celery_script/lib/farmbot_celery_script/utils.ex b/farmbot_celery_script/lib/farmbot_celery_script/utils.ex new file mode 100644 index 00000000..4ba5e242 --- /dev/null +++ b/farmbot_celery_script/lib/farmbot_celery_script/utils.ex @@ -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 diff --git a/farmbot_celery_script/lib/pointer.ex b/farmbot_celery_script/lib/pointer.ex new file mode 100644 index 00000000..1de18f37 --- /dev/null +++ b/farmbot_celery_script/lib/pointer.ex @@ -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 diff --git a/farmbot_celery_script/mix.exs b/farmbot_celery_script/mix.exs new file mode 100644 index 00000000..12df5faa --- /dev/null +++ b/farmbot_celery_script/mix.exs @@ -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 diff --git a/farmbot_celery_script/mix.lock b/farmbot_celery_script/mix.lock new file mode 100644 index 00000000..d202527d --- /dev/null +++ b/farmbot_celery_script/mix.lock @@ -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"}, +} diff --git a/farmbot_celery_script/test/address_test.exs b/farmbot_celery_script/test/address_test.exs new file mode 100644 index 00000000..bc4d8eb5 --- /dev/null +++ b/farmbot_celery_script/test/address_test.exs @@ -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 diff --git a/farmbot_celery_script/test/circular_list_test.exs b/farmbot_celery_script/test/circular_list_test.exs new file mode 100644 index 00000000..7fd313b1 --- /dev/null +++ b/farmbot_celery_script/test/circular_list_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs new file mode 100644 index 00000000..4e9a8747 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/ast/heap_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs new file mode 100644 index 00000000..1d29324f --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/ast/slicer_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs new file mode 100644 index 00000000..a074f8d8 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/ast/unslicer_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs b/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs new file mode 100644 index 00000000..48968223 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/ast_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs new file mode 100644 index 00000000..16c8138a --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/farm_proc_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs new file mode 100644 index 00000000..b55c1a2e --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/instruction_set_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs new file mode 100644 index 00000000..f9fbd179 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/proc_storage_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs new file mode 100644 index 00000000..cb2fd981 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/resolver_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs new file mode 100644 index 00000000..15d2d659 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/sys_call_handler_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs new file mode 100644 index 00000000..c7efdc99 --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time/utils_test.exs @@ -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 diff --git a/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs b/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs new file mode 100644 index 00000000..e78f049a --- /dev/null +++ b/farmbot_celery_script/test/farmbot_celery_script/run_time_test.exs @@ -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 diff --git a/farmbot_celery_script/test/pointer_test.exs b/farmbot_celery_script/test/pointer_test.exs new file mode 100644 index 00000000..13e9ef15 --- /dev/null +++ b/farmbot_celery_script/test/pointer_test.exs @@ -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 diff --git a/farmbot_celery_script/test/support/fixtures.ex b/farmbot_celery_script/test/support/fixtures.ex new file mode 100644 index 00000000..acf9f55a --- /dev/null +++ b/farmbot_celery_script/test/support/fixtures.ex @@ -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 diff --git a/farmbot_celery_script/test/test_helper.exs b/farmbot_celery_script/test/test_helper.exs new file mode 100644 index 00000000..869559e7 --- /dev/null +++ b/farmbot_celery_script/test/test_helper.exs @@ -0,0 +1 @@ +ExUnit.start() diff --git a/farmbot_core/.formatter.exs b/farmbot_core/.formatter.exs deleted file mode 100644 index 525446d4..00000000 --- a/farmbot_core/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/farmbot_core/.tool-versions b/farmbot_core/.tool-versions deleted file mode 120000 index d54b92fc..00000000 --- a/farmbot_core/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -../.tool-versions \ No newline at end of file diff --git a/farmbot_core/.tool-versions b/farmbot_core/.tool-versions new file mode 100644 index 00000000..cff5eb62 --- /dev/null +++ b/farmbot_core/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.0.4 +elixir 1.6.6-otp-21 diff --git a/farmbot_core/README.md b/farmbot_core/README.md deleted file mode 100644 index 2fff78b8..00000000 --- a/farmbot_core/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# FarmbotCore -Core Farmbot Services. -This includes Logging, Configuration, Asset management and Firmware. diff --git a/farmbot_core/config/config.exs b/farmbot_core/config/config.exs index 4d4ca05f..10f23e08 100644 --- a/farmbot_core/config/config.exs +++ b/farmbot_core/config/config.exs @@ -25,19 +25,16 @@ config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_configs.sqlite3", - priv: "priv/config", - pool_size: 1 + priv: "priv/config" config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_logs.sqlite3", - priv: "priv/logger", - pool_size: 1 + priv: "priv/logger" config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_assets.sqlite3", - priv: "priv/asset", - pool_size: 1 + priv: "priv/asset" diff --git a/farmbot_core/lib/asset_storage/asset.ex b/farmbot_core/lib/asset_storage/asset.ex index a62f83a9..e16e5081 100644 --- a/farmbot_core/lib/asset_storage/asset.ex +++ b/farmbot_core/lib/asset_storage/asset.ex @@ -25,6 +25,46 @@ defmodule Farmbot.Asset do alias Repo.Snapshot require Farmbot.Logger import Ecto.Query + import Farmbot.Config, only: [update_config_value: 4] + require Logger + + @device_fields ~W(id name timezone) + @farm_events_fields ~W(calendar end_time executable_id executable_type id repeat start_time time_unit) + @peripherals_fields ~W(id label mode pin) + @pin_bindings_fields ~W(id pin_num sequence_id special_action) + @points_fields ~W(id meta name pointer_type tool_id x y z) + @regimens_fields ~W(farm_event_id id name regimen_items) + @sensors_fields ~W(id label mode pin) + @sequences_fields ~W(args body id kind name) + @tools_fields ~W(id name) + + def to_asset(body, kind) when is_binary(kind) do + camel_kind = Module.concat(["Farmbot", "Asset", Macro.camelize(kind)]) + to_asset(body, camel_kind) + end + + def to_asset(body, Device), do: resource_decode(body, @device_fields, Device) + def to_asset(body, FarmEvent), do: resource_decode(body, @farm_events_fields, FarmEvent) + def to_asset(body, Peripheral), do: resource_decode(body, @peripherals_fields, Peripheral) + def to_asset(body, PinBinding), do: resource_decode(body, @pin_bindings_fields, PinBinding) + def to_asset(body, Point), do: resource_decode(body, @points_fields, Point) + def to_asset(body, Regimen), do: resource_decode(body, @regimens_fields, Regimen) + def to_asset(body, Sensor), do: resource_decode(body, @sensors_fields, Sensor) + def to_asset(body, Sequence), do: resource_decode(body, @sequences_fields, Sequence) + def to_asset(body, Tool), do: resource_decode(body, @tools_fields, Tool) + + def resource_decode(data, fields, kind) when is_list(data), + do: Enum.map(data, &resource_decode(&1, fields, kind)) + + def resource_decode(data, fields, kind) do + data + |> Map.take(fields) + |> Enum.map(&string_to_atom/1) + |> into_struct(kind) + end + + def string_to_atom({k, v}), do: {String.to_atom(k), v} + def into_struct(data, kind), do: struct(kind, data) def fragment_sync(verbosity \\ 1) do Farmbot.Logger.busy verbosity, "Syncing" @@ -43,6 +83,43 @@ defmodule Farmbot.Asset do :ok end + def full_sync(verbosity \\ 1, fetch_fun) do + Farmbot.Logger.busy verbosity, "Syncing" + Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :syncing}) + results = try do + fetch_fun.() + rescue + ex -> + Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :sync_error}) + message = Exception.message(ex) + Logger.error "Fetching resources failed: #{message}" + update_config_value(:bool, "settings", "needs_http_sync", true) + {:error, message} + end + + case results do + {:ok, all_sync_cmds} when is_list(all_sync_cmds) -> + Repo.transaction fn() -> + :ok = Farmbot.Asset.clear_all_data() + for cmd <- all_sync_cmds do + apply_sync_cmd(cmd) + end + end + destroy_all_sync_cmds() + Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :synced}) + Farmbot.Logger.success verbosity, "Synced" + update_config_value(:bool, "settings", "needs_http_sync", false) + :ok + {:error, reason} when is_binary(reason) -> + destroy_all_sync_cmds() + Farmbot.Registry.dispatch(__MODULE__, {:sync_status, :sync_error}) + Farmbot.Logger.error verbosity, "Sync error: #{reason}" + update_config_value(:bool, "settings", "needs_http_sync", true) + :ok + end + + end + def apply_sync_cmd(cmd) do mod = Module.concat(["Farmbot", "Asset", cmd.kind]) if Code.ensure_loaded?(mod) do @@ -64,7 +141,8 @@ defmodule Farmbot.Asset do destroy_sync_cmd(cmd) end - defp dispatch_sync(diff) do + @doc false + def dispatch_sync(diff) do for deletion <- diff.deletions do Farmbot.Registry.dispatch(__MODULE__, {:deletion, deletion}) end @@ -127,10 +205,17 @@ defmodule Farmbot.Asset do Use the `Farmbot.Asset.Registry` for these types of events. """ def register_sync_cmd(remote_id, kind, body) when is_binary(kind) do - SyncCmd.changeset(struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body})) + new_sync_cmd(remote_id, kind, body) + |> SyncCmd.changeset() |> Repo.insert!() end + def new_sync_cmd(remote_id, kind, body) + when is_integer(remote_id) when is_binary(kind) + do + struct(SyncCmd, %{remote_id: remote_id, kind: kind, body: body}) + end + @doc "Destroy all sync cmds locally." def destroy_all_sync_cmds do Repo.delete_all(SyncCmd) @@ -140,6 +225,7 @@ defmodule Farmbot.Asset do Repo.all(SyncCmd) end + def destroy_sync_cmd(%SyncCmd{id: nil} = cmd), do: {:ok, cmd} def destroy_sync_cmd(%SyncCmd{} = cmd) do Repo.delete(cmd) end @@ -217,11 +303,21 @@ defmodule Farmbot.Asset do Repo.one(from(p in Peripheral, where: p.id == ^peripheral_id)) end + @doc "Get a peripheral by it's pin." + def get_peripheral_by_number(number) do + Repo.one(from(p in Peripheral, where: p.pin == ^number)) + end + @doc "Get a Sensor by it's id." def get_sensor_by_id(sensor_id) do Repo.one(from(s in Sensor, where: s.id == ^sensor_id)) end + @doc "Get a peripheral by it's pin." + def get_sensor_by_number(number) do + Repo.one(from(s in Sensor, where: s.pin == ^number)) + end + @doc "Get a Sequence by it's id." def get_sequence_by_id(sequence_id) do Repo.one(from(s in Sequence, where: s.id == ^sequence_id)) @@ -271,7 +367,7 @@ defmodule Farmbot.Asset do @doc "Fetches all regimens that use a particular sequence." def get_regimens_using_sequence(sequence_id) do - uses_seq = &match?(^sequence_id, Map.fetch!(&1, :sequence_id)) + uses_seq = &match?(^sequence_id, Map.fetch!(&1, "sequence_id")) Repo.all(Regimen) |> Enum.filter(&Enum.find(Map.fetch!(&1, :regimen_items), uses_seq)) diff --git a/farmbot_core/lib/asset_storage/asset_logger.ex b/farmbot_core/lib/asset_storage/asset_logger.ex new file mode 100644 index 00000000..b67c604a --- /dev/null +++ b/farmbot_core/lib/asset_storage/asset_logger.ex @@ -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 diff --git a/farmbot_core/lib/asset_storage/on_start_task.ex b/farmbot_core/lib/asset_storage/on_start_task.ex new file mode 100644 index 00000000..74335d90 --- /dev/null +++ b/farmbot_core/lib/asset_storage/on_start_task.ex @@ -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 diff --git a/farmbot_core/lib/asset_storage/point.ex b/farmbot_core/lib/asset_storage/point.ex index 1057097d..89aaec4d 100644 --- a/farmbot_core/lib/asset_storage/point.ex +++ b/farmbot_core/lib/asset_storage/point.ex @@ -21,7 +21,7 @@ defmodule Farmbot.Asset.Point do @optional_fields [:tool_id] def changeset(%Point{} = point, params \\ %{}) do - %Point{} = point + point |> cast(params, @required_fields ++ @optional_fields) |> validate_required(@required_fields) end diff --git a/farmbot_core/lib/asset_storage/sensor.ex b/farmbot_core/lib/asset_storage/sensor.ex index af0972f3..ce7d7111 100644 --- a/farmbot_core/lib/asset_storage/sensor.ex +++ b/farmbot_core/lib/asset_storage/sensor.ex @@ -3,7 +3,7 @@ defmodule Farmbot.Asset.Sensor do Sensors are descriptors for pins/modes. """ - alias Farmbot.Asset.Sensor + alias Farmbot.Asset.Sensor use Ecto.Schema import Ecto.Changeset @@ -16,7 +16,7 @@ defmodule Farmbot.Asset.Sensor do @required_fields [:id, :pin, :mode, :label] def changeset(%Sensor{} = sensor, params \\ %{}) do - %Sensor{} = sensor + sensor |> cast(params, @required_fields) |> validate_required(@required_fields) |> unique_constraint(:id) diff --git a/farmbot_core/lib/asset_storage/sequence.ex b/farmbot_core/lib/asset_storage/sequence.ex index 500df726..926d2630 100644 --- a/farmbot_core/lib/asset_storage/sequence.ex +++ b/farmbot_core/lib/asset_storage/sequence.ex @@ -7,6 +7,7 @@ defmodule Farmbot.Asset.Sequence do alias Farmbot.EctoTypes.TermType use Ecto.Schema import Ecto.Changeset + require Farmbot.Logger schema "sequences" do field(:name, :string) @@ -26,9 +27,14 @@ defmodule Farmbot.Asset.Sequence do @behaviour Farmbot.Asset.FarmEvent def schedule_event(%Sequence{} = sequence, _now) do - case Farmbot.CeleryScript.schedule_sequence(sequence) do - %{status: :crashed} = proc -> {:error, Csvm.FarmProc.get_crash_reason(proc)} - _ -> :ok - end + Farmbot.Logger.busy 1, "[#{sequence.name}] Sequence init." + Farmbot.Core.CeleryScript.sequence(sequence, fn(result) -> + case result do + :ok -> + Farmbot.Logger.success 1, "[#{sequence.name}] Sequence complete." + {:error, _} -> + Farmbot.Logger.error 1, "[#{sequence.nam}] Sequece failed!" + end + end) end end diff --git a/farmbot_core/lib/asset_storage/supervisor.ex b/farmbot_core/lib/asset_storage/supervisor.ex index eefa2f6d..f313e2a0 100644 --- a/farmbot_core/lib/asset_storage/supervisor.ex +++ b/farmbot_core/lib/asset_storage/supervisor.ex @@ -8,11 +8,14 @@ defmodule Farmbot.Asset.Supervisor do def init([]) do children = [ - {Farmbot.Asset.Repo, [] }, - {Farmbot.Regimen.NameProvider, [] }, - {Farmbot.FarmEvent.Supervisor, [] }, - {Farmbot.Regimen.Supervisor, [] }, - {Farmbot.PinBinding.Supervisor, [] }, + {Farmbot.Asset.Logger, []}, + {Farmbot.Asset.Repo, []}, + {Farmbot.Regimen.NameProvider, []}, + {Farmbot.FarmEvent.Supervisor, []}, + {Farmbot.Regimen.Supervisor, []}, + {Farmbot.PinBinding.Supervisor, []}, + {Farmbot.Peripheral.Supervisor, []}, + {Farmbot.Asset.OnStartTask, []}, ] Supervisor.init(children, [strategy: :one_for_one]) end diff --git a/farmbot_core/lib/asset_storage/sync_cmd.ex b/farmbot_core/lib/asset_storage/sync_cmd.ex index 5877b5a9..233e5ecd 100644 --- a/farmbot_core/lib/asset_storage/sync_cmd.ex +++ b/farmbot_core/lib/asset_storage/sync_cmd.ex @@ -13,7 +13,7 @@ defmodule Farmbot.Asset.SyncCmd do timestamps() end - @required_fields [:remote_id, :kind] + @required_fields [:kind, :remote_id] def changeset(%SyncCmd{} = cmd, params \\ %{}) do cmd diff --git a/farmbot_core/lib/bot_state/bot_state.ex b/farmbot_core/lib/bot_state/bot_state.ex index 1ef73a31..86fe478c 100644 --- a/farmbot_core/lib/bot_state/bot_state.ex +++ b/farmbot_core/lib/bot_state/bot_state.ex @@ -111,8 +111,9 @@ defmodule Farmbot.BotState do @doc false def handle_call(:fetch, _from, state) do - Farmbot.Registry.dispatch(__MODULE__, state) - {:reply, state, [], state} + new_state = handle_event({:informational_settings, %{cache_bust: :rand.uniform(1000)}}, state) + Farmbot.Registry.dispatch(__MODULE__, new_state) + {:reply, state, [], new_state} end # TODO(Connor) - Fix this to use event system. @@ -181,7 +182,7 @@ defmodule Farmbot.BotState do {:noreply, [], new_state} end - def handle_info({Farmbot.Registry, {Farmbot.Asset.Repo, {:sync_status, status}}}, state) do + def handle_info({Farmbot.Registry, {_, {:sync_status, status}}}, state) do event = {:informational_settings, %{sync_status: status}} new_state = handle_event(event, state) Farmbot.Registry.dispatch(__MODULE__, new_state) diff --git a/farmbot_core/lib/bot_state/location_data.ex b/farmbot_core/lib/bot_state/location_data.ex index ce282ad2..48b837d9 100644 --- a/farmbot_core/lib/bot_state/location_data.ex +++ b/farmbot_core/lib/bot_state/location_data.ex @@ -1,8 +1,8 @@ defmodule Farmbot.BotState.LocationData do @moduledoc false defstruct [ - scaled_encoders: nil, - raw_encoders: nil, - position: nil + scaled_encoders: %{x: -1, y: -1, z: -1}, + raw_encoders: %{x: -1, y: -1, z: -1}, + position: %{x: -1, y: -1, z: -1} ] end diff --git a/farmbot_core/lib/celery_script/celery_script.ex b/farmbot_core/lib/celery_script/celery_script.ex index b6b7885f..c4d8a1f3 100644 --- a/farmbot_core/lib/celery_script/celery_script.ex +++ b/farmbot_core/lib/celery_script/celery_script.ex @@ -1,18 +1,12 @@ -defmodule Farmbot.CeleryScript do - def to_ast(data) do - Csvm.AST.decode(data) +defmodule Farmbot.Core.CeleryScript do + @moduledoc """ + Helpers for executing CeleryScript. + """ + def rpc_request(data, fun) do + Farmbot.CeleryScript.RunTime.rpc_request(Farmbot.CeleryScript.RunTime, data, fun) end - def execute_sequence(%Farmbot.Asset.Sequence{} = seq) do - schedule_sequence(seq) - |> await_sequence() - end - - def schedule_sequence(%Farmbot.Asset.Sequence{} = seq) do - Csvm.queue(seq, seq.id) - end - - def await_sequence(ref) do - Csvm.await(ref) + def sequence(%Farmbot.Asset.Sequence{} = seq, fun) do + Farmbot.CeleryScript.RunTime.sequence(Csvm, seq, seq.id, fun) end end diff --git a/farmbot_core/lib/celery_script/csvm_wrapper.ex b/farmbot_core/lib/celery_script/csvm_wrapper.ex deleted file mode 100644 index 24addd02..00000000 --- a/farmbot_core/lib/celery_script/csvm_wrapper.ex +++ /dev/null @@ -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 diff --git a/farmbot_core/lib/celery_script/io_layer.ex b/farmbot_core/lib/celery_script/io_layer.ex index 0be2f021..d5fa1d48 100644 --- a/farmbot_core/lib/celery_script/io_layer.ex +++ b/farmbot_core/lib/celery_script/io_layer.ex @@ -1,3 +1,47 @@ -defmodule Farmbot.CeleryScript.IOLayer do - @callback handle_io(Csvm.AST.t()) :: {:ok, Csvm.AST.t()} | :ok | {:error, String.t()} +defmodule Farmbot.Core.CeleryScript.IOLayer do + @moduledoc """ + Behaviour for all functions a CeleryScript Runtime IO layer needs to + implement. + """ + alias Farmbot.CeleryScript.AST + @type args :: AST.args() + @type body :: AST.body() + + # Simple IO + @callback write_pin(args, body) :: :ok | {:error, String.t} + @callback read_pin(args, body) :: :ok | {:error, String.t} + @callback set_servo_angle(args, body) :: :ok | {:error, String.t} + @callback send_message(args, body) :: :ok | {:error, String.t} + @callback move_relative(args, body) :: :ok | {:error, String.t} + @callback home(args, body) :: :ok | {:error, String.t} + @callback find_home(args, body) :: :ok | {:error, String.t} + @callback wait(args, body) :: :ok | {:error, String.t} + @callback toggle_pin(args, body) :: :ok | {:error, String.t} + @callback execute_script(args, body) :: :ok | {:error, String.t} + @callback zero(args, body) :: :ok | {:error, String.t} + @callback calibrate(args, body) :: :ok | {:error, String.t} + @callback take_photo(args, body) :: :ok | {:error, String.t} + @callback config_update(args, body) :: :ok | {:error, String.t} + @callback set_user_env(args, body) :: :ok | {:error, String.t} + @callback install_first_party_farmware(args, body) :: :ok | {:error, String.t} + @callback install_farmware(args, body) :: :ok | {:error, String.t} + @callback uninstall_farmware(args, body) :: :ok | {:error, String.t} + @callback update_farmware(args, body) :: :ok | {:error, String.t} + @callback read_status(args, body) :: :ok | {:error, String.t} + @callback sync(args, body) :: :ok | {:error, String.t} + @callback power_off(args, body) :: :ok | {:error, String.t} + @callback reboot(args, body) :: :ok | {:error, String.t} + @callback factory_reset(args, body) :: :ok | {:error, String.t} + @callback change_ownership(args, body) :: :ok | {:error, String.t} + @callback check_updates(args, body) :: :ok | {:error, String.t} + @callback dump_info(args, body) :: :ok | {:error, String.t} + @callback move_absolute(args, body) :: :ok | {:error, String.t} + + # Complex IO. + # @callbcak _if(args, body) :: {:ok, AST.t} | {:error, String.t} + @callback execute(args, body) :: {:ok, AST.t} | {:error, String.t} + + # Special IO. + @callback emergency_lock(args, body) :: any + @callback emergency_unlock(args, body) :: any end diff --git a/farmbot_core/lib/celery_script/run_time_wrapper.ex b/farmbot_core/lib/celery_script/run_time_wrapper.ex new file mode 100644 index 00000000..5e370cf0 --- /dev/null +++ b/farmbot_core/lib/celery_script/run_time_wrapper.ex @@ -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 diff --git a/farmbot_core/lib/celery_script/stub_io_layer.ex b/farmbot_core/lib/celery_script/stub_io_layer.ex index 26ffe833..489ae466 100644 --- a/farmbot_core/lib/celery_script/stub_io_layer.ex +++ b/farmbot_core/lib/celery_script/stub_io_layer.ex @@ -1,9 +1,35 @@ -defmodule Farmbot.CeleryScript.StubIOLayer do - @behaviour Farmbot.CeleryScript.IOLayer - - def handle_io(ast) do - IO.puts "#{ast.kind} not implemented." - # {:error, "#{ast.kind} not implemented."} - :ok - end +defmodule Farmbot.Core.CeleryScript.StubIOLayer do + @behaviour Farmbot.Core.CeleryScript.IOLayer + def calibrate(_args, _body), do: {:error, "Stubbed"} + def change_ownership(_args, _body), do: {:error, "Stubbed"} + def check_updates(_args, _body), do: {:error, "Stubbed"} + def config_update(_args, _body), do: {:error, "Stubbed"} + def dump_info(_args, _body), do: {:error, "Stubbed"} + def emergency_lock(_args, _body), do: {:error, "Stubbed"} + def emergency_unlock(_args, _body), do: {:error, "Stubbed"} + def execute(_args, _body), do: {:error, "Stubbed"} + def execute_script(_args, _body), do: {:error, "Stubbed"} + def factory_reset(_args, _body), do: {:error, "Stubbed"} + def find_home(_args, _body), do: {:error, "Stubbed"} + def home(_args, _body), do: {:error, "Stubbed"} + def install_farmware(_args, _body), do: {:error, "Stubbed"} + def install_first_party_farmware(_args, _body), do: {:error, "Stubbed"} + def move_absolute(_args, _body), do: {:error, "Stubbed"} + def move_relative(_args, _body), do: {:error, "Stubbed"} + def power_off(_args, _body), do: {:error, "Stubbed"} + def read_pin(_args, _body), do: {:error, "Stubbed"} + def read_status(_args, _body), do: {:error, "Stubbed"} + def reboot(_args, _body), do: {:error, "Stubbed"} + def send_message(_args, _body), do: {:error, "Stubbed"} + def set_servo_angle(_args, _body), do: {:error, "Stubbed"} + def set_user_env(_args, _body), do: {:error, "Stubbed"} + def sync(_args, _body), do: {:error, "Stubbed"} + def take_photo(_args, _body), do: {:error, "Stubbed"} + def toggle_pin(_args, _body), do: {:error, "Stubbed"} + def uninstall_farmware(_args, _body), do: {:error, "Stubbed"} + def update_farmware(_args, _body), do: {:error, "Stubbed"} + def wait(_args, _body), do: {:error, "Stubbed"} + def write_pin(_args, _body), do: {:error, "Stubbed"} + def zero(_args, _body), do: {:error, "Stubbed"} + def _if(_args, _body), do: {:error, "Stubbed"} end diff --git a/farmbot_core/lib/celery_script/supervisor.ex b/farmbot_core/lib/celery_script/supervisor.ex index ff436b48..73afd5ff 100644 --- a/farmbot_core/lib/celery_script/supervisor.ex +++ b/farmbot_core/lib/celery_script/supervisor.ex @@ -1,4 +1,4 @@ -defmodule Farmbot.CeleryScript.Supervisor do +defmodule Farmbot.Core.CeleryScript.Supervisor do @moduledoc false use Supervisor @@ -8,7 +8,7 @@ defmodule Farmbot.CeleryScript.Supervisor do def init([]) do children = [ - {Farmbot.CeleryScript.CsvmWrapper, []} + {Farmbot.Core.CeleryScript.RunTimeWrapper, []} ] Supervisor.init(children, [strategy: :one_for_one]) end diff --git a/farmbot_core/lib/celery_script/utils.ex b/farmbot_core/lib/celery_script/utils.ex new file mode 100644 index 00000000..91351505 --- /dev/null +++ b/farmbot_core/lib/celery_script/utils.ex @@ -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 diff --git a/farmbot_core/lib/farmbot_core.ex b/farmbot_core/lib/farmbot_core.ex index 2f534baa..ff7409ca 100644 --- a/farmbot_core/lib/farmbot_core.ex +++ b/farmbot_core/lib/farmbot_core.ex @@ -5,30 +5,18 @@ defmodule Farmbot.Core do """ use Application - def child_spec(opts) do - %{ - id: __MODULE__, - start: {__MODULE__, :start_link, [opts]}, - type: :worker, - restart: :permanent, - shutdown: 500 - } - end - @doc false def start(_, args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__) - def start_link(args), do: Supervisor.start_link(__MODULE__, args, name: __MODULE__) - def init([]) do children = [ - {Farmbot.Registry, [] }, - {Farmbot.Logger.Supervisor, [] }, - {Farmbot.Config.Supervisor, [] }, - {Farmbot.Asset.Supervisor, [] }, - {Farmbot.Firmware.Supervisor, [] }, - {Farmbot.BotState, [] }, - {Farmbot.CeleryScript.Supervisor, [] }, + {Farmbot.Registry, []}, + {Farmbot.Logger.Supervisor, []}, + {Farmbot.Config.Supervisor, []}, + {Farmbot.Firmware.Supervisor, []}, + {Farmbot.Asset.Supervisor, []}, + {Farmbot.BotState, []}, + {Farmbot.Core.CeleryScript.Supervisor, []}, ] Supervisor.init(children, [strategy: :one_for_one]) end diff --git a/farmbot_core/lib/firmware/firmware.ex b/farmbot_core/lib/firmware/firmware.ex index 4e6075fe..5ea9b6be 100644 --- a/farmbot_core/lib/firmware/firmware.ex +++ b/farmbot_core/lib/firmware/firmware.ex @@ -22,12 +22,14 @@ defmodule Farmbot.Firmware do end @doc "Calibrate an axis." - def calibrate(axis) do + def calibrate(axis) when is_binary(axis) do + axis = String.to_atom(axis) GenStage.call(__MODULE__, {:calibrate, [axis]}, @call_timeout) end @doc "Find home on an axis." - def find_home(axis) do + def find_home(axis) when is_binary(axis) do + axis = String.to_atom(axis) GenStage.call(__MODULE__, {:find_home, [axis]}, @call_timeout) end @@ -37,12 +39,14 @@ defmodule Farmbot.Firmware do end @doc "Home an axis." - def home(axis) do + def home(axis) when is_binary(axis) do + axis = String.to_atom(axis) GenStage.call(__MODULE__, {:home, [axis]}, @call_timeout) end @doc "Manually set an axis's current position to zero." - def zero(axis) do + def zero(axis) when is_binary(axis) do + axis = String.to_atom(axis) GenStage.call(__MODULE__, {:zero, [axis]}, @call_timeout) end @@ -107,6 +111,14 @@ defmodule Farmbot.Firmware do GenStage.call(__MODULE__, :params_reported) end + def get_pin_value(pin_num) do + GenStage.call(__MODULE__, {:call, {:get_pin_value, pin_num}}) + end + + def get_current_position do + GenStage.call(__MODULE__, {:call, :get_current_position}) + end + @doc "Start the firmware services." def start_link(args) do GenStage.start_link(__MODULE__, args, name: __MODULE__) @@ -121,6 +133,11 @@ defmodule Farmbot.Firmware do handler_mod: nil, idle: false, timer: nil, + location_data: %{ + position: %{x: -1, y: -1, z: -1}, + scaled_encoders: %{x: -1, y: -1, z: -1}, + raw_encoders: %{x: -1, y: -1, z: -1}, + }, pins: %{}, params: %{}, params_reported: false, @@ -155,6 +172,7 @@ defmodule Farmbot.Firmware do def init([]) do handler_mod = Application.get_env(:farmbot_core, :behaviour)[:firmware_handler] || raise("No fw handler.") + |> IO.inspect(label: "FW Handler") case handler_mod.start_link() do {:ok, handler} -> @@ -165,9 +183,9 @@ defmodule Farmbot.Firmware do struct(State, initial), subscribe_to: [handler], dispatcher: GenStage.BroadcastDispatcher } - {:error, reason} -> + :ignore -> + Farmbot.Logger.error 1, "Failed to initialize firmware. Falling back to stub implementation." replace_firmware_handler(Farmbot.Firmware.StubHandler) - Farmbot.Logger.error 1, "Failed to initialize firmware: #{inspect reason} Falling back to stub implementation." init([]) end @@ -181,7 +199,7 @@ defmodule Farmbot.Firmware do unless :queue.is_empty(state.queue) do list = :queue.to_list(state.queue) for cmd <- list do - :ok = do_reply(%{state | current: cmd}, {:error, reason}) + :ok = do_reply(%{state | current: cmd}, {:error, "Firmware handler crash"}) end end end @@ -212,7 +230,7 @@ defmodule Farmbot.Firmware do :ok -> timer = start_timer(current, state.timeout_ms) {:noreply, [], %{state | current: current, timer: timer}} - {:error, _} = res -> + {:error, reason} = res when is_binary(reason) -> do_reply(state, res) {:noreply, [], %{state | current: nil, queue: :queue.new()}} end @@ -229,13 +247,23 @@ defmodule Farmbot.Firmware do end end + def handle_call({:call, {:get_pin_value, pin_num}}, _from, state) do + {:reply, state.pins[pin_num], [], state} + end + + def handle_call({:call, :get_current_position}, _from, state) do + {:reply, state.location_data.position, [], state} + end + def handle_call(:params_reported, _, state) do {:reply, state.params_reported, [], state} end - def handle_call({fun, _}, _from, state = %{initialized: false}) + def handle_call({fun, args}, from, state = %{initialized: false}) when fun not in [:read_all_params, :update_param, :emergency_unlock, :emergency_lock, :request_software_version] do - {:reply, {:error, :uninitialized}, [], state} + next_current = struct(Command, from: from, fun: fun, args: args) + do_queue_cmd(next_current, state) + # {:reply, {:error, "uninitialized"}, [], state} end def handle_call({fun, args}, from, state) do @@ -244,7 +272,7 @@ defmodule Farmbot.Firmware do cond do fun == :emergency_lock -> if current_current do - do_reply(state, {:error, :emergency_lock}) + do_reply(state, {:error, "emergency_lock"}) end do_begin_cmd(next_current, state, []) match?(%Command{}, current_current) -> @@ -264,7 +292,7 @@ defmodule Farmbot.Firmware do else {:noreply, dispatch, %{state | current: current, timer: timer}} end - {:error, _} = res -> + {:error, reason} = res when is_binary(reason) -> do_reply(%{state | current: current}, res) {:noreply, dispatch, %{state | current: nil}} end @@ -281,11 +309,16 @@ defmodule Farmbot.Firmware do # if after handling the current buffer of gcodes, # Try to start the next command in the queue if it exists. if List.last(gcodes) == :idle && state.current == nil do - case :queue.out(state.queue) do - {{:value, next_current}, new_queue} -> - do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs) - {:empty, queue} -> # nothing to do if the queue is empty. - {:noreply, diffs, %{state | queue: queue}} + if state.initialized do + case :queue.out(state.queue) do + {{:value, next_current}, new_queue} -> + do_begin_cmd(next_current, %{state | queue: new_queue, current: next_current}, diffs) + {:empty, queue} -> # nothing to do if the queue is empty. + {:noreply, diffs, %{state | queue: queue}} + end + else + Farmbot.Logger.warn 1, "Fw not initialized yet" + {:noreply, diffs, state} end else {:noreply, diffs, state} @@ -314,7 +347,7 @@ defmodule Farmbot.Firmware do maybe_cancel_timer(state.timer, state.current) if state.current do Farmbot.Logger.error 1, "Got #{code} while executing `#{inspect state.current}`." - do_reply(state, {:error, :firmware_error}) + do_reply(state, {:error, "Firmware error. See log."}) {nil, %{state | current: nil}} else {nil, state} @@ -368,15 +401,21 @@ defmodule Farmbot.Firmware do end defp handle_gcode({:report_current_position, x, y, z}, state) do - {:location_data, %{position: %{x: x, y: y, z: z}}, state} + position = %{position: %{x: x, y: y, z: z}} + new_state = %{state | location_data: Map.merge(state.location_data, position)} + {:location_data, position, new_state} end defp handle_gcode({:report_encoder_position_scaled, x, y, z}, state) do - {:location_data, %{scaled_encoders: %{x: x, y: y, z: z}}, state} + scaled_encoders = %{scaled_encoders: %{x: x, y: y, z: z}} + new_state = %{state | location_data: Map.merge(state.location_data, scaled_encoders)} + {:location_data, scaled_encoders, new_state} end defp handle_gcode({:report_encoder_position_raw, x, y, z}, state) do - {:location_data, %{raw_encoders: %{x: x, y: y, z: z}}, state} + raw_encoders = %{raw_encoders: %{x: x, y: y, z: z}} + new_state = %{state | location_data: Map.merge(state.location_data, raw_encoders)} + {:location_data, raw_encoders, new_state} end defp handle_gcode({:report_end_stops, xa, xb, ya, yb, za, zb}, state) do @@ -444,17 +483,17 @@ defmodule Farmbot.Firmware do end defp handle_gcode(:report_axis_timeout_x, state) do - do_reply(state, {:error, :axis_timeout_x}) + do_reply(state, {:error, "Axis X timeout"}) {nil, %{state | timer: nil}} end defp handle_gcode(:report_axis_timeout_y, state) do - do_reply(state, {:error, :axis_timeout_y}) + do_reply(state, {:error, "Axis Y timeout"}) {nil, %{state | timer: nil}} end defp handle_gcode(:report_axis_timeout_z, state) do - do_reply(state, {:error, :axis_timeout_z}) + do_reply(state, {:error, "Axis Z timeout"}) {nil, %{state | timer: nil}} end @@ -487,9 +526,9 @@ defmodule Farmbot.Firmware do maybe_cancel_timer(state.timer, state.current) if state.current do do_reply(state, :ok) - {:informational_settings, %{busy: true}, %{state | current: nil}} + {:informational_settings, %{busy: false}, %{state | current: nil}} else - {:informational_settings, %{busy: true}, state} + {:informational_settings, %{busy: false}, state} end end @@ -582,7 +621,7 @@ defmodule Farmbot.Firmware do :ok _ -> report_calibration_callback(tries - 1, param, val) end - {:error, reason} -> + {:error, reason} when is_binary(reason) -> Farmbot.Logger.error 1, "Failed to set #{param}: #{val} (#{inspect reason})" report_calibration_callback(tries - 1, param, val) end @@ -597,7 +636,7 @@ defmodule Farmbot.Firmware do EstopTimer.cancel_timer() :ok = GenServer.reply from, reply %Command{fun: :emergency_lock, from: from} -> - :ok = GenServer.reply from, {:error, :emergency_lock} + :ok = GenServer.reply from, {:error, "Emergency Lock"} %Command{fun: _fun, from: from} -> # Farmbot.Logger.success 3, "FW Replying: #{fun}: #{inspect from}" :ok = GenServer.reply from, reply diff --git a/farmbot_core/lib/firmware/supervisor.ex b/farmbot_core/lib/firmware/supervisor.ex index 44b8813a..ab9c7218 100644 --- a/farmbot_core/lib/firmware/supervisor.ex +++ b/farmbot_core/lib/firmware/supervisor.ex @@ -5,7 +5,7 @@ defmodule Farmbot.Firmware.Supervisor do @doc "Reinitializes the Firmware stack. Warning has MANY SIDE EFFECTS." def reinitialize do Farmbot.Firmware.UartHandler.AutoDetector.start_link([]) - Supervisor.terminate_child(Farmbot.Bootstrap.Supervisor, Farmbot.Firmware.Supervisor) + Supervisor.terminate_child(Farmbot.Core, Farmbot.Firmware.Supervisor) end @doc false diff --git a/farmbot_core/lib/firmware/uart_handler/auto_detector.ex b/farmbot_core/lib/firmware/uart_handler/auto_detector.ex index 7c2be4c8..cc834abe 100644 --- a/farmbot_core/lib/firmware/uart_handler/auto_detector.ex +++ b/farmbot_core/lib/firmware/uart_handler/auto_detector.ex @@ -14,7 +14,7 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do alias Circuits.UART alias Farmbot.Firmware.{UartHandler, StubHandler, Utils} import Utils - require Farmbot.Logger + require Logger use GenServer #TODO(Connor) - Maybe make this configurable? @@ -48,12 +48,12 @@ defmodule Farmbot.Firmware.UartHandler.AutoDetector do case auto_detect() do [dev] -> dev = "/dev/#{dev}" - Farmbot.Logger.success 3, "detected target UART: #{dev}" + Logger.debug "detected target UART: #{dev}" replace_firmware_handler(UartHandler) Application.put_env(:farmbot_core, :uart_handler, tty: dev) dev _ -> - Farmbot.Logger.error 1, "Could not detect a UART device." + Logger.debug "Could not detect a UART device." replace_firmware_handler(StubHandler) :error end diff --git a/farmbot_core/lib/firmware/uart_handler/uart_handler.ex b/farmbot_core/lib/firmware/uart_handler/uart_handler.ex index d1707345..4f6f5de0 100644 --- a/farmbot_core/lib/firmware/uart_handler/uart_handler.ex +++ b/farmbot_core/lib/firmware/uart_handler/uart_handler.ex @@ -109,13 +109,13 @@ defmodule Farmbot.Firmware.UartHandler do hw = get_config_value(:string, "settings", "firmware_hardware") gen_stage_opts = [ dispatcher: GenStage.BroadcastDispatcher, - subscribe_to: [ConfigStorage.Dispatcher] ] case open_tty(tty) do {:ok, nerves} -> - {:producer_consumer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts} - err -> - {:stop, err} + {:producer, %State{nerves: nerves, tty: tty, hw: hw}, gen_stage_opts} + {:error, reason} -> + Farmbot.Logger.error 1, "Uart handler failed to initialize: #{inspect reason}" + :ignore end end diff --git a/farmbot_core/lib/firmware/utils.ex b/farmbot_core/lib/firmware/utils.ex index 0aad803b..73f6ed66 100644 --- a/farmbot_core/lib/firmware/utils.ex +++ b/farmbot_core/lib/firmware/utils.ex @@ -19,6 +19,8 @@ defmodule Farmbot.Firmware.Utils do @doc "Changes `:digital` => 0, and `:analog` => 1" def extract_pin_mode(:digital), do: 0 def extract_pin_mode(:analog), do: 1 + def extract_pin_mode(0), do: 0 + def extract_pin_mode(1), do: 1 # https://github.com/arduino/Arduino/blob/2bfe164b9a5835e8cb6e194b928538a9093be333/hardware/arduino/avr/cores/arduino/Arduino.h#L43-L45 diff --git a/farmbot_core/lib/peripheral/supervisor.ex b/farmbot_core/lib/peripheral/supervisor.ex new file mode 100644 index 00000000..0e9f8738 --- /dev/null +++ b/farmbot_core/lib/peripheral/supervisor.ex @@ -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 diff --git a/farmbot_core/lib/peripheral/worker.ex b/farmbot_core/lib/peripheral/worker.ex new file mode 100644 index 00000000..061f3243 --- /dev/null +++ b/farmbot_core/lib/peripheral/worker.ex @@ -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 diff --git a/farmbot_core/lib/pin_binding/manager.ex b/farmbot_core/lib/pin_binding/manager.ex index 5b75089b..789b8612 100644 --- a/farmbot_core/lib/pin_binding/manager.ex +++ b/farmbot_core/lib/pin_binding/manager.ex @@ -154,20 +154,29 @@ defmodule Farmbot.PinBinding.Manager do %{state | registered: Map.delete(state.registered, pin_num), signal: Map.delete(state.signal, pin_num)} end - defp do_execute(%PinBinding{sequence_id: sequence_id}) when is_number(sequence_id) do + defp do_execute(%PinBinding{sequence_id: sequence_id} = binding) when is_number(sequence_id) do sequence_id |> Farmbot.Asset.get_sequence_by_id!() - |> Farmbot.CeleryScript.schedule_sequence() + |> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding)) end - defp do_execute(%PinBinding{special_action: action}) when is_binary(action) do + defp do_execute(%PinBinding{special_action: action} = binding) when is_binary(action) do %Sequence{ id: 0, name: action, kind: action, args: %{}, body: [] } - |> Farmbot.CeleryScript.schedule_sequence() + |> Farmbot.Core.CeleryScript.sequence(&execute_results(&1, binding)) + end + + @doc false + def execute_results(:ok, binding) do + Farmbot.Logger.success(1, "Pin Binding #{binding} execution complete.") + end + + def execute_results({:error, _}, binding) do + Farmbot.Logger.error(1, "Pin Binding #{binding} execution failed.") end defp debounce_timer(pin) do diff --git a/farmbot_core/lib/regimen/manager.ex b/farmbot_core/lib/regimen/manager.ex index 16a1a599..fdc44d71 100644 --- a/farmbot_core/lib/regimen/manager.ex +++ b/farmbot_core/lib/regimen/manager.ex @@ -3,7 +3,7 @@ defmodule Farmbot.Regimen.Manager do require Farmbot.Logger use GenServer - alias Farmbot.CeleryScript + alias Farmbot.Core.CeleryScript alias Farmbot.Asset alias Asset.Regimen import Farmbot.Regimen.NameProvider @@ -134,7 +134,14 @@ defmodule Farmbot.Regimen.Manager do defp do_item(item, regimen, state) do if item do sequence = Farmbot.Asset.get_sequence_by_id!(item.sequence_id) - CeleryScript.schedule_sequence(sequence) + CeleryScript.sequence(sequence, fn(results) -> + case results do + :ok -> + Farmbot.Logger.success(1, "[#{sequence.name}] executed by [#{regimen.name}] complete.") + {:error, _} -> + Farmbot.Logger.error(1, "[#{sequence.name}] executed by [#{regimen.name}] failed.") + end + end) end next_item = List.first(regimen.regimen_items) diff --git a/farmbot_core/mix.exs b/farmbot_core/mix.exs index 10580b86..9d07c3fa 100644 --- a/farmbot_core/mix.exs +++ b/farmbot_core/mix.exs @@ -1,9 +1,9 @@ defmodule FarmbotCore.MixProject do use Mix.Project - @target System.get_env("MIX_TARGET") || "host" @version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim() @branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim() + @elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim() defp commit do System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim() @@ -20,7 +20,7 @@ defmodule FarmbotCore.MixProject do [ app: :farmbot_core, description: "The Brains of the Farmbot Project", - elixir: "~> 1.7", + elixir: @elixir_version, make_clean: ["clean"], make_env: make_env(), make_cwd: __DIR__, @@ -56,6 +56,7 @@ defmodule FarmbotCore.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ + {:farmbot_celery_script, path: "../farmbot_celery_script"}, # Arduino Firmware stuff. {:elixir_make, "~> 0.4", runtime: false}, {:nerves_uart, "~> 1.2"}, diff --git a/farmbot_core/mix.lock b/farmbot_core/mix.lock index 4d0c7e5b..a36da61a 100644 --- a/farmbot_core/mix.lock +++ b/farmbot_core/mix.lock @@ -2,7 +2,6 @@ "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "csvm": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "f1543e8047934026747bd6dc57e9a20ce130d698", [branch: "integrate_csvm"]}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.5.0", "b0433a36d0e2430e3d50291b1c65f53c37d56f83665b43d79963684865beab68", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, @@ -10,8 +9,6 @@ "ecto": {:hex, :ecto, "2.2.10", "e7366dc82f48f8dd78fcbf3ab50985ceeb11cb3dc93435147c6e13f2cda0992e", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, "esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"}, - "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, - "excoveralls": {:hex, :excoveralls, "0.10.1", "407d50ac8fc63dfee9175ccb4548e6c5512b5052afa63eedb9cd452a32a91495", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.14.3", "b5f6f5dcc4f1fba340762738759209e21914516df6be440d85772542d4a5e412", [:rebar3], [{:certifi, "2.4.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, diff --git a/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs b/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs new file mode 100644 index 00000000..3ad2555a --- /dev/null +++ b/farmbot_core/priv/config/migrations/20180702143518_add_ntp_and_dns_configs.exs @@ -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 diff --git a/farmbot_core/priv/config/migrations/20180802155935_neets_http_sync.exs b/farmbot_core/priv/config/migrations/20180802155935_neets_http_sync.exs new file mode 100644 index 00000000..d36547ee --- /dev/null +++ b/farmbot_core/priv/config/migrations/20180802155935_neets_http_sync.exs @@ -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 diff --git a/farmbot_ext/.formatter.exs b/farmbot_ext/.formatter.exs deleted file mode 100644 index 525446d4..00000000 --- a/farmbot_ext/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/farmbot_ext/README.md b/farmbot_ext/README.md deleted file mode 100644 index 267274ad..00000000 --- a/farmbot_ext/README.md +++ /dev/null @@ -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). - diff --git a/farmbot_ext/config/config.exs b/farmbot_ext/config/config.exs index 6096c46c..33975ded 100644 --- a/farmbot_ext/config/config.exs +++ b/farmbot_ext/config/config.exs @@ -34,22 +34,19 @@ config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_configs.sqlite3", - priv: "../farmbot_core/priv/config", - pool_size: 1 + priv: "../farmbot_core/priv/config" config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_logs.sqlite3", - priv: "../farmbot_core/priv/logger", - pool_size: 1 + priv: "../farmbot_core/priv/logger" config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], database: ".#{Mix.env}_assets.sqlite3", - priv: "../farmbot_core/priv/asset", - pool_size: 1 + priv: "../farmbot_core/priv/asset" config :farmbot_ext, :behaviour, authorization: Farmbot.Bootstrap.Authorization, diff --git a/farmbot_ext/lib/amqp/auto_sync_transport.ex b/farmbot_ext/lib/amqp/auto_sync_transport.ex index f2394cb7..c565a58f 100644 --- a/farmbot_ext/lib/amqp/auto_sync_transport.ex +++ b/farmbot_ext/lib/amqp/auto_sync_transport.ex @@ -2,7 +2,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do use GenServer use AMQP require Farmbot.Logger - import Farmbot.Config, only: [get_config_value: 3] + import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] @exchange "amq.topic" @@ -21,10 +21,14 @@ defmodule Farmbot.AMQP.AutoSyncTransport do {:ok, _} = AMQP.Queue.declare(chan, jwt.bot <> "_auto_sync", [auto_delete: false]) :ok = AMQP.Queue.bind(chan, jwt.bot <> "_auto_sync", @exchange, [routing_key: "bot.#{jwt.bot}.sync.#"]) {:ok, _tag} = Basic.consume(chan, jwt.bot <> "_auto_sync", self(), [no_ack: true]) - + Farmbot.Registry.subscribe() {:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])} end + def terminate(_reason, _state) do + update_config_value(:bool, "settings", "needs_http_sync", true) + end + # Confirmation sent by the broker after registering this process as a consumer def handle_info({:basic_consume_ok, _}, state) do {:noreply, state} @@ -43,7 +47,7 @@ defmodule Farmbot.AMQP.AutoSyncTransport do def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do device = state.bot - ["bot", ^device, "sync", asset_kind, _id_str] = String.split(key, ".") + ["bot", ^device, "sync", asset_kind, id_str] = String.split(key, ".") data = Farmbot.JSON.decode!(payload) body = data["body"] case asset_kind do @@ -57,9 +61,15 @@ defmodule Farmbot.AMQP.AutoSyncTransport do "FirmwareConfig" -> Farmbot.SettingsSync.apply_fw_map(Farmbot.Config.get_config_as_map()["hardware_params"], body) _ -> - _cmd = Farmbot.Asset.register_sync_cmd(body["id"], asset_kind, body) - if get_config_value(:bool, "settings", "auto_sync") do - Farmbot.Asset.fragment_sync() + if !get_config_value(:bool, "settings", "needs_http_sync") do + id = String.to_integer(id_str) + body = if body, do: Farmbot.Asset.to_asset(body, asset_kind), else: nil + _cmd = Farmbot.Asset.register_sync_cmd(id, asset_kind, body) + if get_config_value(:bool, "settings", "auto_sync") do + Farmbot.Asset.fragment_sync() + end + else + IO.puts "not accepting sync_cmd from amqp because bot needs http sync first." end end @@ -68,4 +78,10 @@ defmodule Farmbot.AMQP.AutoSyncTransport do {:noreply, state} end + def handle_info({Farmbot.Registry, {Farmbot.Config, {"settings", "auto_sync", true}}}, state) do + Farmbot.AutoSyncTask.maybe_auto_sync() + {:noreply, state} + end + + def handle_info({Farmbot.Registry, _}, state), do: {:noreply, state} end diff --git a/farmbot_ext/lib/amqp/bot_state_transport.ex b/farmbot_ext/lib/amqp/bot_state_transport.ex index f3bf89b4..6549696f 100644 --- a/farmbot_ext/lib/amqp/bot_state_transport.ex +++ b/farmbot_ext/lib/amqp/bot_state_transport.ex @@ -8,6 +8,10 @@ defmodule Farmbot.AMQP.BotStateTransport do defstruct [:conn, :chan, :bot, :state_cache] alias __MODULE__, as: State + def force do + GenServer.cast(__MODULE__, :force) + end + @doc false def start_link(args) do GenServer.start_link(__MODULE__, args, [name: __MODULE__]) @@ -21,11 +25,19 @@ defmodule Farmbot.AMQP.BotStateTransport do {:ok, struct(State, [conn: conn, chan: chan, bot: jwt.bot])} end + def handle_cast(:force, %{state_cache: bot_state} = state) do + push_bot_state(state.chan, state.bot, bot_state) + {:noreply, state} + end + def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, %{state_cache: bot_state} = state) do + # IO.puts "no state change" {:noreply, state} end def handle_info({Farmbot.Registry, {Farmbot.BotState, bot_state}}, state) do + # IO.puts "pushing state" + state.state_cache cache = push_bot_state(state.chan, state.bot, bot_state) {:noreply, %{state | state_cache: cache}} end diff --git a/farmbot_ext/lib/amqp/celery_script_transport.ex b/farmbot_ext/lib/amqp/celery_script_transport.ex index 5d7e5d68..726fbfc4 100644 --- a/farmbot_ext/lib/amqp/celery_script_transport.ex +++ b/farmbot_ext/lib/amqp/celery_script_transport.ex @@ -2,6 +2,7 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do use GenServer use AMQP require Farmbot.Logger + require Logger import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] @exchange "amq.topic" @@ -48,28 +49,25 @@ defmodule Farmbot.AMQP.CeleryScriptTransport do def handle_info({:basic_deliver, payload, %{routing_key: key}}, state) do device = state.bot ["bot", ^device, "from_clients"] = String.split(key, ".") - reply = handle_celery_script(payload, state) - :ok = AMQP.Basic.publish state.chan, @exchange, "bot.#{device}.from_device", reply + spawn_link fn() -> + {_us, _results} = :timer.tc __MODULE__, :handle_celery_script, [payload, state] + # IO.puts "#{results.args.label} took: #{us}µs" + end {:noreply, state} end @doc false - def handle_celery_script(payload, _state) do + def handle_celery_script(payload, state) do json = Farmbot.JSON.decode!(payload) - %Farmbot.Asset.Sequence{ - name: json["args"]["label"], - args: json["args"], - body: json["body"], - kind: "sequence", - id: -1} - |> Farmbot.CeleryScript.execute_sequence() - |> case do - %{status: :crashed} = proc -> - expl = %{args: %{message: Csvm.FarmProc.get_crash_reason(proc)}} - %{args: %{label: json["args"]["label"]}, kind: "rpc_error", body: [expl]} - _ -> - %{args: %{label: json["args"]["label"]}, kind: "rpc_ok"} - end - |> Farmbot.JSON.encode!() + # IO.inspect(json, label: "RPC_REQUEST") + Farmbot.Core.CeleryScript.rpc_request(json, fn(results_ast) -> + reply = Farmbot.JSON.encode!(results_ast) + if results_ast.kind == :rpc_error do + [%{args: %{message: message}}] = results_ast.body + Logger.error(message) + end + AMQP.Basic.publish state.chan, @exchange, "bot.#{state.bot}.from_device", reply + results_ast + end) end end diff --git a/farmbot_ext/lib/amqp/connection_worker.ex b/farmbot_ext/lib/amqp/connection_worker.ex index d6819c6b..63432171 100644 --- a/farmbot_ext/lib/amqp/connection_worker.ex +++ b/farmbot_ext/lib/amqp/connection_worker.ex @@ -1,6 +1,7 @@ defmodule Farmbot.AMQP.ConnectionWorker do use GenServer require Farmbot.Logger + require Logger import Farmbot.Config, only: [update_config_value: 4] def start_link(args) do @@ -38,6 +39,12 @@ defmodule Farmbot.AMQP.ConnectionWorker do username: bot, password: token, virtual_host: vhost] - AMQP.Connection.open(opts) + case AMQP.Connection.open(opts) do + {:ok, conn} -> {:ok, conn} + {:error, reason} -> + Logger.error "Error connecting to AMPQ: #{inspect reason}" + Process.sleep(5000) + open_connection(token, bot, mqtt_server, vhost) + end end end diff --git a/farmbot_ext/lib/amqp/log_transport.ex b/farmbot_ext/lib/amqp/log_transport.ex index daa66134..a8448c17 100644 --- a/farmbot_ext/lib/amqp/log_transport.ex +++ b/farmbot_ext/lib/amqp/log_transport.ex @@ -79,7 +79,7 @@ defmodule Farmbot.AMQP.LogTransport do :ok = AMQP.Basic.publish chan, @exchange, "bot.#{bot}.logs", json end - defp add_position_to_log(%{} = log, %{position: pos}) do + defp add_position_to_log(%{} = log, %{position: %{} = pos}) do Map.merge(log, pos) end end diff --git a/farmbot_ext/lib/auto_sync_task.ex b/farmbot_ext/lib/auto_sync_task.ex new file mode 100644 index 00000000..8ee7b880 --- /dev/null +++ b/farmbot_ext/lib/auto_sync_task.ex @@ -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 diff --git a/farmbot_ext/lib/bootstrap/supervisor.ex b/farmbot_ext/lib/bootstrap/supervisor.ex index 64c10aa0..8ca262b8 100644 --- a/farmbot_ext/lib/bootstrap/supervisor.ex +++ b/farmbot_ext/lib/bootstrap/supervisor.ex @@ -99,13 +99,15 @@ defmodule Farmbot.Bootstrap.Supervisor do success_msg = "Successful Bootstrap authorization: #{email} - #{server}" Farmbot.Logger.success(2, success_msg) update_config_value(:bool, "settings", "first_boot", false) + update_config_value(:bool, "settings", "needs_http_sync", true) update_config_value(:string, "authorization", "token", token) children = [ {Farmbot.HTTP.Supervisor, []}, {Farmbot.SettingsSync, []}, {Farmbot.AMQP.Supervisor , []}, - {Farmbot.Bootstrap.AuthTask, []} + {Farmbot.Bootstrap.AuthTask, []}, + {Farmbot.AutoSyncTask, []}, ] opts = [strategy: :one_for_one] diff --git a/farmbot_ext/lib/http/http.ex b/farmbot_ext/lib/http/http.ex index 6b7efa1c..2f98a87f 100644 --- a/farmbot_ext/lib/http/http.ex +++ b/farmbot_ext/lib/http/http.ex @@ -5,6 +5,7 @@ defmodule Farmbot.HTTP do use GenServer alias Farmbot.HTTP.{Adapter, Error, Response} + alias Farmbot.JSON @adapter Application.get_env(:farmbot_ext, :behaviour)[:http_adapter] @adapter || raise("No http adapter.") @@ -15,6 +16,36 @@ defmodule Farmbot.HTTP do @typep headers :: Adapter.headers @typep opts :: Adapter.opts + alias Farmbot.Asset.{ + Device, + FarmEvent, + Peripheral, + PinBinding, + Point, + Regimen, + Sensor, + Sequence, + Tool, + } + + def device, do: fetch_and_decode("/api/device.json", Device) + def farm_events, do: fetch_and_decode("/api/farm_events.json", FarmEvent) + def peripherals, do: fetch_and_decode("/api/peripherals.json", Peripheral) + def pin_bindings, do: fetch_and_decode("/api/pin_bindings.json", PinBinding) + def points, do: fetch_and_decode("/api/points.json", Point) + def regimens, do: fetch_and_decode("/api/regimens.json", Regimen) + def sensors, do: fetch_and_decode("/api/sensors.json", Sensor) + def sequences, do: fetch_and_decode("/api/sequences.json", Sequence) + def tools, do: fetch_and_decode("/api/tools.json", Tool) + + def fetch_and_decode(url, kind) do + url + |> get!() + |> Map.fetch!(:body) + |> JSON.decode!() + |> Farmbot.Asset.to_asset(kind) + end + @doc """ Make an http request. Will not raise. * `method` - can be any http verb diff --git a/farmbot_ext/mix.exs b/farmbot_ext/mix.exs index ea4062c5..dcc83ad5 100644 --- a/farmbot_ext/mix.exs +++ b/farmbot_ext/mix.exs @@ -1,14 +1,13 @@ defmodule Farmbot.Ext.MixProject do use Mix.Project @version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim() - @branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim() + @elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim() def project do [ app: :farmbot_ext, version: @version, - branch: @branch, - elixir: "~> 1.6", + elixir: @elixir_version, start_permanent: Mix.env() == :prod, elixirc_paths: ["lib", "vendor"], deps: deps() diff --git a/farmbot_ext/mix.lock b/farmbot_ext/mix.lock index 5fddedbc..27dfafcd 100644 --- a/farmbot_ext/mix.lock +++ b/farmbot_ext/mix.lock @@ -4,7 +4,6 @@ "certifi": {:hex, :certifi, "2.4.2", "75424ff0f3baaccfd34b1214184b6ef616d89e420b258bb0a5ea7d7bc628f7f0", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, - "csvm": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "fcc87c97ad73e7d3cdd17fb9bfbbe4f6f19b2882", [branch: "integrate_csvm"]}, "db_connection": {:hex, :db_connection, "1.1.3", "89b30ca1ef0a3b469b1c779579590688561d586694a3ce8792985d4d7e575a61", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, "decimal": {:hex, :decimal, "1.6.0", "bfd84d90ff966e1f5d4370bdd3943432d8f65f07d3bab48001aebd7030590dcc", [:mix], [], "hexpm"}, "dialyxir": {:hex, :dialyxir, "1.0.0-rc.4", "71b42f5ee1b7628f3e3a6565f4617dfb02d127a0499ab3e72750455e986df001", [:mix], [{:erlex, "~> 0.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm"}, @@ -15,6 +14,7 @@ "esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, "excoveralls": {:hex, :excoveralls, "0.9.1", "14fd20fac51ab98d8e79615814cc9811888d2d7b28e85aa90ff2e30dcf3191d6", [:mix], [{:hackney, ">= 0.12.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, + "farmbot_celery_script": {:git, "https://github.com/Farmbot-Labs/CeleryScript-Runtime.git", "391cc58962abf1f39936202afcb079ce1114024d", [branch: "integrate_farmbot_celery_script"]}, "fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"}, "gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, diff --git a/farmbot_os/.formatter.exs b/farmbot_os/.formatter.exs deleted file mode 100644 index 525446d4..00000000 --- a/farmbot_os/.formatter.exs +++ /dev/null @@ -1,4 +0,0 @@ -# Used by "mix format" -[ - inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] -] diff --git a/farmbot_os/.tool-versions b/farmbot_os/.tool-versions deleted file mode 120000 index d54b92fc..00000000 --- a/farmbot_os/.tool-versions +++ /dev/null @@ -1 +0,0 @@ -../.tool-versions \ No newline at end of file diff --git a/farmbot_os/.tool-versions b/farmbot_os/.tool-versions new file mode 100644 index 00000000..cff5eb62 --- /dev/null +++ b/farmbot_os/.tool-versions @@ -0,0 +1,2 @@ +erlang 21.0.4 +elixir 1.6.6-otp-21 diff --git a/farmbot_os/config/host/auth_secret_ci.exs b/farmbot_os/config/host/auth_secret_ci.exs index 5e7c8a91..4602805d 100644 --- a/farmbot_os/config/host/auth_secret_ci.exs +++ b/farmbot_os/config/host/auth_secret_ci.exs @@ -12,20 +12,17 @@ config :farmbot_ext, config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "config-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot_os, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], diff --git a/farmbot_os/config/host/dev.exs b/farmbot_os/config/host/dev.exs index 077aa77a..ee92ac36 100644 --- a/farmbot_os/config/host/dev.exs +++ b/farmbot_os/config/host/dev.exs @@ -7,20 +7,18 @@ config :farmbot_ext, config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"), - pool_size: 1 + pool_size: 1, + database: Path.join(data_path, "config-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot_os, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], @@ -34,4 +32,11 @@ config :farmbot_os, :behaviour, config :farmbot_os, Farmbot.System.NervesHub, farmbot_nerves_hub_handler: Farmbot.Host.NervesHubHandler +config :farmbot_core, :behaviour, + leds_handler: Farmbot.Leds.StubHandler, + pin_binding_handler: Farmbot.PinBinding.StubHandler, + celery_script_io_layer: Farmbot.OS.IOLayer, + firmware_handler: Farmbot.Firmware.UartHandler + +config :farmbot_core, :uart_handler, tty: "/dev/ttyACM0" import_config("auth_secret.exs") diff --git a/farmbot_os/config/target/dev.exs b/farmbot_os/config/target/dev.exs index a474c08c..893909dc 100644 --- a/farmbot_os/config/target/dev.exs +++ b/farmbot_os/config/target/dev.exs @@ -4,8 +4,8 @@ local_key = if File.exists?(local_file), do: [File.read!(local_file)], else: [] config :logger, [ utc_log: true, - # handle_otp_reports: true, - # handle_sasl_reports: true, + handle_otp_reports: true, + handle_sasl_reports: true, backends: [RingLogger] ] @@ -17,7 +17,7 @@ config :farmbot_core, :behaviour, firmware_handler: Farmbot.Firmware.StubHandler, leds_handler: Farmbot.Target.Leds.AleHandler, pin_binding_handler: Farmbot.Target.PinBinding.AleHandler, - celery_script_io_layer: Farmbot.CeleryScript.StubIOLayer + celery_script_io_layer: Farmbot.OS.IOLayer data_path = Path.join("/", "root") config :farmbot_ext, @@ -26,28 +26,25 @@ config :farmbot_ext, config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "config-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot_os, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], init_children: [ - {Farmbot.Target.Leds.AleHandler, []} + {Farmbot.Target.Leds.AleHandler, []}, + {Farmbot.Firmware.UartHandler.AutoDetector, []}, ], platform_children: [ - {Farmbot.Firmware.UartHandler.AutoDetector, []}, {Farmbot.Target.Bootstrap.Configurator, []}, {Farmbot.Target.Network, []}, {Farmbot.Target.SSHConsole, []}, diff --git a/farmbot_os/config/target/prod.exs b/farmbot_os/config/target/prod.exs index 2422056a..4f9ea3d7 100644 --- a/farmbot_os/config/target/prod.exs +++ b/farmbot_os/config/target/prod.exs @@ -4,7 +4,8 @@ config :farmbot_core, :behaviour, firmware_handler: Farmbot.Firmware.StubHandler, leds_handler: Farmbot.Target.Leds.AleHandler, pin_binding_handler: Farmbot.Target.PinBinding.AleHandler, - celery_script_io_layer: Farmbot.CeleryScript.StubIOLayer + celery_script_io_layer: Farmbot.OS.IOLayer + data_path = Path.join("/", "root") config :farmbot_ext, @@ -13,28 +14,25 @@ config :farmbot_ext, config :farmbot_core, Farmbot.Config.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "config-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "config-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Logger.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "logs-#{Mix.env()}.sqlite3") config :farmbot_core, Farmbot.Asset.Repo, adapter: Sqlite.Ecto2, loggers: [], - database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3"), - pool_size: 1 + database: Path.join(data_path, "repo-#{Mix.env()}.sqlite3") config :farmbot_os, ecto_repos: [Farmbot.Config.Repo, Farmbot.Logger.Repo, Farmbot.Asset.Repo], init_children: [ - {Farmbot.Target.Leds.AleHandler, []} + {Farmbot.Target.Leds.AleHandler, []}, + {Farmbot.Firmware.UartHandler.AutoDetector, []}, ], platform_children: [ - {Farmbot.Firmware.UartHandler.AutoDetector, []}, {Farmbot.Target.Bootstrap.Configurator, []}, {Farmbot.Target.Network, []}, {Farmbot.Target.SSHConsole, []}, diff --git a/farmbot_os/lib/celery_script/ast.ex b/farmbot_os/lib/celery_script/ast.ex new file mode 100644 index 00000000..0b8712fc --- /dev/null +++ b/farmbot_os/lib/celery_script/ast.ex @@ -0,0 +1,2 @@ +require Protocol +Protocol.derive(Jason.Encoder, Farmbot.CeleryScript.AST) diff --git a/farmbot_os/lib/celery_script/io_layer.ex b/farmbot_os/lib/celery_script/io_layer.ex index c57c9e39..f0f2881f 100644 --- a/farmbot_os/lib/celery_script/io_layer.ex +++ b/farmbot_os/lib/celery_script/io_layer.ex @@ -1,215 +1,157 @@ defmodule Farmbot.OS.IOLayer do - @behaviour Farmbot.CeleryScript.IOLayer - import Farmbot.Config, only: [get_config_value: 3, update_config_value: 4] - alias Farmbot.Firmware.Vec3 - require Farmbot.Logger + @behaviour Farmbot.Core.CeleryScript.IOLayer + alias Farmbot.OS.IOLayer.{ + FindHome, + If, + MoveAbsolute, + ReadPin, + Sync, + TogglePin, + WritePin, + } - #TODO(Connor) - these instructions are skipped so far: - # change_ownership(email, new_token) - # execute_script(farmware) - # factory_reset(arduino_firmware) - # install_farmware - # install_first_party_farmware + def emergency_lock(_args, _body) do + Farmbot.Firmware.emergency_lock() + end - def handle_io(%{kind: :read_status}) do + def emergency_unlock(_args, _body) do + Farmbot.Firmware.emergency_unlock() + end + + def move_relative(%{x: x, y: y, z: z, speed: speed}, []) do + import Farmbot.Core.CeleryScript.Utils + %{x: cur_x, y: cur_y, z: cur_z} = Farmbot.Firmware.get_current_position() + location = new_vec3(cur_x, cur_y, cur_z) + offset = new_vec3(x, y, z) + move_absolute(%{location: location, offset: offset, speed: speed}, []) + end + + def move_absolute(args, body) do + MoveAbsolute.execute(args, body) + end + + def toggle_pin(args, body) do + TogglePin.execute(args, body) + end + + def write_pin(args, body) do + WritePin.execute(args, body) + end + + def read_pin(args, body) do + ReadPin.execute(args, body) + end + + def set_servo_angle(%{pin_number: pin_number, pin_value: value}, []) do + case Farmbot.Firmware.set_servo_angle(pin_number, value) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + def home(%{axis: "all"}, []) do + case Farmbot.Firmware.home_all() do + :ok -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + def home(%{axis: axis}, []) do + case Farmbot.Firmware.home(axis) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + def find_home(args, body) do + FindHome.execute(args, body) + end + + def wait(%{milliseconds: millis}, []) do + Process.sleep(millis) + :ok + end + + def zero(_args, _body) do + {:error, "not implemented: zero"} + end + + def calibrate(_args, _body) do + {:error, "not implemented: calibrate"} + end + + def config_update(_args, _body) do + {:error, "not implemented: config_update"} + end + + def set_user_env(_args, _body) do + IO.inspect {:error, "not implemented: set_user_env"} + :ok + end + + def install_first_party_farmware(_args, _body) do + {:error, "not implemented: install_first_party_farmware"} + end + + def install_farmware(_args, _body) do + {:error, "not implemented: install_farmware"} + end + + def uninstall_farmware(_args, _body) do + {:error, "not implemented: uninstall_farmware"} + end + + def update_farmware(_args, _body) do + {:error, "not implemented: update_farmware"} + end + + def take_photo(_args, body) do + execute_script(%{package: "take-photo"}, body) + end + + def execute_script(_args, _body) do + {:error, "not implemented: execute_script"} + end + + def read_status(_args, _body) do Farmbot.BotState.fetch() :ok end - def handle_io(%{kind: :sync}) do - case Farmbot.Asset.fragment_sync(1) do - :ok -> :ok - {:error, _} -> {:error, "Sync failed."} + def send_message(_args, _body) do + {:error, "not implemented: send_message"} + end + + def sync(args, body) do + Sync.execute(args, body) + end + + def power_off(_,_), do: Farmbot.System.shutdown("CeleryScript") + def reboot(_,_), do: Farmbot.System.reboot("CeleryScript") + def factory_reset(_,_), do: Farmbot.System.factory_reset("CeleryScript") + + def dump_info(_args, _body) do + {:error, "not implemented: dump_info"} + end + + def change_ownership(_args, _body) do + {:error, "not implemented: change_ownership"} + end + + def check_updates(_args, _body) do + {:error, "not implemented: check_updates"} + end + + def _if(args, body) do + If.execute(args, body) + end + + def execute(%{sequence_id: sid}, _body) do + case Farmbot.Asset.get_sequence_by_id(sid) do + nil -> {:error, "no sequence by id: #{sid}"} + %Farmbot.Asset.Sequence{} = seq -> + IO.warn "FIXME" + {:ok, Farmbot.CeleryScript.AST.decode(seq)} end end - - def handle_io(%{kind: :calibrate, args: %{axis: "all"}}) do - do_reduce([:z, :y, :x], fn(axis) -> - calibrate(axis) - end) - end - - def handle_io(%{kind: :calibrate, args: %{axis: axis}}) do - calibrate(axis) - end - - def handle_io(%{kind: :check_updates, args: %{package: :farmbot_os}}) do - case Farmbot.System.Updates.check_updates() do - {:error, reason} -> {:error, reason} - nil -> :ok - {%Version{} = version, url} -> - case Farmbot.System.Updates.download_and_apply_update({version, url}) do - :ok -> :ok - {:error, reason} -> {:error, reason} - end - end - end - - def handle_io(%{kind: :emergency_lock}) do - case Farmbot.Firmware.emergency_lock() do - {:error, :emergency_lock} -> - Farmbot.Logger.error(1, "Farmbot is E Stopped") - :ok - {:error, reason} -> {:error, reason} - end - end - - def handle_io(%{kind: :emergency_unlock}) do - case Farmbot.Firmware.emergency_unlock do - :ok -> - Farmbot.Logger.success 1, "Bot is Successfully unlocked." - :ok - {:error, reason} -> {:error, reason} - end - end - - def handle_io(%{kind: :execute, args: %{sequence_id: id}}) do - case Farmbot.Asset.get_sequence_by_id(id) do - nil -> {:error, "Could not find that sequence. Try syncing."} - seq -> {:ok, Csvm.AST.decode(seq)} - end - end - - def handle_io(%{kind: :factory_reset, args: %{package: :farmbot_os}}) do - :ok = Farmbot.BotState.enter_maintenance_mode() - update_config_value(:bool, "settings", "disable_factory_reset", false) - Farmbot.Logger.warn 1, "Farmbot OS going down for factory reset!" - Farmbot.System.factory_reset "CeleryScript request." - end - - def handle_io(%{kind: :find_home, args: %{axis: "all"}}) do - do_reduce([:z, :y, :x], fn(axis) -> - find_home(axis) - end) - end - - def handle_io(%{kind: :find_home, args: %{axis: axis}}) do - find_home(axis) - end - - def handle_io(%{kind: :home, args: %{axis: "all"}}) do - case Farmbot.Firmware.home_all() do - :ok -> :ok - {:error, reason} -> {:error, reason} - end - end - - def handle_io(%{kind: :home, args: %{axis: axis}}) do - home(axis) - end - - def handle_io(%{kind: :move_absolute, args: %{speed: speed, location: loc, offset: offset}}) do - with %Vec3{} = loc <- ast_to_vec3(loc), - %Vec3{} = offset <- ast_to_vec3(offset) do - x = loc.x + offset.x - y = loc.y + offset.y - z = loc.z + offset.z - move_absolute(x, y, z, speed) - end - end - - def handle_io(%{kind: :move_relative, args: args}) do - %{location_data: %{position: %{x: cx, y: cy, z: cz}}} = Farmbot.BotState.fetch() - move_absolute(cx + args.x, cy + args.y, cz + args.z, args.speed) - end - - def handle_io(%{kind: :power_off}) do - :ok = Farmbot.BotState.enter_maintenance_mode() - Farmbot.System.shutdown("CeleryScript request") - :ok - end - - def handle_io(ast) do - IO.puts "#{ast.kind} is not implemented by FarmbotOS yet." - {:error, "#{ast.kind} is not implemented by FarmbotOS yet."} - end - - defp move_absolute(x, y, z, speed) do - speed_x = (speed / 100) * (movement_max_spd(:x)) |> round() - speed_y = (speed / 100) * (movement_max_spd(:y)) |> round() - speed_z = (speed / 100) * (movement_max_spd(:z)) |> round() - Farmbot.Firmware.move_absolute(new_vec3(x, y, z), speed_x, speed_y, speed_z) - |> case do - :ok -> :ok - {:error, reason} -> {:error, "movement error: #{reason}"} - end - end - - def calibrate(axis) do - Farmbot.Firmware.calibrate(axis) - |> case do - :ok -> :ok - {:error, reason} -> {:error, "calibration error: #{reason}"} - end - end - - defp find_home(axis) do - do_find_home(movement_enable_endpoints(axis), encoder_enabled(axis), axis) - end - - defp do_find_home(false, false, axis) do - {:error, "Could not find home on #{axis} axis because endpoints and encoders are disabled."} - end - - defp do_find_home(ep, ec, axis) when ep == true or ec == true do - Farmbot.Logger.busy 2, "Finding home on #{axis} axis." - case Farmbot.Firmware.find_home(axis) do - :ok -> :ok - {:error, reason} -> {:error, reason} - end - end - - defp home(axis) do - unless get_config_value(:bool, "settings", "firmware_input_log") do - Farmbot.Logger.busy 1, "Moving to (0, 0, 0)" - end - - case Farmbot.Firmware.home(axis) do - :ok -> :ok - {:error, reason} -> {:error, reason} - end - end - - defp movement_enable_endpoints(axis) do - case get_config_value(:float, "hardware_params", "movement_enable_endpoints_#{axis}") do - nil -> false - 0 -> false - 1 -> true - end - end - - defp encoder_enabled(axis) do - case get_config_value(:float, "hardware_params", "encoder_enabled_#{axis}") do - nil -> false - 0 -> false - 1 -> true - end - end - - defp movement_max_spd(axis) do - get_config_value(:float, "hardware_params", "movement_max_spd_#{axis}") || 1.0 - end - - defp new_vec3(x, y, z) do - Vec3.new(x, y, z) - end - - defp ast_to_vec3(%{kind: :coordinate, args: %{x: x, y: y, z: z}}) do - new_vec3(x, y, z) - end - - defp ast_to_vec3(ast) do - {:error, "Cannont convert #{inspect ast} to vec3"} - end - - defp do_reduce([arg | rest], fun) do - case fun.(arg) do - :ok -> do_reduce(rest, fun) - {:error, reason} -> {:error, reason} - end - end - - defp do_reduce([], _) do - :ok - end end diff --git a/farmbot_os/lib/celery_script/io_layer/_if.ex b/farmbot_os/lib/celery_script/io_layer/_if.ex new file mode 100644 index 00000000..b2985292 --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/_if.ex @@ -0,0 +1,69 @@ +defmodule Farmbot.OS.IOLayer.If do + alias Farmbot.CeleryScript.AST + alias Farmbot.Asset + alias Asset.{Peripheral, Sensor} + + def execute(%{lhs: lhs, op: op, rhs: rhs}, _body) do + left = eval_lhs(lhs) + cond do + is_number(left) or is_nil(left) -> eval_if(left, op, rhs) + match?({:error, _}, left) -> left + end + end + + defp eval_lhs(axis) when axis in ["x", "y", "z"] do + Farmbot.Firmware.get_current_position() + |> Enum.find(fn({a, _}) -> axis == to_string(a) end) + |> elem(1) + end + + # handles looking up a pin from a peripheral. + defp eval_lhs(%AST{kind: :named_pin} = named_pin) do + id = named_pin.args.pin_id + type = named_pin.args.pin_type + case fetch_resource(type, id) do + {:ok, number} -> + eval_lhs({:pin, number}) + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + defp eval_lhs({:pin, pin}) do + case Farmbot.Firmware.get_pin_value(pin) do + %{value: value} -> value + nil -> {:error, "Could not find firmware pin value #{pin}"} + end + end + + defp eval_if(nil, "is_undefined", _), do: {:ok, true} + defp eval_if(_, "is_undefined", _), do: {:ok, false} + defp eval_if(nil, _, _), + do: {:error, "Could not eval IF because left hand side of if statement is undefined."} + + defp eval_if(lhs, ">", rhs) when lhs > rhs, do: {:ok, true} + defp eval_if(_lhs, ">", _rhs), do: {:ok, false} + + defp eval_if(lhs, "<", rhs) when lhs < rhs, do: {:ok, true} + defp eval_if(_lhs, "<", _rhs), do: {:ok, false} + + defp eval_if(lhs, "is", rhs) when lhs == rhs, do: {:ok, true} + defp eval_if(_lhs, "is", _rhs), do: {:ok, false} + + defp eval_if(lhs, "not", rhs) when lhs != rhs, do: {:ok, true} + defp eval_if(_lhs, "not", _rhs), do: {:ok, false} + defp eval_if(_, op, _), do: {:error, "Unknown operator: #{op}"} + + defp fetch_resource("Peripheral", id) do + case Asset.get_peripheral_by_id(id) do + %Peripheral{pin: number} -> {:ok, number} + nil -> {:error, "Could not find Peripheral by id: #{id}"} + end + end + + defp fetch_resource("Sensor", id) do + case Asset.get_sensor_by_id(id) do + %Sensor{pin: number} -> {:ok, number} + nil -> {:error, "Could not find Sensor by id: #{id}"} + end + end +end diff --git a/farmbot_os/lib/celery_script/io_layer/find_home.ex b/farmbot_os/lib/celery_script/io_layer/find_home.ex new file mode 100644 index 00000000..c5232ad3 --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/find_home.ex @@ -0,0 +1,41 @@ +defmodule Farmbot.OS.IOLayer.FindHome do + require Farmbot.Logger + import Farmbot.Config, only: [get_config_value: 3] + + def execute(%{axis: "all"}, _) do + do_reduce(["z", "y", "x"]) + end + + def execute(%{axis: axis}, _) do + ep = get_config_value(:float, "hardware_params", "movement_enable_endpoints_#{axis}") + ec = get_config_value(:float, "hardware_params", "encoder_enabled_#{axis}") + do_find_home(ep, ec, axis) + end + + defp do_reduce([axis | rest]) do + case execute(%{axis: axis}, []) do + :ok -> do_reduce(rest) + {:error, reason} -> {:error, reason} + end + end + + defp do_reduce([]), do: :ok + + defp do_find_home(ep, ec, axis) + + defp do_find_home(ep, ec, axis) when ((ep == 0) or (ep == nil)) and ((ec == 0) or (ec == nil)) do + {:error, "Could not find home on #{axis} axis because endpoints and encoders are disabled."} + end + + defp do_find_home(ep, ec, axis) when ep == 1 or ec == 1 do + Farmbot.Logger.busy 2, "Finding home on #{axis} axis." + case Farmbot.Firmware.find_home(axis) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + defp do_find_home(ep, ec, _axis) do + {:error, "Unknown state of endpoints: #{ep} or encoders: #{ec}"} + end +end diff --git a/farmbot_os/lib/celery_script/io_layer/move_absolute.ex b/farmbot_os/lib/celery_script/io_layer/move_absolute.ex new file mode 100644 index 00000000..392955cc --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/move_absolute.ex @@ -0,0 +1,55 @@ +defmodule Farmbot.OS.IOLayer.MoveAbsolute do + alias Farmbot.Firmware.Vec3 + import Farmbot.Config, only: [get_config_value: 3] + require Farmbot.Logger + + def execute(%{location: %{x: _, y: _, z: _} = pos_a, + offset: %{x: _, y: _, z: _} = pos_b, + speed: speed}, []) do + pos = vec3_math(pos_a, :+, pos_b) + maybe_log_busy(pos) + speed_x = (speed / 100) * (get_config_value(:float, "hardware_params", "movement_max_spd_x") || 1) + speed_y = (speed / 100) * (get_config_value(:float, "hardware_params", "movement_max_spd_y") || 1) + speed_z = (speed / 100) * (get_config_value(:float, "hardware_params", "movement_max_spd_z") || 1) + case Farmbot.Firmware.move_absolute(pos, speed_x |> round(), speed_y |> round(), speed_z |> round()) do + :ok -> :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + def execute(%{location: location, offset: offset, speed: speed}, body) do + with {:ok, location_vec3} <- to_vec3(location), + {:ok, offset_vec3} <- to_vec3(offset) do + execute(%{location: location_vec3, offset: offset_vec3, speed: speed}, body) + end + end + + defp maybe_log_busy(%Vec3{} = pos) do + unless get_config_value(:bool, "settings", "firmware_input_log") do + Farmbot.Logger.busy 1, "Moving to #{inspect pos}" + end + end + + def vec3_math(%{x: xa, y: ya, z: za}, fun, %{x: xb, y: yb, z: zb}) do + res_x = apply(Kernel, fun, [xa || 0, xb || 0]) + res_y = apply(Kernel, fun, [ya || 0, yb || 0]) + res_z = apply(Kernel, fun, [za || 0, zb || 0]) + %Vec3{x: res_x, y: res_y, z: res_z} + end + + def to_vec3(%{x: x, y: y, z: z}), do: {:ok, %{x: x, y: y, z: z}} + + def to_vec3(%{args: %{x: x, y: y, z: z}}), do: {:ok, %{x: x, y: y, z: z}} + + def to_vec3(%{kind: :point, args: %{pointer_id: id}}) do + case Farmbot.Asset.get_point_by_id(id) do + %Farmbot.Asset.Point{x: x, y: y, z: z} -> {:ok, %{x: x, y: y, z: z}} + _ -> {:error, "Could not find Plant by id: #{id}. Try Syncing."} + end + end + + def to_vec3(%{kind: kind} = info) do + Farmbot.Logger.error 1, "Unknown vector type: #{inspect info}" + {:error, "Can't convert #{kind} to vec3."} + end +end diff --git a/farmbot_os/lib/celery_script/io_layer/read_pin.ex b/farmbot_os/lib/celery_script/io_layer/read_pin.ex new file mode 100644 index 00000000..c3047b7a --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/read_pin.ex @@ -0,0 +1,84 @@ +defmodule Farmbot.OS.IOLayer.ReadPin do + alias Farmbot.CeleryScript.AST + require Farmbot.Logger + alias Farmbot.Asset + alias Asset.{Peripheral, Sensor} + + @digital 0 + @analog 1 + + def execute(%{pin_number: %AST{kind: :named_pin} = named_pin, pin_mode: mode}, _) do + id = named_pin.args.pin_id + type = named_pin.args.pin_type + case fetch_resource(type, id) do + %Peripheral{pin: pin_num, label: name} -> do_read(pin_num, mode, name) + %Sensor{pin: pin_num, label: name} -> do_read(pin_num, mode, name) + {:error, reason} -> {:error, reason} + end + end + + def execute(%{pin_number: pin_num, pin_mode: mode}, _) when is_number(pin_num) do + case fetch_resource(nil, pin_num) do + %Peripheral{pin: pin_num, label: name} -> + do_read(pin_num, mode, name) + %Sensor{pin: pin_num, label: name} -> + do_read(pin_num, mode, name) + {:ok, ^pin_num} -> + do_read(pin_num, mode, "Pin #{pin_num}") + {:error, reason} -> {:error, reason} + end + end + + defp do_read(pin_num, mode, msg) do + case Farmbot.Firmware.read_pin(pin_num, mode) do + :ok -> + case Farmbot.Firmware.get_pin_value(pin_num) do + %{mode: ^mode, value: val} -> + log_success(msg, pin_num, mode, val) + :ok + nil -> {:error, "Firmware didn't report pin value."} + end + {:error, reason} -> {:error, reason} + end + end + + defp log_success(msg, _num, @digital, 1) do + Farmbot.Logger.success 1, "#{msg} value is 1 (digital)" + end + + defp log_success(msg, _num, @digital, 0) do + Farmbot.Logger.success 1, "#{msg} value is 0 (digital)" + end + + defp log_success(msg, _num, @analog, val) do + Farmbot.Logger.success 1, "#{msg} value is #{val} (analog)" + end + + defp fetch_resource("Peripheral", id) do + case Asset.get_peripheral_by_id(id) do + %Peripheral{} = per -> per + nil -> {:error, "Could not find pin by id: #{id}"} + end + end + + defp fetch_resource("Sensor", id) do + case Asset.get_sensor_by_id(id) do + %Sensor{} = sen -> sen + nil -> {:error, "Could not find pin by id: #{id}"} + end + end + + defp fetch_resource(nil, number) do + try_lookup_sensor(number) || + try_lookup_peripheral(number) || + {:ok, number} + end + + defp try_lookup_peripheral(number) do + Asset.get_peripheral_by_number(number) + end + + defp try_lookup_sensor(number) do + Asset.get_sensor_by_number(number) + end +end diff --git a/farmbot_os/lib/celery_script/io_layer/sync.ex b/farmbot_os/lib/celery_script/io_layer/sync.ex new file mode 100644 index 00000000..1662fae2 --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/sync.ex @@ -0,0 +1,60 @@ +defmodule Farmbot.OS.IOLayer.Sync do + import Farmbot.Config, only: [get_config_value: 3] + import Farmbot.Asset, only: [fragment_sync: 1, full_sync: 2] + require Farmbot.Logger + alias Farmbot.HTTP + alias Farmbot.Asset.{ + Device, + FarmEvent, + Peripheral, + PinBinding, + Point, + Regimen, + Sensor, + Sequence, + Tool, + } + + def execute(_, []) do + case get_config_value(:bool, "settings", "needs_http_sync") do + true -> full_sync(1, &http_sync/0) + false -> fragment_sync(1) + end + end + + def http_sync do + Farmbot.Logger.debug 3, "Starting HTTP sync." + {time, results} = :timer.tc(fn() -> + {:ok, pid} = Task.Supervisor.start_link() + [ + Task.Supervisor.async_nolink(pid, fn -> {Device, HTTP.device() |> List.wrap() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {FarmEvent, HTTP.farm_events() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Peripheral, HTTP.peripherals() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {PinBinding, HTTP.pin_bindings() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Point, HTTP.points() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Regimen, HTTP.regimens() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Sensor, HTTP.sensors() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Sequence, HTTP.sequences() |> list_to_sync_cmds()} end), + Task.Supervisor.async_nolink(pid, fn -> {Tool, HTTP.tools() |> list_to_sync_cmds()} end), + ] + |> Enum.map(&Task.yield(&1)) + |> Enum.map(fn({:ok, {_kind, list}}) -> list end) + |> List.flatten() + end) + Farmbot.Logger.debug 3, "HTTP requests took: #{time}us." + {:ok, results} + end + + def list_to_sync_cmds(list, acc \\ []) + def list_to_sync_cmds([], results), do: results + def list_to_sync_cmds([data | rest], acc) do + list_to_sync_cmds(rest, [to_sync_cmd(data) | acc]) + end + + def to_sync_cmd(%kind{} = data) do + kind = Module.split(kind) + |> List.last() + + Farmbot.Asset.new_sync_cmd(data.id, kind, data) + end +end diff --git a/farmbot_os/lib/celery_script/io_layer/toggle_pin.ex b/farmbot_os/lib/celery_script/io_layer/toggle_pin.ex new file mode 100644 index 00000000..c1dcdbb5 --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/toggle_pin.ex @@ -0,0 +1,33 @@ +defmodule Farmbot.OS.IOLayer.TogglePin do + require Farmbot.Logger + + @digital 0 + @analog 1 + + def execute(%{pin_number: num}, []) do + case Farmbot.Firmware.get_pin_value(num) do + %{value: 0, mode: @digital} -> high(num) + %{value: 1, mode: @digital} -> low(num) + %{value: _, mode: @analog} -> unknown(num) + nil -> unknown(num) + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + defp unknown(num) do + Farmbot.Logger.warn 2, "Unknown pin value or analog pin. Writing digital low." + low(num) + end + + defp high(num) do + args = %{pin_mode: @digital, pin_number: num, pin_value: 1} + jump(args) + end + + defp low(num) do + args = %{pin_mode: @digital, pin_number: num, pin_value: 0} + jump(args) + end + + defp jump(args), do: Farmbot.OS.IOLayer.write_pin(args, []) +end diff --git a/farmbot_os/lib/celery_script/io_layer/write_pin.ex b/farmbot_os/lib/celery_script/io_layer/write_pin.ex new file mode 100644 index 00000000..1bb396c4 --- /dev/null +++ b/farmbot_os/lib/celery_script/io_layer/write_pin.ex @@ -0,0 +1,78 @@ +defmodule Farmbot.OS.IOLayer.WritePin do + alias Farmbot.CeleryScript.AST + alias Farmbot.Asset + alias Asset.Peripheral + require Farmbot.Logger + + @digital 0 + @analog 1 + + def execute(%{pin_number: %AST{kind: :named_pin, args: %{pin_type: "BoxLed3"}}, pin_mode: @digital, pin_value: value}, _body) do + log_success("BoxLed3", "BoxLed3", @digital, value) + Farmbot.Leds.white4(value_to_led(value)) + :ok + end + + def execute(%{pin_number: %AST{kind: :named_pin, args: %{pin_type: "BoxLed4"}}, pin_mode: @digital, pin_value: value}, _body) do + log_success("BoxLed4", "BoxLed4", @digital, value) + Farmbot.Leds.white5(value_to_led(value)) + :ok + end + + def execute(%{pin_number: %AST{kind: :named_pin} = named_pin, pin_mode: mode, pin_value: val}, _body) do + id = named_pin.args.pin_id + type = named_pin.args.pin_type + case fetch_resource(type, id) do + %Peripheral{pin: num, label: name} -> do_write(num, mode, val, name) + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + def execute(%{pin_mode: mode, pin_value: value, pin_number: num}, []) do + case fetch_resource(nil, num) do + %Peripheral{pin: num, label: name} -> + do_write(num, mode, value, name) + {:ok, ^num} -> do_write(num, mode, value, "Pin #{num}") + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + defp do_write(num, mode, value, msg) do + case Farmbot.Firmware.write_pin(num, mode, value) do + :ok -> + log_success(msg, num, mode, value) + :ok + {:error, reason} when is_binary(reason) -> {:error, reason} + end + end + + defp log_success(msg, _num, @digital, 1) do + Farmbot.Logger.success 1, "#{msg} turned ON" + end + + defp log_success(msg, _num, @digital, 0) do + Farmbot.Logger.success 1, "#{msg} turned OFF" + end + + defp log_success(msg, _num, @analog, val) do + Farmbot.Logger.success 1, "#{msg} set to #{val} (analog)" + end + + defp fetch_resource("Peripheral", id) do + case Asset.get_peripheral_by_id(id) do + %Peripheral{} = per -> per + nil -> {:error, "Could not find pin by id: #{id}"} + end + end + + defp fetch_resource(nil, number) do + try_lookup_peripheral(number) || {:ok, number} + end + + defp try_lookup_peripheral(number) do + Asset.get_peripheral_by_number(number) + end + + defp value_to_led(1), do: :solid + defp value_to_led(_), do: :off +end diff --git a/farmbot_os/lib/core_start.ex b/farmbot_os/lib/core_start.ex index 4e8b02be..e8c77e04 100644 --- a/farmbot_os/lib/core_start.ex +++ b/farmbot_os/lib/core_start.ex @@ -1,6 +1,7 @@ defmodule Farmbot.System.CoreStart do @moduledoc false use Supervisor + require Logger @doc false def start_link(args) do @@ -8,7 +9,23 @@ defmodule Farmbot.System.CoreStart do end def init([]) do - {:ok, _} = Application.ensure_all_started(:farmbot_core) + :ok = start_core_app(Farmbot.BootState.read()) Supervisor.init([], [strategy: :one_for_one]) end + + defp start_core_app(state) do + case Application.ensure_all_started(:farmbot_core) do + {:ok, _} -> + Farmbot.BootState.write(:UPANDRUNNING) + :ok + {:error, {:farmbot_core, {{:shutdown, {:failed_to_start_child, child, reason}}, _}}} -> + msg = "Failed to start farmbot_core while in state: #{inspect state} child: #{child} => #{inspect reason}" + maybe_reset(msg) + :ok + end + end + + defp maybe_reset(msg) do + Farmbot.System.factory_reset(msg) + end end diff --git a/farmbot_os/lib/test.ex b/farmbot_os/lib/test.ex new file mode 100644 index 00000000..e635fea1 --- /dev/null +++ b/farmbot_os/lib/test.ex @@ -0,0 +1,32 @@ +defmodule Test do + import Ecto.Query + alias Farmbot.CeleryScript + alias CeleryScript.RunTime.FarmProc + import CeleryScript.Utils + + def test(amnt) do + state = :sys.get_state(CeleryScript.RunTime) + fun = state.process_io_layer + seq = Farmbot.Asset.Repo.one(from s in Farmbot.Asset.Sequence, where: s.name == "new sequence 12") + ast = CeleryScript.AST.decode(seq) + heap = CeleryScript.AST.slice(ast) + page = addr(seq.id) + proc0 = FarmProc.new(fun, page, heap) + reduce(proc0, amnt) + end + + def reduce(proc, count, acc \\ []) + + def reduce(proc, 0, acc) do + [proc | acc] + end + + def reduce(proc, count, acc) do + next = FarmProc.step(proc) + if next.status == :waiting do + reduce(next, count, acc) + else + reduce(next, count - 1, [proc | acc]) + end + end +end diff --git a/farmbot_os/mix.exs b/farmbot_os/mix.exs index 27bdccab..36281c2e 100644 --- a/farmbot_os/mix.exs +++ b/farmbot_os/mix.exs @@ -1,17 +1,17 @@ defmodule Farmbot.OS.MixProject do use Mix.Project - @target System.get_env("MIX_TARGET") || "host" @version Path.join([__DIR__, "..", "VERSION"]) |> File.read!() |> String.trim() @branch System.cmd("git", ~w"rev-parse --abbrev-ref HEAD") |> elem(0) |> String.trim() @commit System.cmd("git", ~w"rev-parse --verify HEAD") |> elem(0) |> String.trim() System.put_env("NERVES_FW_VCS_IDENTIFIER", @commit) System.put_env("NERVES_FW_MISC", @branch) + @elixir_version Path.join([__DIR__, "..", "ELIXIR_VERSION"]) |> File.read!() |> String.trim() def project do [ app: :farmbot_os, - elixir: "~> 1.6", + elixir: @elixir_version, target: @target, version: @version, branch: @branch, diff --git a/farmbot_os/mix.lock.host b/farmbot_os/mix.lock.host new file mode 100644 index 00000000..1947b9f9 --- /dev/null +++ b/farmbot_os/mix.lock.host @@ -0,0 +1,66 @@ +%{ + "amqp": {:hex, :amqp, "1.0.3", "06a6d909abc71d82b7c3133ca491899ca18fce857d0697dd060c29de1ef498d8", [:mix], [{:amqp_client, "~> 3.7.3", [hex: :amqp_client, repo: "hexpm", optional: false]}, {:goldrush, "~> 0.1.0", [hex: :goldrush, repo: "hexpm", optional: false]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "~> 3.5", [hex: :lager, repo: "hexpm", optional: false]}, {:rabbit_common, "~> 3.7.3", [hex: :rabbit_common, repo: "hexpm", optional: false]}, {:ranch, "~> 1.4", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "~> 1.4", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "~> 2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "amqp_client": {:hex, :amqp_client, "3.7.8", "5ec44ad152aed8519ef557189fa21e779f60578d21bcab36cabe381b451728ee", [:make, :rebar3], [{:rabbit_common, "3.7.8", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"}, + "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], [], "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"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "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"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, + "distillery": {:hex, :distillery, "2.0.10", "e9f1f1d3f4a89996a3e1a555872feed8a3a73e3d10b51886941382d29ca58f99", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [:mix], [{:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: true]}, {:decimal, "~> 1.2", [hex: :decimal, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.8.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:poolboy, "~> 1.5", [hex: :poolboy, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.13.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: true]}], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, + "esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"}, + "fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"}, + "gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"}, + "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [: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"}, + "httpoison": {:hex, :httpoison, "1.3.1", "7ac607311f5f706b44e8b3fab736d0737f2f62a31910ccd9afe7227b43edb7f0", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "jsx": {:hex, :jsx, "2.8.2", "7acc7d785b5abe8a6e9adbde926a24e481f29956dd8b4df49e3e4e7bcc92a018", [:mix, :rebar3], [], "hexpm"}, + "lager": {:hex, :lager, "3.6.3", "fe78951d174616273f87f0dbc3374d1430b1952e5efc4e1c995592d30a207294", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"}, + "logger_backend_ecto": {:hex, :logger_backend_ecto, "1.3.0", "6bb1a9d2b0ac1ee04049df94e49ea469f1f0db774d2fa05d673dc796a5ad9ed7", [:mix], [{:sqlite_ecto2, "~> 2.2", [hex: :sqlite_ecto2, repo: "hexpm", optional: true]}], "hexpm"}, + "logger_backend_sqlite": {:hex, :logger_backend_sqlite, "2.1.0", "c67dfe52e41d02c96cbeafcc0e0d613f7d6a2d029cd297ee004aa2f37a908d6e", [:mix], [{:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "meck": {:hex, :meck, "0.8.12", "1f7b1a9f5d12c511848fec26bbefd09a21e1432eadb8982d9a8aceb9891a3cf2", [:rebar3], [], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "nerves": {:hex, :nerves, "1.3.4", "9523cc1936f173c99cf15a132c2b24f9c6f1a5cfe3327bbcd518ff7e441327d3", [:mix], [{:distillery, "2.0.10", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "nerves_hub_cli": {:hex, :nerves_hub_cli, "0.5.1", "9e00c23678c4f34c05b7c2fda4ad04c79a129336ea8644f8e8ce827d35bedfb5", [:mix], [{:nerves_hub_core, "~> 0.2", [hex: :nerves_hub_core, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_hub_core": {:hex, :nerves_hub_core, "0.2.0", "ee627e0c5fd8c2511cf6e975d914c5993fabf4a1de1febca54d83910a2f476c3", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_leds": {:hex, :nerves_leds, "0.8.0", "193692767dca1a201b09113d242648493b9be0087bab83ebee99c3b0a254f5e1", [:mix], [], "hexpm"}, + "nerves_runtime": {:git, "https://github.com/nerves-project/nerves_runtime.git", "ea804f9326c649681e3fcf72e8dd81d26a6508b6", []}, + "nerves_uart": {:hex, :nerves_uart, "1.2.0", "195424116b925cd3bf9d666be036c2a80655e6ca0f8d447e277667a60005c50e", [:mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "pbcs": {:hex, :pbcs, "0.1.1", "199c7fd4af3351758378355909145a2d187c565555ed16bde30b5055114652ed", [:mix], [], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.6.4", "35618dd2cc009b69b000f785452f6b370f76d099ece199733fea27bc473f809d", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, + "rabbit_common": {:hex, :rabbit_common, "3.7.8", "e1410371c5814f85092b6dda85aa25fad945e7a41a9c4e34a59e61bd59a8c3b2", [:make, :rebar3], [{:jsx, "2.8.2", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.6.3", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "1.5.0", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "ranch": {:hex, :ranch, "1.5.0", "f04166f456790fee2ac1aa05a02745cc75783c2bfb26d39faf6aefc9a3d3a58a", [:rebar3], [], "hexpm"}, + "ranch_proxy_protocol": {:hex, :ranch_proxy_protocol, "2.0.0", "623c732025f9d66d123a8ccc1735e5f43d7eb9b20aa09457c9609ef05f7e8ace", [:rebar3], [{:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "recon": {:hex, :recon, "2.3.2", "4444c879be323b1b133eec5241cb84bd3821ea194c740d75617e106be4744318", [:rebar3], [], "hexpm"}, + "rsa": {:hex, :rsa, "0.0.1", "a63069f88ce342ffdf8448b7cdef4b39ba7dee3c1510644a39385c7e63ba246f", [:mix], [], "hexpm"}, + "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, + "shoehorn": {:hex, :shoehorn, "0.4.0", "f3830e22e1c58b502e8c436623804c4eb6ed15f5d0bdbacdeb448cddf4795951", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.5", "f111a48188b0640effb7f2952071c4cf285501d3ce090820a7c2fc20af3867e9", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, + "websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"}, + "x509": {:hex, :x509, "0.5.1", "6a5d00e35ba30da7ce51224253df1e8a1a01545eeba01642009e09e6f5d70c85", [:mix], [], "hexpm"}, +} diff --git a/farmbot_os/mix.lock.rpi3 b/farmbot_os/mix.lock.rpi3 new file mode 100644 index 00000000..bf0939ab --- /dev/null +++ b/farmbot_os/mix.lock.rpi3 @@ -0,0 +1,92 @@ +%{ + "amqp": {:hex, :amqp, "1.0.3", "06a6d909abc71d82b7c3133ca491899ca18fce857d0697dd060c29de1ef498d8", [:mix], [{:amqp_client, "~> 3.7.3", [hex: :amqp_client, repo: "hexpm", optional: false]}, {:goldrush, "~> 0.1.0", [hex: :goldrush, repo: "hexpm", optional: false]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "~> 3.5", [hex: :lager, repo: "hexpm", optional: false]}, {:rabbit_common, "~> 3.7.3", [hex: :rabbit_common, repo: "hexpm", optional: false]}, {:ranch, "~> 1.4", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "~> 1.4", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "~> 2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "amqp_client": {:hex, :amqp_client, "3.7.8", "5ec44ad152aed8519ef557189fa21e779f60578d21bcab36cabe381b451728ee", [:make, :rebar3], [{:rabbit_common, "3.7.8", [hex: :rabbit_common, repo: "hexpm", optional: false]}], "hexpm"}, + "artificery": {:hex, :artificery, "0.2.6", "f602909757263f7897130cbd006b0e40514a541b148d366ad65b89236b93497a", [:mix], [], "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"}, + "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, + "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "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"}, + "dhcp_server": {:hex, :dhcp_server, "0.6.0", "6cc0cf110b8d112455f033ae49eda570e9aeeb42a2fd1c79cc437835ecaa0716", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "1.0.0-rc.3", "774306f84973fc3f1e2e8743eeaa5f5d29b117f3916e5de74c075c02f1b8ef55", [:mix], [], "hexpm"}, + "distillery": {:hex, :distillery, "2.0.10", "e9f1f1d3f4a89996a3e1a555872feed8a3a73e3d10b51886941382d29ca58f99", [:mix], [{:artificery, "~> 0.2", [hex: :artificery, repo: "hexpm", optional: false]}], "hexpm"}, + "dns": {:hex, :dns, "2.1.2", "81c46d39f7934f0e73368355126e4266762cf227ba61d5889635d83b2d64a493", [:mix], [{:socket, "~> 0.3.13", [hex: :socket, repo: "hexpm", optional: false]}], "hexpm"}, + "earmark": {:hex, :earmark, "1.2.6", "b6da42b3831458d3ecc57314dff3051b080b9b2be88c2e5aa41cd642a5b044ed", [:mix], [], "hexpm"}, + "ecto": {:hex, :ecto, "2.2.9", "031d55df9bb430cb118e6f3026a87408d9ce9638737bda3871e5d727a3594aae", [: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_ale": {:hex, :elixir_ale, "1.1.0", "06e77697fa0bd7aff5f9040d8be8ba7947e5833de2a12d1a25f54332556b4e90", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "elixir_make": {:hex, :elixir_make, "0.4.2", "332c649d08c18bc1ecc73b1befc68c647136de4f340b548844efc796405743bf", [:mix], [], "hexpm"}, + "esqlite": {:hex, :esqlite, "0.2.4", "3a8a352c190afe2d6b828b252a6fbff65b5cc1124647f38b15bdab3bf6fd4b3e", [:rebar3], [], "hexpm"}, + "farmbot_system_rpi3": {:hex, :farmbot_system_rpi3, "1.6.1-farmbot.1", "421cdd2c383229a776ee6142a493eeba5ab1902d0d888035748454208e28fc28", [:mix], [{:nerves, "~> 1.3", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.6.5", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.1.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm"}, + "fs": {:hex, :fs, "3.4.0", "6d18575c250b415b3cad559e6f97a4c822516c7bc2c10bfbb2493a8f230f5132", [:rebar3], [], "hexpm"}, + "fwup": {:hex, :fwup, "0.3.0", "2c360815565fcbc945ebbb34b58f156efacb7f8d64766f1cb3426919bb3f41ea", [:mix], [], "hexpm"}, + "gen_stage": {:hex, :gen_stage, "0.14.0", "65ae78509f85b59d360690ce3378d5096c3130a0694bab95b0c4ae66f3008fad", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.16.0", "4a7e90408cef5f1bf57c5a39e2db8c372a906031cc9b1466e963101cb927dafc", [:mix], [], "hexpm"}, + "goldrush": {:hex, :goldrush, "0.1.9", "f06e5d5f1277da5c413e84d5a2924174182fb108dabb39d5ec548b27424cd106", [:rebar3], [], "hexpm"}, + "hackney": {:hex, :hackney, "1.15.0", "287a5d2304d516f63e56c469511c42b016423bcb167e61b611f6bad47e3ca60e", [: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"}, + "httpoison": {:hex, :httpoison, "1.3.1", "7ac607311f5f706b44e8b3fab736d0737f2f62a31910ccd9afe7227b43edb7f0", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, + "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "joken": {:hex, :joken, "1.5.0", "42a0953e80bd933fc98a0874e156771f78bf0e92abe6c3a9c22feb6da28efb0b", [:mix], [{:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"}, + "lager": {:hex, :lager, "3.6.3", "fe78951d174616273f87f0dbc3374d1430b1952e5efc4e1c995592d30a207294", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"}, + "lager_logger": {:hex, :lager_logger, "1.0.5", "2b58be52fe1e0fb82656180fc54e45618aa2dc619090b00e6d3fb4707c6a1fe5", [:mix], [{:lager, ">= 2.1.0", [hex: :lager, repo: "hexpm", optional: false]}], "hexpm"}, + "logger_backend_ecto": {:hex, :logger_backend_ecto, "1.3.0", "6bb1a9d2b0ac1ee04049df94e49ea469f1f0db774d2fa05d673dc796a5ad9ed7", [:mix], [{:sqlite_ecto2, "~> 2.2", [hex: :sqlite_ecto2, repo: "hexpm", optional: true]}], "hexpm"}, + "logger_backend_sqlite": {:hex, :logger_backend_sqlite, "2.1.0", "c67dfe52e41d02c96cbeafcc0e0d613f7d6a2d029cd297ee004aa2f37a908d6e", [:mix], [{:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, + "mdns": {:hex, :mdns, "1.0.2", "c8228dd44d3fdd55e9842cb7111c9145f2eeaa8b7adac75012ee0e250962215e", [:mix], [{:dns, "~> 2.0", [hex: :dns, repo: "hexpm", optional: false]}], "hexpm"}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, + "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], [], "hexpm"}, + "muontrap": {:hex, :muontrap, "0.4.0", "f3c48f5e2cbb89b6406d28e488fbd0da1ce0ca00af332860913999befca9688a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves": {:hex, :nerves, "1.3.4", "9523cc1936f173c99cf15a132c2b24f9c6f1a5cfe3327bbcd518ff7e441327d3", [:mix], [{:distillery, "2.0.10", [hex: :distillery, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "nerves_firmware": {:hex, :nerves_firmware, "0.4.0", "ac2fed915a7ca4bb69f567d9b742d77cffc3a6a56420ce65e870c8c34119b935", [:mix], [], "hexpm"}, + "nerves_firmware_ssh": {:hex, :nerves_firmware_ssh, "0.4.0", "494d97e06de0cc7218b0e2b40372f18aae5507c9f3d659a2ba8a8c90239ae51d", [:mix], [{:nerves_runtime, "~> 0.4", [hex: :nerves_runtime, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_hub": {:hex, :nerves_hub, "0.2.1", "135e727260c8bb9b985078d93f09271e59f61e5dd39b336b7dc4d583c5c6c50d", [:mix], [{:fwup, "~> 0.3.0", [hex: :fwup, repo: "hexpm", optional: false]}, {:hackney, "~> 1.10", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:nerves_hub_cli, "~> 0.5", [hex: :nerves_hub_cli, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.8", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:phoenix_channel_client, "~> 0.4", [hex: :phoenix_channel_client, repo: "hexpm", optional: false]}, {:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_hub_cli": {:hex, :nerves_hub_cli, "0.5.1", "9e00c23678c4f34c05b7c2fda4ad04c79a129336ea8644f8e8ce827d35bedfb5", [:mix], [{:nerves_hub_core, "~> 0.2", [hex: :nerves_hub_core, repo: "hexpm", optional: false]}, {:pbcs, "~> 0.1", [hex: :pbcs, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_hub_core": {:hex, :nerves_hub_core, "0.2.0", "ee627e0c5fd8c2511cf6e975d914c5993fabf4a1de1febca54d83910a2f476c3", [:mix], [{:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.1 or ~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.3", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_init_gadget": {:hex, :nerves_init_gadget, "0.5.2", "51ea3bc6c07ccdb1823769bb0c69eaa943d5d3da3d8f4e9de3ba02a71f76bf8d", [:mix], [{:mdns, "~> 1.0", [hex: :mdns, repo: "hexpm", optional: false]}, {:nerves_firmware_ssh, "~> 0.2", [hex: :nerves_firmware_ssh, repo: "hexpm", optional: false]}, {:nerves_network, "~> 0.3", [hex: :nerves_network, repo: "hexpm", optional: false]}, {:nerves_runtime, "~> 0.3", [hex: :nerves_runtime, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.1", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:ring_logger, "~> 0.4", [hex: :ring_logger, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_leds": {:hex, :nerves_leds, "0.8.0", "193692767dca1a201b09113d242648493b9be0087bab83ebee99c3b0a254f5e1", [:mix], [], "hexpm"}, + "nerves_network": {:hex, :nerves_network, "0.5.3", "526ab769c31f33ecef2a3c8b431ce66f36189aece4cafebc1d604a1509830bc9", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nerves_network_interface, "~> 0.4.4", [hex: :nerves_network_interface, repo: "hexpm", optional: false]}, {:nerves_wpa_supplicant, "~> 0.5", [hex: :nerves_wpa_supplicant, repo: "hexpm", optional: false]}, {:one_dhcpd, "~> 0.2.0", [hex: :one_dhcpd, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.7", [hex: :system_registry, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_network_interface": {:hex, :nerves_network_interface, "0.4.4", "200b1a84bc1a7fdeaf3a1e0e2d4e9b33e240b034e73f39372768d43f8690bae0", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_runtime": {:hex, :nerves_runtime, "0.9.1", "ed0c77f65a30abb0fb25edaa0243850105ce593a78f5beda756dbb48d04f5991", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:system_registry, "~> 0.5", [hex: :system_registry, repo: "hexpm", optional: false]}, {:uboot_env, "~> 0.1", [hex: :uboot_env, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_system_br": {:hex, :nerves_system_br, "1.6.5", "1e51f4a53aa31ff349b5882430ffce8024c43373b103fb40875b8da24a24eff1", [:mix], [], "hexpm"}, + "nerves_system_farmbot_rpi3": {:hex, :nerves_system_farmbot_rpi3, "1.5.0-farmbot.0", "dc025f4b48d007de3f11de67e6a0f8cd0d25842689689368cd3548c4a47ef719", [:mix], [{:nerves, "~> 1.3", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_system_br, "1.5.2", [hex: :nerves_system_br, repo: "hexpm", optional: false]}, {:nerves_system_linter, "~> 0.3.0", [hex: :nerves_system_linter, repo: "hexpm", optional: false]}, {:nerves_toolchain_arm_unknown_linux_gnueabihf, "1.1.0", [hex: :nerves_toolchain_arm_unknown_linux_gnueabihf, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_system_linter": {:hex, :nerves_system_linter, "0.3.0", "84e0f63c8ac196b16b77608bbe7df66dcf352845c4e4fb394bffd2b572025413", [:mix], [], "hexpm"}, + "nerves_time": {:hex, :nerves_time, "0.2.0", "c8ae5cc020cd5e5b9f166f614b3dff30e10b25828715743aa97749cbfe0c5c0a", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:muontrap, "~> 0.4", [hex: :muontrap, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_toolchain_arm_unknown_linux_gnueabihf": {:hex, :nerves_toolchain_arm_unknown_linux_gnueabihf, "1.1.0", "ca466a656f8653346a8551a35743f7c41046f3d53e945723e970cb4a7811e617", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}, {:nerves_toolchain_ctng, "~> 1.5.0", [hex: :nerves_toolchain_ctng, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_toolchain_ctng": {:hex, :nerves_toolchain_ctng, "1.5.0", "34b8f5664858ff6ce09730b26221441398acd1fa361b8c6d744d9ec18238c16b", [:mix], [{:nerves, "~> 1.0", [hex: :nerves, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_uart": {:hex, :nerves_uart, "1.2.0", "195424116b925cd3bf9d666be036c2a80655e6ca0f8d447e277667a60005c50e", [:mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "nerves_wpa_supplicant": {:hex, :nerves_wpa_supplicant, "0.5.1", "5fc2654d9ddbee6fa72f7f6fa90e7b8ee765b0775e57b93ec7ca485c421074cf", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, + "one_dhcpd": {:hex, :one_dhcpd, "0.2.0", "18eb8ce7101ad7b79e67f3d7ee7f648f42e02b8fa4c1cb3f24f403bf6860f81d", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm"}, + "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, + "pbcs": {:hex, :pbcs, "0.1.1", "199c7fd4af3351758378355909145a2d187c565555ed16bde30b5055114652ed", [:mix], [], "hexpm"}, + "phoenix_channel_client": {:hex, :phoenix_channel_client, "0.4.0", "dc1da56f539587a6cc32da7b5c17da3d4066411fe6508035142a595cf451a1d2", [:mix], [{:websocket_client, "~> 1.3", [hex: :websocket_client, repo: "hexpm", optional: true]}], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.12.0", "1fb3c2e48b4b66d75564d8d63df6d53655469216d6b553e7e14ced2b46f97622", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug": {:hex, :plug, "1.6.4", "35618dd2cc009b69b000f785452f6b370f76d099ece199733fea27bc473f809d", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"}, + "poolboy": {:hex, :poolboy, "1.5.1", "6b46163901cfd0a1b43d692657ed9d7e599853b3b21b95ae5ae0a777cf9b6ca8", [:rebar], [], "hexpm"}, + "rabbit_common": {:hex, :rabbit_common, "3.7.8", "e1410371c5814f85092b6dda85aa25fad945e7a41a9c4e34a59e61bd59a8c3b2", [:make, :rebar3], [{:jsx, "2.8.2", [hex: :jsx, repo: "hexpm", optional: false]}, {:lager, "3.6.3", [hex: :lager, repo: "hexpm", optional: false]}, {:ranch, "1.5.0", [hex: :ranch, repo: "hexpm", optional: false]}, {:ranch_proxy_protocol, "1.5.0", [hex: :ranch_proxy_protocol, repo: "hexpm", optional: false]}, {:recon, "2.3.2", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm"}, + "ranch": {:hex, :ranch, "1.6.2", "6db93c78f411ee033dbb18ba8234c5574883acb9a75af0fb90a9b82ea46afa00", [:rebar3], [], "hexpm"}, + "ranch_proxy_protocol": {:hex, :ranch_proxy_protocol, "2.1.1", "3c4723327166d2d63c0405f4914e2e471c6de362cc844e9b203af5763e7c9d25", [:rebar3], [{:ranch, "1.6.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "recon": {:hex, :recon, "2.3.2", "4444c879be323b1b133eec5241cb84bd3821ea194c740d75617e106be4744318", [:rebar3], [], "hexpm"}, + "ring_logger": {:hex, :ring_logger, "0.6.1", "568dbd9eebf26c661427de5204d8a91adaa740993f3ac5aa450257f62b6e7178", [:mix], [], "hexpm"}, + "rsa": {:hex, :rsa, "0.0.1", "a63069f88ce342ffdf8448b7cdef4b39ba7dee3c1510644a39385c7e63ba246f", [:mix], [], "hexpm"}, + "sbroker": {:hex, :sbroker, "1.0.0", "28ff1b5e58887c5098539f236307b36fe1d3edaa2acff9d6a3d17c2dcafebbd0", [:rebar3], [], "hexpm"}, + "shoehorn": {:hex, :shoehorn, "0.4.0", "f3830e22e1c58b502e8c436623804c4eb6ed15f5d0bdbacdeb448cddf4795951", [:mix], [{:distillery, "~> 2.0", [hex: :distillery, repo: "hexpm", optional: false]}], "hexpm"}, + "socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm"}, + "sqlite_ecto2": {:hex, :sqlite_ecto2, "2.2.5", "f111a48188b0640effb7f2952071c4cf285501d3ce090820a7c2fc20af3867e9", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 1.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "2.2.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.13", [hex: :postgrex, repo: "hexpm", optional: true]}, {:sbroker, "~> 1.0", [hex: :sbroker, repo: "hexpm", optional: false]}, {:sqlitex, "~> 1.4", [hex: :sqlitex, repo: "hexpm", optional: false]}], "hexpm"}, + "sqlitex": {:hex, :sqlitex, "1.4.3", "a50f12d6aeb25f4ebb128453386c09bbba8f5abd3c7713dc5eaa92f359926ac5", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:esqlite, "~> 0.2.4", [hex: :esqlite, repo: "hexpm", optional: false]}], "hexpm"}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "system_registry": {:hex, :system_registry, "0.8.1", "7df1f66f0e4fcd0940ecd0473d2787d69d2abd6267d21e8f8ecbab58a14415ce", [:mix], [], "hexpm"}, + "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "timex": {:hex, :timex, "3.4.1", "e63fc1a37453035e534c3febfe9b6b9e18583ec7b37fd9c390efdef97397d70b", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.19", "7962a3997bf06303b7d1772988ede22260f3dae1bf897408ebdac2b4435f4e6a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "uboot_env": {:hex, :uboot_env, "0.1.0", "176d277c8461d849614d8b82595060bf03ace6ca59d49c5b53707d90cb9e2f5a", [:mix], [], "hexpm"}, + "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, + "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm"}, + "websocket_client": {:hex, :websocket_client, "1.3.0", "2275d7daaa1cdacebf2068891c9844b15f4fdc3de3ec2602420c2fb486db59b6", [:rebar3], [], "hexpm"}, + "x509": {:hex, :x509, "0.5.1", "6a5d00e35ba30da7ce51224253df1e8a1a01545eeba01642009e09e6f5d70c85", [:mix], [], "hexpm"}, +} diff --git a/farmbot_os/platform/target/configurator/captive_portal/captive_portal.ex b/farmbot_os/platform/target/configurator/captive_portal.ex similarity index 88% rename from farmbot_os/platform/target/configurator/captive_portal/captive_portal.ex rename to farmbot_os/platform/target/configurator/captive_portal.ex index 063dcd5b..94f73e87 100644 --- a/farmbot_os/platform/target/configurator/captive_portal/captive_portal.ex +++ b/farmbot_os/platform/target/configurator/captive_portal.ex @@ -101,16 +101,30 @@ defmodule Farmbot.Target.Bootstrap.Configurator.CaptivePortal do defp setup_dnsmasq(ip_addr, interface) do dnsmasq_conf = build_dnsmasq_conf(ip_addr, interface) - File.mkdir!("/tmp/dnsmasq") + File.mkdir_p!("/tmp/dnsmasq") :ok = File.write("/tmp/dnsmasq/#{@dnsmasq_conf_file}", dnsmasq_conf) dnsmasq_cmd = "dnsmasq -k --dhcp-lease " <> "/tmp/dnsmasq/#{@dnsmasq_pid_file} " <> "--conf-dir=/tmp/dnsmasq" dnsmasq_port = Port.open({:spawn, dnsmasq_cmd}, [:binary]) - dnsmasq_os_pid = dnsmasq_port|> Port.info() |> Keyword.get(:os_pid) - {:ok, {dnsmasq_port, dnsmasq_os_pid}} - rescue - ex -> {:error, ex} + get_dnsmasq_info(dnsmasq_port, ip_addr, interface) + end + + defp get_dnsmasq_info(nil, ip_addr, interface) do + Farmbot.Logger.warn 1, "dnsmasq failed to start." + Process.sleep(1000) + setup_dnsmasq(ip_addr, interface) + end + + defp get_dnsmasq_info(dnsmasq_port, ip_addr, interface) when is_port(dnsmasq_port) do + case Port.info(dnsmasq_port, :os_pid) do + {:os_pid, dnsmasq_os_pid} -> + {dnsmasq_port, dnsmasq_os_pid} + nil -> + Farmbot.Logger.warn 1, "dnsmasq not ready yet." + Process.sleep(1000) + setup_dnsmasq(ip_addr, interface) + end end defp build_dnsmasq_conf(ip_addr, interface) do diff --git a/farmbot_os/platform/target/network/network.ex b/farmbot_os/platform/target/network/network.ex index 532416c1..fb8f8660 100644 --- a/farmbot_os/platform/target/network/network.ex +++ b/farmbot_os/platform/target/network/network.ex @@ -113,6 +113,10 @@ defmodule Farmbot.Target.Network do end end + def test_dns(hostname) when is_binary(hostname) do + test_dns(to_charlist(hostname)) + end + def test_dns(hostname) do :ok = :inet_db.clear_cache() # IO.puts "testing dns: #{hostname}" diff --git a/farmbot_os/priv/static/templates/config_wireless_step_2_PSK.html.eex b/farmbot_os/priv/static/templates/config_wireless_step_2_PSK.html.eex index a295c919..9ea3b2de 100644 --- a/farmbot_os/priv/static/templates/config_wireless_step_2_PSK.html.eex +++ b/farmbot_os/priv/static/templates/config_wireless_step_2_PSK.html.eex @@ -1,6 +1,5 @@ - - - + + Configure Farmbot's Network