Merge pull request #319 from ConnorRigby/master

BIG HTTP REFACTOR
pull/327/head
Connor Rigby 2017-06-12 13:52:12 -07:00 committed by GitHub
commit 41d46577fc
187 changed files with 6615 additions and 5603 deletions

1
.gitignore vendored
View File

@ -40,3 +40,4 @@ dump.rdb
# this file isnt stored here but just in case.
fwup-key.priv
.env

28
.iex.exs 100644
View File

@ -0,0 +1,28 @@
if Code.ensure_loaded? Farmbot do
alias Farmbot.{
Auth,
Context,
EventSupervisor,
HTTP,
Regimen,
SysFormatter,
BotState,
Database,
FarmEvent,
ImageWatcher,
RegimenRunner,
CeleryScript,
DebugLog,
FarmEventRunner,
Lib,
Sequence.Runner,
Token,
Configurator,
EasterEggs,
Farmware,
Serial,
Transport
}
context = Context.new()
end

View File

@ -1 +1 @@
3.1.6
4.0.0

View File

@ -4,10 +4,6 @@ use Mix.Config
target = Mix.Project.config[:target]
env = Mix.env()
# Transports
mqtt_transport = Farmbot.Transport.GenMqtt
redis_transport = Farmbot.Transport.Redis
config :logger, utc_log: true
# I force colors because they are important.
@ -16,23 +12,12 @@ config :logger, :console, colors: [enabled: true, info: :cyan]
# Iex needs colors too.
config :iex, :colors, enabled: true
# frontend <-> bot transports.
config :farmbot, transports: [
{mqtt_transport, name: mqtt_transport},
# {redis_transport, name: redis_transport}
]
# bot <-> firmware transports.
config :farmbot, expected_fw_version: "GENESIS.V.01.12.EXPERIMENTAL"
config :farmbot, expected_fw_version: "GENESIS.V.01.13.EXPERIMENTAL"
# Rollbar
config :farmbot, rollbar_access_token: "dcd79b191ab84aa3b28259cbb80e2060"
# give the ability to start a redis server instance in dev mode.
config :farmbot, :redis,
server: System.get_env("REDIS_SERVER") || false,
port: System.get_env("REDIS_SERVER_PORT") || 6379
# This is usually in the `priv` dir of :tzdata, but our fs is read only.
config :tzdata, :data_dir, "/tmp"
config :tzdata, :autoupdate, :disabled
@ -42,10 +27,6 @@ config :fs, path: "/tmp/images"
# import config specific to our nerves_target
IO.puts "using #{target} - #{env} configuration."
import_config "hardware/#{target}/hardware.exs"
import_config "hardware/#{target}/#{env}.exs"
config :nerves, :firmware,
rootfs_additions: "config/hardware/#{target}/rootfs-additions-#{env}"
# Import configuration specific to out environment.
import_config "#{env}.exs"

View File

@ -1,9 +0,0 @@
use Mix.Config
config :farmbot,
configurator_port: System.get_env("CONFIGURATOR_PORT") || 5000,
tty: {:system, "ARDUINO_TTY"}
config :wobserver,
mode: :plug,
remote_url_prefix: "/wobserver"

View File

@ -0,0 +1,26 @@
use Mix.Config
config :farmbot,
config_file_name: System.get_env("CONFIG_FILE_NAME") || "default_config.json",
configurator_port: System.get_env("CONFIGURATOR_PORT") || 5000,
path: "/tmp/farmbot",
tty: {:system, "ARDUINO_TTY"}
config :farmbot, :redis,
server: System.get_env("REDIS_SERVER") || false,
port: System.get_env("REDIS_SERVER_PORT") || 6379
# Transports
mqtt_transport = Farmbot.Transport.GenMqtt
# redis_transport = Farmbot.Transport.Redis
# frontend <-> bot transports.
config :farmbot, transports: [
{mqtt_transport, name: mqtt_transport},
# {redis_transport, name: redis_transport}
]
config :wobserver,
mode: :plug,
remote_url_prefix: "/wobserver"

View File

@ -1,5 +0,0 @@
use Mix.Config
config :farmbot,
path: "/tmp/farmbot",
config_file_name: System.get_env("CONFIG_FILE_NAME") || "default_config.json",
configurator_port: System.get_env("CONFIGURATOR_PORT") || 5000

View File

@ -0,0 +1,2 @@
use Mix.Config
import_config "dev.exs"

View File

@ -1,14 +1,18 @@
use Mix.Config
import_config "dev.exs"
config :farmbot,
config_file_name: "default_config.json",
configurator_port: 5001,
path: "/tmp/farmbot_test",
config_file_name: "default_config.json",
# tty: "/dev/tnt1",
logger: false
config :farmbot_simulator, :tty, "tnt0"
config :farmbot, :redis,
server: false
config :farmbot, transports: []
# We hopefully don't need logger ¯\_(ツ)_/¯
config :logger, :console, format: ""
config :farmbot,
path: "/tmp/farmbot_test",
config_file_name: "default_config.json",
tty: "/dev/tnt1",
logger: false
config :farmbot_simulator, :tty, "tnt0"

View File

@ -0,0 +1,32 @@
use Mix.Config
config :wobserver,
mode: :plug,
remote_url_prefix: "/wobserver"
# Transports
mqtt_transport = Farmbot.Transport.GenMqtt
redis_transport = Farmbot.Transport.Redis
# frontend <-> bot transports.
config :farmbot, transports: [
{mqtt_transport, name: mqtt_transport},
{redis_transport, name: redis_transport}
]
config :farmbot, :redis,
server: true,
port: 6379
config :farmbot,
configurator_port: 80,
path: "/state",
config_file_name: "default_config_rpi3.json"
config :logger, :console,
format: "\n$time $metadata[$level] $levelpad$message\n",
metadata: [:module]
# In production, we want a cron job for checking for updates.
config :quantum, cron: [ "5 1 * * *": {Farmbot.System.Updates, :do_update_check}]
config :nerves_interim_wifi, regulatory_domain: "US" #FIXME

View File

@ -1,9 +0,0 @@
use Mix.Config
config :farmbot,
path: "/state",
config_file_name: "default_config_rpi3.json",
configurator_port: 80
config :farmbot, :redis,
server: true,
port: 6379

View File

@ -1,7 +1,23 @@
use Mix.Config
# Transports
mqtt_transport = Farmbot.Transport.GenMqtt
redis_transport = Farmbot.Transport.Redis
# frontend <-> bot transports.
config :farmbot, transports: [
{mqtt_transport, name: mqtt_transport},
{redis_transport, name: redis_transport}
]
config :farmbot, :redis,
server: true,
port: 6379
config :farmbot,
configurator_port: 80
configurator_port: 80,
path: "/state",
config_file_name: "default_config_rpi3.json"
# In production, we want a cron job for checking for updates.
config :quantum, cron: [ "5 1 * * *": {Farmbot.System.Updates, :do_update_check}]

View File

@ -0,0 +1,28 @@
if Code.ensure_loaded? Farmbot do
alias Farmbot.{
Auth,
Context,
EventSupervisor,
HTTP,
Regimen,
SysFormatter,
BotState,
Database,
FarmEvent,
ImageWatcher,
RegimenRunner,
CeleryScript,
DebugLog,
FarmEventRunner,
Lib,
Sequence.Runner,
Token,
Configurator,
EasterEggs,
Farmware,
Serial,
Transport
}
context = Context.new()
end

View File

@ -5,10 +5,6 @@ via shell environment variables.
## Firmware Signing
We Produce signed releases in PROD environment. export `PRIV_KEY_FILE` to be the private key file.
## IO debugger
If you want more verbose logs you can export `DEBUG_LOG`. This will cause (a lot of) messages
to be displayed on the current tty.
## Mix Environment
you can set `MIX_ENV=prod` or `MIX_ENV=dev` (default) to change the environment
of the farmbot application.
@ -24,7 +20,7 @@ into a static website that gets served by `Plug`
Webpack is configured via a package called `ex_webpack`. Default behavior it to
watch the web source files for changes and recompile. This adds extra time to the
initial compile of the application and can be just generally annoying. to disable
this export `USE_WEBPACK=false`
this export `NO_WEBPACK=true`
## Configurator
The Configurator app is started by default on port `5000`.

View File

@ -0,0 +1 @@
"{\"id\":52,\"name\":\"ancient-paper-798\",\"webcam_url\":null}"

View File

@ -0,0 +1 @@
"{\"foo\": \"uh\", \"bar\": 1, \"id\": 2}"

View File

@ -0,0 +1 @@
"[{\"foo\": \"uh\", \"bar\": 1, \"id\": 2}]"

View File

@ -0,0 +1 @@
"[{\"id\":1,\"start_time\":\"2017-05-13T07:00:00.000Z\",\"end_time\":\"2017-05-23T07:00:00.000Z\",\"repeat\":2,\"time_unit\":\"daily\",\"executable_id\":2,\"executable_type\":\"Sequence\",\"calendar\":[\"2017-05-19T07:00:00.000Z\",\"2017-05-21T07:00:00.000Z\"]},{\"id\":2,\"start_time\":\"2017-05-12T07:00:00.000Z\",\"end_time\":\"2017-05-19T07:00:00.000Z\",\"repeat\":1,\"time_unit\":\"daily\",\"executable_id\":1,\"executable_type\":\"Sequence\",\"calendar\":[\"2017-05-18T07:00:00.000Z\"]}]"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"[{\"id\":1,\"pin\":13,\"mode\":0,\"label\":\"LED\"}]"

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
"{\"id\":71,\"created_at\":\"2017-05-16T22:23:07.424Z\",\"updated_at\":\"2017-05-16T22:23:07.424Z\",\"device_id\":8,\"meta\":{},\"name\":\"Slot One.\",\"pointer_type\":\"ToolSlot\",\"radius\":50.0,\"x\":10.0,\"y\":10.0,\"z\":10.0,\"tool_id\":1}"

View File

@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAsIgWhfZew/k4wE8hWf82RErhP1fsy8v6NbiR5HY06miqWNuI
F1tKg+CHe+NoyaADUJ+Qfu/LIMvSN7USEHls9mEl5Kx1H27hbPgu+TuqqQTgS2rF
snLjWPTQFigH5KNm5HSoreq2/cXRGdXd9IuIwyVh4AFyvSxI7/GrWlKL6M1kZv39
N56zlDt086L5OKltOGIVJF6uJypsXy02omEH4L/zd2npZKD8Y7kaFfNs5mnWw65I
Wr2tqjF7cp8ESN93ChwM7z4I+Xg+EghsEkHVNS4pdVgE5dyB10P/pNjjS52rs6uJ
D3M/Tigr9wS5T/qqbNmcksVT1aP8rWAys4QnIQIDAQABAoIBADUBR7IFncK+LDoi
CGOba9HpoeSBJAq1PnWu669rhsvzjWKM2DobIS6j1kpup+ISd6xXnO1gVt+ME5zC
c6AatYrs9JHK7of3pRwxEPmo3r9NRYOflajVMkpdh7V/Y49VOOnT1WoTFcrxAK3/
N1vcIb5mlRLLnIYMrAHP0KGYM4Y81dpsoeNFO8odWseE06uHVTbRBIAHbNvRy6zd
LnEidEh9soNNcVDrulXbAFmiPfoO/Bjbp0RYhg/vMhadNernktw9zrXFUQxAAjVR
SC8Svl07Sz6KcwL72Vf3hgzJCzCCpOyU8w8dPAMRXrY4r5niCfIJlXxqH9EiH80h
NmMMaHkCgYEA38FL1iuAI2lh4S33/YMdLpk5SMrSLZJPX/CqppmFfEaNQROFOiAl
1LDP5JbG9W37icf6qJsYsVeO0AMR3YC1CtxXQ7Wzif/TSVo24Ww9iSZTcz7Y6lVo
tIW+iwUyAXE3pnxHXS2j8Zj/Z6vm2B5oYbMy+NlF/mhVkCOXByi4xkcCgYEAyfij
Sqsh4TmJJCPB4/MEd+MrdggPAGVqKLtOggGc2OsQpqsviHFMTgEN60Gu7jeATcQM
61Vi0HLvPjX7DGWw1tvTlBLZvSp+fniME94Z+mk48jBILwXCqTuM0ZXNcU0XjgIA
jLk+E8/I3Sw9eAs5Z95r+R5gtNkT0TcO/C8yk1cCgYEA3LTJnSOjbUqRZY/2QXWG
32P8ATUuRA1BhhzZ9yMPbBobUslybHcxWa5eIdgnwAcQSkObl5wEq0j2cW/Vu2st
KN1Wpk8gHUremkgGQiyGNjY7sj2XsO02LnqODIq/XHTUs796lQpj3/dOVnBVb2/u
/g/Ig3WteNhpLZgtbL5aJBkCgYEAsHtUpEBZQGZ4EV41ZCvLsb6NEXwFL8FuO90/
wpYKKfls+VYIGN93X4nIUdN5OarBsDIpX9GioKZtqxycG78YAQbhIDhAjuz8zyIi
tJGUfZ1IJ0hNKtmLuTjR2aledSx58pqJRG3xcnpT9/9aTvTv2nUeP/ZtZllw2ZWU
wIO1W80CgYEAkJ7LRsAUfwV/xn3aywvE3uwESQwrfBixcl6BGP3idSJYNvTx+SO9
XzLg5hbdCqTst8jNvz6VDvExRRUFNgi38BvO55wo0JLDKOz/uj3xaSDFX7L5KeSV
z+3rvi4ulZHA87Y9MDrrZ1KzMFWHJ9YWFO9Cfua4130eSRD2FJNP28o=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1 @@
"[{\"id\":1,\"name\":\"Test Regimen 456\",\"color\":\"gray\",\"device_id\":8,\"regimen_items\":[{\"id\":1,\"regimen_id\":1,\"sequence_id\":1,\"time_offset\":300000},{\"id\":2,\"regimen_id\":1,\"sequence_id\":1,\"time_offset\":173100000},{\"id\":3,\"regimen_id\":1,\"sequence_id\":1,\"time_offset\":345900000}]}]"

View File

@ -0,0 +1 @@
"[{\"id\":1,\"name\":\"Goto 0, 0, 0\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"location\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"},{\"id\":2,\"name\":\"Every Node\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"tool_id\":1},\"kind\":\"tool\"}}},{\"kind\":\"move_relative\",\"args\":{\"x\":4,\"y\":5,\"z\":6,\"speed\":800}},{\"kind\":\"write_pin\",\"args\":{\"pin_number\":1,\"pin_value\":2,\"pin_mode\":0}},{\"kind\":\"read_pin\",\"args\":{\"pin_number\":4,\"pin_mode\":0,\"label\":\"foo\"}},{\"kind\":\"wait\",\"args\":{\"milliseconds\":4}},{\"kind\":\"send_message\",\"args\":{\"message\":\"Bot is at position {{ x }}, {{ y }}, {{ z }}.\",\"message_type\":\"success\"}},{\"kind\":\"_if\",\"args\":{\"lhs\":\"x\",\"op\":\"is\",\"rhs\":0,\"_then\":{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},\"_else\":{\"kind\":\"nothing\",\"args\":{}}}},{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},{\"kind\":\"execute_script\",\"args\":{\"label\":\"plant-detection\"}},{\"kind\":\"take_photo\",\"args\":{}},{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"x\":1,\"y\":2,\"z\":3},\"kind\":\"coordinate\"}}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"}]"

View File

@ -0,0 +1 @@
"{\"id\":2,\"name\":\"Every Node\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"tool_id\":1},\"kind\":\"tool\"}}},{\"kind\":\"move_relative\",\"args\":{\"x\":4,\"y\":5,\"z\":6,\"speed\":800}},{\"kind\":\"write_pin\",\"args\":{\"pin_number\":1,\"pin_value\":2,\"pin_mode\":0}},{\"kind\":\"read_pin\",\"args\":{\"pin_number\":4,\"pin_mode\":0,\"label\":\"foo\"}},{\"kind\":\"wait\",\"args\":{\"milliseconds\":4}},{\"kind\":\"send_message\",\"args\":{\"message\":\"Bot is at position {{ x }}, {{ y }}, {{ z }}.\",\"message_type\":\"success\"}},{\"kind\":\"_if\",\"args\":{\"lhs\":\"x\",\"op\":\"is\",\"rhs\":0,\"_then\":{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},\"_else\":{\"kind\":\"nothing\",\"args\":{}}}},{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},{\"kind\":\"execute_script\",\"args\":{\"label\":\"plant-detection\"}},{\"kind\":\"take_photo\",\"args\":{}},{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"x\":1,\"y\":2,\"z\":3},\"kind\":\"coordinate\"}}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"}"

View File

@ -0,0 +1 @@
"{\"token\":{\"unencoded\":{\"sub\":\"admin@admin.com\",\"iat\":1495032010,\"jti\":\"e23f2724-02ec-4968-b679-b8144b277bbf\",\"iss\":\"//192.168.29.165:3000\",\"exp\":1498488010,\"mqtt\":\"192.168.29.165\",\"os_update_server\":\"https://api.github.com/repos/farmbot/farmbot_os/releases/latest\",\"fw_update_server\":\"https://api.github.com/repos/Farmbot/farmbot-arduino-firmware/releases/latest\",\"bot\":\"device_8\"},\"encoded\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0OTUwMzIwMTAsImp0aSI6ImUyM2YyNzI0LTAyZWMtNDk2OC1iNjc5LWI4MTQ0YjI3N2JiZiIsImlzcyI6Ii8vMTkyLjE2OC4yOS4xNjU6MzAwMCIsImV4cCI6MTQ5ODQ4ODAxMCwibXF0dCI6IjE5Mi4xNjguMjkuMTY1Iiwib3NfdXBkYXRlX3NlcnZlciI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvZmFybWJvdC9mYXJtYm90X29zL3JlbGVhc2VzL2xhdGVzdCIsImZ3X3VwZGF0ZV9zZXJ2ZXIiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL0Zhcm1ib3QvZmFybWJvdC1hcmR1aW5vLWZpcm13YXJlL3JlbGVhc2VzL2xhdGVzdCIsImJvdCI6ImRldmljZV84In0.UcBgq4pxoXeR6TYv9lYd90LAlGczZMjuvqT1Yc4R8xIk_Jy6bumhq7mI-Hoi9iKBhPU3XMpifXoIyqb1UdC1MBJyHMpPYjZoJLmm4v3XEug_rTu4RcaO7r_r1dZAh2C5TPVXBydcDe02loGC4_YmQPwWixhqJO_6vFF7JEDHir4bihbdfV-P4uZhpUcw-I1Eht4zCMjlmWaL5xcKUdSf-TuSQGNi0Ib0GkZs2wXan2bgv_wBfFEaZ4vmoZO1NM43jaykDssOaxP9hN7FKDdJ4mXL7r9XS7KtXpVQPycUYsfr-lPvid9cfKQFv-STakiDot8uGOYr1CH6I9erQMlhnQ\"},\"user\":{\"id\":6,\"device_id\":8,\"name\":\"Administrator\",\"email\":\"admin@admin.com\",\"created_at\":\"2017-05-16T22:23:06.508Z\",\"updated_at\":\"2017-05-17T14:38:56.800Z\",\"verified_at\":\"2017-05-16T22:23:06.522Z\",\"verification_token\":\"e29250dc-0b63-4532-916e-37463ba5c343\",\"agreed_to_terms_at\":null}}"

