commit
41d46577fc
|
@ -40,3 +40,4 @@ dump.rdb
|
|||
|
||||
# this file isnt stored here but just in case.
|
||||
fwup-key.priv
|
||||
.env
|
||||
|
|
|
@ -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
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
|
@ -0,0 +1,2 @@
|
|||
use Mix.Config
|
||||
import_config "dev.exs"
|
|
@ -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"
|
|
@ -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
|
|
@ -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
|
|
@ -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}]
|
|
@ -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
|
|
@ -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`.
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
"{\"id\":52,\"name\":\"ancient-paper-798\",\"webcam_url\":null}"
|
|
@ -0,0 +1 @@
|
|||
"{\"foo\": \"uh\", \"bar\": 1, \"id\": 2}"
|
|
@ -0,0 +1 @@
|
|||
"[{\"foo\": \"uh\", \"bar\": 1, \"id\": 2}]"
|
|
@ -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
|
@ -0,0 +1 @@
|
|||
"[{\"id\":1,\"pin\":13,\"mode\":0,\"label\":\"LED\"}]"
|
File diff suppressed because one or more lines are too long
|
@ -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}"
|
|
@ -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-----
|
|
@ -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}]}]"
|
|
@ -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\"}]"
|
|
@ -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\"}"
|
|
@ -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}}"
|
|
@ -0,0 +1 @@
|
|||
"[{\"id\":1,\"name\":\"Trench Digging Tool\",\"status\":\"active\"}]"
|
|
@ -0,0 +1 @@
|
|||
"{\"id\":1,\"name\":\"Trench Digging Tool\",\"status\":\"active\"}"
|
|
@ -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
65
idea.md
|
@ -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.
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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" ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
defmodule Farmbot.CeleryScript.Error do
|
||||
defexception [
|
||||
{:context, nil},
|
||||
:message
|
||||
]
|
||||
end
|
|
@ -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] ->
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
defmodule Farmbot.Database.Selectors.Error do
|
||||
@moduledoc "Error message for selectors."
|
||||
defexception [
|
||||
:syncable_id,
|
||||
:syncable,
|
||||
:message,
|
||||
]
|
||||
end
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
defmodule Farmbot.Database.Syncable.Error do
|
||||
@moduledoc "Syncable error"
|
||||
defexception [:message]
|
||||
end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
Loading…
Reference in New Issue