View File

@ -0,0 +1 @@
"[{\"id\":1,\"name\":\"Trench Digging Tool\",\"status\":\"active\"}]"

View File

@ -0,0 +1 @@
"{\"id\":1,\"name\":\"Trench Digging Tool\",\"status\":\"active\"}"

View File

@ -1,44 +1,42 @@
[
{
"request": {
"body": "",
"headers": {
"Content-Type": "application/json",
"User-Agent": "FarmbotOS/3.1.6 (host) host ()",
"Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0OTUwMzIwMTAsImp0aSI6ImUyM2YyNzI0LTAyZWMtNDk2OC1iNjc5LWI4MTQ0YjI3N2JiZiIsImlzcyI6Ii8vMTkyLjE2OC4yOS4xNjU6MzAwMCIsImV4cCI6MTQ5ODQ4ODAxMCwibXF0dCI6IjE5Mi4xNjguMjkuMTY1Iiwib3NfdXBkYXRlX3NlcnZlciI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvZmFybWJvdC9mYXJtYm90X29zL3JlbGVhc2VzL2xhdGVzdCIsImZ3X3VwZGF0ZV9zZXJ2ZXIiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL0Zhcm1ib3QvZmFybWJvdC1hcmR1aW5vLWZpcm13YXJlL3JlbGVhc2VzL2xhdGVzdCIsImJvdCI6ImRldmljZV84In0.UcBgq4pxoXeR6TYv9lYd90LAlGczZMjuvqT1Yc4R8xIk_Jy6bumhq7mI-Hoi9iKBhPU3XMpifXoIyqb1UdC1MBJyHMpPYjZoJLmm4v3XEug_rTu4RcaO7r_r1dZAh2C5TPVXBydcDe02loGC4_YmQPwWixhqJO_6vFF7JEDHir4bihbdfV-P4uZhpUcw-I1Eht4zCMjlmWaL5xcKUdSf-TuSQGNi0Ib0GkZs2wXan2bgv_wBfFEaZ4vmoZO1NM43jaykDssOaxP9hN7FKDdJ4mXL7r9XS7KtXpVQPycUYsfr-lPvid9cfKQFv-STakiDot8uGOYr1CH6I9erQMlhnQ"
},
"method": "get",
"options": {
"follow_redirect": true,
"ssl_options": {
"versions": [
"tlsv1.2"
]
},
"recv_timeout": 25000,
"connect_timeout": 25000
},
"request_body": "",
"url": "http://localhost:3000/api/sequences"
},
"response": {
"body": "[{\"id\":1,\"name\":\"Goto 0, 0, 0\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"location\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"},{\"id\":2,\"name\":\"Every Node\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"tool_id\":1},\"kind\":\"tool\"}}},{\"kind\":\"move_relative\",\"args\":{\"x\":4,\"y\":5,\"z\":6,\"speed\":800}},{\"kind\":\"write_pin\",\"args\":{\"pin_number\":1,\"pin_value\":2,\"pin_mode\":0}},{\"kind\":\"read_pin\",\"args\":{\"pin_number\":4,\"pin_mode\":0,\"label\":\"foo\"}},{\"kind\":\"wait\",\"args\":{\"milliseconds\":4}},{\"kind\":\"send_message\",\"args\":{\"message\":\"Bot is at position {{ x }}, {{ y }}, {{ z }}.\",\"message_type\":\"success\"}},{\"kind\":\"_if\",\"args\":{\"lhs\":\"x\",\"op\":\"is\",\"rhs\":0,\"_then\":{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},\"_else\":{\"kind\":\"nothing\",\"args\":{}}}},{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},{\"kind\":\"execute_script\",\"args\":{\"label\":\"plant-detection\"}},{\"kind\":\"take_photo\",\"args\":{}},{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"x\":1,\"y\":2,\"z\":3},\"kind\":\"coordinate\"}}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"}]",
"headers": {
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Content-Type": "application/json; charset=utf-8",
"ETag": "W/\"043855152d68e55106aec3dea025254c\"",
"Cache-Control": "max-age=0, private, must-revalidate",
"Set-Cookie": "_farmbot_session=SVg4OVA4UWwrV0NSOWNhQXJRTDhJM004MVFVc0JGRVJSRVo5dGhVWjJpSUUyOTYrL280aXhjL0IzQUZxRFZuRENpNjZnQkN0RmJjTFprbWdXS09URFE9PS0tN3R6VVhwRXZLcmNubUVZNUV1NzByZz09--7116cfe97a29a0978dcae7aff681d49918994043; path=/; HttpOnly",
"X-Request-Id": "df2ab2c2-fc8e-4745-9579-1f4767a1696b",
"X-Runtime": "0.012067",
"Vary": "Origin",
"Connection": "close",
"Server": "thin"
},
"status_code": 200,
"type": "ok"
}
}
]
[{
"request": {
"body": "",
"headers": {
"Content-Type": "application/json",
"User-Agent": "FarmbotOS/3.1.6 (host) host ()",
"Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbkBhZG1pbi5jb20iLCJpYXQiOjE0OTUwMzIwMTAsImp0aSI6ImUyM2YyNzI0LTAyZWMtNDk2OC1iNjc5LWI4MTQ0YjI3N2JiZiIsImlzcyI6Ii8vMTkyLjE2OC4yOS4xNjU6MzAwMCIsImV4cCI6MTQ5ODQ4ODAxMCwibXF0dCI6IjE5Mi4xNjguMjkuMTY1Iiwib3NfdXBkYXRlX3NlcnZlciI6Imh0dHBzOi8vYXBpLmdpdGh1Yi5jb20vcmVwb3MvZmFybWJvdC9mYXJtYm90X29zL3JlbGVhc2VzL2xhdGVzdCIsImZ3X3VwZGF0ZV9zZXJ2ZXIiOiJodHRwczovL2FwaS5naXRodWIuY29tL3JlcG9zL0Zhcm1ib3QvZmFybWJvdC1hcmR1aW5vLWZpcm13YXJlL3JlbGVhc2VzL2xhdGVzdCIsImJvdCI6ImRldmljZV84In0.UcBgq4pxoXeR6TYv9lYd90LAlGczZMjuvqT1Yc4R8xIk_Jy6bumhq7mI-Hoi9iKBhPU3XMpifXoIyqb1UdC1MBJyHMpPYjZoJLmm4v3XEug_rTu4RcaO7r_r1dZAh2C5TPVXBydcDe02loGC4_YmQPwWixhqJO_6vFF7JEDHir4bihbdfV-P4uZhpUcw-I1Eht4zCMjlmWaL5xcKUdSf-TuSQGNi0Ib0GkZs2wXan2bgv_wBfFEaZ4vmoZO1NM43jaykDssOaxP9hN7FKDdJ4mXL7r9XS7KtXpVQPycUYsfr-lPvid9cfKQFv-STakiDot8uGOYr1CH6I9erQMlhnQ"
},
"method": "get",
"options": {
"follow_redirect": true,
"ssl_options": {
"versions": [
"tlsv1.2"
]
},
"recv_timeout": 25000,
"connect_timeout": 25000
},
"request_body": "",
"url": "http://localhost:3000/api/sequences"
},
"response": {
"body": "[{\"id\":1,\"name\":\"Goto 0, 0, 0\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"location\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"},{\"id\":2,\"name\":\"Every Node\",\"color\":\"gray\",\"body\":[{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"tool_id\":1},\"kind\":\"tool\"}}},{\"kind\":\"move_relative\",\"args\":{\"x\":4,\"y\":5,\"z\":6,\"speed\":800}},{\"kind\":\"write_pin\",\"args\":{\"pin_number\":1,\"pin_value\":2,\"pin_mode\":0}},{\"kind\":\"read_pin\",\"args\":{\"pin_number\":4,\"pin_mode\":0,\"label\":\"foo\"}},{\"kind\":\"wait\",\"args\":{\"milliseconds\":4}},{\"kind\":\"send_message\",\"args\":{\"message\":\"Bot is at position {{ x }}, {{ y }}, {{ z }}.\",\"message_type\":\"success\"}},{\"kind\":\"_if\",\"args\":{\"lhs\":\"x\",\"op\":\"is\",\"rhs\":0,\"_then\":{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},\"_else\":{\"kind\":\"nothing\",\"args\":{}}}},{\"kind\":\"execute\",\"args\":{\"sequence_id\":1}},{\"kind\":\"execute_script\",\"args\":{\"label\":\"plant-detection\"}},{\"kind\":\"take_photo\",\"args\":{}},{\"kind\":\"move_absolute\",\"args\":{\"offset\":{\"kind\":\"coordinate\",\"args\":{\"x\":0,\"y\":0,\"z\":0}},\"speed\":800,\"location\":{\"args\":{\"x\":1,\"y\":2,\"z\":3},\"kind\":\"coordinate\"}}}],\"args\":{\"is_outdated\":false,\"version\":4},\"kind\":\"sequence\"}]",
"headers": {
"X-Frame-Options": "SAMEORIGIN",
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"Content-Type": "application/json; charset=utf-8",
"ETag": "W/\"043855152d68e55106aec3dea025254c\"",
"Cache-Control": "max-age=0, private, must-revalidate",
"Set-Cookie": "_farmbot_session=SVg4OVA4UWwrV0NSOWNhQXJRTDhJM004MVFVc0JGRVJSRVo5dGhVWjJpSUUyOTYrL280aXhjL0IzQUZxRFZuRENpNjZnQkN0RmJjTFprbWdXS09URFE9PS0tN3R6VVhwRXZLcmNubUVZNUV1NzByZz09--7116cfe97a29a0978dcae7aff681d49918994043; path=/; HttpOnly",
"X-Request-Id": "df2ab2c2-fc8e-4745-9579-1f4767a1696b",
"X-Runtime": "0.012067",
"Vary": "Origin",
"Connection": "close",
"Server": "thin"
},
"status_code": 200,
"type": "ok"
}
}]

65
idea.md
View File

@ -1,65 +0,0 @@
# Nerves Runtime Bootstrapper
Application gets split into (up too)? three parts.
## Boot Up
Boot up could go something like:
```
Power ON
├── Hardware Bootloader (uboot, etc)
├── Linux Kernel
├── Erlinit
└── Nerves Bootloader
└──Nerves Bootstrapper
├── Hardware init (driver loading)
├── Filesystem init (see block diagram)
├── Network Init
└── HTTP Init (Maybe this could be generalized as a `Transport` as to not be locked into http?)
└── Download an archive of some sort containing our actual application code (this is the hard part)
├── Extract/Verify (with black magic?) package
└── Somehow shim into (and supervise) this package
```
## Partitions
So a partition map would look a little different
```
______________________________________________________________________________
| A | B | C | D |
| Bootloader | Nerves Bootstrapper | Application Code | Application Data |
| (per platform) | (Read only) | (Read Only) | (Read/Write) |
|_________________|_____________________|__________________|__________________|
```
* A - Bootloader
* Will be different per platform.
* Black magic
* B - Nerves Bootstrapper
* Will contain `linux` rootfs with `erlinit` and friends.
* Should be read only, and _probably_ not need a mirror/backup partition. (Hopefully)
* Should contain `Nerves.Bootloader`
* C - Application Code
* One (or maybe many???) Erlang releases containing beam files and friends.
* Read only at runtime. `BootStrapper` should be the only thing with access to overwriting.
* Should be persistent, but if mangled somehow, BootStrapper can fix it/Request new Firmware
* Hot code reloading
* Probably have backup/mirror partition.
* D - Application Data
* General Data partition
## Implementation
### Compile
* Application require the `NervesBootstrapper` dep, giving some configuration.
* `NervesBootstrapper` Provides a plugin for Distillery or something that:
* Constructs the (cross compiled) release, and `Bootstrapper` firmware.
* Bootstrapper will be a `.fw` file.
* Application code will be a archive of some sort of just your release.
`mix firmware` Could still compile a fw file with your application code baked in thanks to `fwup`
### Running
`mix firmware.burn` could Still burn an entire firmware with your application code baked in.
## Problems
Biggest problem i can see is shimming into a second Erlang release. Maybe we can have two
instances of `erlinit`?
Semantics of bringing up hardware will need to be hashed out.

View File

@ -21,15 +21,20 @@ defmodule Farmbot do
def init(_args) do
context = Farmbot.Context.new()
# ctx_tracker = %Farmbot.Context.Tracker{pid: Farmbot.Context.Tracker}
children = [
worker(Farmbot.DebugLog, [], restart: :permanent),
supervisor(Registry, [:duplicate, Farmbot.Registry]),
supervisor(FBSYS,
[context, [name: FBSYS ]], restart: :permanent),
worker(Farmbot.Auth,
[context, [name: Farmbot.Auth ]], restart: :permanent),
worker(Farmbot.HTTP,
[context, [name: Farmbot.HTTP ]], restart: :permanent),
worker(Farmbot.Database,
[context, [name: Farmbot.Database ]], restart: :permanent),
@ -42,16 +47,16 @@ defmodule Farmbot do
supervisor(Farmbot.Transport.Supervisor,
[context, [name: Farmbot.Transport.Supervisor ]], restart: :permanent),
supervisor(Farmware.Supervisor,
[context, [name: Farmware.Supervisor ]], restart: :permanent),
worker(Farmbot.ImageWatcher,
[context, [name: Farmbot.ImageWatcher ]], restart: :permanent),
worker(Task, [Farmbot.Serial.Handler.OpenTTY, :open_ttys, [__MODULE__]],
restart: :transient),
supervisor(Farmbot.Serial.Supervisor,
[context, [name: Farmbot.Serial.Supervisor ]], restart: :permanent),
supervisor(Farmbot.Configurator, [], restart: :permanent),
supervisor(Farmbot.Farmware.Supervisor, [context,
[name: Farmbot.Farmware.Supervisor ]], restart: :permanent)
]
opts = [strategy: :one_for_one]
supervise(children, opts)

View File

@ -3,19 +3,16 @@ defmodule Farmbot.Auth do
Gets a token and device information
"""
@ssl_hack [
ssl: [{:versions, [:'tlsv1.2']}],
follow_redirect: true
]
@timeout_time (2 * 3_600_000)
use GenServer
require Logger
alias Farmbot.System.FS
alias FS.ConfigStorage, as: CS
alias Farmbot.Token
alias Farmbot.Auth.Subscription, as: Sub
alias Farmbot.Context
alias Farmbot.{Token, Context, DebugLog, System, HTTP}
alias System.FS
alias FS.ConfigStorage, as: CS
alias Farmbot.Auth.Subscription, as: Sub
use GenServer
use DebugLog
use Context, requires: [HTTP]
@typedoc """
The public key that lives at http://<server>/api/public_key
@ -23,7 +20,7 @@ defmodule Farmbot.Auth do
@type public_key :: binary
@typedoc false
@type auth :: pid
@type auth :: pid | atom
@typedoc """
Encrypted secret
@ -53,12 +50,12 @@ defmodule Farmbot.Auth do
@doc """
Gets the public key from the API
"""
@spec get_public_key(server) :: {:ok, public_key} | {:error, term}
def get_public_key(server) do
case HTTPoison.get("#{server}/api/public_key", [], @ssl_hack) do
{:ok, %HTTPoison.Response{body: body, status_code: 200}} ->
@spec get_public_key(Context.t, server) :: {:ok, public_key} | {:error, term}
def get_public_key(%Context{} = ctx, server) do
case HTTP.get(ctx, "#{server}/api/public_key") do
{:ok, %HTTP.Response{body: body, status_code: 200}} ->
decode_key(body)
{:error, %HTTPoison.Error{reason: reason}} ->
{:error, reason} ->
{:error, reason}
end
end
@ -92,11 +89,11 @@ defmodule Farmbot.Auth do
@doc """
Get a token from the server with given token
"""
@spec get_token_from_server(secret, server, boolean)
@spec get_token_from_server(Context.t, secret, server, boolean)
:: {:ok, Token.t} | {:error, term}
def get_token_from_server(secret, server, should_broadcast?)
def get_token_from_server(context, secret, server, should_broadcast?)
def get_token_from_server(nil, _server, sbc) do
def get_token_from_server(_context, nil, _server, sbc) do
thing = {:error, :no_secret}
if sbc do
broadcast(thing)
@ -105,7 +102,7 @@ defmodule Farmbot.Auth do
end
# This one shouldn't happen anymore I think.
def get_token_from_server(_secret, nil, sbc) do
def get_token_from_server(_context, _secret, nil, sbc) do
thing = {:error, :no_server}
if sbc do
broadcast(thing)
@ -113,43 +110,55 @@ defmodule Farmbot.Auth do
thing
end
def get_token_from_server(secret, server, sbc) do
def get_token_from_server(%Context{} = ctx, secret, server, sbc) do
# I am not sure why this is done this way other than it works.
user = %{credentials: secret |> :base64.encode_to_string |> to_string}
user = %{credentials: secret |> :base64.encode_to_string |> to_string}
payload = Poison.encode!(%{user: user})
req = HTTPoison.post("#{server}/api/tokens",
payload, ["Content-Type": "application/json"], @ssl_hack)
req = HTTP.post(ctx, "#{server}/api/tokens", payload, [], [])
case req do
# bad Password
{:ok, %HTTPoison.Response{status_code: 422}} ->
{:ok, %HTTP.Response{status_code: 422}} ->
thing = {:error, :bad_password}
maybe_broadcast(sbc, thing)
thing
# Token invalid. Need to try to get a new token here.
{:ok, %HTTPoison.Response{status_code: 401}} ->
{:ok, %HTTP.Response{status_code: 401}} ->
thing = {:error, :expired_token}
maybe_broadcast(sbc, thing)
thing
# We won
{:ok, %HTTPoison.Response{body: body, status_code: 200}} ->
{:ok, %HTTP.Response{body: body, status_code: 200}} ->
maybe_retry(ctx, secret, server, sbc, body)
Logger.info ">> got a token!", type: :success
save_secret(secret)
remove_last_factory_reset_reason()
{:ok, token} = body |> Poison.decode! |> Map.get("token") |> Token.create
{:ok, token} = body
|> Poison.decode!
|> Map.get("token")
|> Token.create
token = %{token | unencoded: %{token.unencoded | iss: server}}
maybe_broadcast(sbc, {:new_token, token})
{:ok, token}
# HTTP errors
{:error, %HTTPoison.Error{reason: reason}} ->
thing = {:error, reason}
{:error, _reason} = thing ->
maybe_broadcast(sbc, thing)
thing
end
end
defp maybe_retry(%Context{} = ctx, sec, server, sbc, body) do
case Poison.decode(body) do
{:ok, _} -> :ok
{:error, _} -> get_token_from_server(%Context{} = ctx, sec, server, sbc)
end
end
@spec maybe_broadcast(boolean, any) :: no_return
defp maybe_broadcast(bool, thing) do
if bool do
@ -213,18 +222,15 @@ defmodule Farmbot.Auth do
def try_log_in!(auth, retry, error_str) do
Logger.info ">> is logging in..."
# disable broadcasting
:ok = GenServer.call(auth, {:set_broadcast, false})
# Try to get a token.
case try_log_in(auth) do
{:ok, %Token{} = token} = success ->
:ok = GenServer.call(auth, {:set_broadcast, true})
Logger.info ">> Is logged in", type: :success
broadcast({:new_token, token})
success
er -> # no need to print message becasetry_log_indoes it for us.
try do
{:ok, %Token{} = token} = success = try_log_in(auth)
:ok = GenServer.call(auth, {:set_broadcast, true})
Logger.info ">> Is logged in", type: :success
broadcast({:new_token, token})
success
rescue
er ->
# sleep for a second, then try again untill we are out of retry
Process.sleep(1000)
try_log_in!(retry + 1, "Try #{retry}: #{inspect er}\n" <> error_str)
@ -236,23 +242,25 @@ defmodule Farmbot.Auth do
"""
@spec interim(auth, email, password, server) :: :ok
def interim(auth, email, pass, server) do
GenServer.call(auth, {:interim, {email,pass,server}})
GenServer.call(auth, {:interim, {email, pass, server}})
end
@doc """
Starts the Auth GenServer
"""
def start_link(context, opts), do: GenServer.start_link(__MODULE__, context, opts)
def start_link(context, opts),
do: GenServer.start_link(__MODULE__, context, opts)
@typedoc """
State for this GenServer
"""
@type state :: %{
server: nil | server,
secret: nil | secret,
timer: any,
interim: nil | interim,
token: nil | Token.t,
context: Context.t,
server: nil | server,
secret: nil | secret,
timer: reference,
interim: nil | interim,
token: nil | Token.t,
broadcast: boolean
}
@ -260,16 +268,18 @@ defmodule Farmbot.Auth do
def init(context) do
Logger.info(">> Authorization init!")
timer = s_a(self())
{:ok, sub} = Sub.start_link(%Context{context | auth: self()}, [])
timer = s_a(self())
context = %Context{context | auth: self()}
{:ok, sub} = Sub.start_link(context, [])
{:ok, server} = load_server()
state = %{
server: server,
sub: sub,
secret: load_secret(),
interim: nil,
token: nil,
timer: timer,
context: context,
server: server,
sub: sub,
secret: load_secret(),
interim: nil,
token: nil,
timer: timer,
broadcast: true
}
{:ok, state}
@ -303,29 +313,31 @@ defmodule Farmbot.Auth do
end
# Match on the token first.
def handle_call(:try_log_in, _, %{token: %Token{}= _old, server: server, secret: secret} = state) do
def handle_call(:try_log_in, _,
%{token: %Token{}= _old, server: server, secret: secret} = s)
do
Logger.info ">> already has a token. Fetching another.", type: :busy
secret = secret || load_secret()
case get_token_from_server(secret, server, state.broadcast) do
case get_token_from_server(s.context, secret, server, s.broadcast) do
{:ok, %Token{} = token} ->
{:reply, {:ok, token}, %{state | token: token}}
{:reply, {:ok, token}, %{s | token: token}}
e ->
{:reply, e, clear_state(state)}
{:reply, e, clear_state(s)}
end
end
# Next choice will be interim
def handle_call(:try_log_in, _, %{interim: {email, pass, server}} = state) do
def handle_call(:try_log_in, _, %{interim: {email, pass, ser}} = state) do
Logger.info ">> is trying to log in with credentials.", type: :busy
{:ok, pub_key} = get_public_key(server)
{:ok, pub_key} = get_public_key(state.context, ser)
{:ok, secret } = encrypt(email, pass, pub_key)
case get_token_from_server(secret, server, state.broadcast) do
case get_token_from_server(state.context, secret, ser, state.broadcast) do
{:ok, %Token{} = token} ->
next_state = %{state |
interim: nil,
token: token,
secret: secret,
server: server
server: ser
}
{:reply, {:ok, token}, next_state}
e -> {:reply, e, clear_state(state)}
@ -333,25 +345,26 @@ defmodule Farmbot.Auth do
end
def handle_call(:try_log_in, _,
%{secret: secret, server: server} = state) when is_binary(secret) do
%{secret: secret, server: server} = s) when is_binary(secret)
do
Logger.info ">> is trying to log in with a secret.", type: :busy
case get_token_from_server(secret, server, state.broadcast) do
case get_token_from_server(s.context, secret, server, s.broadcast) do
{:ok, %Token{} = t} ->
{:reply, {:ok, t}, %{state | token: t}}
e -> {:reply, e, clear_state(state)}
{:reply, {:ok, t}, %{s | token: t}}
e -> {:reply, e, clear_state(s)}
end
end
def handle_call(:try_log_in, _, %{secret: nil, server: server} = state) do
def handle_call(:try_log_in, _, %{secret: nil, server: server} = s) do
Logger.info ">> is trying to load old secret.", type: :busy
# Try to load the secret file
secret = load_secret()
case get_token_from_server(secret, server, state.broadcast) do
case get_token_from_server(s.context, secret, server, s.broadcast) do
{:ok, %Token{} = token} ->
{:reply, {:ok, token}, token}
{:reply, {:ok, token}, %{s | token: token}}
e ->
{:reply, e, state}
{:reply, e, s}
end
end

View File

@ -20,7 +20,7 @@ defmodule Farmbot.BotState do
@doc """
Sets the position to givin position.
"""
@spec set_pos(context, integer,integer,integer) :: :ok
@spec set_pos(context, integer, integer, integer) :: :ok
def set_pos(%Context{} = context, x, y, z)
when is_integer(x) and is_integer(y) and is_integer(z) do
GenServer.call(context.hardware, {:set_pos, {x, y, z}})
@ -58,8 +58,8 @@ defmodule Farmbot.BotState do
Sets the current end stops
"""
@spec set_end_stops(context, Farmbot.BotState.Hardware.State.end_stops) :: :ok
def set_end_stops(%Context{} = context, {xa,xb,ya,yb,za,zc}) do
GenServer.cast(context.hardware, {:set_end_stops, {xa,xb,ya,yb,za,zc}})
def set_end_stops(%Context{} = context, {xa, xb, ya, yb, za, zc}) do
GenServer.cast(context.hardware, {:set_end_stops, {xa, xb, ya, yb, za, zc}})
end
@doc """
@ -144,6 +144,12 @@ defmodule Farmbot.BotState do
GenServer.call(context.configuration, {:update_config, "user_env", map})
end
@doc """
Gets the user environment.
"""
@spec get_user_env(context) :: map
def get_user_env(%Context{} = ctx), do: get_config(ctx, :user_env)
@doc """
Locks the bot
"""
@ -193,7 +199,10 @@ defmodule Farmbot.BotState do
def set_sync_msg(%Context{} = ctx, :synced = thing),
do: do_set_sync_msg(ctx, thing)
def set_sync_msg(%Context{} = ctx, :maintenance = thing),
do: do_set_sync_msg(ctx, thing)
defp do_set_sync_msg(%Context{} = context, thing) do
GenServer.cast(context.configuration, {:update_info, :sync_status, thing})
GenServer.cast(context.configuration, {:update_sync_message, thing})
end
end

View File

@ -1,6 +1,6 @@
alias Farmbot.BotState.Hardware.State, as: Hardware
alias Farmbot.BotState.Configuration.State, as: Configuration
alias Farmbot.BotState.ProcessTracker, as: PT
alias Farmbot.Farmware.Manager.State, as: FarmwareManagerState
defmodule Farmbot.BotState.Monitor do
@moduledoc """
this is the master state tracker. It receives the states from
@ -16,13 +16,15 @@ defmodule Farmbot.BotState.Monitor do
context: Context.t,
hardware: Hardware.t,
configuration: Configuration.t,
process_info: PT.t
process_info: %{
farmwares: %{name: binary, uuid: binary, version: binary}
}
}
defstruct [
context: nil,
hardware: %Hardware{},
configuration: %Configuration{},
process_info: %PT.State{}
process_info: %{farmwares: %{}}
]
end
@ -47,8 +49,9 @@ defmodule Farmbot.BotState.Monitor do
dispatch(new_state)
end
def handle_cast(%PT.State{} = new_things, %State{} = old_state) do
new_state = %State{old_state | process_info: new_things}
def handle_cast(%FarmwareManagerState{farmwares: fws}, %State{} = old_state) do
new_process_info = %{old_state.process_info | farmwares: fws}
new_state = %{old_state | process_info: new_process_info}
dispatch(new_state)
end
@ -57,10 +60,4 @@ defmodule Farmbot.BotState.Monitor do
GenStage.async_notify(new_state.context.monitor, new_state)
{:noreply, [], new_state}
end
#
# @spec dispatch(any, State.t) :: {:reply, any, [], State.t }
# defp dispatch(reply, new_state) do
# GenStage.async_notify(new_state.context.monitor, new_state)
# {:reply, reply, [], new_state}
# end
end

View File

@ -1,15 +0,0 @@
defmodule Farmbot.ProcessRunner do
@moduledoc """
Behavior for FarmProcess Runners (events, and farmware)
"""
alias Farmbot.Context
@typedoc """
The body of this process
"""
@type stuff :: Farmbot.BotState.ProcessTracker.Info.stuff
@callback start_process(Context.t, stuff) :: any
@callback stop_process(Context.t, stuff) :: any
end

View File

@ -1,26 +0,0 @@
defmodule Farmbot.BotState.ProcessSupervisor do
@moduledoc """
Supervises various things
"""
use Supervisor
require Logger
alias Farmbot.Context
@doc """
Starts the Farm Procss Supervisor
"""
def start_link(%Context{} = ctx, opts),
do: Supervisor.start_link(__MODULE__, ctx, opts)
def init(ctx) do
Logger.info ">> Starting FarmProcess Supervisor"
children = [
worker(Farmbot.BotState.ProcessTracker,
[ctx, [name: Farmbot.BotState.ProcessTracker]],
[restart: :permanent])
]
opts = [strategy: :one_for_one]
supervise(children, opts)
end
end

View File

@ -1,208 +0,0 @@
defmodule Farmbot.BotState.ProcessTracker do
@moduledoc """
Module responsible for `process_info` in the BotState tree.
These will be the "user accessable" processes to control.
FarmbotJS will see the uuids here. If they are registered, that does not
mean they are running.
"""
use GenServer
require Logger
alias Nerves.Lib.UUID
alias Farmbot.RegimenRunner
alias Farmbot.Context
defmodule Info do
@moduledoc false
defstruct [:name, :uuid, :status, :stuff]
@typedoc """
Status of this process
"""
@type status :: atom
@type stuff :: any
@type kind :: :regimen | :farmware
@type t ::
%__MODULE__{name: String.t, uuid: binary, status: status, stuff: stuff}
end
defmodule State do
@moduledoc false
defstruct [regimens: [], farmwares: [], context: nil]
@type uuid :: binary
@type kind :: :farmware | :regimen
@type t ::
%__MODULE__{
regimens: [Info.t],
farmwares: [Info.t],
context: Context.t
}
end
def init(%Context{} = ctx), do: {:ok, %State{context: ctx}}
@doc """
Starts the Process Tracker
"""
def start_link(%Context{} = context, opts),
do: GenServer.start_link(__MODULE__, context, opts)
@doc """
Registers a kind, name with a database entry to be tracked
"""
@spec register(Context.t, State.kind, String.t, map) :: no_return
def register(%Context{} = context, kind, name, stuff) do
GenServer.cast(context.process_tracker, {:register, kind, name, stuff})
end
@doc """
DeRegisters a pid.
"""
@spec deregister(Context.t, State.uuid) :: no_return
def deregister(%Context{} = context, uuid),
do: GenServer.cast(context.process_tracker, {:deregister, uuid})
@doc """
starts a process by its uuid or info struct
"""
@spec start_process(Context.t, State.uuid | Info.t)
:: {:ok, pid} | {:error, term}
def start_process(%Context{} = ctx, %Info{uuid: uuid}),
do: start_process(ctx, uuid)
def start_process(%Context{} = ctx, uuid),
do: GenServer.call(ctx.process_tracker, {:start_process, uuid})
@doc """
Stops a process by it's uuid.
"""
@spec stop_process(Context.t, State.uuid) :: :ok | {:error, term}
def stop_process(%Context{} = ctx, uuid),
do: GenServer.call(ctx.process_tracker, {:stop_process, uuid})
@doc """
Lookup a uuid by its kind and name
"""
@spec lookup(Context.t, State.kind, String.t) :: Info.t
def lookup(%Context{} = ctx, kind, name) do
GenServer.call(ctx.process_tracker, {:lookup, kind, name})
end
# GenServer stuffs
def handle_call({:lookup, kind, name}, _, state) do
key = kind_to_key(kind)
list = Map.get(state, key)
f = Enum.find(list, fn(info) -> info.name == name end)
dispatch(f, state)
end
def handle_call({:start_process, uuid}, _, state) do
thing = nest_the_loops(uuid, state)
if thing do
{key, info} = thing
Logger.info ">> is starting a #{key} #{info.name}"
mod = key_to_module(key)
r = mod.start_process(info.stuff)
# TODO(Connor) update status here
dispatch(r, state)
else
Logger.info ">> could not find #{uuid} to start!"
dispatch({:error, :no_uuid}, state)
end
end
def handle_call({:stop_process, uuid}, _, state) do
thing = nest_the_loops(uuid, state)
if thing do
{key, info} = thing
Logger.info ">> is stoping a #{key} #{info.name}"
r = key_to_module(key).stop_process(info.stuff)
# update status here
dispatch(r, state)
else
Logger.info ">> could not find #{uuid} to stop!"
dispatch({:error, :no_uuid}, state)
end
end
def handle_call(:state, _, state), do: dispatch(state, state)
def handle_call(_call, _, state), do: dispatch(:no, state)
def handle_cast({:register, kind, name, stuff}, state) do
Logger.info ">> is registering a #{kind} as #{name}"
uuid = UUID.generate
key = kind_to_key(kind)
new_list = [
%Info{name: name,
uuid: uuid,
status: :not_running,
stuff: stuff} | Map.get(state, key)]
new_state = %{state | key => new_list}
dispatch(new_state)
end
def handle_cast({:deregister, uuid}, state) do
thing = nest_the_loops(uuid, state)
if thing do
{kind, info} = thing
Logger.info ">> is deregistering #{uuid} #{kind} #{info.name}"
list = Map.get(state, kind)
new_list = List.delete(list, info)
dispatch(%{state | kind => new_list})
else
Logger.info ">> could not find #{uuid}"
dispatch(state)
end
end
def handle_cast(_cast, _, state), do: dispatch(state)
def handle_info(_info, _, state), do: dispatch(state)
def terminate(_reason, _state), do: :ok
@spec nest_the_loops(State.uuid, State.t) :: {State.kind, Info.t} | nil
defp nest_the_loops(uuid, state) do
# I have to enumerate over all the processes "kind"s here...
# this is the most javascript elixir i have ever wrote.
# loop over all the keys
Enum.find_value(Map.from_struct(state), fn({key, value}) ->
# loop over the values of those keys/kinds
Enum.find_value(value, fn(info) ->
do_find(uuid, info, key)
end)
end)
end
defp do_find(uuid, info, key) do
if uuid == info.uuid do
# return the kind and the info
{key, info}
else
false
end
end
@spec dispatch(State.t) :: {:noreply, State.t}
defp dispatch(state) do
cast(state)
{:noreply, state}
end
@spec dispatch(term, State.t) :: {:reply, term, State.t}
defp dispatch(reply, state) do
cast(state)
{:reply, reply, state}
end
@spec cast(State.t) :: no_return
defp cast(state), do: GenServer.cast(state.context.monitor, state)
@spec kind_to_key(any) :: :regimens | :farmwares | no_return
defp kind_to_key(:regimen), do: :regimens
defp kind_to_key(:farmware), do: :farmwares
@spec key_to_module(any) :: Farmware | RegimenRunner
defp key_to_module(:regimens), do: RegimenRunner
defp key_to_module(:farmwares), do: Farmware
end

View File

@ -1,4 +1,4 @@
defmodule Farmbot.StateTracker do
defmodule Farmbot.BotState.StateTracker do
@moduledoc """
Common functionality for modules that need to track
simple states that can be easily represented as a struct with key value

View File

@ -5,6 +5,7 @@ defmodule Farmbot.BotState.Supervisor do
"""
alias Farmbot.Context
use Farmbot.DebugLog, name: BotStateSupervisor
@use_logger Application.get_env(:farmbot, :logger, true)
@ -14,23 +15,16 @@ defmodule Farmbot.BotState.Supervisor do
def init(ctx) do
children = [
worker(Farmbot.BotState.Monitor,
[ctx, [name: Farmbot.BotState.Monitor]],
[restart: :permanent]),
[ctx, [name: Farmbot.BotState.Monitor]]),
worker(Farmbot.BotState.Configuration,
[ctx, [name: Farmbot.BotState.Configuration]],
[restart: :permanent]),
[ctx, [name: Farmbot.BotState.Configuration]]),
worker(Farmbot.BotState.Hardware,
[ctx, [name: Farmbot.BotState.Hardware]],
[restart: :permanent]),
worker(Farmbot.BotState.ProcessSupervisor,
[ctx, [name: Farmbot.BotState.ProcessSupervisor]],
[restart: :permanent]),
[ctx, [name: Farmbot.BotState.Hardware]]),
worker(EasterEggs,
[name: EasterEggs], [restart: :permanent])
[name: EasterEggs])
]
opts = [strategy: :one_for_one]
@ -43,7 +37,16 @@ defmodule Farmbot.BotState.Supervisor do
# like position and some configuraion.
sup = Supervisor.start_link(__MODULE__, ctx, opts)
EasterEggs.start_cron_job
if @use_logger, do: Logger.add_backend(Logger.Backends.FarmbotLogger)
# TODO change this stuff to tasks
if @use_logger do
debug_log "Using Farmbot Logger"
Logger.flush()
backend = Logger.Backends.FarmbotLogger
{:ok, _pid} = Logger.add_backend(backend)
:ok = GenEvent.call(Logger, backend, {:context, ctx})
else
debug_log "Not using Farmbot Logger"
end
sup
end
end

View File

@ -5,7 +5,7 @@ defmodule Farmbot.BotState.Configuration do
use GenServer
require Logger
alias Farmbot.StateTracker
alias Farmbot.BotState.StateTracker
@behaviour StateTracker
use StateTracker,
@ -48,7 +48,14 @@ defmodule Farmbot.BotState.Configuration do
distance_mm_z: integer,
sync_status: sync_msg
},
informational_settings: map # TODO type this
informational_settings: %{
locked: boolean,
controller_version: binary,
target: binary,
commit: binary,
sync_status: sync_msg,
firmware_version: binary
}
}
@version Mix.Project.config()[:version]
@ -191,13 +198,25 @@ defmodule Farmbot.BotState.Configuration do
end
def handle_cast({:update_info, key, value}, %State{} = state) do
new_info = Map.put(state.informational_settings, key, value)
new_state = %State{state | informational_settings: new_info}
dispatch new_state
dispatch do_update_info(state, key, value)
end
def handle_cast({:update_sync_message, thing}, %State{} = state) do
if state.informational_settings.locked do
dispatch state
else
dispatch do_update_info(state, :sync_status, thing)
end
end
def handle_cast(event, %State{} = state) do
Logger.error ">> got an unhandled cast in Configuration: #{inspect event}"
dispatch state
end
@spec do_update_info(State.t, binary | atom, term) :: State.t
defp do_update_info(%State{} = state, key, value) do
new_info = Map.put(state.informational_settings, key, value)
%State{state | informational_settings: new_info}
end
end

View File

@ -4,30 +4,31 @@ defmodule Farmbot.BotState.Hardware do
"""
require Logger
alias Farmbot.StateTracker
alias Farmbot.CeleryScript.{Ast, Command}
alias Farmbot.BotState.StateTracker
alias Farmbot.CeleryScript.Command
@behaviour StateTracker
use StateTracker,
name: __MODULE__,
model: [
location: [-1,-1,-1],
end_stops: {-1,-1,-1,-1,-1,-1},
# credo:disable-for-next-line
location: [ -1, -1 ,-1 ],
end_stops: { -1, -1, -1, -1, -1, -1 },
mcu_params: %{},
pins: %{},
pins: %{},
]
@type t :: %__MODULE__.State{
location: location,
end_stops: end_stops,
location: location,
end_stops: end_stops,
mcu_params: mcu_params,
pins: pins,
pins: pins,
}
@type location :: [number, ...]
@type location :: [number, ...]
@type mcu_params :: map
@type pins :: map
@type end_stops :: {integer,integer,integer,integer,integer,integer}
@type pins :: map
@type end_stops :: {integer, integer, integer, integer, integer, integer}
# Callback that happens when this module comes up
def load do
@ -40,7 +41,7 @@ defmodule Farmbot.BotState.Hardware do
@doc """
Takes a Hardware State object, and makes it happen
"""
@spec set_initial_params(State.t, Ast.context)
@spec set_initial_params(State.t, Context.t)
:: {:ok, :no_params} | :ok | {:error, term}
def set_initial_params(%State{} = state, %Farmbot.Context{} = context) do
# BUG(Connor): The first param is rather unstable for some reason.
@ -87,7 +88,7 @@ defmodule Farmbot.BotState.Hardware do
end
def handle_call({:set_pos, {x, y, z}}, _from, %State{} = state) do
dispatch [x, y, z], %State{state | location: [x,y,z]}
dispatch [x, y, z], %State{state | location: [x, y, z]}
end
def handle_call(event, _from, %State{} = state) do
@ -105,12 +106,10 @@ defmodule Farmbot.BotState.Hardware do
pin_state = state.pins
new_pin_value =
case Map.get(pin_state, Integer.to_string(pin)) do
nil ->
%{mode: -1, value: value}
%{mode: mode, value: _} ->
%{mode: mode, value: value}
nil -> %{mode: -1, value: value}
%{mode: mode, value: _} -> %{mode: mode, value: value}
end
Logger.info ">> set pin: #{pin}: #{new_pin_value.value}"
Logger.info ">> Pin #{pin} is #{new_pin_value.value}"
new_pin_state = Map.put(pin_state, Integer.to_string(pin), new_pin_value)
dispatch %State{state | pins: new_pin_state}
end
@ -140,8 +139,8 @@ defmodule Farmbot.BotState.Hardware do
end
end
def handle_cast({:set_end_stops, {xa,xb,ya,yb,za,zc}}, %State{} = state) do
dispatch %State{state | end_stops: {xa,xb,ya,yb,za,zc}}
def handle_cast({:set_end_stops, {xa, xb, ya, yb, za, zc}}, state) do
dispatch %State{state | end_stops: {xa, xb, ya, yb, za, zc}}
end
# catch all.

View File

@ -5,6 +5,7 @@ defmodule Farmbot.CeleryScript.Ast do
"""
alias Farmbot.Context
alias Farmbot.CeleryScript.Error
defimpl Inspect, for: __MODULE__ do
def inspect(thing, _) do
@ -24,9 +25,9 @@ defmodule Farmbot.CeleryScript.Ast do
Type for CeleryScript Ast's.
"""
@type t :: %__MODULE__{
args: args,
body: [t,...],
kind: String.t,
args: args,
body: [t, ...],
kind: String.t,
comment: String.t | nil
}
@ -37,10 +38,9 @@ defmodule Farmbot.CeleryScript.Ast do
Parses json and traverses the tree and turns everything can
possibly be parsed.
"""
@spec parse({:ok, map}) :: t
@spec parse({:ok, map} | map | [map, ...]) :: t
def parse(map_or_json_map)
@spec parse(map) :: t
def parse(%{"kind" => kind, "args" => args} = thing) do
body = thing["body"] || []
comment = thing["comment"]
@ -64,14 +64,13 @@ defmodule Farmbot.CeleryScript.Ast do
end
# You can give a list of nodes.
@spec parse([map,...]) :: [t,...]
def parse(body) when is_list(body) do
Enum.reduce(body, [], fn(blah, acc) ->
acc ++ [parse(blah)]
end)
end
def parse(_), do: %__MODULE__{kind: "nothing", args: %{}, body: []}
def parse(other_thing), do: raise Error, message: "#{inspect other_thing} could not be parsed as CeleryScript."
# TODO: This is a pretty heavy memory leak, what should happen is
# The corpus should create a bunch of atom, and then this should be

View File

@ -6,8 +6,9 @@ defmodule Farmbot.CeleryScript.Command do
this means minimal logging, minimal bot state changeing (if its not the
result of a gcode) etc.
"""
alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.{Ast, Error}
alias Farmbot.Database.Selectors
alias Farmbot.Context
require Logger
use Farmbot.DebugLog
@ -38,33 +39,38 @@ defmodule Farmbot.CeleryScript.Command do
@doc ~s"""
Convert an ast node to a coodinate or return :error.
"""
@spec ast_to_coord(Ast.context, Ast.t) :: Ast.context
@spec ast_to_coord(Context.t, Ast.t) :: Context.t
def ast_to_coord(context, ast)
def ast_to_coord(
%Farmbot.Context{} = context,
%Context{} = context,
%Ast{kind: "coordinate",
args: %{x: _x, y: _y, z: _z},
body: []} = already_done),
do: Farmbot.Context.push_data(context, already_done)
do: Context.push_data(context, already_done)
def ast_to_coord(
%Farmbot.Context{} = context,
%Context{} = context,
%Ast{kind: "tool", args: %{tool_id: tool_id}, body: []})
do
%{body: ts} = Farmbot.Database.Syncable.Point.get_tool(context, tool_id)
next_context = coordinate(%{x: ts.x, y: ts.y, z: ts.z}, [], context)
point_map = %{
x: ts.x,
y: ts.y,
z: ts.z
}
next_context = coordinate(point_map, [], context)
raise_if_not_context_or_return_context("coordinate", next_context)
end
# is this one a good idea?
# there might be two expectations here: it could return the current position,
# or 0
def ast_to_coord(%Farmbot.Context{} = context, %Ast{kind: "nothing", args: _, body: _}) do
def ast_to_coord(%Context{} = context, %Ast{kind: "nothing", args: _, body: _}) do
next_context = coordinate(%{x: 0, y: 0, z: 0}, [], context)
raise_if_not_context_or_return_context("coordinate", next_context)
end
def ast_to_coord(%Farmbot.Context{} = context,
def ast_to_coord(%Context{} = context,
%Ast{kind: "point",
args: %{pointer_type: pt_t, pointer_id: pt_id},
body: _}) do
@ -73,8 +79,10 @@ defmodule Farmbot.CeleryScript.Command do
raise_if_not_context_or_return_context("coordinate", next_context)
end
def ast_to_coord(%Farmbot.Context{} = context, %Ast{} = ast) do
raise "No implicit conversion from #{inspect ast} to coordinate! context: #{inspect context}"
def ast_to_coord(%Context{} = context, %Ast{} = ast) do
raise Error, context: context,
message: "No implicit conversion from #{inspect ast} " <>
" to coordinate! context: #{inspect context}"
end
@doc """
@ -84,7 +92,7 @@ defmodule Farmbot.CeleryScript.Command do
def pairs_to_tuples(config_pairs) do
Enum.map(config_pairs, fn(%Ast{} = thing) ->
if thing.args.label == nil do
Logger.error("Label was nil! #{inspect config_pairs}")
Logger.info("Label was nil! #{inspect config_pairs}", type: :error)
end
{thing.args.label, thing.args.value}
end)
@ -94,42 +102,81 @@ defmodule Farmbot.CeleryScript.Command do
defp maybe_print_comment(comment, fun_name),
do: Logger.info ">> [#{fun_name}] - #{comment}"
@doc """
Helper method to read pin or raise an error.
"""
def read_pin_or_raise(%Context{} = ctx, number, pairs) do
if Enum.find(pairs, fn(pair) ->
match?(%Ast{kind: "pair", args: %{label: "eager_read_pin"}}, pair)
end) do
ast = %Ast{
kind: "read_pin",
args: %{label: "", pin_mode: 0, pin_number: String.to_integer(number)},
body: []
}
do_command(ast, ctx)
else
raise Error, context: ctx,
message: "Could not get value of pin #{number}. " <>
"You should manually use read_pin block before this step."
end
end
@doc """
Makes sure serial is not unavailable.
"""
def ensure_gcode({:error, reason}, %Context{} = context) do
raise Error, context: context,
message: "Could not execute gcode. #{inspect reason}"
end
def ensure_gcode(_, %Context{} = ctx), do: ctx
@doc ~s"""
Executes an ast tree.
"""
@spec do_command(Ast.t, Ast.context) :: Ast.context | no_return
def do_command(%Ast{} = ast, context) do
kind = ast.kind
module = Module.concat Farmbot.CeleryScript.Command, Macro.camelize(kind)
# print the comment if it exists
maybe_print_comment(ast.comment, kind)
if Code.ensure_loaded?(module) do
try do
next_context = Kernel.apply(module, :run, [ast.args, ast.body, context])
raise_if_not_context_or_return_context(kind, next_context)
rescue
e ->
debug_log("Could not execute: #{inspect ast}, #{inspect e}")
Logger.error ">> could not execute #{inspect ast} #{inspect e}"
stack_trace = System.stacktrace
reraise(e, stack_trace)
end
else
raise ">> has no instruction for #{inspect ast}"
@spec do_command(Ast.t, Context.t) :: Context.t | no_return
def do_command(%Ast{} = ast, %Context{} = context) do
try do
do_execute_command(ast, context)
rescue
e in Farmbot.CeleryScript.Error ->
Logger.error "Failed to execute CeleryScript: #{e.message}"
reraise e, System.stacktrace()
exception ->
Logger.error "Unknown error happend executing CeleryScript."
# debug_log "CeleryScript Error: #{inspect exception}"
stacktrace = System.stacktrace()
opts = [custom: %{context: context}]
ExRollbar.report(:error, exception, stacktrace, opts)
reraise exception, stacktrace
end
end
def do_command(not_cs_node, _) do
raise ">> can not handle: #{inspect not_cs_node}"
raise Farmbot.CeleryScript.Error,
message: "Can not handle: #{inspect not_cs_node}"
end
defp raise_if_not_context_or_return_context(_, %Farmbot.Context{} = next), do: next
defp do_execute_command(%Ast{} = ast, %Context{} = context) do
kind = ast.kind
module = Module.concat Farmbot.CeleryScript.Command, Macro.camelize(kind)
if Code.ensure_loaded?(module) do
maybe_print_comment(ast.comment, ast.kind)
next_context = apply(module, :run, [ast.args, ast.body, context])
raise_if_not_context_or_return_context(kind, next_context)
else
raise Farmbot.CeleryScript.Error, context: context,
message: "No instruction for #{inspect ast}"
end
end
defp raise_if_not_context_or_return_context(_, %Context{} = next), do: next
defp raise_if_not_context_or_return_context(last_kind, not_context) do
raise "[#{last_kind}] bad return value! #{inspect not_context}"
raise Farmbot.CeleryScript.Error,
message: "[#{last_kind}] bad return value! #{inspect not_context}"
end
# behaviour
@callback run(Ast.args, [Ast.t], Ast.context) :: Ast.context
@callback run(Ast.args, [Ast.t], Context.t) :: Context.t
end

View File

@ -4,6 +4,7 @@ defmodule Farmbot.CeleryScript.Command.If do
"""
alias Farmbot.CeleryScript.{Command, Ast}
import Command, only: [do_command: 2, read_pin_or_raise: 3]
alias Farmbot.Context
use Farmbot.DebugLog
@ -18,9 +19,9 @@ defmodule Farmbot.CeleryScript.Command.If do
rhs: integer},
body: []
"""
@spec run(%{}, [], Ast.context) :: Ast.context
def run(%{_else: else_, _then: then_, lhs: lhs, op: op, rhs: rhs }, [], ctx) do
left = lhs |> eval_lhs(ctx)
@spec run(%{}, [], Context.t) :: Context.t
def run(%{_else: else_, _then: then_, lhs: lhs, op: op, rhs: rhs }, pairs, ctx) do
left = lhs |> eval_lhs(ctx, pairs)
unless is_integer(left) do
raise "could not evaluate left hand side of if statment! #{inspect lhs}"
end
@ -29,25 +30,33 @@ defmodule Farmbot.CeleryScript.Command.If do
end
# figure out what the user wanted
@spec eval_lhs(binary, Ast.context) :: integer
@spec eval_lhs(binary, Context.t, [Ast.t]) :: integer
defp eval_lhs(lhs, %Farmbot.Context{} = context) do
defp eval_lhs(lhs, %Farmbot.Context{} = context, pairs) do
[x, y, z] = Farmbot.BotState.get_current_pos(context)
case lhs do
"x" -> x
"y" -> y
"z" -> z
"pin" <> number ->
thing = number |> String.trim |> String.to_integer
%{value: val} = Farmbot.BotState.get_pin(context, thing)
val
_ ->
nil
"pin" <> number -> lookup_pin(context, number, pairs)
_ -> nil
end
end
@spec lookup_pin(Context.t, binary, [Ast.t]) :: integer | no_return
defp lookup_pin(context, number, pairs) do
thing = number |> String.trim |> String.to_integer
pin_map = Farmbot.BotState.get_pin(context, thing)
case pin_map do
%{value: val} -> val
nil ->
new_context = read_pin_or_raise(context, number, pairs)
lookup_pin(new_context, number, [])
end
end
@spec eval_if({integer, String.t, integer},
Ast.t, Ast.t, Ast.context) :: Ast.context
Ast.t, Ast.t, Context.t) :: Context.t
defp eval_if({lhs, ">", rhs}, then_, else_, context) do
if lhs > rhs,
@ -78,6 +87,6 @@ defmodule Farmbot.CeleryScript.Command.If do
defp print_and_execute(%Ast{} = ast, bool, %Context{} = ctx) do
debug_log "if evaluated: #{bool}, doing: #{inspect ast}"
Command.do_command(ast, ctx)
do_command(ast, ctx)
end
end

View File

@ -12,13 +12,13 @@ defmodule Farmbot.CeleryScript.Command.Calibrate do
args: %{axis: "x" | "y" | "z"}
body: []
"""
@spec run(%{axis: String.t}, [], Ast.context) :: Ast.context
@spec run(%{axis: String.t}, [], Context.t) :: Context.t
def run(%{axis: axis}, [], context) do
do_write(axis, context)
context
end
@spec do_write(binary, Ast.context) :: no_return
@spec do_write(binary, Context.t) :: no_return
defp do_write("x", context), do: UartHan.write(context, "F14")
defp do_write("y", context), do: UartHan.write(context, "F15")
defp do_write("z", context), do: UartHan.write(context, "F16")

View File

@ -13,7 +13,7 @@ defmodule Farmbot.CeleryScript.Command.CheckUpdates do
body: []
"""
@type package :: String.t # "farmbot_os"
@spec run(%{package: package}, [], Ast.context) :: Ast.context
@spec run(%{package: package}, [], Context.t) :: Context.t
def run(%{package: package}, [], context) do
case package do
"arduino_firmware" ->

View File

@ -18,7 +18,7 @@ defmodule Farmbot.CeleryScript.Command.ConfigUpdate do
"""
@spec run(%{package: Command.package},
[Command.pair],
Ast.context) :: Ast.context
Context.t) :: Context.t
def run(%{package: "arduino_firmware"}, config_pairs, context) do
# check the version to make sure we have a good connection to the firmware
:ok = check_version(context)
@ -88,7 +88,7 @@ defmodule Farmbot.CeleryScript.Command.ConfigUpdate do
case results do
:timeout ->
write_and_read(context, {param_int, param_str}, val, tries + 1)
_ -> :ok
_ -> context
end
# # HACK read the param back because sometimes the firmware decides

View File

@ -18,7 +18,7 @@ defmodule Farmbot.CeleryScript.Command.Coordinate do
"""
@type coord_args :: %{x: x, y: y, z: z}
@type t :: %Ast{kind: String.t, args: coord_args, body: []}
@spec run(coord_args, [], Ast.context) :: Ast.context
@spec run(coord_args, [], Context.t) :: Context.t
def run(%{x: _x, y: _y, z: _z} = args, [], context) do
result = %Ast{kind: "coordinate", args: args, body: []}
Farmbot.Context.push_data(context, result)

View File

@ -5,7 +5,16 @@ defmodule Farmbot.CeleryScript.Command.DataUpdate do
alias Farmbot.CeleryScript.Command
alias Farmbot.Database
require Logger
alias Database.Syncable.{
Device,
FarmEvent,
Peripheral,
Point,
Regimen,
Sequence,
Tool
}
use Farmbot.DebugLog
@behaviour Command
@typedoc """
@ -19,23 +28,37 @@ defmodule Farmbot.CeleryScript.Command.DataUpdate do
args: %{value: String.t},
body: [Pair.t]
"""
@spec run(%{value: String.t}, [Pair.t], Ast.context) :: Ast.context
@spec run(%{value: String.t}, [Pair.t], Context.t) :: Context.t
def run(%{value: verb}, pairs, context) do
verb = parse_verb_str(verb)
Enum.each(pairs, fn(%{args: %{label: s, value: nowc}}) ->
syncable = s |> parse_syncable_str()
value = nowc |> parse_val_str()
:ok = Database.set_awaiting(context, syncable, verb, value)
if syncable do
value = nowc |> parse_val_str()
:ok = Database.set_awaiting(context, syncable, verb, value)
else
raise Farmbot.CeleryScript.Error,
message: "Could not translate syncable: #{s}"
end
end)
context
end
@type number_or_wildcard :: non_neg_integer | binary # "*"
@type syncable :: Farmbot.Database.syncable
@type syncable :: Farmbot.Database.syncable | nil
@spec parse_syncable_str(binary) :: syncable
defp parse_syncable_str("regimens"), do: Regimen
defp parse_syncable_str("peripherals"), do: Peripheral
defp parse_syncable_str("sequences"), do: Sequence
defp parse_syncable_str("farm_events"), do: FarmEvent
defp parse_syncable_str("tools"), do: Tool
defp parse_syncable_str("points"), do: Point
defp parse_syncable_str("device"), do: Device
defp parse_syncable_str(str) do
Module.concat([Farmbot.Database.Syncable, Macro.camelize(str)])
debug_log "no such syncable: #{str}"
nil
end
@spec parse_val_str(binary) :: number_or_wildcard

View File

@ -3,7 +3,7 @@ defmodule Farmbot.CeleryScript.Command.EmergencyLock do
EmergencyLock
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.{Command, Error}
require Logger
@behaviour Command
@ -13,10 +13,10 @@ defmodule Farmbot.CeleryScript.Command.EmergencyLock do
args: %{},
body: []
"""
@spec run(%{}, [], Ast.context) :: Ast.context
@spec run(%{}, [], Context.t) :: Context.t
def run(%{}, [], context) do
if Farmbot.BotState.locked?(context) do
raise "Bot is already locked"
raise Error, message: "Bot is already locked"
else
do_lock(context)
end

View File

@ -3,7 +3,7 @@ defmodule Farmbot.CeleryScript.Command.EmergencyUnlock do
EmergencyUnlock
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.{Command, Error}
@behaviour Command
@doc ~s"""
@ -11,7 +11,7 @@ defmodule Farmbot.CeleryScript.Command.EmergencyUnlock do
args: %{},
body: []
"""
@spec run(%{}, [], Ast.context) :: Ast.context
@spec run(%{}, [], Context.t) :: Context.t
def run(%{}, [], context) do
if Farmbot.BotState.locked?(context) do
:ok = Farmbot.Serial.Handler.emergency_unlock(context)
@ -19,7 +19,7 @@ defmodule Farmbot.CeleryScript.Command.EmergencyUnlock do
:ok = Farmbot.BotState.set_sync_msg(context, :sync_now)
context
else
raise "Bot is not locked"
raise Error, message: "Bot is not locked"
end
end

View File

@ -3,8 +3,8 @@ defmodule Farmbot.CeleryScript.Command.Execute do
Execute
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.{Ast, Command, Error}
alias Farmbot.Database.Syncable.Sequence
@behaviour Command
@ -13,23 +13,12 @@ defmodule Farmbot.CeleryScript.Command.Execute do
args: %{sequence_id_id: integer}
body: []
"""
@spec run(%{sequence_id: integer}, [], Ast.context) :: Ast.context
def run(%{sequence_id: id} = args, [], context) do
context.database
|> Farmbot.Database.get_by_id(Sequence, id)
|> Ast.parse
|> merge_args(args)
|> delete_me()
|> Command.do_command(context)
end
defp merge_args(ast, args), do: %{ast | args: Map.merge(ast.args, args)}
defp delete_me(ast) do
%{ast | body: [blerp() | ast.body]}
end
# CONTENTS UNDER PRESSURE
defp blerp do
%Farmbot.CeleryScript.Ast{args: %{}, body: [], comment: nil, kind: "ping_parent"}
@spec run(%{sequence_id: integer}, [], Context.t) :: Context.t
def run(%{sequence_id: id}, [], context) do
sequence = Farmbot.Database.get_by_id(context, Sequence, id)
unless sequence do
raise Error, message: "Could not find sequence by id: #{id}"
end
sequence |> Map.get(:body) |> Ast.parse |> Command.do_command(context)
end
end

View File

@ -3,25 +3,27 @@ defmodule Farmbot.CeleryScript.Command.ExecuteScript do
ExecuteScript
"""
alias Farmbot.CeleryScript.Command
require Logger
alias Farmbot.CeleryScript.{Command, Error}
alias Farmbot.Farmware
alias Farmware.{Manager, Runtime}
import Farmbot.Lib.Helpers
@behaviour Command
@doc ~s"""
Executes a farmware
args: %{label: String.t},
args: %{label: uuid},
body: [pair]
NOTE this is a shortcut to starting a process by uuid
"""
@spec run(%{label: String.t}, [Command.Pair.t], Ast.context) :: Ast.context
def run(%{label: farmware}, env_vars, context) do
@spec run(%{label: binary},
[Command.Pair.t], Context.t) :: Context.t | no_return
def run(%{label: uuid}, env_vars, context) when is_uuid(uuid) do
Command.set_user_env(%{}, env_vars, context)
info = Farmbot.BotState.ProcessTracker.lookup(context, :farmware, farmware)
if info do
Command.start_process(%{label: info.uuid}, [], context)
else
Logger.error ">> Could not locate: #{farmware}"
case Manager.lookup(context, uuid) do
{:ok, %Farmware{} = fw} -> Runtime.execute(context, fw)
{:error, e} ->
raise Error,
message: "Could not locate farmware: #{e}",
context: context
end
context
end
end

View File

@ -14,7 +14,7 @@ defmodule Farmbot.CeleryScript.Command.Explanation do
"""
@type explanation_type ::
%Ast{kind: String.t, args: %{message: String.t}, body: []}
@spec run(%{message: String.t}, [], Ast.context) :: Ast.context
@spec run(%{message: String.t}, [], Context.t) :: Context.t
def run(%{message: message}, [], context) do
result = %Ast{kind: "explanation", args: %{message: message}, body: []}
Farmbot.Context.push_data(context, result)

View File

@ -3,23 +3,24 @@ defmodule Farmbot.CeleryScript.Command.FactoryReset do
FactoryReset
"""
# alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.{Command, Ast}
require Logger
alias Farmbot.CeleryScript.{Command}
alias Farmbot.Context
require Logger
@behaviour Command
import Command
import Command
@doc ~s"""
Factory resets bot.
args: %{package: "farmbot_os" | "arduino_firmware"}
body: []
"""
@spec run(%{package: binary}, [], Ast.context) :: Ast.context
@spec run(%{package: binary}, [], Context.t) :: Context.t
def run(%{package: "farmbot_os"}, [], context) do
Logger.info(">> Going down for factory reset in 5 seconds!", type: :warn)
spawn fn ->
Farmbot.BotState.set_sync_msg(context, :maintenance)
Process.sleep 5000
do_fac_reset_fw(context)
# do_fac_reset_fw(context)
Farmbot.System.factory_reset("I was asked by a CeleryScript command.")
end
context
@ -30,29 +31,55 @@ defmodule Farmbot.CeleryScript.Command.FactoryReset do
context
end
@spec do_fac_reset_fw(Ast.context, boolean) :: no_return
@spec do_fac_reset_fw(Context.t, boolean) :: no_return
defp do_fac_reset_fw(context, reboot \\ false) do
Logger.info(">> Going to reset my arduino!", type: :warn)
params =
context
|> Farmbot.BotState.get_all_mcu_params()
|> Enum.map(fn({key, _value}) ->
if key do
param = key |> String.to_existing_atom()
Farmbot.BotState.set_param(context, param, -1)
end
pair(%{label: key, value: -1}, [], context)
end)
config_update(%{package: "arduino_firmware"}, params, context)
Farmbot.BotState.set_sync_msg(context, :maintenance)
params_map = Farmbot.BotState.get_all_mcu_params(context)
context1 = to_pairs(Map.to_list(params_map), context)
{params, context2} = get_params(Enum.count(params_map), context1)
context3 = config_update(%{package: "arduino_firmware"}, params, context2)
file = "#{Farmbot.System.FS.path()}/config.json"
config_file = file |> File.read!() |> Poison.decode!()
f = %{config_file | "hardware" => %{config_file["hardware"] | "params" => %{}}}
Farmbot.System.FS.transaction fn() ->
File.write file, Poison.encode!(f)
end, true
GenServer.stop(context.serial, :reset)
if reboot, do: Farmbot.System.reboot()
GenServer.stop(context3.serial, :reset)
if reboot do
Farmbot.System.reboot()
else
Farmbot.BotState.set_sync_msg(context3, :sync_now)
context3
end
end
@spec to_pairs([{atom, binary}], Context.t) :: Context.t
defp to_pairs(params_list, context_accumulator)
defp to_pairs([], %Context{} = acc), do: acc
defp to_pairs([{key, _value} | rest], %Context{} = acc) do
# have some side effects.
if key do
param = String.to_atom(key)
Farmbot.BotState.set_param(acc, param, -1)
to_pairs(rest, pair(%{label: key, value: -1}, [], acc))
else
acc
end
end
defp get_params(count, context, acc \\ [])
defp get_params(0, %Context{} = context, params) do
{params, context}
end
defp get_params(count, %Context{data_stack: [param | rest]} = ctx, acc) do
get_params(count - 1, %{ctx | data_stack: rest}, [param | acc])
end
end

View File

@ -13,7 +13,7 @@ defmodule Farmbot.CeleryScript.Command.FindHome do
body: []
"""
@type axis :: String.t # "x" | "y" | "z" | "all"
@spec run(%{axis: axis}, [], Ast.context) :: Ast.context
@spec run(%{axis: axis}, [], Context.t) :: Context.t
def run(%{axis: "all"}, [], context) do
run(%{axis: "z"}, [], context) # <= FindHome z FIRST to prevent plant damage
run(%{axis: "y"}, [], context)

View File

@ -4,7 +4,6 @@ defmodule Farmbot.CeleryScript.Command.Home do
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.Ast
@behaviour Command
@doc ~s"""
@ -13,7 +12,7 @@ defmodule Farmbot.CeleryScript.Command.Home do
body: []
"""
@type axis :: String.t # "x" | "y" | "z" | "all"
@spec run(%{axis: axis}, [], Ast.context) :: Ast.context
@spec run(%{axis: axis}, [], Context.t) :: Context.t
def run(%{axis: "all"}, [], context) do
run(%{axis: "z"}, [], context) # <= Home z FIRST to prevent plant damage
run(%{axis: "y"}, [], context)

View File

@ -3,9 +3,7 @@ defmodule Farmbot.CeleryScript.Command.InstallFarmware do
Install Farmware
"""
# alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.Command
require Logger
@behaviour Command
@doc ~s"""
@ -13,9 +11,9 @@ defmodule Farmbot.CeleryScript.Command.InstallFarmware do
args: %{url: String.t},
body: []
"""
@spec run(%{url: String.t}, [], Ast.context) :: Ast.context
@spec run(%{url: String.t}, [], Context.t) :: Context.t
def run(%{url: url}, [], context) do
Farmware.install(context, url)
Farmbot.Farmware.Manager.install!(context, url)
context
end
end

View File

@ -5,7 +5,7 @@ defmodule Farmbot.CeleryScript.Command.MoveAbsolute do
alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.Command
import Command, only: [ast_to_coord: 2]
import Command, only: [ast_to_coord: 2, ensure_gcode: 2]
alias Farmbot.Lib.Maths
require Logger
alias Farmbot.Serial.Handler, as: UartHan
@ -28,7 +28,7 @@ defmodule Farmbot.CeleryScript.Command.MoveAbsolute do
offset: coordinate_ast | Ast.t,
location: coordinate_ast | Ast.t
}
@spec run(move_absolute_args, [], Ast.context) :: Ast.context
@spec run(move_absolute_args, [], Context.t) :: Context.t
def run(%{speed: s, offset: offset, location: location}, [], context) do
new_context = ast_to_coord(context, location)
{location, new_context1} = Farmbot.Context.pop_data(new_context)
@ -38,15 +38,15 @@ defmodule Farmbot.CeleryScript.Command.MoveAbsolute do
a = {location.args.x, location.args.y, location.args.z}
b = {offset.args.x, offset.args.y, offset.args.z }
do_move(a, b, s, context)
new_context3
do_move(a, b, s, new_context3)
end
defp do_move({xa, ya, za}, {xb, yb, zb}, speed, context) do
{ combined_x, combined_y, combined_z } = { xa + xb, ya + yb, za + zb }
{x, y, z} = do_math(combined_x, combined_y, combined_z, context)
UartHan.write(context, "G00 X#{x} Y#{y} Z#{z} S#{speed}")
context
|> UartHan.write("G00 X#{x} Y#{y} Z#{z} S#{speed}")
|> ensure_gcode(context)
end
defp do_math(combined_x, combined_y, combined_z, context) do

View File

@ -4,7 +4,6 @@ defmodule Farmbot.CeleryScript.Command.MoveRelative do
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.Ast
@behaviour Command
@type x :: Command.Coordinate.x
@ -16,17 +15,17 @@ defmodule Farmbot.CeleryScript.Command.MoveRelative do
args: %{speed: number, x: number, y: number, z: number}
body: []
"""
@spec run(%{speed: number, x: x, y: y, z: z}, [], Ast.context)
:: Ast.context
@spec run(%{speed: number, x: x, y: y, z: z}, [], Context.t)
:: Context.t
def run(%{speed: speed, x: x, y: y, z: z}, [], context) do
# make a coordinate of the relative movement we want to do
loc = %{x: x, y: y, z: z}
new_context1 = Command.coordinate(loc, [], context)
new_context1 = Command.coordinate(loc, [], context)
{location, new_context2} = Farmbot.Context.pop_data(new_context1)
# get the current position, then turn it into another coord.
[cur_x,cur_y,cur_z] = Farmbot.BotState.get_current_pos(context)
[cur_x, cur_y, cur_z] = Farmbot.BotState.get_current_pos(context)
# Make another coord for the offset
coord_args = %{x: cur_x, y: cur_y, z: cur_z}

View File

@ -14,7 +14,7 @@ defmodule Farmbot.CeleryScript.Command.Nothing do
body: []
"""
@type nothing_ast :: %Ast{kind: String.t, args: %{}, body: []}
@spec run(%{}, [], Ast.context) :: Ast.context
@spec run(%{}, [], Context.t) :: Context.t
def run(args, body, context) do
thing = %Ast{kind: "nothing", args: args, body: body}
Farmbot.Context.push_data(context, thing)

View File

@ -15,7 +15,7 @@ defmodule Farmbot.CeleryScript.Command.Pair do
args: %{label: String.t, value: any},
body: []
"""
@spec run(%{label: String.t, value: any}, [], Ast.context) :: Ast.context
@spec run(%{label: String.t, value: any}, [], Context.t) :: Context.t
def run(%{label: label, value: value}, [], context) do
data = %Ast{ kind: "pair",
args: %{label: label, value: value},

View File

@ -11,10 +11,13 @@ defmodule Farmbot.CeleryScript.Command.PowerOff do
args: %{},
body: []
"""
@spec run(%{}, [], Ast.context) :: Ast.context
@spec run(%{}, [], Context.t) :: Context.t
def run(%{}, [], context) do
Farmbot.System.power_off()
spawn fn ->
Farmbot.BotState.set_sync_msg(context, :maintenance)
Process.sleep(2000)
Farmbot.System.power_off()
end
context
# ^ lol
end
end

View File

@ -14,7 +14,7 @@ defmodule Farmbot.CeleryScript.Command.ReadStatus do
"""
@spec run(%{}, [], Ast.context) :: Ast.context
def run(%{}, [], context) do
Farmbot.Transport.force_state_push()
Farmbot.Transport.force_state_push(context)
context
end
end

View File

@ -3,8 +3,9 @@ defmodule Farmbot.CeleryScript.Command.Reboot do
Reboot
"""
alias Farmbot.CeleryScript.Command
alias Farmbot.CeleryScript.Ast
require Logger
alias Farmbot.CeleryScript.Command
alias Farmbot.Context
@behaviour Command
@doc ~s"""
@ -12,10 +13,14 @@ defmodule Farmbot.CeleryScript.Command.Reboot do
args: %{},
body: []
"""
@spec run(%{}, [], Ast.context) :: Ast.context
def run(%{}, [], context) do
Farmbot.System.reboot()
@spec run(%{}, [], Context.t) :: Context.t
def run(%{}, [], %Context{} = context) do
spawn fn ->
Logger.warn ">> was told to reboot. See you soon!"
Farmbot.BotState.set_sync_msg(context, :maintenance)
Process.sleep(2000)
Farmbot.System.reboot()
end
context
# ^ LOL
end
end

View File

@ -3,19 +3,17 @@ defmodule Farmbot.CeleryScript.Command.RemoveFarmware do
Uninstall Farmware
"""
alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.Command
require Logger
alias Farmbot.CeleryScript.{Command, Ast}
@behaviour Command
@doc ~s"""
Uninstall a farmware
args: %{package: String.t},
args: %{package: uuid},
body: []
"""
@spec run(%{package: String.t}, [], Ast.context) :: Ast.context
def run(%{package: package}, [], context) do
Farmware.uninstall(context, package)
@spec run(%{package: binary}, [], Ast.context) :: Ast.context
def run(%{package: uuid}, [], context) do
Farmbot.Farmware.Manager.uninstall!(context, uuid)
context
end
end

View File

@ -16,7 +16,7 @@ defmodule Farmbot.CeleryScript.Command.RpcRequest do
@spec run(%{label: String.t}, [Ast.t, ...], Ast.context) :: Ast.context
def run(%{label: id}, more_stuff, context) do
more_stuff
|> Enum.reduce({[],[]}, fn(ast, {win, fail}) ->
|> Enum.reduce({[], []}, fn(ast, {win, fail}) ->
fun_name = String.to_atom(ast.kind)
if function_exported?(Command, fun_name, 3) do
# actually do the stuff here?
@ -43,7 +43,7 @@ defmodule Farmbot.CeleryScript.Command.RpcRequest do
# there were no failed asts.
context1 = Command.rpc_ok(%{label: id}, [], context)
{item, context2} = Farmbot.Context.pop_data(context1)
Farmbot.Transport.emit(item)
Farmbot.Transport.emit(context, item)
context2
end
@ -51,7 +51,7 @@ defmodule Farmbot.CeleryScript.Command.RpcRequest do
# there were some failed asts.
context1 = Command.rpc_error(%{label: id}, failed, context)
{item, context2} = Farmbot.Context.pop_data(context1)
Farmbot.Transport.emit(item)
Farmbot.Transport.emit(context, item)
context2
end
end

View File

@ -3,7 +3,9 @@ defmodule Farmbot.CeleryScript.Command.SendMessage do
SendMessage
"""
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.Context
# import Command, only: [read_pin_or_raise: 3]
require Logger
@behaviour Command
@ -23,25 +25,35 @@ defmodule Farmbot.CeleryScript.Command.SendMessage do
@spec run(%{message: String.t, message_type: message_type},
[Ast.t], Ast.context) :: Ast.context
def run(%{message: m, message_type: m_type}, channels, context) do
rendered = Mustache.render(m, get_message_stuff(context))
def run(%{message: m, message_type: m_type}, pairs, %Context{} = context) do
rendered = Mustache.render(m, get_message_stuff(context, pairs))
Logger.info ">> #{rendered}",
type: m_type, channels: parse_channels(channels)
type: m_type, channels: parse_channels(pairs)
context
end
@spec get_message_stuff(Ast.context)
@spec get_message_stuff(Ast.context, [Ast.t])
:: %{x: Command.x, y: Command.y, z: Command.z}
defp get_message_stuff(context) do
defp get_message_stuff(%Context{} = context, _pairs) do
[x, y, z] = Farmbot.BotState.get_current_pos(context)
%{x: x, y: y, z: z}
coords = %{x: x, y: y, z: z}
pins = Map.new(0..70, fn(num) ->
pin_val =
case Farmbot.BotState.get_pin(context, num) do
%{value: val} -> val
_ -> :unknown
# read_pin_or_raise(context, num, pairs)
end
{:"pin#{num}", pin_val}
end)
Map.merge(coords, pins)
end
@spec parse_channels([Ast.t]) :: [message_channel]
defp parse_channels(l) do
{ch, _} = Enum.partition(l, fn(channel_ast) ->
channel_ast.args["channel_name"]
channels = Enum.filter(l, fn(ast) -> ast.kind == "channel" end)
Enum.map(channels, fn(%{kind: "channel", args: %{channel_name: ch}}) ->
ch
end)
ch
end
end

View File

@ -4,6 +4,7 @@ defmodule Farmbot.CeleryScript.Command.Sequence do
"""
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.Context
require Logger
@behaviour Command
@ -13,13 +14,25 @@ defmodule Farmbot.CeleryScript.Command.Sequence do
args: %{},
body: [Ast.t]
"""
@spec run(%{}, [Ast.t], Ast.context) :: Ast.context
def run(args, body, context) do
@spec run(%{}, [Ast.t], Context.t) :: Context.t
def run(args, body, %Context{} = context) do
# rebuild the ast node
ast = %Ast{kind: "sequence", args: args, body: body}
# Logger.debug "Starting sequence: #{inspect ast}"
{:ok, pid} = Farmbot.SequenceRunner.start_link(ast, context)
next_context = Farmbot.SequenceRunner.wait(pid)
{:ok, pid} = Farmbot.Sequence.Manager.start_link(context, ast, self())
next_context = wait_for_sequence(pid, context)
next_context
end
@spec wait_for_sequence(pid, Context.t) :: Context.t
defp wait_for_sequence(pid, old_context) do
receive do
{^pid, %Context{} = ctx} ->
Logger.info "Sequence complete.", type: :success
ctx
{^pid, {:error, _reason}} ->
Logger.error "Sequence completed with error. See log."
old_context
end
end
end

View File

@ -16,9 +16,6 @@ defmodule Farmbot.CeleryScript.Command.SetUserEnv do
envs = Command.pairs_to_tuples(env_pairs)
map = envs |> Map.new
Farmbot.BotState.set_user_env(context, map)
env = envs |> Map.new
Farmware.Worker.add_envs(context, env)
context
end
end

View File

@ -1,19 +0,0 @@
defmodule Farmbot.CeleryScript.Command.StartProcess do
@moduledoc """
StartProcess
"""
alias Farmbot.CeleryScript.{Command, Ast}
@behaviour Command
@doc ~s"""
Starts a FarmProcess
args: %{label: String.t},
body: []
"""
@spec run(%{label: String.t}, [], Ast.context) :: Ast.context
def run(%{label: uuid}, [], context) do
Farmbot.BotState.ProcessTracker.start_process(context, uuid)
context
end
end

View File

@ -1,11 +1,10 @@
defmodule Farmbot.CeleryScript.Command.TakePhoto do
@moduledoc """
TestCs
Take a photo
"""
# alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.{Command, Ast}
require Logger
alias Farmbot.Farmware
@behaviour Command
@doc ~s"""
@ -15,11 +14,9 @@ defmodule Farmbot.CeleryScript.Command.TakePhoto do
"""
@spec run(%{}, [], Ast.context) :: Ast.context
def run(%{}, [], context) do
i = Farmbot.BotState.ProcessTracker.lookup context, :farmware, "take-photo"
if i do
Command.start_process(%{label: i.uuid}, [], context)
else
raise "take-photo is not installed!"
case Farmware.Manager.lookup_by_name(context, "take-photo") do
{:ok, %Farmware{} = fw} -> Farmware.Runtime.execute(context, fw)
{:error, e} -> raise "Could not execute take photo: #{inspect e}"
end
end
end

View File

@ -33,7 +33,7 @@ defmodule Farmbot.CeleryScript.Command.TogglePin do
write_pin(args, [], context)
# if it was on (or analog) turn it off. (for safetey)
_ ->
args = %{pin_number: pin, pin_mode: @digital, pin_value: 1}
args = %{pin_number: pin, pin_mode: @digital, pin_value: 0}
write_pin(args, [], context)
end
end

View File

@ -3,9 +3,7 @@ defmodule Farmbot.CeleryScript.Command.UpdateFarmware do
Update Farmware
"""
# alias Farmbot.CeleryScript.Ast
alias Farmbot.CeleryScript.{Command, Ast}
require Logger
@behaviour Command
@doc ~s"""
@ -14,8 +12,8 @@ defmodule Farmbot.CeleryScript.Command.UpdateFarmware do
body: []
"""
@spec run(%{package: String.t}, [], Ast.context) :: Ast.context
def run(%{package: package}, [], context) do
Farmware.update(context, package)
def run(%{package: uuid}, [], context) do
Farmbot.Farmware.Manager.update!(context, uuid)
context
end
end

View File

@ -0,0 +1,6 @@
defmodule Farmbot.CeleryScript.Error do
defexception [
{:context, nil},
:message
]
end

View File

@ -91,7 +91,7 @@ defmodule Farmbot.Configurator.Router do
Logger.info ">> router got credentials"
{:ok, _body, conn} = read_body(conn)
%{"email" => email,"pass" => pass,"server" => server} = conn.body_params
%{"email" => email, "pass" => pass, "server" => server} = conn.body_params
Farmbot.Auth.interim(context().auth, email, pass, server)
conn |> send_resp(200, "OK")
end
@ -244,7 +244,7 @@ defmodule Farmbot.Configurator.Router do
else
Logger.info "doing some magic..."
herp = Nerves.UART.enumerate()
|> Map.drop(["ttyS0","ttyAMA0"])
|> Map.drop(["ttyS0", "ttyAMA0"])
|> Map.keys
case herp do
[tty] ->

View File

@ -79,7 +79,7 @@ defmodule Farmbot.Configurator.SocketHandler do
def websocket_info(:force_state_push, req, stage) do
spawn fn() ->
Farmbot.Transport.force_state_push
Farmbot.Transport.force_state_push(Farmbot.Context.new())
end
{:ok, req, stage}
end

View File

@ -13,71 +13,111 @@ defmodule Farmbot.Context do
:hardware,
:monitor,
:configuration,
:farmware_worker,
:farmware_tracker
:http,
:transport,
:farmware_manager,
:regimen_supervisor
]
@enforce_keys modules
defstruct [ {:data_stack, []} | modules ]
keys = [{:data_stack, []}, :ref]
defstruct Enum.concat(keys, modules)
defimpl Inspect, for: __MODULE__ do
def inspect(thing, _) do
default_context =
Farmbot.Context.new()
|> Map.from_struct
|> Map.delete(:data_stack)
@doc false
defmacro __using__(opts) do
reqs = Keyword.fetch!(opts, :requires)
quote do
alias Farmbot.Context
@behaviour Context.Consumer
thing = thing |> Map.from_struct() |> Map.delete(:data_stack)
if thing == default_context do
"#Context<default>"
else
"#Context<#{thing}>"
end
@doc false
def requirements, do: unquote(reqs)
end
end
@typedoc false
@type database :: Farmbot.Database.database
defimpl Inspect, for: __MODULE__ do
def inspect(%{ref: ref}, _) when is_reference(ref) do
"#Reference<" <> rest = inspect ref
info = String.trim(rest, ">")
"#Context<#{info}>"
end
def inspect(_, _) do
"#Context<:invalid>"
end
end
@behaviour Access
def fetch(%__MODULE__{} = ctx, key), do: Map.fetch(ctx, key)
def get(%__MODULE__{} = ctx, key, _default), do: Map.fetch(ctx, key)
def get_and_update(%__MODULE__{}, _, _), do: raise "Cant update #{__MODULE__} struct!"
def pop(%__MODULE__{}, _), do: raise "Cant pop #{__MODULE__} struct!"
@typedoc false
@type auth :: Farmbot.Auth.auth
@type database :: Farmbot.Database.database
@typedoc false
@type network :: Farmbot.System.Network.netman
@type auth :: Farmbot.Auth.auth
@typedoc false
@type serial :: Farmbot.Serial.Handler.handler
@type network :: Farmbot.System.Network.netman
@typedoc false
@type hardware :: Farmbot.BotState.Hardware.hardware
@type serial :: Farmbot.Serial.Handler.handler
@typedoc false
@type monitor :: Farmbot.BotState.Monitor.monitor
@type hardware :: Farmbot.BotState.Hardware.hardware
@typedoc false
@type configuration :: Farmbot.BotState.Configuration.configuration
@type monitor :: Farmbot.BotState.Monitor.monitor
@typedoc false
@type farmware_tracker :: Farmware.tracker
@type configuration :: Farmbot.BotState.Configuration.configuration
@typedoc false
@type farmware_worker :: Farmware.worker
@type http :: Farmbot.HTTP.http
@typedoc false
@type transport :: Farmbot.Transport.transport
@typedoc false
@type farmware_manager :: Farmbot.Farmware.Manager.manager
@typedoc false
@type regimen_supervisor :: Farmbot.Regimen.Supervisor.supervisor
@typedoc """
List of usable modules
"""
@type modules :: Farmbot.Database |
Farmbot.Auth |
Farmbot.System.Network |
Farmbot.Serial.Handler |
Farmbot.BotState.Hardware |
Farmbot.BotState.Monitor |
Farmbot.BotState.Configuration |
Farmbot.HTTP |
Farmbot.Transport |
Farmbot.Farmware.Manager |
Farmbot.Regimen.Supervisor
@typedoc """
Stuff to be passed from one CS Node to another
"""
@type t :: %__MODULE__{
database: database,
auth: auth,
network: network,
serial: serial,
configuration: configuration,
monitor: monitor,
hardware: hardware,
farmware_worker: farmware_worker,
farmware_tracker: farmware_worker,
data_stack: [Ast.t]
database: database,
auth: auth,
network: network,
serial: serial,
configuration: configuration,
monitor: monitor,
hardware: hardware,
http: http,
transport: transport,
farmware_manager: farmware_manager,
ref: reference,
regimen_supervisor: regimen_supervisor,
data_stack: [Ast.t]
}
@spec push_data(t, Ast.t) :: t
@ -96,18 +136,21 @@ defmodule Farmbot.Context do
Returns an empty context object for those times you don't care about
side effects or execution.
"""
@spec new :: Ast.context
@spec new :: Context.t
def new do
%__MODULE__{ data_stack: [],
farmware_worker: Farmware.Worker,
farmware_tracker: Farmware.Tracker,
configuration: Farmbot.BotState.Configuration,
hardware: Farmbot.BotState.Hardware,
monitor: Farmbot.BotState.Monitor,
database: Farmbot.Database,
network: Farmbot.System.Network,
serial: Farmbot.Serial.Handler,
auth: Farmbot.Auth,
ref: make_ref(),
regimen_supervisor: Farmbot.Regimen.Supervisor,
farmware_manager: Farmbot.Farmware.Manager,
configuration: Farmbot.BotState.Configuration,
transport: Farmbot.Transport,
hardware: Farmbot.BotState.Hardware,
database: Farmbot.Database,
monitor: Farmbot.BotState.Monitor,
network: Farmbot.System.Network,
serial: Farmbot.Serial.Handler,
auth: Farmbot.Auth,
http: Farmbot.HTTP
}
end
end

View File

@ -0,0 +1,11 @@
defmodule Farmbot.Context.Consumer do
@moduledoc """
Behaviour for consuming a context object.
"""
alias Farmbot.Context
@doc """
A list of requirements required by a `Consumer`
"""
@callback requirements :: [Context.modules]
end

View File

@ -0,0 +1,29 @@
defmodule Farmbot.Context.Supervisor do
@moduledoc """
Helpful macros for a supervisor that uses a Context object.
"""
alias Farmbot.Context
defmacro __using__(_) do
quote do
use Farmbot.DebugLog,
name: __MODULE__ |> Module.split() |> Enum.take(-2) |> Enum.join
alias Farmbot.Context
use Supervisor
@doc "Start a #{__MODULE__} Supervisor"
def start_link(%Context{} = ctx, opts) do
debug_log "Starting supervisor"
Supervisor.start_link(__MODULE__, ctx, opts)
end
def init(%Context{} = _ctx) do
children = []
opts = [strategy: :one_for_one]
supervise(children, opts)
end
defoverridable([init: 1])
end
end
end

View File

@ -1,39 +0,0 @@
defmodule Farmbot.Context.Tracker do
@moduledoc """
Tracks the current context.
"""
alias Farmbot.Context
@doc "Gets current context"
def get_context(tracker), do: GenServer.call(tracker, :get_context)
modules =
Context.new() |> Map.from_struct |> Map.delete(:data_stack) |> Map.keys
for module <- modules do
@doc "Gets the #{module} part from a context."
def unquote(module)(tracker) when is_pid(tracker),
do: GenServer.call(tracker, unquote(module))
end
@doc """
Starts a context tracker.
"""
def start_link(%Context{} = context, opts) do
GenServer.start_link(__MODULE__, context, opts)
end
def init(context), do: {:ok, context}
def handle_call(:get_context, _from, context), do: {:reply, context, context}
for module <- modules do
def handle_call(unquote(module), _from, context),
do: {:reply, context[unquote(module)], context}
end
end

View File

@ -0,0 +1,24 @@
defmodule Farmbot.Context.Worker do
@moduledoc """
Helper macro for worker modules concerned with context.
"""
defmacro __using__(_) do
quote do
use Farmbot.DebugLog,
name: __MODULE__ |> Module.split() |> Enum.take(-2) |> Enum.join
use GenServer
alias Farmbot.Context
@doc "Start a #{__MODULE__} worker"
def start_link(%Context{} = ctx, opts) do
debug_log "Starting worker: #{__MODULE__} with #{inspect opts}"
GenServer.start_link(__MODULE__, ctx, opts)
end
def init(ctx), do: {:ok, %{context: ctx}}
defoverridable([init: 1])
end
end
end

View File

@ -43,17 +43,6 @@ defmodule Farmbot.Database do
@typedoc false
@type syncable_object :: map
@typedoc """
State of the DB
"""
@type state :: %{
by_kind_and_id: %{ required({syncable, db_id}) => ref_id },
awaiting: %{ required(syncable) => boolean },
by_kind: %{ required(syncable) => [ref_id] },
refs: %{ required(ref_id) => resource_map },
all: [ref_id],
}
# This pulls all the module names by their filename.
syncable_modules =
"lib/farmbot/database/syncable/"
@ -70,29 +59,54 @@ defmodule Farmbot.Database do
@spec all_syncable_modules :: [syncable]
def all_syncable_modules, do: unquote(syncable_modules)
defp set_syncing(ctx, msg) do
:ok = Farmbot.BotState.set_sync_msg(ctx, msg)
:ok
end
@doc """
Sync up with the API.
"""
# TODO(Connor) this is slow.
@spec sync(Context.t) :: :ok | no_return
def sync(%Context{} = ctx) do
Farmbot.BotState.set_sync_msg(ctx, :syncing)
try do
for module_name <- all_syncable_modules() do
# see: `syncable.ex`. This is some macro magic.
debug_log "#{module_name} Sync begin."
:ok = module_name.fetch({__MODULE__,
:commit_records, [ctx, module_name]})
debug_log "#{module_name} Sync finish."
set_syncing(ctx, :syncing)
for module_name <- all_syncable_modules() do
if get_awaiting(ctx, module_name) do
:ok = do_sync(ctx, module_name)
else
debug_log "#{module_name} already up to date."
:ok
end
Farmbot.BotState.set_sync_msg(ctx, :synced)
:ok
end
set_syncing(ctx, :synced)
Logger.info ">> is synced!", type: :success
:ok
end
defp do_sync(context, module_name, retries \\ 0)
defp do_sync(%Context{} = _ctx, module_name, retries) when retries > 4 do
debug_log "#{module_name} failed to sync too many times. (#{retries})"
Logger.error ">> failed to sync #{module_name} to many times."
:ok
end
defp do_sync(%Context{} = ctx, module_name, retries) do
# see: `syncable.ex`. This is some macro magic.
debug_log "#{module_name} Sync begin."
try do
:ok = module_name.fetch(ctx, {__MODULE__,
:commit_records, [ctx, module_name]})
rescue
e ->
Logger.info ">> Encountered error syncing, #{inspect e}", type: :error
Farmbot.BotState.set_sync_msg(ctx, :sync_error)
debug_log "#{module_name} Sync error: #{inspect e}"
do_sync(ctx, module_name, retries + 1)
end
debug_log "#{module_name} Sync finish."
:ok = unset_awaiting(ctx, module_name)
end
@doc """
@ -130,8 +144,10 @@ defmodule Farmbot.Database do
Sets awaiting api resources.
"""
@spec set_awaiting(Context.t, syncable, verb, any) :: :ok | no_return
def set_awaiting(%Context{database: db}, syncable, verb, value) do
def set_awaiting(%Context{database: db} = ctx, syncable, verb, value) do
debug_log("setting awaiting: #{syncable} #{verb}")
# FIXME(connor) YAY SIDE EFFECTS
set_syncing(ctx, :sync_now)
GenServer.call(db, {:set_awaiting, syncable, verb, value})
end
@ -154,16 +170,50 @@ defmodule Farmbot.Database do
Get a resource by its kind and id.
"""
@spec get_by_id(Context.t, syncable, db_id) :: resource_map | nil
def get_by_id(%Context{database: db}, kind, id), do: GenServer.call(db, {:get_by, kind, id})
def get_by_id(%Context{database: db}, kind, id),
do: GenServer.call(db, {:get_by, kind, id})
@doc """
Get all resources of this kind.
"""
@spec get_all(Context.t, syncable) :: [resource_map]
def get_all(%Context{database: db}, kind), do: GenServer.call(db, {:get_all, kind})
def get_all(%Context{database: db}, kind),
do: GenServer.call(db, {:get_all, kind})
## GenServer
defmodule State do
@moduledoc false
alias Farmbot.Database.DB
defimpl Inspect, for: __MODULE__ do
def inspect(thing, _) do
"#DatabaseState<#{inspect thing.all}>"
end
end
defstruct [
:by_kind_and_id,
:awaiting,
:by_kind,
:refs,
:all
]
@type t :: %{
by_kind_and_id: %{ required({DB.syncable, DB.db_id}) => DB.ref_id },
awaiting: %{ required(DB.syncable) => boolean },
by_kind: %{ required(DB.syncable) => [DB.ref_id] },
refs: %{ required(DB.ref_id) => DB.resource_map },
all: [DB.ref_id],
}
end
@typedoc """
State of the DB
"""
@type state :: State.t
@doc """
Start the Database
"""
@ -176,7 +226,7 @@ defmodule Farmbot.Database do
initial_refs = %{}
initial_all = []
state = %{
state = %State{
by_kind_and_id: initial_by_kind_and_id,
awaiting: initial_awaiting,
by_kind: initial_by_kind,
@ -214,6 +264,21 @@ defmodule Farmbot.Database do
{:reply, Map.fetch!(state.awaiting, module), state}
end
def handle_call(
{:set_awaiting, syncable, :remove, int_or_wildcard}, _, state)
do
new_state =
case int_or_wildcard do
"*" -> remove_all_syncable(state, syncable)
num -> remove_syncable(state, syncable, num)
end
{
:reply,
:ok,
%{ new_state | awaiting: %{ state.awaiting | syncable => true } }
}
end
def handle_call({:set_awaiting, syncable, _verb, _}, _, state) do
{:reply, :ok, %{ state | awaiting: %{ state.awaiting | syncable => true} }}
end
@ -222,6 +287,51 @@ defmodule Farmbot.Database do
{:reply, :ok, %{ state | awaiting: %{ state.awaiting | syncable => false} }}
end
@spec remove_all_syncable(state, syncable) :: state
defp remove_all_syncable(state, syncable) do
new_all = Enum.reject(state.all, fn({s, _, _}) -> s == syncable end)
new_by_kind_and_id = state.by_kind_and_id
|> Enum.reject(fn({{s, _}, _}) -> s == syncable end)
|> Map.new
new_refs = state.refs
|> Enum.reject(fn({{s, _, _}, _}) -> s == syncable end)
|> Map.new()
%{
state |
by_kind_and_id: new_by_kind_and_id,
by_kind: %{state.by_kind | syncable => []},
refs: new_refs,
all: new_all
}
end
@spec remove_syncable(state, syncable, integer) :: state
defp remove_syncable(state, syncable, num) do
new_all = Enum.reject(state.all, fn({s, _, id}) ->
(s == syncable) && (id == num)
end)
new_by_kind_and_id = state.by_kind_and_id
|> Enum.reject(fn({{s, id}, _}) -> (s == syncable) && (id == num) end)
|> Map.new
new_refs = state.refs
|> Enum.reject(fn({{s, _, id}, _}) ->
(s == syncable) && (id == num)
end)
|> Map.new()
%{
state |
by_kind_and_id: new_by_kind_and_id,
all: new_all,
refs: new_refs,
}
end
# returns all the references of syncable
@spec get_all_by_kind(state, syncable) :: [resource_map]
defp get_all_by_kind(state, syncable) do
@ -251,7 +361,7 @@ defmodule Farmbot.Database do
defp reindex(state, record) do
# get some info
kind = Map.fetch!(record, :__struct__)
id = Map.fetch!(record, :id)
id = Map.fetch!(record, :id)
# Do we have it already?
maybe_old = get_by_kind_and_id(state, kind, id)

View File

@ -3,25 +3,57 @@ defmodule Farmbot.Database.Selectors do
Instead of litering the codebase with map/reduce/filter functions,
consider putting database query functions into this module.
"""
alias Farmbot.Database
alias Farmbot.{Database, Context, DebugLog}
alias Farmbot.Database.Syncable
alias Farmbot.Context
alias Syncable.Point
alias __MODULE__.Error, as: SelectorError
alias Syncable.{Point, Device}
use DebugLog
@spec find_point(Context.t, binary, integer) :: Syncable.t
# TODO(Rick): Add pattern match to DB param?
def find_point(context, "Plant" = pt, id), do: _find_point(context, pt, id)
def find_point(context, "ToolSlot" = pt, id), do: _find_point(context, pt, id)
def find_point(context, "GenericPointer" = pt, id), do: _find_point(context, pt, id)
@doc """
Find a Point with a particular type.
* "Plant"
* "ToolSlot"
* "GenericPointer"
"""
@spec find_point(Context.t, binary, integer) :: Syncable.t | no_return
def find_point(%Context{} = context, "Plant" = pt, id),
do: do_find_point(context, pt, id)
@spec find_point(Context.t, binary, integer) :: Point.t
defp _find_point(%Context{} = ctx, point_t, point_id) do
result = Database.get_by_id(ctx, Point, point_id) || raise "" <>
"Can't find #{point_t} with ID #{point_id}"
def find_point(%Context{} = context, "ToolSlot" = pt, id),
do: do_find_point(context, pt, id)
def find_point(%Context{} = context, "GenericPointer" = pt, id),
do: do_find_point(context, pt, id)
@spec do_find_point(Context.t, binary, integer) :: Point.t
defp do_find_point(%Context{} = ctx, point_t, point_id) do
result = Database.get_by_id(ctx, Point, point_id) || raise SelectorError, [
syncable: Point, syncable_id: point_id, message: "does not exist."
]
case result.body.pointer_type do
type when type == point_t -> result
_ -> raise "POINT FAILURE: id/types don't match: #{point_id}/#{point_t}"
_ -> raise SelectorError, [
syncable: Point, syncable_id: point_id, message: "does not match type: #{point_t}"
]
end
end
@doc """
Get this device. Raises.
"""
@spec get_device(Context.t) :: Syncable.body | no_return
def get_device(%Context{} = ctx) do
case Database.get_all(ctx, Device) do
[device] -> device.body
[_device | _] ->
raise SelectorError, [
syncable: Device, syncable_id: nil, message: "Too many devices."
]
[] ->
raise SelectorError, [
syncable: Device, syncable_id: nil, message: "No devices."
]
end
end
end

View File

@ -0,0 +1,8 @@
defmodule Farmbot.Database.Selectors.Error do
@moduledoc "Error message for selectors."
defexception [
:syncable_id,
:syncable,
:message,
]
end

View File

@ -6,25 +6,33 @@ defmodule Farmbot.Database.Syncable do
@enforce_keys [:ref_id, :body]
defstruct @enforce_keys
@typedoc """
Module structs.
"""
@type body :: map
@type ref_id :: Farmbot.Database.ref_id
@type t :: %__MODULE__{ref_id: ref_id, body: map}
@type t :: %__MODULE__{ref_id: ref_id, body: body}
alias Farmbot.Context
import Farmbot.HTTP.Helpers
alias __MODULE__.Error
@doc """
Pipe a HTTP request thru this. Trust me :tm:
"""
def parse_resp({:error, message}, _module), do: {:error, message}
def parse_resp({:ok, %{status_code: 200, body: resp_body}}, module) do
stuff = resp_body |> Poison.decode!
def parse_resp({:ok, %{status_code: code, body: resp_body}}, module)
when is_2xx(code) do
parsed = resp_body |> Poison.decode!
cond do
is_list(stuff) -> Enum.map(stuff, fn(item) -> module.to_struct(item) end)
is_map(stuff) -> module.to_struct(stuff)
true -> {:error, "Hashes and arrays only, please."}
is_list(parsed) -> Enum.map(parsed, fn(i) -> module.to_struct(i) end)
is_map(parsed) -> module.to_struct(parsed)
true -> raise Error,
message: "Don't know how to handle: #{inspect parsed}"
end
end
def parse_resp({:ok, whatevs}, _module) do
{:error, whatevs}
end
def parse_resp({:ok, whatevs}, _module), do: {:error, whatevs}
@doc ~s"""
Builds common functionality for all Syncable Resources. `args` takes two keywords.
@ -50,8 +58,8 @@ defmodule Farmbot.Database.Syncable do
"""
defmacro __using__(args) do
model = Keyword.get(args, :model) || raise "You need a model!"
{singular, plural} = Keyword.get(args, :endpoint) || raise "Syncable requ"
<> "ires a endpoint: {singular_url, plural_url}"
{singular, plural} = Keyword.get(args, :endpoint) || raise Error,
message: "Syncable requires a endpoint: {singular_url, plural_url}"
quote do
alias Farmbot.HTTP
import Farmbot.Database.Syncable, only: [parse_resp: 2]
@ -70,26 +78,34 @@ defmodule Farmbot.Database.Syncable do
@doc """
Fetches all `#{__MODULE__}` objects from the API.
"""
def fetch(then) do
result = "/api" <> unquote(plural)
|> HTTP.get()
|> parse_resp(__MODULE__)
def fetch(%Context{} = context, then) do
url = "/api" <> plural_url()
result = context |> HTTP.get(url) |> parse_resp(__MODULE__)
if function_exported?(__MODULE__, :on_sync, 2) do
apply __MODULE__, :on_sync, [context, result]
end
case then do
{module, function, args} -> apply(module, function, [result | args])
anon -> anon.(result)
{module, fun, args} -> apply(module, fun, [result | args])
anon when is_function(anon) -> anon.(result)
end
end
@doc """
Fetches a specific `#{__MODULE__}` from the API, by it's id.
"""
def fetch(id, then) do
result = "/api" <> unquote(singular) <> "/#{id}"
|> HTTP.get()
|> parse_resp(__MODULE__)
def fetch(%Context{} = context, id, then) do
url = "/api" <> unquote(singular) <> "/#{id}"
result = context |> HTTP.get(url) |> parse_resp(__MODULE__)
if function_exported?(__MODULE__, :on_sync, 2) do
apply __MODULE__, :on_sync, [context, result]
end
case then do
{module, function, args} -> apply(module, function, [result | args])
anon -> anon.(result)
{module, fun, args} -> apply(module, fun, [result | args])
anon when is_function(anon) -> anon.(result)
end
end

View File

@ -5,5 +5,33 @@ defmodule Farmbot.Database.Syncable.Peripheral do
alias Farmbot.Database
alias Database.Syncable
use Syncable, model: [], endpoint: {"/peripherals", "/peripherals"}
alias Farmbot.Context
alias Farmbot.CeleryScript.{Command, Ast}
use Syncable, model: [
:pin,
:mode,
:label
], endpoint: {"/peripherals", "/peripherals"}
def on_sync(context, object_or_list)
def on_sync(%Context{} = _, []), do: :ok
def on_sync(%Context{} = context, [%__MODULE__{} = first | rest]) do
on_sync(context, first)
on_sync(context, rest)
end
def on_sync(%Context{} = context, %__MODULE__{pin: pin, mode: mode, label: label}) do
spawn fn ->
:ok = Farmbot.BotState.set_pin_mode(context, pin, mode)
ast = %Ast{
kind: "read_pin",
args: %{pin_number: pin, pin_mode: mode, label: label},
body: []
}
Command.do_command(ast, context)
end
end
end

View File

@ -5,7 +5,8 @@ defmodule Farmbot.Database.Syncable.Point do
alias Farmbot.Context
alias Farmbot.Database
alias Database.Syncable
alias Database.{Syncable, Selectors}
alias Selectors.Error, as: SelectorError
use Syncable, model: [
:pointer_type,
:created_at,
@ -29,7 +30,10 @@ defmodule Farmbot.Database.Syncable.Point do
end
unless maybe_point do
raise "Could not find tool_slot with tool_id: #{tool_id}"
raise SelectorError,
syncable: __MODULE__,
syncable_id: tool_id,
message: "Could not find tool_slot with tool_id: #{tool_id}"
end
maybe_point

View File

@ -5,5 +5,11 @@ defmodule Farmbot.Database.Syncable.Sequence do
alias Farmbot.Database
alias Database.Syncable
use Syncable, model: [], endpoint: {"/sequences", "/sequences"}
use Syncable, model: [
:version,
:body,
:args,
:kind,
:name
], endpoint: {"/sequences", "/sequences"}
end

View File

@ -5,5 +5,8 @@ defmodule Farmbot.Database.Syncable.Tool do
alias Farmbot.Database
alias Database.Syncable
use Syncable, model: [], endpoint: {"/tools", "/tools"}
use Syncable, model: [
:name,
:status
], endpoint: {"/tools", "/tools"}
end

View File

@ -0,0 +1,4 @@
defmodule Farmbot.Database.Syncable.Error do
@moduledoc "Syncable error"
defexception [:message]
end

View File

@ -26,22 +26,28 @@ defmodule Farmbot.DebugLog do
"""
defmacro __using__(opts) do
color = Keyword.get(opts, :color)
name = Keyword.get(opts, :name)
name = Keyword.get(opts, :name)
quote do
defp get_module do
unquote(name) || __MODULE__ |> Module.split() |> List.last
if unquote(name) do
defp get_module, do: unquote(name)
else
defp get_module, do: __MODULE__ |> Module.split() |> List.last
end
if unquote(color) do
defp debug_log(str) do
GenEvent.notify(Farmbot.DebugLog,
{get_module(), {unquote(color), str}})
end
else
defp debug_log(str) do
GenEvent.notify Farmbot.DebugLog, {get_module(), {:BLUE, str}}
end
end # if color
end # quote

View File

@ -2,15 +2,16 @@ defmodule Farmbot.FarmEventRunner do
@moduledoc """
Checks the database every 60 seconds for FarmEvents
"""
use GenServer
require Logger
alias Farmbot.CeleryScript.Ast
alias Farmbot.Context
use Farmbot.DebugLog
alias Farmbot.Database
alias Database.Syncable.{Sequence, Regimen, FarmEvent}
alias Farmbot.{Context, DebugLog, Database, CeleryScript}
alias CeleryScript.Ast
use DebugLog
use GenServer
alias Database.Syncable.{
Sequence,
Regimen,
FarmEvent
}
@checkup_time 10_000
@ -28,6 +29,7 @@ defmodule Farmbot.FarmEventRunner do
def handle_info(:checkup, {context, state}) do
now = get_now()
# debug_log "Doing checkup: #{inspect now}"
new_state = if now do
all_events =
context
@ -51,17 +53,13 @@ defmodule Farmbot.FarmEventRunner do
@spec start_events(Context.t, [Sequence.t | Regimen.t], DateTime.t)
:: no_return
defp start_events(_context, [], _now), do: :ok
defp start_events(context, [event | rest], now) do
defp start_events(%Context{} = context, [event | rest], now) do
cond do
match?(%Sequence{}, event) ->
ast = Ast.parse(event)
{:ok, _pid} = Elixir.Farmbot.SequenceRunner.start_link(ast, context)
Farmbot.CeleryScript.Command.do_command(ast, context)
match?(%Regimen{}, event) ->
r = event.__struct__
|> Module.split
|> List.last
|> Module.concat(Supervisor)
{:ok, _pid} = r.add_child(context, event, now)
{:ok, _pid} = Farmbot.Regimen.Supervisor.add_child(context, event, now)
true ->
Logger.error ">> Doesn't know how to handle event: #{inspect event}"
end
@ -187,7 +185,7 @@ defmodule Farmbot.FarmEventRunner do
debug_log "== NOW: #{inspect now_str}"
debug_log "== LAST: #{inspect last_time_str}"
debug_log "== MAYBE NEXT: #{inspect maybe_next_str}"
debug_log "== #{Enum.count calendar} events are scheduled to happend after: #{inspect last_time_str}\n"
debug_log "== #{Enum.count calendar} events are scheduled to happen after: #{inspect last_time_str}\n"
end
defp get_last_time_str(nil), do: "none"
@ -204,6 +202,11 @@ defmodule Farmbot.FarmEventRunner do
@spec lookup(Context.t, Sequence | Regimen, integer) :: Sequence.t | Regimen.t
defp lookup(%Context{} = ctx, module, sr_id) do
Database.get_by_id(ctx, module, sr_id)
item = Database.get_by_id(ctx, module, sr_id)
unless item do
raise "Could not find #{inspect module} by id: #{sr_id}"
end
item.body
end
end

View File

@ -1,242 +1,104 @@
defmodule Farmware do
defmodule Farmbot.Farmware do
@moduledoc """
Interface with Farmware.
Farmware Data Type
"""
alias Farmbot.Farmware.Installer
defmodule Manifest do
@moduledoc false
use HTTPoison.Base
@schema_location "https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests/master/schema.json"
defmodule Meta do
@moduledoc """
Stuff on the Manifest we dont really care about that much.
"""
@spec process_response_body(binary) :: {binary, list}
def process_response_body(body) do
f = body
|> Poison.decode!
|> validate!()
|> Enum.map(fn({k, v}) -> {String.to_atom(k), v} end)
{f, body}
end
defstruct [
:author,
:language,
:description,
:version,
:min_os_version_major,
:zip
]
@spec validate!(map) :: map | no_return
defp validate!(body) do
case ExJsonSchema.Validator.validate(schema(), body) do
:ok -> body
error -> raise "could not validate package! #{inspect error}"
@typedoc "Various garbage we don't care that much about."
@type t :: %__MODULE__{
min_os_version_major: binary,
description: binary,
language: binary,
version: binary,
author: binary,
zip: binary
}
defimpl Inspect, for: __MODULE__ do
def inspect(thing, _) do
"#FarmwareMeta<#{thing.description}>"
end
end
@spec schema :: map
defp schema do
HTTPoison.get!(@schema_location).body
|> Poison.decode!
|> ExJsonSchema.Schema.resolve
end
end
alias Farmbot.System.FS
alias Farmware.{Tracker, FarmScript}
alias Farmbot.BotState.ProcessTracker, as: PT
alias Farmbot.Context
require Logger
defstruct [
:executable,
:uuid,
:name,
:meta,
:args,
:url,
:path
]
@behaviour Farmbot.ProcessRunner
@typedoc false
@type uuid :: binary
@spec raise_if_exists(binary, map) :: no_return
defp raise_if_exists(path, manifest) do
if File.exists?(path) do
raise "Could not install Farmware! #{manifest[:package]} already exists!"
end
@typedoc """
The url used to get updates, reinstall, etc.
"""
@type url :: binary
@typedoc "Farmware Struct"
@type t :: %__MODULE__{
executable: binary,
uuid: uuid,
name: binary,
url: url,
args: [binary],
meta: Meta.t,
path: Path.t
}
defimpl Inspect, for: __MODULE__ do
def inspect(%{name: name}, _), do: "#Farmware<#{name}>"
def inspect(_thing, _), do: "#Farmware<:invalid>"
end
@doc """
Installs a package from a manifest url
Creates a new Farmware Struct
"""
@spec install(Context.t, binary) :: map | no_return
def install(%Context{} = ctx, manifest_url) do
Logger.info "Getting Farmware Manifest: #{manifest_url}"
{manifest, json} = Manifest.get!(manifest_url).body
path = FS.path() <> "/farmware/#{manifest[:package]}"
raise_if_exists(path, manifest)
zip_url = manifest[:zip]
Logger.info "Getting Farmware Package: #{zip_url}"
zip_file_path = Downloader.run(zip_url, "/tmp/#{manifest[:package]}.zip")
Logger.info "Unpacking Farmware Package"
:ok = do_install_and_cleanup(path, zip_file_path, json, manifest)
Logger.info "Validating Farmware package"
if File.exists?(path <> "/manifest.json") do
register(ctx, manifest)
else
error_clean_up(path)
end
end
defp do_install_and_cleanup(path, zip_file_path, json, manifest) do
FS.transaction fn() ->
Logger.info "Installing farmware!"
File.mkdir!(path)
unzip_file(zip_file_path, path)
File.write(path <> "/manifest.json", json)
end, true
File.rm! "/tmp/#{manifest[:package]}.zip"
end
defp error_clean_up(path) do
Logger.info "invalid farmware package!"
FS.transaction fn() ->
File.rm_rf!(path)
end, true
raise "Not valid Farmware!"
end
defp register(%Context{} = ctx, manifest) do
Logger.info ">> is installing Farmware: #{manifest[:package]}"
PT.register(ctx, :farmware, manifest[:package], manifest[:package])
manifest
end
@doc """
Uninstalls a Farmware package
"""
@spec uninstall(Context.t, binary) :: no_return
def uninstall(%Context{} = ctx, package_name) do
path = FS.path() <> "/farmware/#{package_name}"
if File.exists?(path) do
Logger.info "uninstalling farmware: #{package_name}", type: :busy
i = Farmbot.BotState.ProcessTracker.lookup(ctx, :farmware, package_name)
deregister(ctx, i)
FS.transaction fn() ->
File.rm_rf!(path)
end, true
else
msg = "can not find farmware: #{package_name} to uninstall"
Logger.info msg, type: :error
end
end
@spec deregister(Context.t, map | nil) :: :ok
defp deregister(%Context{} = _, nil), do: :ok
defp deregister(%Context{} = ctx, i),
do: Farmbot.BotState.ProcessTracker.deregister ctx, i.uuid
@doc """
Forces an update for a Farmware package
"""
@spec update(Context.t, binary) :: no_return
def update(%Context{} = ctx, package_name) do
path = FS.path() <> "/farmware/#{package_name}"
if File.exists?(path) do
url =
path <> "/manifest.json"
|> File.read!
|> Poison.decode!
|> Map.get("url")
uninstall(ctx, package_name)
install(ctx, url)
else
raise "Could not find Farmware to update! #{package_name}"
end
end
@doc """
Starts a farm process. Callback from ProcessTracker
"""
def start_process(%Context{} = ctx, package_name),
do: execute(ctx, package_name)
@doc """
Stops a farm process. Callback from ProcessTracker
"""
def stop_process(%Context{} = _ctx, _package_name), do: :ok
@doc """
Executes a Farmware Package
arguments is a list of strings to pass too the script
"""
@spec execute(Context.t, binary) :: no_return
@spec execute(Context.t, binary, any) :: no_return
def execute(%Context{} = ctx, package_name, envs \\ []) do
path = FS.path() <> "/farmware/#{package_name}"
if File.exists?(path) do
manifest =
path <> "/manifest.json"
|> File.read!
|> Poison.decode!
exe = manifest["executable"]
args = manifest["args"]
item = %FarmScript{executable: exe,
args: args,
path: path,
name: package_name,
envs: envs}
Tracker.add(ctx, item)
else
msg = ">> Could not find FarmWare: #{package_name}"
Logger.info msg, type: :error
raise msg
end
end
@spec info(String.t) :: map | no_return
def info(package_name) do
path = FS.path() <> "/farmware/#{package_name}"
if File.exists?(path) do
path <> "/manifest.json"
|> File.read!
|> Poison.decode!
else
msg = ">> Could not find FarmWare: #{package_name}"
Logger.info msg, type: :error
raise msg
end
end
@doc """
Checks if a package is installed
"""
@spec installed?(binary) :: boolean
def installed?(package_name) do
path = FS.path() <> "/farmware/#{package_name}"
File.exists?(path)
end
@doc """
Lists all installed Farmware Packages
"""
@spec list :: [binary]
def list, do: File.ls!(FS.path() <> "/farmware/")
@doc """
Downloads and updates first party farmwares.
"""
@spec get_first_party_farmware(Context.t) :: no_return
def get_first_party_farmware(%Context{} = ctx) do
farmwares = HTTPoison.get!("https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests/master/manifest.json").body
|> Poison.decode!
for %{"name" => name, "manifest" => manifest} <- farmwares do
maybe_install(ctx, name, manifest)
end
end
defp maybe_install(%Context{} = ctx, name, manifest) do
if installed?(name) do
# if its installed already update it
update(ctx, name)
else
# if not just install it.
install(ctx, manifest)
end
end
defp unzip_file(zip_file, path) when is_bitstring(zip_file) do
cwd = File.cwd!
File.cd! path
:zip.unzip(String.to_charlist(zip_file))
File.cd! cwd
def new(%{
"package" => name,
"language" => language,
"description" => description,
"author" => author,
"version" => version,
"min_os_version_major" => min_os_version_major,
"url" => url,
"zip" => zip,
"executable" => exe,
"args" => args
}) do
%__MODULE__{
executable: exe,
uuid: Nerves.Lib.UUID.generate(),
args: args,
name: name,
url: url,
path: "#{Installer.package_path()}/#{name}",
meta: %Meta{
min_os_version_major: min_os_version_major ,
description: description,
language: language,
version: version,
author: author,
zip: zip
},
}
end
end

View File

@ -1,141 +0,0 @@
defmodule Farmware.FarmScript do
@moduledoc """
A FarmScript is basically a sandboxed shell script.
This will allow "plugins" to the Farmbot system for people who don't
speak Elixir.
* right now we can support `python`, `sh` (no not `bash`), `mruby` (no not ruby) and `elixir`
* actually we can't support Elixir lol
* there can only be one script executing at a time (because there is only one gantry)
* probably only has access to `celery_script` nodes?
* how to stop `System.cmd`
* does std::farmbot take priority or scripts?
* how to handle failures?
"""
@typedoc """
The things required to describe a FarmScript
"""
@type t ::
%__MODULE__{executable: binary,
args: [binary],
path: binary,
name: binary,
envs: [{binary, binary}]}
@enforce_keys [:executable, :args, :path, :name, :envs]
defstruct [:executable, :args, :path, :name, :envs]
@clean_up_timeout 900_000 # fifteen minutes
require Logger
alias Farmbot.CeleryScript.{Command, Ast}
alias Farmbot.Context
@doc """
Executes a farmscript?
takes a FarmScript, and some environment vars. [{KEY, VALUE}]
Exits if anything unexpected happens for saftey.
"""
@spec run(Context.t, t, [{charlist, charlist}]) :: pid
def run(%Context{} = ctx, %__MODULE__{} = thing, env) do
# make sure we have this package installed.
blah = System.find_executable(thing.executable)
unless blah, do: raise "Could not find: #{thing.executable}!"
Logger.info ">> is setting environment for #{thing.name}"
# why is there two ways to pass envs to farmware?
extra_env = build_extra_env(thing.envs)
# get a token. this will raise if there is no token.
api_env = get_token(ctx)
cwd = File.cwd!
File.cd!(thing.path)
port =
Port.open({:spawn_executable, blah},
[:stream,
:binary,
:exit_status,
:hide,
:use_stdio,
:stderr_to_stdout,
args: thing.args,
env: env ++ extra_env ++ [api_env]])
# Handles the life of a farmware.
handle_port(port, thing, ctx)
# change back to where we started.
File.cd!(cwd)
end
# Credo made me do it
@spec build_extra_env(list) :: [{charlist,charlist}]
defp build_extra_env(envs) do
Enum.map(envs, fn({k, v}) ->
if is_bitstring(k),
do: {String.to_charlist(k), String.to_charlist(v)}, else: {k, v}
end)
end
# this will raise if there is no token. This is unintended security lol.
@spec get_token(Context.t) :: {charlist, charlist}
defp get_token(%Context{} = context) do
{:ok, token} = Farmbot.Auth.get_token(context.auth)
{'API_TOKEN', String.to_charlist(token.encoded)}
end
defp handle_port(port, %__MODULE__{} = thing, %Context{} = ctx) do
receive do
{^port, {:exit_status, 0}} ->
Logger.info ">> [#{thing.name}] completed!"
{^port, {:exit_status, s}} ->
Logger.error ">> [#{thing.name}] completed with errors! (#{s})"
{^port, {:data, stuff}} ->
spawn fn() -> handle_script_output(stuff, thing, ctx) end
handle_port(port, thing, ctx)
after
@clean_up_timeout ->
Logger.error ">> [#{thing.name}] Timed out"
kill_port(port)
end
end
@spec kill_port(port) :: no_return
defp kill_port(port) do
info = Port.info(port)
if info, do: System.cmd("kill", ["#{info[:os_pid]}"])
end
# pattern matching is cool
defp handle_script_output(string, thing, ctx) do
l = String.split(string, "\n")
do_sort(l, "", thing, ctx)
end
defp do_sort(list, acc, thing, ctx)
defp do_sort(["<<< " <> json | tail ], acc, thing, ctx) do
case Poison.decode(json) do
{:ok, thing} ->
ast_node = Ast.parse(thing)
Command.do_command(ast_node, ctx)
_ -> Logger.error ">> Got invalid Celery Script from: #{thing.name}"
end
do_sort(tail, acc, thing, ctx)
end
# SHHHHH
defp do_sort(["EVAL " <> some_code | tail ], acc, thing, ctx) do
Code.eval_string(some_code)
do_sort(tail, acc, thing, ctx)
end
defp do_sort([string | tail], acc, thing, ctx) do
do_sort(tail, acc <> "\n" <> string, thing, ctx)
end
defp do_sort([], acc, thing, _ctx) do
Logger.info ">> [#{thing.name}] "<> String.trim(acc)
end
end

View File

@ -0,0 +1,218 @@
defmodule Farmbot.Farmware.Installer do
@moduledoc """
Handles the installing and uninstalling of packages
"""
alias Farmbot.{Context, Farmware, System}
alias Farmware.Manager
alias Farmware.Installer.{Repository, Error}
alias System.FS
use Farmbot.DebugLog, name: FarmwareInstaller
@version Mix.Project.config[:version]
@doc """
Installs a farmware.
Does not register to the Manager.
"""
@spec install!(Context.t, binary) :: Farmware.t | no_return
def install!(%Context{} = ctx, url) do
:ok = ensure_dirs!()
schema = ensure_schema!()
%{body: binary} = Farmbot.HTTP.get!(ctx, url)
json = Poison.decode!(binary)
:ok = validate_json!(schema, json)
debug_log "Installing a Farmware from: #{url}"
ensure_correct_os_version!(json)
package_path = "#{package_path()}/#{json["package"]}"
case check_package(package_path, json) do
:needs_install ->
dl_path = Farmbot.HTTP.download_file!(ctx,
json["zip"], "/tmp/#{json["package"]}.zip")
FS.transaction fn() ->
File.mkdir_p!(package_path)
unzip! dl_path, package_path
File.write! "#{package_path}/manifest.json", binary
end, true
fw = Farmware.new(json)
debug_log "Installed new Farmware: #{inspect fw}"
fw
{:noop, fw} ->
debug_log "#{inspect fw} is installed and up to date."
fw
end
end
defp check_package(path, json) do
check_package_exists(path) || check_manifests(path, json)
end
defp check_package_exists(path) do
unless File.exists?(path), do: :needs_install
end
defp check_manifests(path, new) do
case File.read("#{path}/manifest.json") do
{:ok, bin} ->
current = Poison.decode!(bin)
check_manifests_version(current, new)
_ -> :needs_install
end
end
# Checks two manifests. If the new one has a newer version,
# say upgrade, if not noop
defp check_manifests_version(
%{"version" => current_version} = current, %{"version" => new_version})
do
case Version.compare(current_version, new_version) do
# if bot versions are equal noop
:eq -> {:noop, Farmware.new(current)}
# if current version is greater
# noop (but something weird might be going on)
:gt -> {:noop, Farmware.new(current)}
# if the current version is less than new version we install.
:lg -> :needs_install
end
end
defp ensure_correct_os_version!(%{"min_os_version_major" => major}) do
ver_int = @version |> String.first() |> String.to_integer
if major > ver_int do
raise Error, message: "Version mismatch! " <>
"Farmbot is: #{ver_int} Farmware requires: #{major}"
else
:ok
end
end
@doc """
Uninstalls a farmware.
Does no unregister from the Manager.
"""
@spec uninstall!(Context.t, Farmware.t) :: :ok | no_return
def uninstall!(%Context{} = _ctx, %Farmware{} = fw) do
:ok = ensure_dirs!()
debug_log "Uninstalling a Farmware from: #{inspect fw}"
package_path = fw.path
FS.transaction fn() ->
File.rm_rf!(package_path)
end, true
end
@doc """
Enables a repo to be synced on bootup
"""
@spec enable_repo!(Context.t, atom) :: :ok | no_return
def enable_repo!(%Context{} = ctx, module) when is_atom(module) do
debug_log "repo: #{module} is syncing"
# make sure we have a valid repository here.
ensure_module!(module)
:ok = ensure_dirs!()
url = module.url()
%{body: binary} = Farmbot.HTTP.get!(ctx, url)
json = Poison.decode!(binary)
repository = Repository.validate!(json)
# do the installs
for entry <- repository.entries do
:ok = Manager.install!(ctx, entry.manifest)
end
:ok = set_synced!(module)
end
@doc """
Lists all the farmwares
"""
@spec list_installed! :: [Farmware.t]
def list_installed! do
ensure_dirs!()
dirs = File.ls! package_path()
try do
Enum.map(dirs, fn(dir) ->
package_dir = "#{package_path()}/#{dir}"
json = "#{package_dir}/manifest.json" |> File.read! |> Poison.decode!
ensure_correct_os_version!(json)
Farmware.new(json)
end)
rescue
e ->
FS.transaction fn() ->
File.rm_rf!(package_path())
end, true
reraise e, Elixir.System.stacktrace()
end
end
defp ensure_dirs! do
ensure_dir! path()
ensure_dir! repo_path()
ensure_dir! package_path()
end
defp ensure_dir!(path) do
debug_log "Ensuring dir: #{inspect path}"
unless File.exists?(path) do
FS.transaction fn() ->
File.mkdir_p!(path)
end, true
end
:ok
end
defp set_synced!(module) when is_atom(module) do
path = "#{repo_path()}/#{module}"
FS.transaction fn() ->
File.write! path, "#{:os.system_time}"
end, true
debug_log "#{module} was synced"
:ok
end
defp ensure_module!(module) do
{:module, real} = Code.ensure_loaded(module)
if function_exported?(real, :url, 0) do
debug_log "repo #{module} is loaded"
:ok
else
raise "Could not load repository: #{inspect module}"
end
end
def path, do: "#{FS.path()}/farmware"
def repo_path, do: "#{path()}/repos"
def package_path, do: "#{path()}/packages"
defp unzip!(zip_file, path) when is_bitstring(zip_file) do
debug_log "Unzipping #{zip_file} to #{path}"
cwd = File.cwd!
File.cd! path
:zip.unzip(String.to_charlist(zip_file))
debug_log "Done unzipping #{zip_file} to #{path}"
File.cd! cwd
end
@doc """
Ensures the schema has been resolved.
"""
def ensure_schema! do
schema_path()
|> File.read!()
|> Poison.decode!()
|> ExJsonSchema.Schema.resolve()
end
defp schema_path,
do: "#{:code.priv_dir(:farmbot)}/static/farmware_schema.json"
defp validate_json!(schema, json) do
case ExJsonSchema.Validator.validate(schema, json) do
:ok -> :ok
{:error, reason} -> raise Error,
message: "Could not parse manifest: #{inspect reason}"
end
end
end

View File

@ -0,0 +1,11 @@
defmodule Farmbot.Farmware.Installer.Error do
@moduledoc """
Farmware Installer Error
"""
defexception [:message]
@doc false
def exception(value) when is_binary(value) do
%__MODULE__{message: value}
end
end

View File

@ -0,0 +1,50 @@
defmodule Farmbot.Farmware.Installer.Repository do
@moduledoc """
Data Access Object for Farmware Repository
"""
defmodule Entry do
@moduledoc false
@enforce_keys [:name, :manifest]
defstruct [:name, :manifest]
@typedoc """
* `name` is the name of the Farmware
* `manifest` is the url to the manifest
"""
@type t :: %__MODULE__{ name: binary, manifest: binary }
@doc """
Validates json
"""
@spec validate!(any) :: t
def validate!(%{"name" => name, "manifest" => manifest}) do
%__MODULE__{name: name, manifest: manifest}
end
def validate!(err), do: raise "Repo entry not valid: #{inspect err}"
end
defstruct [:entries]
@typedoc """
A repository is just a list of entries.
"""
@type t :: %__MODULE__{entries: [Entry.t]}
@doc """
Validates an entire repo from json
"""
@spec validate!(any, [Entry.t]) :: t
def validate!(json_list, acc \\ [])
def validate!([json_entry | rest], acc) do
entry = Entry.validate!(json_entry)
validate_repo!(rest, [entry | acc])
end
defp validate_repo!([], acc), do: %__MODULE__{entries: acc}
@doc """
Must return the url that holds this manifest.
"""
@callback url :: binary
end

View File

@ -0,0 +1,5 @@
defmodule Farmbot.Farmware.Installer.Repository.Farmbot do
@moduledoc false
@behaviour Farmbot.Farmware.Installer.Repository
def url, do: "https://raw.githubusercontent.com/FarmBot-Labs/farmware_manifests/master/manifest.json"
end

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